diff --git a/src/main/java/com/gregor/jprototerm/GhosttyTerminalRenderer.java b/src/main/java/com/gregor/jprototerm/GhosttyTerminalRenderer.java index cbba25c..4cf7365 100644 --- a/src/main/java/com/gregor/jprototerm/GhosttyTerminalRenderer.java +++ b/src/main/java/com/gregor/jprototerm/GhosttyTerminalRenderer.java @@ -51,8 +51,6 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { private final TerminalMetrics metrics; // Decoded kitty images for this renderer's pane (kitty graphics state is per-terminal). private final Map kittyImageCache = new HashMap<>(); - private long[] rowHashes = new long[0]; - private CursorState lastCursor = CursorState.none(); GhosttyTerminalRenderer(TerminalMetrics metrics) { this.metrics = metrics; @@ -66,10 +64,8 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { double height = target.height(); gc.save(); clip(gc, px, py, width, height, target.clip()); - RenderStateSnapshot snapshot = target.snapshotFull(); - drawContent(gc, target, snapshot, px, py, width, height, active, + drawContent(gc, target, target.snapshotFull(), px, py, width, height, active, target.kittyEnabled() && hasKittyGraphics(target)); - rememberSnapshot(snapshot); gc.restore(); } @@ -83,18 +79,12 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { clip(gc, px, py, width, height, target.clip()); if (target.kittyEnabled() && hasKittyGraphics(target)) { // Kitty placements can move without a per-row dirty flag, so always redraw whole. - RenderStateSnapshot snapshot = target.snapshotFull(); - drawContent(gc, target, snapshot, px, py, width, height, active, true); - rememberSnapshot(snapshot); + drawContent(gc, target, target.snapshotFull(), px, py, width, height, active, true); } else { RenderStateSnapshot snapshot = target.snapshot(); int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty(); if (dirty == DIRTY_FULL) { - if (!drawChangedRows(gc, snapshot, px, py, width, height, active)) { - RenderStateSnapshot fullSnapshot = target.snapshotFull(); - drawContent(gc, target, fullSnapshot, px, py, width, height, active, false); - rememberSnapshot(fullSnapshot); - } + drawContent(gc, target, snapshot, px, py, width, height, active, false); } else if (dirty == DIRTY_PARTIAL) { drawDirtyRows(gc, snapshot, px, py, width, height, active); } @@ -103,86 +93,6 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { gc.restore(); } - // Some TUIs repaint the whole viewport for small logical changes. When ghostty gives us - // a full snapshot, compare row content with what we last painted and only redraw rows - // whose cells changed, plus old/new cursor rows because the cursor is an overlay. - private boolean drawChangedRows( - GraphicsContext gc, - RenderStateSnapshot snapshot, - double px, - double py, - double pw, - double ph, - boolean active - ) { - if (snapshot == null || rowHashes.length != snapshot.rows() || snapshot.renderRows().size() != snapshot.rows()) { - return false; - } - - double cellWidth = metrics.cellWidth(); - double lineHeight = metrics.lineHeight(); - gc.setFontSmoothingType(FontSmoothingType.LCD); - gc.setFont(metrics.font()); - double left = px + TerminalMetrics.PADDING; - double top = py + TerminalMetrics.PADDING; - double baseline = top + metrics.baselineOffset(); - double contentBottom = top + snapshot.rows() * lineHeight; - int lastRow = snapshot.rows() - 1; - - CursorState cursor = CursorState.from(snapshot); - long oldCursorRow = lastCursor.viewportRow(); - long newCursorRow = cursor.viewportRow(); - boolean cursorChanged = !cursor.equals(lastCursor); - double bandMin = Double.POSITIVE_INFINITY; - double bandMax = Double.NEGATIVE_INFINITY; - - for (RenderRow row : snapshot.renderRows()) { - int rowIndex = row.row(); - if (rowIndex < 0 || rowIndex >= rowHashes.length) { - return false; - } - - long hash = rowHash(row); - boolean repaint = hash != rowHashes[rowIndex] - || (cursorChanged && (rowIndex == oldCursorRow || rowIndex == newCursorRow)); - if (!repaint) { - continue; - } - - double y0 = Math.floor(top + (rowIndex * lineHeight)); - double y1 = Math.ceil(top + ((rowIndex + 1) * lineHeight)); - gc.setFill(PANE_BACKGROUND); - gc.fillRect(px, y0, pw, y1 - y0); - paintSidePadding(gc, row, px, pw, left, cellWidth, y0, y1 - y0); - drawRow(gc, row, left, top, baseline, cellWidth, lineHeight); - rowHashes[rowIndex] = hash; - bandMin = Math.min(bandMin, y0); - bandMax = Math.max(bandMax, y1); - - if (rowIndex == 0) { - gc.setFill(rowEdgeBackground(row, true)); - gc.fillRect(px, py, pw, top - py); - bandMin = Math.min(bandMin, py); - } - if (rowIndex == lastRow) { - gc.setFill(rowEdgeBackground(row, true)); - gc.fillRect(px, contentBottom, pw, py + ph - contentBottom); - bandMax = Math.max(bandMax, py + ph); - } - } - - lastCursor = cursor; - if (bandMin > bandMax) { - return true; - } - drawCursor(gc, snapshot, left, top, cellWidth, lineHeight); - gc.save(); - clipRect(gc, px, bandMin, pw, bandMax - bandMin); - drawBorder(gc, px, py, pw, ph, active); - gc.restore(); - return true; - } - // Full content render: background, border, all rows, cursor, and (when enabled) kitty // graphics. Used by the kitty direct path and by full redraws. private void drawContent( @@ -280,7 +190,6 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { } paintSidePadding(gc, row, px, pw, left, cellWidth, y0, y1 - y0); drawRow(gc, row, left, top, baseline, cellWidth, lineHeight); - rememberRow(row); bandMin = Math.min(bandMin, y0); bandMax = Math.max(bandMax, y1); // Edge rows also own the top/bottom padding strip; repaint it and extend the @@ -308,7 +217,6 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { if (cursorRowDirty) { drawCursor(gc, snapshot, left, top, cellWidth, lineHeight); } - lastCursor = CursorState.from(snapshot); // Repainting rows clears the side borders within the band; restore just those // segments, clipped to the band so we don't redraw the whole outline. gc.save(); @@ -336,62 +244,6 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { gc.strokeRect(x + 0.5, y + 0.5, width - 1.0, height - 1.0); } - private void rememberSnapshot(RenderStateSnapshot snapshot) { - if (snapshot == null) { - rowHashes = new long[0]; - lastCursor = CursorState.none(); - return; - } - if (rowHashes.length != snapshot.rows()) { - rowHashes = new long[snapshot.rows()]; - } - for (RenderRow row : snapshot.renderRows()) { - rememberRow(row); - } - lastCursor = CursorState.from(snapshot); - } - - private void rememberRow(RenderRow row) { - if (row.row() >= 0 && row.row() < rowHashes.length) { - rowHashes[row.row()] = rowHash(row); - } - } - - private static long rowHash(RenderRow row) { - long hash = 0xcbf29ce484222325L; - hash = mix(hash, row.row()); - 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 = mixColor(hash, cell.foreground().orElse(null)); - hash = mixColor(hash, 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; - } - - private static long mixColor(long hash, RenderColor color) { - if (color == null) { - return mix(hash, -1); - } - return mix(hash, (color.red() << 16) | (color.green() << 8) | color.blue()); - } - - private static long mix(long hash, long value) { - hash ^= value; - return hash * 0x100000001b3L; - } - // Effective background colour of a cell as it is drawn (reverse video swaps fg/bg, an // unset colour falls back to the defaults). private static Color cellBackgroundColor(RenderCell cell) { @@ -823,29 +675,6 @@ final class GhosttyTerminalRenderer extends TerminalRenderer { private record SourceRect(double x, double y, double width, double height) { } - private record CursorState(boolean visible, boolean hasViewport, long viewportX, long viewportY, RenderCursorStyle style) { - private static CursorState none() { - return new CursorState(false, false, -1, -1, null); - } - - private static CursorState from(RenderStateSnapshot snapshot) { - if (snapshot == null) { - return none(); - } - boolean hasViewport = snapshot.cursorViewportHasValue(); - return new CursorState( - snapshot.cursorVisible(), - hasViewport, - hasViewport ? snapshot.cursorViewportX() : -1, - hasViewport ? snapshot.cursorViewportY() : -1, - snapshot.cursorStyle()); - } - - private long viewportRow() { - return visible && hasViewport ? viewportY : -1; - } - } - private static final class KittyPlaceholderBounds { private int minRow = Integer.MAX_VALUE; private int maxRow = Integer.MIN_VALUE;