diff --git a/.gradle/9.4.1/checksums/checksums.lock b/.gradle/9.4.1/checksums/checksums.lock index f74a924..4103b66 100644 Binary files a/.gradle/9.4.1/checksums/checksums.lock and b/.gradle/9.4.1/checksums/checksums.lock differ diff --git a/.gradle/9.4.1/checksums/md5-checksums.bin b/.gradle/9.4.1/checksums/md5-checksums.bin index 9f1f3c5..4c6d967 100644 Binary files a/.gradle/9.4.1/checksums/md5-checksums.bin and b/.gradle/9.4.1/checksums/md5-checksums.bin differ diff --git a/.gradle/9.4.1/checksums/sha1-checksums.bin b/.gradle/9.4.1/checksums/sha1-checksums.bin index 6f9cee4..0932586 100644 Binary files a/.gradle/9.4.1/checksums/sha1-checksums.bin and b/.gradle/9.4.1/checksums/sha1-checksums.bin differ diff --git a/.gradle/9.4.1/executionHistory/executionHistory.bin b/.gradle/9.4.1/executionHistory/executionHistory.bin index f5d8b1d..098edc2 100644 Binary files a/.gradle/9.4.1/executionHistory/executionHistory.bin and b/.gradle/9.4.1/executionHistory/executionHistory.bin differ diff --git a/.gradle/9.4.1/executionHistory/executionHistory.lock b/.gradle/9.4.1/executionHistory/executionHistory.lock index 181a746..f86f1a2 100644 Binary files a/.gradle/9.4.1/executionHistory/executionHistory.lock and b/.gradle/9.4.1/executionHistory/executionHistory.lock differ diff --git a/.gradle/9.4.1/fileHashes/fileHashes.bin b/.gradle/9.4.1/fileHashes/fileHashes.bin index 75da2f8..d4cbe1e 100644 Binary files a/.gradle/9.4.1/fileHashes/fileHashes.bin and b/.gradle/9.4.1/fileHashes/fileHashes.bin differ diff --git a/.gradle/9.4.1/fileHashes/fileHashes.lock b/.gradle/9.4.1/fileHashes/fileHashes.lock index 682cde6..9266c10 100644 Binary files a/.gradle/9.4.1/fileHashes/fileHashes.lock and b/.gradle/9.4.1/fileHashes/fileHashes.lock differ diff --git a/.gradle/9.4.1/fileHashes/resourceHashesCache.bin b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin index 45933e9..58d1c73 100644 Binary files a/.gradle/9.4.1/fileHashes/resourceHashesCache.bin and b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 5b7c9de..6e820d0 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java b/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java index 80d84cb..3e0f0cb 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java +++ b/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java @@ -4,6 +4,8 @@ import dev.jlibghostty.KittyImageCompression; import dev.jlibghostty.KittyImageFormat; import dev.jlibghostty.KittyImageSnapshot; import dev.jlibghostty.KittyPlacement; +import dev.jlibghostty.KittyPlacementLayer; +import dev.jlibghostty.KittyPlaceholder; import dev.jlibghostty.KittyRenderInfo; import dev.jlibghostty.KeyModifiers; import dev.jlibghostty.MouseButton; @@ -30,6 +32,7 @@ import javafx.scene.text.Text; import java.io.ByteArrayInputStream; import java.util.HashMap; +import java.util.List; import java.util.Map; public final class TerminalCanvasView { @@ -39,9 +42,15 @@ public final class TerminalCanvasView { private final Canvas canvas = new Canvas(); private final TerminalWorkspace workspace; private final AppConfig config; - private final Map kittyImageCache = new HashMap<>(); + private final Map kittyImageCache = new HashMap<>(); + private final Map paneRenderCache = new HashMap<>(); private String fontFamily; private double fontSize; + private Font cachedFont; + private FontMetrics cachedMetrics; + private String cachedFontFamily; + private double cachedFontSize; + private String lastRenderKey; private boolean mouseButtonPressed; private MouseButton pressedButton = MouseButton.UNKNOWN; @@ -65,53 +74,111 @@ public final class TerminalCanvasView { public void setFont(String family, double size) { this.fontFamily = family; this.fontSize = size; + cachedFont = null; + cachedMetrics = null; + paneRenderCache.clear(); + lastRenderKey = null; } public void render() { double width = canvas.getWidth(); double height = canvas.getHeight(); workspace.layout(width, height); + Font font = currentFont(); + FontMetrics metrics = currentFontMetrics(); + List panes = workspace.panes(); + + String renderKey = renderKey(width, height, metrics, panes); + if (renderKey.equals(lastRenderKey)) { + return; + } + lastRenderKey = renderKey; GraphicsContext gc = canvas.getGraphicsContext2D(); gc.setFill(Color.rgb(16, 16, 18)); gc.fillRect(0, 0, width, height); gc.setFontSmoothingType(FontSmoothingType.LCD); - for (TerminalPane pane : workspace.panes()) { - drawPane(gc, pane); + paneRenderCache.keySet().removeIf(pane -> !panes.contains(pane)); + for (TerminalPane pane : panes) { + drawPane(gc, pane, font, metrics); } } - private void drawPane(GraphicsContext gc, TerminalPane pane) { - gc.save(); - gc.beginPath(); - gc.rect(pane.x(), pane.y(), pane.width(), pane.height()); - gc.clip(); + private void drawPane(GraphicsContext gc, TerminalPane pane, Font font, FontMetrics metrics) { + if (config.kittyGraphics() && paneHasKittyGraphics(pane)) { + paneRenderCache.remove(pane); + gc.save(); + 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()) { gc.setGlobalAlpha(0.96); } 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.setStroke(workspace.isActive(pane) ? Color.rgb(87, 166, 255) : Color.rgb(52, 57, 65)); 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); - FontMetrics metrics = measureFontMetrics(font); - 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 columns = Math.max(1, (int) ((width - 24.0) / metrics.cellWidth)); + int rows = Math.max(1, (int) ((height - 24.0) / metrics.lineHeight)); pane.resize(columns, rows, (int) Math.round(metrics.cellWidth), (int) Math.round(metrics.lineHeight)); - double left = pane.x() + 12.0; - double top = pane.y() + 12.0; + double left = x + 12.0; + double top = y + 12.0; double baseline = top + metrics.baselineOffset; RenderStateSnapshot snapshot = pane.renderSnapshot(); + Map 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) { for (RenderRow row : snapshot.renderRows()) { drawRow(gc, row, left, top, baseline, metrics.cellWidth, metrics.lineHeight); @@ -123,9 +190,8 @@ public final class TerminalCanvasView { } 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) { @@ -134,13 +200,80 @@ public final class TerminalCanvasView { double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight()); double baselineOffset = -text.getLayoutBounds().getMinY(); - String sample = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - Text cell = new Text(sample); + Text cell = new Text("M"); 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); } + 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 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 kittyPlaceholderBounds(RenderStateSnapshot snapshot) { + if (snapshot == null) { + return Map.of(); + } + + Map 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) { canvas.requestFocus(); TerminalPane pane = paneAt(event.getX(), event.getY()); @@ -244,7 +377,7 @@ public final class TerminalCanvasView { 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 rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight)); long cellWidth = Math.max(1L, Math.round(metrics.cellWidth)); @@ -325,15 +458,19 @@ public final class TerminalCanvasView { double lineHeight ) { for (RenderCell cell : row.cells()) { + if (cell.kittyPlaceholder().isPresent()) { + continue; + } + double x = left + (cell.column() * cellWidth); double cellTop = top + (row.row() * lineHeight); cell.background().ifPresent(background -> { gc.setFill(toFxColor(background)); - fillCellRect(gc, x, cellTop, cellWidth, lineHeight); + gc.fillRect(x, cellTop, cellWidth, lineHeight); }); if (cell.selected()) { gc.setFill(SELECTED_BACKGROUND); - fillCellRect(gc, x, cellTop, cellWidth, lineHeight); + gc.fillRect(x, cellTop, cellWidth, lineHeight); } if (cell.codepoints().length == 0) { 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) { 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 placeholderBounds, + double originX, + double originY, + double cellWidth, + double lineHeight + ) { pane.kittyGraphics().ifPresent(graphics -> { - for (KittyPlacement placement : graphics.placements()) { + for (KittyPlacement placement : graphics.placements(layer)) { Image image = imageFor(placement); if (image == null) { continue; } - KittyRenderInfo renderInfo = placement.renderInfo().orElse(null); - double x = originX; - 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(); + if (placement.virtual()) { + drawVirtualKittyPlacement(gc, placement, image, placeholderBounds, originX, originY, cellWidth, lineHeight); } else { - width = placement.columns() > 0 ? placement.columns() * cellWidth : width; - height = placement.rows() > 0 ? placement.rows() * lineHeight : height; + drawPinnedKittyPlacement(gc, placement, image, originX, originY, cellWidth, lineHeight); } - - gc.drawImage(image, x + placement.xOffset(), y + placement.yOffset(), width, height); } }); } - private Image imageFor(KittyPlacement placement) { - return placement.image() - .map(snapshot -> kittyImageCache.computeIfAbsent(snapshot.id(), ignored -> decodeImage(snapshot))) - .orElse(null); + private static void drawPinnedKittyPlacement( + GraphicsContext gc, + KittyPlacement placement, + 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 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) { return null; } 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 height = Math.toIntExact(snapshot.height()); WritableImage image = new WritableImage(width, height); - byte[] data = snapshot.data(); if (snapshot.format() == KittyImageFormat.RGBA) { 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 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; + } } diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java index f6e179d..3166f10 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPane.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java @@ -30,6 +30,7 @@ public final class TerminalPane implements AutoCloseable { private int rows; private int pixelWidth; private int pixelHeight; + private long renderVersion; private TerminalPane(Terminal terminal, int columns, int rows) { this.terminal = terminal; @@ -114,6 +115,10 @@ public final class TerminalPane implements AutoCloseable { return renderSnapshot.get(); } + public long renderVersion() { + return renderVersion; + } + public Optional kittyGraphics() { synchronized (terminal) { return terminal.kittyGraphics(); @@ -182,6 +187,7 @@ public final class TerminalPane implements AutoCloseable { private void refresh() { renderSnapshot.set(terminal.renderSnapshot()); + renderVersion++; } @Override diff --git a/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java b/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java index e0ad305..072fc6c 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java @@ -9,6 +9,7 @@ public final class TerminalWorkspace implements AutoCloseable { private final List panes = new ArrayList<>(); private int activeIndex; private int hiddenFloatingFocusIndex = -1; + private long version; public TerminalWorkspace(AppConfig config) { this.config = config; @@ -38,10 +39,15 @@ public final class TerminalWorkspace implements AutoCloseable { return activePane() == pane; } + public long version() { + return version; + } + public void focus(TerminalPane pane) { int index = panes.indexOf(pane); - if (index >= 0 && pane.visible()) { + if (index >= 0 && pane.visible() && activeIndex != index) { activeIndex = index; + version++; } } @@ -79,6 +85,7 @@ public final class TerminalWorkspace implements AutoCloseable { public void navigate(Direction direction) { TerminalPane current = activePane(); if (current.floating() && navigateFloatingStack(direction)) { + version++; return; } @@ -87,7 +94,10 @@ public final class TerminalWorkspace implements AutoCloseable { .filter(pane -> pane != current) .filter(pane -> directionFilter(direction, 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() { @@ -105,10 +115,12 @@ public final class TerminalWorkspace implements AutoCloseable { hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex(); floating.forEach(pane -> pane.setVisible(false)); activeIndex = firstVisibleNonFloatingIndex(); + version++; } else { floating.forEach(pane -> pane.setVisible(true)); activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1))); hiddenFloatingFocusIndex = -1; + version++; } } @@ -116,12 +128,14 @@ public final class TerminalWorkspace implements AutoCloseable { TerminalPane pane = openPane(true); panes.add(pane); activeIndex = panes.size() - 1; + version++; } public void nextFloatingPane() { TerminalPane next = nextFloatingAfter(activeIndex); next.setVisible(true); activeIndex = panes.indexOf(next); + version++; } public void closeActivePane() { @@ -136,6 +150,7 @@ public final class TerminalWorkspace implements AutoCloseable { active.close(); activeIndex = adjustIndexAfterRemoval(previous, removed); hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed); + version++; } private TerminalPane nextFloatingAfter(int index) {