diff --git a/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java b/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java index 7b91015..cfbc1ed 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPaneNode.java @@ -198,7 +198,7 @@ final class TerminalPaneNode extends Region { } TerminalRowNode node = rowNode(row.row()); long fingerprint = rowFingerprint(row); - node.render(row); + node.renderChanged(row); rowFingerprints.put(row.row(), fingerprint); } for (RenderRow row : changedRows) { @@ -642,19 +642,27 @@ final class TerminalPaneNode extends Region { long hash = 0xcbf29ce484222325L; hash = mix(hash, row.cells().size()); for (RenderCell cell : row.cells()) { - hash = mix(hash, cell.column()); - hash = mix(hash, cell.inverse() ? 1 : 0); - hash = mix(hash, cell.selected() ? 1 : 0); - hash = mix(hash, colorFingerprint(cell.foreground().orElse(null))); - hash = mix(hash, colorFingerprint(cell.background().orElse(null))); - hash = mix(hash, cell.text().hashCode()); - if (cell.kittyPlaceholder().isPresent()) { - KittyPlaceholder placeholder = cell.kittyPlaceholder().get(); - hash = mix(hash, placeholder.imageId()); - hash = mix(hash, placeholder.placementId()); - hash = mix(hash, placeholder.sourceRow()); - hash = mix(hash, placeholder.sourceColumn()); - } + hash = mix(hash, cellFingerprint(cell)); + } + return hash; + } + + private static long cellFingerprint(RenderCell cell) { + long hash = 0xcbf29ce484222325L; + hash = mix(hash, cell.column()); + hash = mix(hash, cell.inverse() ? 1 : 0); + hash = mix(hash, cell.selected() ? 1 : 0); + hash = mix(hash, colorFingerprint(cell.foreground().orElse(null))); + hash = mix(hash, colorFingerprint(cell.background().orElse(null))); + for (int codepoint : cell.codepoints()) { + hash = mix(hash, codepoint); + } + if (cell.kittyPlaceholder().isPresent()) { + KittyPlaceholder placeholder = cell.kittyPlaceholder().get(); + hash = mix(hash, placeholder.imageId()); + hash = mix(hash, placeholder.placementId()); + hash = mix(hash, placeholder.sourceRow()); + hash = mix(hash, placeholder.sourceColumn()); } return hash; } @@ -674,6 +682,7 @@ final class TerminalPaneNode extends Region { private static final class TerminalRowNode extends Region { private final TerminalMetrics metrics; private final Canvas canvas = new Canvas(); + private long[] cellFingerprints = new long[0]; private TerminalRowNode(TerminalMetrics metrics) { this.metrics = metrics; @@ -681,38 +690,125 @@ final class TerminalPaneNode extends Region { } private void render(RenderRow row) { + prepareCanvas(row); + + GraphicsContext gc = canvas.getGraphicsContext2D(); + gc.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight()); + gc.setFontSmoothingType(FontSmoothingType.LCD); + gc.setFont(metrics.font()); + + paintSidePadding(gc, row, canvas.getWidth(), canvas.getHeight()); + drawRow(gc, row, rowTop(row), metrics.cellWidth(), metrics.lineHeight()); + cellFingerprints = cellFingerprints(row); + } + + private void renderChanged(RenderRow row) { + double oldWidth = canvas.getWidth(); + double oldHeight = canvas.getHeight(); + prepareCanvas(row); + long[] nextFingerprints = cellFingerprints(row); + if (cellFingerprints.length != nextFingerprints.length + || oldWidth != canvas.getWidth() + || oldHeight != canvas.getHeight()) { + render(row); + return; + } + + GraphicsContext gc = canvas.getGraphicsContext2D(); + gc.setFontSmoothingType(FontSmoothingType.LCD); + gc.setFont(metrics.font()); + + int runStart = -1; + int runEnd = -1; + for (int column = 0; column < nextFingerprints.length; column++) { + if (cellFingerprints[column] == nextFingerprints[column]) { + continue; + } + + int start = Math.max(0, column - 1); + int end = Math.min(nextFingerprints.length - 1, column + 1); + if (runStart < 0) { + runStart = start; + runEnd = end; + } else if (start <= runEnd + 1) { + runEnd = Math.max(runEnd, end); + } else { + repaintColumns(gc, row, runStart, runEnd); + runStart = start; + runEnd = end; + } + } + if (runStart >= 0) { + repaintColumns(gc, row, runStart, runEnd); + } + cellFingerprints = nextFingerprints; + } + + private void prepareCanvas(RenderRow row) { double paneWidth = ((Region) getParent()).getWidth(); - double top = TerminalMetrics.PADDING; - double cellWidth = metrics.cellWidth(); - double lineHeight = metrics.lineHeight(); - double rowTop = Math.floor(top + row.row() * lineHeight); - double rowBottom = Math.ceil(top + (row.row() + 1) * lineHeight); + double rowTop = rowTop(row); + double rowBottom = rowBottom(row); double rowHeight = Math.max(1.0, rowBottom - rowTop); resizeRelocate(0.0, rowTop, paneWidth, rowHeight); canvas.setWidth(Math.max(0.0, paneWidth)); canvas.setHeight(rowHeight); - - GraphicsContext gc = canvas.getGraphicsContext2D(); - gc.clearRect(0.0, 0.0, paneWidth, rowHeight); - gc.setFontSmoothingType(FontSmoothingType.LCD); - gc.setFont(metrics.font()); - - paintSidePadding(gc, row, paneWidth, rowHeight); - drawRow(gc, row, rowTop, cellWidth, lineHeight); } private void moveToRow(int row) { double paneWidth = ((Region) getParent()).getWidth(); - double top = TerminalMetrics.PADDING; - double lineHeight = metrics.lineHeight(); - double rowTop = Math.floor(top + row * lineHeight); - double rowBottom = Math.ceil(top + (row + 1) * lineHeight); + double rowTop = rowTop(row); + double rowBottom = rowBottom(row); double rowHeight = Math.max(1.0, rowBottom - rowTop); resizeRelocate(0.0, rowTop, paneWidth, rowHeight); canvas.setWidth(Math.max(0.0, paneWidth)); canvas.setHeight(rowHeight); } + private double rowTop(RenderRow row) { + return rowTop(row.row()); + } + + private double rowTop(int row) { + return Math.floor(TerminalMetrics.PADDING + row * metrics.lineHeight()); + } + + private double rowBottom(RenderRow row) { + return rowBottom(row.row()); + } + + private double rowBottom(int row) { + return Math.ceil(TerminalMetrics.PADDING + (row + 1) * metrics.lineHeight()); + } + + private void repaintColumns(GraphicsContext gc, RenderRow row, int startColumn, int endColumn) { + if (endColumn < startColumn) { + return; + } + + double cellWidth = metrics.cellWidth(); + double lineHeight = metrics.lineHeight(); + double rowTop = rowTop(row); + double contentTop = TerminalMetrics.PADDING + row.row() * lineHeight; + double localCellTop = contentTop - rowTop; + double baseline = TerminalMetrics.PADDING + metrics.baselineOffset() + row.row() * lineHeight - rowTop; + double x = TerminalMetrics.PADDING + startColumn * cellWidth; + double width = (endColumn - startColumn + 1) * cellWidth; + + gc.clearRect(x, 0.0, width, canvas.getHeight()); + if (startColumn == 0) { + gc.setFill(rowEdgeBackground(row, true)); + gc.fillRect(0.0, 0.0, TerminalMetrics.PADDING, canvas.getHeight()); + } + if (endColumn >= row.cells().size() - 1) { + double contentRight = TerminalMetrics.PADDING + row.cells().size() * cellWidth; + gc.setFill(rowEdgeBackground(row, false)); + gc.fillRect(contentRight, 0.0, canvas.getWidth() - contentRight, canvas.getHeight()); + } + + drawRowBackgrounds(gc, row, localCellTop, cellWidth, lineHeight, startColumn, endColumn); + drawRowText(gc, row, baseline, cellWidth, startColumn, endColumn); + } + private void paintSidePadding(GraphicsContext gc, RenderRow row, double paneWidth, double bandHeight) { int columns = row.cells().size(); if (columns == 0) { @@ -730,15 +826,19 @@ final class TerminalPaneNode extends Region { double contentTop = TerminalMetrics.PADDING + row.row() * lineHeight; double localCellTop = contentTop - rowTop; double baseline = TerminalMetrics.PADDING + metrics.baselineOffset() + row.row() * lineHeight - rowTop; - drawRowBackgrounds(gc, row, localCellTop, cellWidth, lineHeight); - drawRowText(gc, row, baseline, cellWidth); + drawRowBackgrounds(gc, row, localCellTop, cellWidth, lineHeight, 0, row.cells().size() - 1); + drawRowText(gc, row, baseline, cellWidth, 0, row.cells().size() - 1); } - private void drawRowBackgrounds(GraphicsContext gc, RenderRow row, double localCellTop, double cellWidth, double lineHeight) { + private void drawRowBackgrounds(GraphicsContext gc, RenderRow row, double localCellTop, + double cellWidth, double lineHeight, int startColumn, int endColumn) { Color runBackground = null; int runStartColumn = 0; int previousColumn = -1; for (RenderCell cell : row.cells()) { + if (cell.column() < startColumn || cell.column() > endColumn) { + continue; + } if (cell.kittyPlaceholder().isPresent()) { flushBackgroundRun(gc, runBackground, localCellTop, cellWidth, lineHeight, runStartColumn, previousColumn); runBackground = null; @@ -777,8 +877,12 @@ final class TerminalPaneNode extends Region { lineHeight); } - private void drawRowText(GraphicsContext gc, RenderRow row, double baseline, double cellWidth) { + private void drawRowText(GraphicsContext gc, RenderRow row, double baseline, + double cellWidth, int startColumn, int endColumn) { for (RenderCell cell : row.cells()) { + if (cell.column() < startColumn || cell.column() > endColumn) { + continue; + } if (cell.kittyPlaceholder().isPresent() || cell.codepoints().length == 0) { continue; } @@ -787,6 +891,19 @@ final class TerminalPaneNode extends Region { gc.fillText(cell.text(), TerminalMetrics.PADDING + cell.column() * cellWidth, baseline); } } + + private static long[] cellFingerprints(RenderRow row) { + int columns = row.cells().size(); + for (RenderCell cell : row.cells()) { + columns = Math.max(columns, cell.column() + 1); + } + + long[] fingerprints = new long[columns]; + for (RenderCell cell : row.cells()) { + fingerprints[cell.column()] = cellFingerprint(cell); + } + return fingerprints; + } } private record KittyImageKey(long id, long number, long width, long height, KittyImageFormat format, int dataLength) {