images work

This commit is contained in:
Gregor Lohaus
2026-05-28 13:37:18 +02:00
parent cf218e2afd
commit 1665dcfaae
12 changed files with 407 additions and 58 deletions

View File

@@ -4,6 +4,8 @@ import dev.jlibghostty.KittyImageCompression;
import dev.jlibghostty.KittyImageFormat; import dev.jlibghostty.KittyImageFormat;
import dev.jlibghostty.KittyImageSnapshot; import dev.jlibghostty.KittyImageSnapshot;
import dev.jlibghostty.KittyPlacement; import dev.jlibghostty.KittyPlacement;
import dev.jlibghostty.KittyPlacementLayer;
import dev.jlibghostty.KittyPlaceholder;
import dev.jlibghostty.KittyRenderInfo; import dev.jlibghostty.KittyRenderInfo;
import dev.jlibghostty.KeyModifiers; import dev.jlibghostty.KeyModifiers;
import dev.jlibghostty.MouseButton; import dev.jlibghostty.MouseButton;
@@ -30,6 +32,7 @@ import javafx.scene.text.Text;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public final class TerminalCanvasView { public final class TerminalCanvasView {
@@ -39,9 +42,15 @@ public final class TerminalCanvasView {
private final Canvas canvas = new Canvas(); private final Canvas canvas = new Canvas();
private final TerminalWorkspace workspace; private final TerminalWorkspace workspace;
private final AppConfig config; private final AppConfig config;
private final Map<Long, Image> kittyImageCache = new HashMap<>(); private final Map<KittyImageKey, Image> kittyImageCache = new HashMap<>();
private final Map<TerminalPane, PaneRenderCache> paneRenderCache = new HashMap<>();
private String fontFamily; private String fontFamily;
private double fontSize; private double fontSize;
private Font cachedFont;
private FontMetrics cachedMetrics;
private String cachedFontFamily;
private double cachedFontSize;
private String lastRenderKey;
private boolean mouseButtonPressed; private boolean mouseButtonPressed;
private MouseButton pressedButton = MouseButton.UNKNOWN; private MouseButton pressedButton = MouseButton.UNKNOWN;
@@ -65,53 +74,111 @@ public final class TerminalCanvasView {
public void setFont(String family, double size) { public void setFont(String family, double size) {
this.fontFamily = family; this.fontFamily = family;
this.fontSize = size; this.fontSize = size;
cachedFont = null;
cachedMetrics = null;
paneRenderCache.clear();
lastRenderKey = null;
} }
public void render() { public void render() {
double width = canvas.getWidth(); double width = canvas.getWidth();
double height = canvas.getHeight(); double height = canvas.getHeight();
workspace.layout(width, height); workspace.layout(width, height);
Font font = currentFont();
FontMetrics metrics = currentFontMetrics();
List<TerminalPane> panes = workspace.panes();
String renderKey = renderKey(width, height, metrics, panes);
if (renderKey.equals(lastRenderKey)) {
return;
}
lastRenderKey = renderKey;
GraphicsContext gc = canvas.getGraphicsContext2D(); GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.rgb(16, 16, 18)); gc.setFill(Color.rgb(16, 16, 18));
gc.fillRect(0, 0, width, height); gc.fillRect(0, 0, width, height);
gc.setFontSmoothingType(FontSmoothingType.LCD); gc.setFontSmoothingType(FontSmoothingType.LCD);
for (TerminalPane pane : workspace.panes()) { paneRenderCache.keySet().removeIf(pane -> !panes.contains(pane));
drawPane(gc, pane); for (TerminalPane pane : panes) {
drawPane(gc, pane, font, metrics);
} }
} }
private void drawPane(GraphicsContext gc, TerminalPane pane) { private void drawPane(GraphicsContext gc, TerminalPane pane, Font font, FontMetrics metrics) {
gc.save(); if (config.kittyGraphics() && paneHasKittyGraphics(pane)) {
gc.beginPath(); paneRenderCache.remove(pane);
gc.rect(pane.x(), pane.y(), pane.width(), pane.height()); gc.save();
gc.clip(); gc.beginPath();
gc.rect(pane.x(), pane.y(), pane.width(), pane.height());
gc.clip();
drawPaneContent(gc, pane, font, metrics, pane.x(), pane.y(), pane.width(), pane.height(), false);
gc.restore();
return;
}
PaneRenderCache cache = paneRenderCache.computeIfAbsent(pane, ignored -> new PaneRenderCache());
String cacheKey = paneCacheKey(pane, metrics);
int imageWidth = Math.max(1, (int) Math.ceil(pane.width()));
int imageHeight = Math.max(1, (int) Math.ceil(pane.height()));
if (cache.image == null || cache.canvas == null || cache.imageWidth != imageWidth || cache.imageHeight != imageHeight || !cacheKey.equals(cache.key)) {
cache.canvas = new Canvas(imageWidth, imageHeight);
drawPaneContent(cache.canvas.getGraphicsContext2D(), pane, font, metrics, 0.0, 0.0, imageWidth, imageHeight, true);
cache.image = new WritableImage(imageWidth, imageHeight);
cache.canvas.snapshot(null, cache.image);
cache.imageWidth = imageWidth;
cache.imageHeight = imageHeight;
cache.key = cacheKey;
}
gc.drawImage(cache.image, pane.x(), pane.y());
}
private void drawPaneContent(
GraphicsContext gc,
TerminalPane pane,
Font font,
FontMetrics metrics,
double x,
double y,
double width,
double height,
boolean clear
) {
if (clear) {
gc.clearRect(x, y, width, height);
}
gc.setFontSmoothingType(FontSmoothingType.LCD);
if (pane.floating()) { if (pane.floating()) {
gc.setGlobalAlpha(0.96); gc.setGlobalAlpha(0.96);
} }
gc.setFill(Color.rgb(9, 10, 12)); gc.setFill(Color.rgb(9, 10, 12));
gc.fillRect(pane.x(), pane.y(), pane.width(), pane.height()); gc.fillRect(x, y, width, height);
gc.setGlobalAlpha(1.0); gc.setGlobalAlpha(1.0);
gc.setStroke(workspace.isActive(pane) ? Color.rgb(87, 166, 255) : Color.rgb(52, 57, 65)); gc.setStroke(workspace.isActive(pane) ? Color.rgb(87, 166, 255) : Color.rgb(52, 57, 65));
gc.setLineWidth(workspace.isActive(pane) ? 2.0 : 1.0); gc.setLineWidth(workspace.isActive(pane) ? 2.0 : 1.0);
gc.strokeRect(pane.x() + 0.5, pane.y() + 0.5, pane.width() - 1.0, pane.height() - 1.0); gc.strokeRect(x + 0.5, y + 0.5, width - 1.0, height - 1.0);
Font font = Font.font(fontFamily, fontSize);
gc.setFont(font); gc.setFont(font);
FontMetrics metrics = measureFontMetrics(font); int columns = Math.max(1, (int) ((width - 24.0) / metrics.cellWidth));
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth)); int rows = Math.max(1, (int) ((height - 24.0) / metrics.lineHeight));
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
pane.resize(columns, rows, (int) Math.round(metrics.cellWidth), (int) Math.round(metrics.lineHeight)); pane.resize(columns, rows, (int) Math.round(metrics.cellWidth), (int) Math.round(metrics.lineHeight));
double left = pane.x() + 12.0; double left = x + 12.0;
double top = pane.y() + 12.0; double top = y + 12.0;
double baseline = top + metrics.baselineOffset; double baseline = top + metrics.baselineOffset;
RenderStateSnapshot snapshot = pane.renderSnapshot(); RenderStateSnapshot snapshot = pane.renderSnapshot();
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds = config.kittyGraphics()
? kittyPlaceholderBounds(snapshot)
: Map.of();
if (config.kittyGraphics()) {
drawKittyGraphics(gc, pane, KittyPlacementLayer.BELOW_TEXT, placeholderBounds, left, top, metrics.cellWidth, metrics.lineHeight);
}
if (snapshot != null) { if (snapshot != null) {
for (RenderRow row : snapshot.renderRows()) { for (RenderRow row : snapshot.renderRows()) {
drawRow(gc, row, left, top, baseline, metrics.cellWidth, metrics.lineHeight); drawRow(gc, row, left, top, baseline, metrics.cellWidth, metrics.lineHeight);
@@ -123,9 +190,8 @@ public final class TerminalCanvasView {
} }
if (config.kittyGraphics()) { if (config.kittyGraphics()) {
drawKittyGraphics(gc, pane, left, top, metrics.cellWidth, metrics.lineHeight); drawKittyGraphics(gc, pane, KittyPlacementLayer.ABOVE_TEXT, placeholderBounds, left, top, metrics.cellWidth, metrics.lineHeight);
} }
gc.restore();
} }
private static FontMetrics measureFontMetrics(Font font) { private static FontMetrics measureFontMetrics(Font font) {
@@ -134,13 +200,80 @@ public final class TerminalCanvasView {
double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight()); double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight());
double baselineOffset = -text.getLayoutBounds().getMinY(); double baselineOffset = -text.getLayoutBounds().getMinY();
String sample = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; Text cell = new Text("M");
Text cell = new Text(sample);
cell.setFont(font); cell.setFont(font);
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth() / sample.length()); double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth());
return new FontMetrics(cellWidth, lineHeight, baselineOffset); return new FontMetrics(cellWidth, lineHeight, baselineOffset);
} }
private Font currentFont() {
if (cachedFont == null || !fontFamily.equals(cachedFontFamily) || fontSize != cachedFontSize) {
cachedFont = Font.font(fontFamily, fontSize);
cachedMetrics = null;
cachedFontFamily = fontFamily;
cachedFontSize = fontSize;
}
return cachedFont;
}
private FontMetrics currentFontMetrics() {
if (cachedMetrics == null) {
cachedMetrics = measureFontMetrics(currentFont());
}
return cachedMetrics;
}
private String renderKey(double width, double height, FontMetrics metrics, List<TerminalPane> panes) {
StringBuilder builder = new StringBuilder();
builder.append(width).append(':')
.append(height).append(':')
.append(workspace.version()).append(':')
.append(fontFamily).append(':')
.append(fontSize).append(':')
.append(metrics.cellWidth).append(':')
.append(metrics.lineHeight);
for (TerminalPane pane : panes) {
builder.append('|')
.append(System.identityHashCode(pane)).append(',')
.append(pane.renderVersion()).append(',')
.append(workspace.isActive(pane)).append(',')
.append(pane.x()).append(',')
.append(pane.y()).append(',')
.append(pane.width()).append(',')
.append(pane.height());
}
return builder.toString();
}
private String paneCacheKey(TerminalPane pane, FontMetrics metrics) {
return pane.renderVersion()
+ ":" + workspace.isActive(pane)
+ ":" + pane.width()
+ ":" + pane.height()
+ ":" + fontFamily
+ ":" + fontSize
+ ":" + metrics.cellWidth
+ ":" + metrics.lineHeight
+ ":" + config.kittyGraphics();
}
private static Map<KittyPlaceholderKey, KittyPlaceholderBounds> kittyPlaceholderBounds(RenderStateSnapshot snapshot) {
if (snapshot == null) {
return Map.of();
}
Map<KittyPlaceholderKey, KittyPlaceholderBounds> result = new HashMap<>();
for (RenderRow row : snapshot.renderRows()) {
for (RenderCell cell : row.cells()) {
cell.kittyPlaceholder().ifPresent(placeholder -> {
KittyPlaceholderKey key = new KittyPlaceholderKey(placeholder.imageId(), placeholder.placementId());
result.computeIfAbsent(key, ignored -> new KittyPlaceholderBounds()).include(row.row(), cell.column(), placeholder);
});
}
}
return result;
}
private void handleMousePressed(MouseEvent event) { private void handleMousePressed(MouseEvent event) {
canvas.requestFocus(); canvas.requestFocus();
TerminalPane pane = paneAt(event.getX(), event.getY()); TerminalPane pane = paneAt(event.getX(), event.getY());
@@ -244,7 +377,7 @@ public final class TerminalCanvasView {
return null; return null;
} }
FontMetrics metrics = measureFontMetrics(Font.font(fontFamily, fontSize)); FontMetrics metrics = currentFontMetrics();
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth)); int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth));
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight)); int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
long cellWidth = Math.max(1L, Math.round(metrics.cellWidth)); long cellWidth = Math.max(1L, Math.round(metrics.cellWidth));
@@ -325,15 +458,19 @@ public final class TerminalCanvasView {
double lineHeight double lineHeight
) { ) {
for (RenderCell cell : row.cells()) { for (RenderCell cell : row.cells()) {
if (cell.kittyPlaceholder().isPresent()) {
continue;
}
double x = left + (cell.column() * cellWidth); double x = left + (cell.column() * cellWidth);
double cellTop = top + (row.row() * lineHeight); double cellTop = top + (row.row() * lineHeight);
cell.background().ifPresent(background -> { cell.background().ifPresent(background -> {
gc.setFill(toFxColor(background)); gc.setFill(toFxColor(background));
fillCellRect(gc, x, cellTop, cellWidth, lineHeight); gc.fillRect(x, cellTop, cellWidth, lineHeight);
}); });
if (cell.selected()) { if (cell.selected()) {
gc.setFill(SELECTED_BACKGROUND); gc.setFill(SELECTED_BACKGROUND);
fillCellRect(gc, x, cellTop, cellWidth, lineHeight); gc.fillRect(x, cellTop, cellWidth, lineHeight);
} }
if (cell.codepoints().length == 0) { if (cell.codepoints().length == 0) {
continue; continue;
@@ -346,14 +483,6 @@ public final class TerminalCanvasView {
} }
} }
private static void fillCellRect(GraphicsContext gc, double x, double y, double width, double height) {
double x1 = Math.floor(x);
double y1 = Math.floor(y);
double x2 = Math.ceil(x + width);
double y2 = Math.ceil(y + height);
gc.fillRect(x1, y1, Math.max(1.0, x2 - x1), Math.max(1.0, y2 - y1));
}
private static Color toFxColor(RenderColor color) { private static Color toFxColor(RenderColor color) {
return Color.rgb(color.red(), color.green(), color.blue()); return Color.rgb(color.red(), color.green(), color.blue());
} }
@@ -381,54 +510,174 @@ public final class TerminalCanvasView {
} }
} }
private void drawKittyGraphics(GraphicsContext gc, TerminalPane pane, double originX, double originY, double cellWidth, double lineHeight) { private void drawKittyGraphics(
GraphicsContext gc,
TerminalPane pane,
KittyPlacementLayer layer,
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds,
double originX,
double originY,
double cellWidth,
double lineHeight
) {
pane.kittyGraphics().ifPresent(graphics -> { pane.kittyGraphics().ifPresent(graphics -> {
for (KittyPlacement placement : graphics.placements()) { for (KittyPlacement placement : graphics.placements(layer)) {
Image image = imageFor(placement); Image image = imageFor(placement);
if (image == null) { if (image == null) {
continue; continue;
} }
KittyRenderInfo renderInfo = placement.renderInfo().orElse(null); if (placement.virtual()) {
double x = originX; drawVirtualKittyPlacement(gc, placement, image, placeholderBounds, originX, originY, cellWidth, lineHeight);
double y = originY;
double width = image.getWidth();
double height = image.getHeight();
if (renderInfo != null) {
x += renderInfo.viewportColumn() * cellWidth;
y += renderInfo.viewportRow() * lineHeight;
width = renderInfo.gridColumns() > 0 ? renderInfo.gridColumns() * cellWidth : renderInfo.pixelWidth();
height = renderInfo.gridRows() > 0 ? renderInfo.gridRows() * lineHeight : renderInfo.pixelHeight();
} else { } else {
width = placement.columns() > 0 ? placement.columns() * cellWidth : width; drawPinnedKittyPlacement(gc, placement, image, originX, originY, cellWidth, lineHeight);
height = placement.rows() > 0 ? placement.rows() * lineHeight : height;
} }
gc.drawImage(image, x + placement.xOffset(), y + placement.yOffset(), width, height);
} }
}); });
} }
private Image imageFor(KittyPlacement placement) { private static void drawPinnedKittyPlacement(
return placement.image() GraphicsContext gc,
.map(snapshot -> kittyImageCache.computeIfAbsent(snapshot.id(), ignored -> decodeImage(snapshot))) KittyPlacement placement,
.orElse(null); Image image,
double originX,
double originY,
double cellWidth,
double lineHeight
) {
KittyRenderInfo renderInfo = placement.renderInfo().orElse(null);
if (renderInfo == null || !renderInfo.viewportVisible()) {
return;
}
double sourceX = renderInfo.sourceX();
double sourceY = renderInfo.sourceY();
double sourceWidth = renderInfo.sourceWidth();
double sourceHeight = renderInfo.sourceHeight();
if (sourceWidth <= 0.0 || sourceHeight <= 0.0) {
return;
}
double x = originX + (renderInfo.viewportColumn() * cellWidth) + placement.xOffset();
double y = originY + (renderInfo.viewportRow() * lineHeight) + placement.yOffset();
double width = renderInfo.pixelWidth() > 0 ? renderInfo.pixelWidth() : renderInfo.gridColumns() * cellWidth;
double height = renderInfo.pixelHeight() > 0 ? renderInfo.pixelHeight() : renderInfo.gridRows() * lineHeight;
if (width <= 0.0 || height <= 0.0) {
return;
}
gc.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, x, y, width, height);
} }
private Image decodeImage(KittyImageSnapshot snapshot) { private static void drawVirtualKittyPlacement(
GraphicsContext gc,
KittyPlacement placement,
Image image,
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds,
double originX,
double originY,
double cellWidth,
double lineHeight
) {
KittyPlaceholderBounds bounds = placeholderBounds.get(new KittyPlaceholderKey(placement.imageId(), placement.placementId()));
if (bounds == null) {
bounds = placeholderBounds.get(new KittyPlaceholderKey(placement.imageId(), 0));
}
if (bounds == null && placement.placementId() == 0) {
bounds = placeholderBounds.entrySet().stream()
.filter(entry -> entry.getKey().imageId() == placement.imageId())
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}
if (bounds == null || bounds.isEmpty()) {
return;
}
SourceRect source = sourceRect(placement, image);
if (source.width() <= 0.0 || source.height() <= 0.0) {
return;
}
long gridColumns = gridColumns(placement, bounds);
long gridRows = gridRows(placement, bounds);
double sourceCellWidth = source.width() / Math.max(1L, gridColumns);
double sourceCellHeight = source.height() / Math.max(1L, gridRows);
double sourceX = source.x() + (bounds.minSourceColumn * sourceCellWidth);
double sourceY = source.y() + (bounds.minSourceRow * sourceCellHeight);
double sourceWidth = bounds.sourceColumns() * sourceCellWidth;
double sourceHeight = bounds.sourceRows() * sourceCellHeight;
double x = originX + (bounds.minColumn * cellWidth);
double y = originY + (bounds.minRow * lineHeight);
double width = bounds.columns() * cellWidth;
double height = bounds.rows() * lineHeight;
if (sourceWidth <= 0.0 || sourceHeight <= 0.0 || width <= 0.0 || height <= 0.0) {
return;
}
gc.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, x, y, width, height);
}
private static long gridColumns(KittyPlacement placement, KittyPlaceholderBounds bounds) {
if (placement.columns() > 0) {
return placement.columns();
}
return Math.max(bounds.maxSourceColumn + 1, bounds.sourceColumns());
}
private static long gridRows(KittyPlacement placement, KittyPlaceholderBounds bounds) {
if (placement.rows() > 0) {
return placement.rows();
}
return Math.max(bounds.maxSourceRow + 1, bounds.sourceRows());
}
private static SourceRect sourceRect(KittyPlacement placement, Image image) {
double sourceX = placement.sourceX();
double sourceY = placement.sourceY();
double sourceWidth = placement.sourceWidth() > 0 ? placement.sourceWidth() : image.getWidth() - sourceX;
double sourceHeight = placement.sourceHeight() > 0 ? placement.sourceHeight() : image.getHeight() - sourceY;
return new SourceRect(sourceX, sourceY, Math.min(sourceWidth, image.getWidth() - sourceX), Math.min(sourceHeight, image.getHeight() - sourceY));
}
private Image imageFor(KittyPlacement placement) {
return placement.image().map(snapshot -> {
byte[] data = snapshot.data();
KittyImageKey key = KittyImageKey.of(snapshot, data);
Image cached = kittyImageCache.get(key);
if (cached != null) {
return cached;
}
kittyImageCache.keySet().removeIf(existing -> existing.id() == snapshot.id());
Image decoded = decodeImage(snapshot, data);
if (decoded != null) {
kittyImageCache.put(key, decoded);
}
return decoded;
}).orElse(null);
}
private boolean paneHasKittyGraphics(TerminalPane pane) {
return pane.kittyGraphics()
.map(graphics -> !graphics.placements().isEmpty())
.orElse(false);
}
private Image decodeImage(KittyImageSnapshot snapshot, byte[] data) {
if (snapshot.compression() != KittyImageCompression.NONE) { if (snapshot.compression() != KittyImageCompression.NONE) {
return null; return null;
} }
if (snapshot.format() == KittyImageFormat.PNG) { if (snapshot.format() == KittyImageFormat.PNG) {
return new Image(new ByteArrayInputStream(snapshot.data())); return new Image(new ByteArrayInputStream(data));
} }
int width = Math.toIntExact(snapshot.width()); int width = Math.toIntExact(snapshot.width());
int height = Math.toIntExact(snapshot.height()); int height = Math.toIntExact(snapshot.height());
WritableImage image = new WritableImage(width, height); WritableImage image = new WritableImage(width, height);
byte[] data = snapshot.data();
if (snapshot.format() == KittyImageFormat.RGBA) { if (snapshot.format() == KittyImageFormat.RGBA) {
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), rgbaToBgra(data), 0, width * 4); image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), rgbaToBgra(data), 0, width * 4);
@@ -454,4 +703,83 @@ public final class TerminalCanvasView {
private record MouseTarget(MouseEncoderSize size, long screenWidth, long screenHeight) { private record MouseTarget(MouseEncoderSize size, long screenWidth, long screenHeight) {
} }
private record KittyImageKey(long id, long number, long width, long height, KittyImageFormat format, int dataLength, long fingerprint) {
private static KittyImageKey of(KittyImageSnapshot snapshot, byte[] data) {
return new KittyImageKey(
snapshot.id(),
snapshot.number(),
snapshot.width(),
snapshot.height(),
snapshot.format(),
data.length,
fingerprint(data)
);
}
private static long fingerprint(byte[] data) {
long hash = 0xcbf29ce484222325L;
for (byte value : data) {
hash ^= Byte.toUnsignedInt(value);
hash *= 0x100000001b3L;
}
return hash;
}
}
private record KittyPlaceholderKey(long imageId, long placementId) {
}
private record SourceRect(double x, double y, double width, double height) {
}
private static final class KittyPlaceholderBounds {
private int minRow = Integer.MAX_VALUE;
private int maxRow = Integer.MIN_VALUE;
private int minColumn = Integer.MAX_VALUE;
private int maxColumn = Integer.MIN_VALUE;
private long minSourceRow = Long.MAX_VALUE;
private long maxSourceRow = Long.MIN_VALUE;
private long minSourceColumn = Long.MAX_VALUE;
private long maxSourceColumn = Long.MIN_VALUE;
private void include(int row, int column, KittyPlaceholder placeholder) {
minRow = Math.min(minRow, row);
maxRow = Math.max(maxRow, row);
minColumn = Math.min(minColumn, column);
maxColumn = Math.max(maxColumn, column);
minSourceRow = Math.min(minSourceRow, placeholder.sourceRow());
maxSourceRow = Math.max(maxSourceRow, placeholder.sourceRow());
minSourceColumn = Math.min(minSourceColumn, placeholder.sourceColumn());
maxSourceColumn = Math.max(maxSourceColumn, placeholder.sourceColumn());
}
private boolean isEmpty() {
return minRow == Integer.MAX_VALUE;
}
private int rows() {
return maxRow - minRow + 1;
}
private int columns() {
return maxColumn - minColumn + 1;
}
private long sourceRows() {
return maxSourceRow - minSourceRow + 1;
}
private long sourceColumns() {
return maxSourceColumn - minSourceColumn + 1;
}
}
private static final class PaneRenderCache {
private Canvas canvas;
private WritableImage image;
private int imageWidth;
private int imageHeight;
private String key;
}
} }

View File

@@ -30,6 +30,7 @@ public final class TerminalPane implements AutoCloseable {
private int rows; private int rows;
private int pixelWidth; private int pixelWidth;
private int pixelHeight; private int pixelHeight;
private long renderVersion;
private TerminalPane(Terminal terminal, int columns, int rows) { private TerminalPane(Terminal terminal, int columns, int rows) {
this.terminal = terminal; this.terminal = terminal;
@@ -114,6 +115,10 @@ public final class TerminalPane implements AutoCloseable {
return renderSnapshot.get(); return renderSnapshot.get();
} }
public long renderVersion() {
return renderVersion;
}
public Optional<KittyGraphics> kittyGraphics() { public Optional<KittyGraphics> kittyGraphics() {
synchronized (terminal) { synchronized (terminal) {
return terminal.kittyGraphics(); return terminal.kittyGraphics();
@@ -182,6 +187,7 @@ public final class TerminalPane implements AutoCloseable {
private void refresh() { private void refresh() {
renderSnapshot.set(terminal.renderSnapshot()); renderSnapshot.set(terminal.renderSnapshot());
renderVersion++;
} }
@Override @Override

View File

@@ -9,6 +9,7 @@ public final class TerminalWorkspace implements AutoCloseable {
private final List<TerminalPane> panes = new ArrayList<>(); private final List<TerminalPane> panes = new ArrayList<>();
private int activeIndex; private int activeIndex;
private int hiddenFloatingFocusIndex = -1; private int hiddenFloatingFocusIndex = -1;
private long version;
public TerminalWorkspace(AppConfig config) { public TerminalWorkspace(AppConfig config) {
this.config = config; this.config = config;
@@ -38,10 +39,15 @@ public final class TerminalWorkspace implements AutoCloseable {
return activePane() == pane; return activePane() == pane;
} }
public long version() {
return version;
}
public void focus(TerminalPane pane) { public void focus(TerminalPane pane) {
int index = panes.indexOf(pane); int index = panes.indexOf(pane);
if (index >= 0 && pane.visible()) { if (index >= 0 && pane.visible() && activeIndex != index) {
activeIndex = index; activeIndex = index;
version++;
} }
} }
@@ -79,6 +85,7 @@ public final class TerminalWorkspace implements AutoCloseable {
public void navigate(Direction direction) { public void navigate(Direction direction) {
TerminalPane current = activePane(); TerminalPane current = activePane();
if (current.floating() && navigateFloatingStack(direction)) { if (current.floating() && navigateFloatingStack(direction)) {
version++;
return; return;
} }
@@ -87,7 +94,10 @@ public final class TerminalWorkspace implements AutoCloseable {
.filter(pane -> pane != current) .filter(pane -> pane != current)
.filter(pane -> directionFilter(direction, current, pane)) .filter(pane -> directionFilter(direction, current, pane))
.min(Comparator.comparingDouble(pane -> distance(current, pane))) .min(Comparator.comparingDouble(pane -> distance(current, pane)))
.ifPresent(pane -> activeIndex = panes.indexOf(pane)); .ifPresent(pane -> {
activeIndex = panes.indexOf(pane);
version++;
});
} }
public void toggleFloating() { public void toggleFloating() {
@@ -105,10 +115,12 @@ public final class TerminalWorkspace implements AutoCloseable {
hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex(); hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex();
floating.forEach(pane -> pane.setVisible(false)); floating.forEach(pane -> pane.setVisible(false));
activeIndex = firstVisibleNonFloatingIndex(); activeIndex = firstVisibleNonFloatingIndex();
version++;
} else { } else {
floating.forEach(pane -> pane.setVisible(true)); floating.forEach(pane -> pane.setVisible(true));
activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1))); activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1)));
hiddenFloatingFocusIndex = -1; hiddenFloatingFocusIndex = -1;
version++;
} }
} }
@@ -116,12 +128,14 @@ public final class TerminalWorkspace implements AutoCloseable {
TerminalPane pane = openPane(true); TerminalPane pane = openPane(true);
panes.add(pane); panes.add(pane);
activeIndex = panes.size() - 1; activeIndex = panes.size() - 1;
version++;
} }
public void nextFloatingPane() { public void nextFloatingPane() {
TerminalPane next = nextFloatingAfter(activeIndex); TerminalPane next = nextFloatingAfter(activeIndex);
next.setVisible(true); next.setVisible(true);
activeIndex = panes.indexOf(next); activeIndex = panes.indexOf(next);
version++;
} }
public void closeActivePane() { public void closeActivePane() {
@@ -136,6 +150,7 @@ public final class TerminalWorkspace implements AutoCloseable {
active.close(); active.close();
activeIndex = adjustIndexAfterRemoval(previous, removed); activeIndex = adjustIndexAfterRemoval(previous, removed);
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed); hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
version++;
} }
private TerminalPane nextFloatingAfter(int index) { private TerminalPane nextFloatingAfter(int index) {