Compare commits
18 Commits
scene-grap
...
codex-perf
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ac07218fe | |||
| 6bf69e8572 | |||
| 07585a314c | |||
| bdb33450f1 | |||
|
|
2c020bb6cb | ||
|
|
71a533ec34 | ||
|
|
54b08c7eca | ||
|
|
2fcdb286af | ||
|
|
e6848ec684 | ||
|
|
38822d66b8 | ||
|
|
586150de59 | ||
|
|
494d2c40cf | ||
|
|
a99cbdc61a | ||
|
|
86f7174eee | ||
|
|
137db24023 | ||
|
|
d8faf8d6df | ||
|
|
9903e9174f | ||
|
|
9b7247a4e0 |
@@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(/home/anon
|
|||||||
connection.project.dir=
|
connection.project.dir=
|
||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
gradle.user.home=
|
gradle.user.home=
|
||||||
java.home=/nix/store/c3pl7bqrx3d2rc3dh98z6yaj0mv1p52g-openjdk-21.0.10+7/lib/openjdk
|
java.home=/home/anon/.local/lib/graalvm-jdk-21.0.9+7.1
|
||||||
jvm.arguments=
|
jvm.arguments=
|
||||||
offline.mode=false
|
offline.mode=false
|
||||||
override.workspace.settings=true
|
override.workspace.settings=true
|
||||||
|
|||||||
8
flake.lock
generated
8
flake.lock
generated
@@ -70,11 +70,11 @@
|
|||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780079529,
|
"lastModified": 1780258814,
|
||||||
"narHash": "sha256-AxlGTL8c5xSLcQHvWlm994IdOqxsN8iKrA02Cpv7vso=",
|
"narHash": "sha256-8rxL7xaZ/loYg3zdt0w5+hfNyHFVknDZN360NzrtCsQ=",
|
||||||
"ref": "refs/heads/main",
|
"ref": "refs/heads/main",
|
||||||
"rev": "68121d50b52fb56038871c97c97e7a12ffe987c2",
|
"rev": "6a3d5aa0b0b1f738c958e2a2f0249574c07d9c4d",
|
||||||
"revCount": 20,
|
"revCount": 23,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
|
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -244,8 +244,7 @@ public final class Compositor {
|
|||||||
drawTabBar(gc, canvas.getWidth(), topInset);
|
drawTabBar(gc, canvas.getWidth(), topInset);
|
||||||
}
|
}
|
||||||
for (TerminalPane pane : panes) {
|
for (TerminalPane pane : panes) {
|
||||||
pane.paintFull(gc, isActive(pane));
|
paneContentVersion.put(pane, pane.paintFull(gc, isActive(pane)));
|
||||||
paneContentVersion.put(pane, pane.contentVersion());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +261,7 @@ public final class Compositor {
|
|||||||
if (drawn != null && drawn == pane.contentVersion()) {
|
if (drawn != null && drawn == pane.contentVersion()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pane.paintIncremental(gc, isActive(pane));
|
paneContentVersion.put(pane, pane.paintIncremental(gc, isActive(pane)));
|
||||||
paneContentVersion.put(pane, pane.contentVersion());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +382,10 @@ public final class Compositor {
|
|||||||
double ey = localY(event.getY(), pane, target);
|
double ey = localY(event.getY(), pane, target);
|
||||||
KeyModifiers modifiers = modifiers(event);
|
KeyModifiers modifiers = modifiers(event);
|
||||||
for (int i = 0; i < rows; i++) {
|
for (int i = 0; i < rows; i++) {
|
||||||
sent |= send(pane, target, MouseInput.press(wheelButton, ex, ey, modifiers), mouseButtonPressed, event);
|
if (!send(pane, target, MouseInput.press(wheelButton, ex, ey, modifiers), mouseButtonPressed, event)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
|
|||||||
@@ -12,14 +12,22 @@ import dev.jlibghostty.RenderColor;
|
|||||||
import dev.jlibghostty.RenderCursorStyle;
|
import dev.jlibghostty.RenderCursorStyle;
|
||||||
import dev.jlibghostty.RenderRow;
|
import dev.jlibghostty.RenderRow;
|
||||||
import dev.jlibghostty.RenderStateSnapshot;
|
import dev.jlibghostty.RenderStateSnapshot;
|
||||||
|
import javafx.geometry.Rectangle2D;
|
||||||
|
import javafx.scene.SnapshotParameters;
|
||||||
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.PixelFormat;
|
import javafx.scene.image.PixelFormat;
|
||||||
|
import javafx.scene.image.PixelBuffer;
|
||||||
|
import javafx.scene.image.PixelReader;
|
||||||
import javafx.scene.image.WritableImage;
|
import javafx.scene.image.WritableImage;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.text.FontSmoothingType;
|
import javafx.scene.text.FontSmoothingType;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -39,6 +47,9 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
// The default cell background (used for cells with no explicit bg, and as the foreground
|
// The default cell background (used for cells with no explicit bg, and as the foreground
|
||||||
// for reverse-video cells whose background is the terminal default).
|
// for reverse-video cells whose background is the terminal default).
|
||||||
private static final Color PANE_BACKGROUND = Color.rgb(9, 10, 12);
|
private static final Color PANE_BACKGROUND = Color.rgb(9, 10, 12);
|
||||||
|
private static final Color ACTIVE_BORDER = Color.rgb(87, 166, 255);
|
||||||
|
private static final Color INACTIVE_BORDER = Color.rgb(52, 57, 65);
|
||||||
|
private static final Color CURSOR_FILL = Color.rgb(225, 229, 235, 0.28);
|
||||||
|
|
||||||
// A full-screen redraw asks for one Color per cell; most cells share a handful of colors,
|
// A full-screen redraw asks for one Color per cell; most cells share a handful of colors,
|
||||||
// so cache them by packed RGB instead of allocating a Color each time. Bounded so a
|
// so cache them by packed RGB instead of allocating a Color each time. Bounded so a
|
||||||
@@ -48,6 +59,7 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
private final TerminalMetrics metrics;
|
private final TerminalMetrics metrics;
|
||||||
// Decoded kitty images for this renderer's pane (kitty graphics state is per-terminal).
|
// Decoded kitty images for this renderer's pane (kitty graphics state is per-terminal).
|
||||||
private final Map<KittyImageKey, Image> kittyImageCache = new HashMap<>();
|
private final Map<KittyImageKey, Image> kittyImageCache = new HashMap<>();
|
||||||
|
private final SoftwareBackbuffer software = new SoftwareBackbuffer();
|
||||||
|
|
||||||
GhosttyTerminalRenderer(TerminalMetrics metrics) {
|
GhosttyTerminalRenderer(TerminalMetrics metrics) {
|
||||||
this.metrics = metrics;
|
this.metrics = metrics;
|
||||||
@@ -61,8 +73,14 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
double height = target.height();
|
double height = target.height();
|
||||||
gc.save();
|
gc.save();
|
||||||
clip(gc, px, py, width, height, target.clip());
|
clip(gc, px, py, width, height, target.clip());
|
||||||
drawContent(gc, target, target.snapshotFull(), px, py, width, height, active,
|
boolean withKitty = target.kittyEnabled() && hasKittyGraphics(target);
|
||||||
target.kittyEnabled() && hasKittyGraphics(target));
|
RenderStateSnapshot snapshot = target.snapshotFull();
|
||||||
|
if (withKitty) {
|
||||||
|
drawContent(gc, target, snapshot, px, py, width, height, active, true);
|
||||||
|
software.invalidate();
|
||||||
|
} else {
|
||||||
|
software.paintFull(gc, snapshot, px, py, width, height, active);
|
||||||
|
}
|
||||||
gc.restore();
|
gc.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,13 +95,14 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
if (target.kittyEnabled() && hasKittyGraphics(target)) {
|
if (target.kittyEnabled() && hasKittyGraphics(target)) {
|
||||||
// Kitty placements can move without a per-row dirty flag, so always redraw whole.
|
// Kitty placements can move without a per-row dirty flag, so always redraw whole.
|
||||||
drawContent(gc, target, target.snapshotFull(), px, py, width, height, active, true);
|
drawContent(gc, target, target.snapshotFull(), px, py, width, height, active, true);
|
||||||
|
software.invalidate();
|
||||||
} else {
|
} else {
|
||||||
RenderStateSnapshot snapshot = target.snapshot();
|
RenderStateSnapshot snapshot = target.snapshot();
|
||||||
int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty();
|
int dirty = snapshot == null ? DIRTY_FULL : snapshot.dirty();
|
||||||
if (dirty == DIRTY_FULL) {
|
if (dirty == DIRTY_FULL) {
|
||||||
drawContent(gc, target, snapshot, px, py, width, height, active, false);
|
software.paintFullOrShifted(gc, target.snapshotFull(), px, py, width, height, active);
|
||||||
} else if (dirty == DIRTY_PARTIAL) {
|
} else if (dirty == DIRTY_PARTIAL) {
|
||||||
drawDirtyRows(gc, snapshot, px, py, width, height, active);
|
software.paintDirty(gc, target, snapshot, px, py, width, height, active);
|
||||||
}
|
}
|
||||||
// dirty == FALSE: nothing visible changed.
|
// dirty == FALSE: nothing visible changed.
|
||||||
}
|
}
|
||||||
@@ -163,10 +182,17 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
|
|
||||||
double contentBottom = top + snapshot.rows() * lineHeight;
|
double contentBottom = top + snapshot.rows() * lineHeight;
|
||||||
int lastRow = snapshot.rows() - 1;
|
int lastRow = snapshot.rows() - 1;
|
||||||
|
List<RenderRow> rows = snapshot.renderRows();
|
||||||
|
boolean allRowsDirty = allRowsDirty(snapshot, rows);
|
||||||
|
if (allRowsDirty) {
|
||||||
|
gc.setFill(PANE_BACKGROUND);
|
||||||
|
gc.fillRect(px, py, pw, ph);
|
||||||
|
}
|
||||||
|
|
||||||
boolean cursorRowDirty = false;
|
boolean cursorRowDirty = false;
|
||||||
double bandMin = Double.POSITIVE_INFINITY;
|
double bandMin = Double.POSITIVE_INFINITY;
|
||||||
double bandMax = Double.NEGATIVE_INFINITY;
|
double bandMax = Double.NEGATIVE_INFINITY;
|
||||||
for (RenderRow row : snapshot.renderRows()) {
|
for (RenderRow row : rows) {
|
||||||
if (!row.dirty()) {
|
if (!row.dirty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -174,8 +200,10 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
// would leave sub-pixel seams between rows.
|
// would leave sub-pixel seams between rows.
|
||||||
double y0 = Math.floor(top + (row.row() * lineHeight));
|
double y0 = Math.floor(top + (row.row() * lineHeight));
|
||||||
double y1 = Math.ceil(top + ((row.row() + 1) * lineHeight));
|
double y1 = Math.ceil(top + ((row.row() + 1) * lineHeight));
|
||||||
gc.setFill(PANE_BACKGROUND);
|
if (!allRowsDirty) {
|
||||||
gc.fillRect(px, y0, pw, y1 - y0);
|
gc.setFill(PANE_BACKGROUND);
|
||||||
|
gc.fillRect(px, y0, pw, y1 - y0);
|
||||||
|
}
|
||||||
paintSidePadding(gc, row, px, pw, left, cellWidth, y0, y1 - y0);
|
paintSidePadding(gc, row, px, pw, left, cellWidth, y0, y1 - y0);
|
||||||
drawRow(gc, row, left, top, baseline, cellWidth, lineHeight);
|
drawRow(gc, row, left, top, baseline, cellWidth, lineHeight);
|
||||||
bandMin = Math.min(bandMin, y0);
|
bandMin = Math.min(bandMin, y0);
|
||||||
@@ -213,8 +241,21 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
gc.restore();
|
gc.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean allRowsDirty(RenderStateSnapshot snapshot, List<RenderRow> rows) {
|
||||||
|
if (rows.size() != snapshot.rows()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
RenderRow row = rows.get(i);
|
||||||
|
if (!row.dirty() || row.row() != i) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void drawBorder(GraphicsContext gc, double x, double y, double width, double height, boolean active) {
|
private void drawBorder(GraphicsContext gc, double x, double y, double width, double height, boolean active) {
|
||||||
gc.setStroke(active ? Color.rgb(87, 166, 255) : Color.rgb(52, 57, 65));
|
gc.setStroke(active ? ACTIVE_BORDER : INACTIVE_BORDER);
|
||||||
gc.setLineWidth(active ? 2.0 : 1.0);
|
gc.setLineWidth(active ? 2.0 : 1.0);
|
||||||
gc.strokeRect(x + 0.5, y + 0.5, width - 1.0, height - 1.0);
|
gc.strokeRect(x + 0.5, y + 0.5, width - 1.0, height - 1.0);
|
||||||
}
|
}
|
||||||
@@ -266,7 +307,7 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
gc.fillRect(paneX, contentBottom, paneWidth, paneY + paneHeight - contentBottom);
|
gc.fillRect(paneX, contentBottom, paneWidth, paneY + paneHeight - contentBottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void drawRow(
|
private void drawRow(
|
||||||
GraphicsContext gc,
|
GraphicsContext gc,
|
||||||
RenderRow row,
|
RenderRow row,
|
||||||
double left,
|
double left,
|
||||||
@@ -275,45 +316,108 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
double cellWidth,
|
double cellWidth,
|
||||||
double lineHeight
|
double lineHeight
|
||||||
) {
|
) {
|
||||||
|
drawRowBackgrounds(gc, row, left, top, cellWidth, lineHeight);
|
||||||
|
drawRowText(gc, row, left, baseline, cellWidth, lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawRowBackgrounds(
|
||||||
|
GraphicsContext gc,
|
||||||
|
RenderRow row,
|
||||||
|
double left,
|
||||||
|
double top,
|
||||||
|
double cellWidth,
|
||||||
|
double lineHeight
|
||||||
|
) {
|
||||||
|
Color runBackground = null;
|
||||||
|
int runStartColumn = 0;
|
||||||
|
int previousColumn = -1;
|
||||||
for (RenderCell cell : row.cells()) {
|
for (RenderCell cell : row.cells()) {
|
||||||
if (cell.kittyPlaceholder().isPresent()) {
|
if (cell.kittyPlaceholder().isPresent()) {
|
||||||
|
flushBackgroundRun(gc, runBackground, left, top, cellWidth, lineHeight, row.row(), runStartColumn, previousColumn);
|
||||||
|
runBackground = null;
|
||||||
|
previousColumn = -1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double x = left + (cell.column() * cellWidth);
|
Color bg = cell.selected() ? SELECTED_BACKGROUND : cellBackgroundOverride(cell);
|
||||||
double cellTop = top + (row.row() * lineHeight);
|
if (bg == null) {
|
||||||
|
flushBackgroundRun(gc, runBackground, left, top, cellWidth, lineHeight, row.row(), runStartColumn, previousColumn);
|
||||||
// Resolve fg/bg (null bg = terminal default, painted by the pane background).
|
runBackground = null;
|
||||||
// Avoid Optional.map's allocation on this hot path.
|
previousColumn = -1;
|
||||||
var fgOpt = cell.foreground();
|
|
||||||
var bgOpt = cell.background();
|
|
||||||
Color fg = fgOpt.isPresent() ? toFxColor(fgOpt.get()) : DEFAULT_FOREGROUND;
|
|
||||||
Color bg = bgOpt.isPresent() ? toFxColor(bgOpt.get()) : null;
|
|
||||||
|
|
||||||
// Reverse video: ghostty does not bake inverse into the resolved colours, so we
|
|
||||||
// swap them here, falling back to the terminal defaults for whichever is unset.
|
|
||||||
if (cell.inverse()) {
|
|
||||||
Color swappedBg = fg;
|
|
||||||
fg = (bg != null) ? bg : PANE_BACKGROUND;
|
|
||||||
bg = swappedBg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bg != null) {
|
|
||||||
gc.setFill(bg);
|
|
||||||
gc.fillRect(x, cellTop, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
if (cell.selected()) {
|
|
||||||
gc.setFill(SELECTED_BACKGROUND);
|
|
||||||
gc.fillRect(x, cellTop, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
if (cell.codepoints().length == 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double y = baseline + (row.row() * lineHeight);
|
if (runBackground == null || bg != runBackground || cell.column() != previousColumn + 1) {
|
||||||
gc.setFill(fg);
|
flushBackgroundRun(gc, runBackground, left, top, cellWidth, lineHeight, row.row(), runStartColumn, previousColumn);
|
||||||
gc.fillText(cell.text(), x, y);
|
runBackground = bg;
|
||||||
|
runStartColumn = cell.column();
|
||||||
|
}
|
||||||
|
previousColumn = cell.column();
|
||||||
}
|
}
|
||||||
|
flushBackgroundRun(gc, runBackground, left, top, cellWidth, lineHeight, row.row(), runStartColumn, previousColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void flushBackgroundRun(
|
||||||
|
GraphicsContext gc,
|
||||||
|
Color background,
|
||||||
|
double left,
|
||||||
|
double top,
|
||||||
|
double cellWidth,
|
||||||
|
double lineHeight,
|
||||||
|
int row,
|
||||||
|
int startColumn,
|
||||||
|
int endColumn
|
||||||
|
) {
|
||||||
|
if (background == null || endColumn < startColumn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gc.setFill(background);
|
||||||
|
gc.fillRect(
|
||||||
|
left + (startColumn * cellWidth),
|
||||||
|
top + (row * lineHeight),
|
||||||
|
(endColumn - startColumn + 1) * cellWidth,
|
||||||
|
lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawRowText(
|
||||||
|
GraphicsContext gc,
|
||||||
|
RenderRow row,
|
||||||
|
double left,
|
||||||
|
double baseline,
|
||||||
|
double cellWidth,
|
||||||
|
double lineHeight
|
||||||
|
) {
|
||||||
|
for (RenderCell cell : row.cells()) {
|
||||||
|
if (cell.kittyPlaceholder().isPresent() || cell.codepoints().length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.setFill(cellForegroundColor(cell));
|
||||||
|
gc.fillText(cell.text(), left + (cell.column() * cellWidth), baseline + (row.row() * lineHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background override for a cell: null means the pane default background already covers it.
|
||||||
|
private static Color cellBackgroundOverride(RenderCell cell) {
|
||||||
|
if (cell.inverse()) {
|
||||||
|
var fg = cell.foreground();
|
||||||
|
return fg.isPresent() ? toFxColor(fg.get()) : DEFAULT_FOREGROUND;
|
||||||
|
}
|
||||||
|
var bgOpt = cell.background();
|
||||||
|
Color bg = bgOpt.isPresent() ? toFxColor(bgOpt.get()) : null;
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color cellForegroundColor(RenderCell cell) {
|
||||||
|
var fgOpt = cell.foreground();
|
||||||
|
var bgOpt = cell.background();
|
||||||
|
Color fg = fgOpt.isPresent() ? toFxColor(fgOpt.get()) : DEFAULT_FOREGROUND;
|
||||||
|
Color bg = bgOpt.isPresent() ? toFxColor(bgOpt.get()) : null;
|
||||||
|
|
||||||
|
if (cell.inverse()) {
|
||||||
|
return (bg != null) ? bg : PANE_BACKGROUND;
|
||||||
|
}
|
||||||
|
return fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Color toFxColor(RenderColor color) {
|
private static Color toFxColor(RenderColor color) {
|
||||||
@@ -337,8 +441,8 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
|
|
||||||
double x = left + (snapshot.cursorViewportX() * cellWidth);
|
double x = left + (snapshot.cursorViewportX() * cellWidth);
|
||||||
double y = top + (snapshot.cursorViewportY() * lineHeight);
|
double y = top + (snapshot.cursorViewportY() * lineHeight);
|
||||||
gc.setStroke(Color.rgb(225, 229, 235));
|
gc.setStroke(DEFAULT_FOREGROUND);
|
||||||
gc.setFill(Color.rgb(225, 229, 235, 0.28));
|
gc.setFill(CURSOR_FILL);
|
||||||
gc.setLineWidth(1.5);
|
gc.setLineWidth(1.5);
|
||||||
|
|
||||||
RenderCursorStyle style = snapshot.cursorStyle();
|
RenderCursorStyle style = snapshot.cursorStyle();
|
||||||
@@ -563,6 +667,655 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class SoftwareBackbuffer {
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
private int[] pixels = new int[0];
|
||||||
|
private PixelBuffer<IntBuffer> pixelBuffer;
|
||||||
|
private WritableImage image;
|
||||||
|
private long[] rowHashes = new long[0];
|
||||||
|
private CursorState lastCursor = CursorState.none();
|
||||||
|
private GlyphCache glyphs;
|
||||||
|
// Half-open [min, max) vertical span of buffer rows written since the last present, so
|
||||||
|
// present() can upload only that band to the GPU instead of the whole pane texture.
|
||||||
|
private int dirtyMinY = Integer.MAX_VALUE;
|
||||||
|
private int dirtyMaxY = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
private void invalidate() {
|
||||||
|
rowHashes = new long[0];
|
||||||
|
lastCursor = CursorState.none();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record that buffer rows [y0, y1) changed; clamped to the buffer in dirtyRegion().
|
||||||
|
private void markDirtyRows(int y0, int y1) {
|
||||||
|
if (y0 < dirtyMinY) {
|
||||||
|
dirtyMinY = y0;
|
||||||
|
}
|
||||||
|
if (y1 > dirtyMaxY) {
|
||||||
|
dirtyMaxY = y1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetDirty() {
|
||||||
|
dirtyMinY = Integer.MAX_VALUE;
|
||||||
|
dirtyMaxY = Integer.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The region to hand PixelBuffer.updateBuffer: a full-width band covering the rows
|
||||||
|
// written this frame (clamped to the buffer), or EMPTY when nothing changed.
|
||||||
|
private Rectangle2D dirtyRegion() {
|
||||||
|
int y0 = Math.max(0, dirtyMinY);
|
||||||
|
int y1 = Math.min(height, dirtyMaxY);
|
||||||
|
if (y0 >= y1) {
|
||||||
|
return Rectangle2D.EMPTY;
|
||||||
|
}
|
||||||
|
return new Rectangle2D(0, y0, width, y1 - y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintFull(GraphicsContext gc, RenderStateSnapshot snapshot,
|
||||||
|
double px, double py, double paneWidth, double paneHeight, boolean active) {
|
||||||
|
ensure(paneWidth, paneHeight);
|
||||||
|
fillRect(0, 0, width, height, argbPre(PANE_BACKGROUND));
|
||||||
|
if (snapshot != null) {
|
||||||
|
paintSnapshot(snapshot);
|
||||||
|
drawCursor(snapshot);
|
||||||
|
rememberSnapshot(snapshot);
|
||||||
|
} else {
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
drawBorder(active);
|
||||||
|
present(gc, px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintFullOrShifted(GraphicsContext gc, RenderStateSnapshot snapshot,
|
||||||
|
double px, double py, double paneWidth, double paneHeight, boolean active) {
|
||||||
|
ensure(paneWidth, paneHeight);
|
||||||
|
if (snapshot == null || !canDiff(snapshot)) {
|
||||||
|
paintFull(gc, snapshot, px, py, paneWidth, paneHeight, active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long[] currentHashes = hashes(snapshot);
|
||||||
|
int scroll = inferScroll(currentHashes);
|
||||||
|
if (scroll != 0) {
|
||||||
|
scrollContentPixels(scroll);
|
||||||
|
scrollHashes(scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
CursorState cursor = CursorState.from(snapshot);
|
||||||
|
int oldCursorRow = shiftedCursorRow(lastCursor, scroll);
|
||||||
|
int newCursorRow = cursor.viewportRow();
|
||||||
|
boolean cursorChanged = !cursor.equals(lastCursor);
|
||||||
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
|
int rowIndex = row.row();
|
||||||
|
boolean repaint = currentHashes[rowIndex] != rowHashes[rowIndex]
|
||||||
|
|| rowIndex == newCursorRow
|
||||||
|
|| (cursorChanged && rowIndex == oldCursorRow);
|
||||||
|
if (repaint) {
|
||||||
|
paintRow(row);
|
||||||
|
rowHashes[rowIndex] = currentHashes[rowIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastCursor = cursor;
|
||||||
|
drawCursor(snapshot);
|
||||||
|
drawBorder(active);
|
||||||
|
present(gc, px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintDirty(GraphicsContext gc, RenderTarget target, RenderStateSnapshot snapshot,
|
||||||
|
double px, double py, double paneWidth, double paneHeight, boolean active) {
|
||||||
|
ensure(paneWidth, paneHeight);
|
||||||
|
if (snapshot == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rowHashes.length != snapshot.rows()) {
|
||||||
|
paintFull(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CursorState cursor = CursorState.from(snapshot);
|
||||||
|
int oldCursorRow = lastCursor.viewportRow();
|
||||||
|
int newCursorRow = cursor.viewportRow();
|
||||||
|
boolean cursorChanged = !cursor.equals(lastCursor);
|
||||||
|
boolean[] repainted = new boolean[snapshot.rows()];
|
||||||
|
boolean needsCursorDraw = cursorChanged;
|
||||||
|
|
||||||
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
|
if (!row.dirty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
paintRow(row);
|
||||||
|
rowHashes[row.row()] = rowHash(row);
|
||||||
|
repainted[row.row()] = true;
|
||||||
|
if (row.row() == newCursorRow) {
|
||||||
|
needsCursorDraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorChanged) {
|
||||||
|
if (!repaintCursorRow(snapshot, oldCursorRow, repainted)) {
|
||||||
|
paintFullOrShifted(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (repaintedRowHasCursor(newCursorRow, repainted)
|
||||||
|
&& !repaintCursorRow(snapshot, newCursorRow, repainted)) {
|
||||||
|
paintFullOrShifted(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastCursor = cursor;
|
||||||
|
if (needsCursorDraw) {
|
||||||
|
drawCursor(snapshot);
|
||||||
|
}
|
||||||
|
drawBorder(active);
|
||||||
|
present(gc, px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean repaintCursorRow(RenderStateSnapshot snapshot, int rowIndex, boolean[] repainted) {
|
||||||
|
if (rowIndex < 0 || rowIndex >= repainted.length || repainted[rowIndex]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
RenderRow row = rowByIndex(snapshot, rowIndex);
|
||||||
|
if (row == null || !row.dirty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
paintRow(row);
|
||||||
|
rowHashes[rowIndex] = rowHash(row);
|
||||||
|
repainted[rowIndex] = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean repaintedRowHasCursor(int rowIndex, boolean[] repainted) {
|
||||||
|
return rowIndex >= 0 && rowIndex < repainted.length && repainted[rowIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderRow rowByIndex(RenderStateSnapshot snapshot, int rowIndex) {
|
||||||
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
|
if (row.row() == rowIndex) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensure(double paneWidth, double paneHeight) {
|
||||||
|
int nextWidth = Math.max(1, (int) Math.round(paneWidth));
|
||||||
|
int nextHeight = Math.max(1, (int) Math.round(paneHeight));
|
||||||
|
if (nextWidth == width && nextHeight == height && image != null) {
|
||||||
|
ensureGlyphs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
width = nextWidth;
|
||||||
|
height = nextHeight;
|
||||||
|
pixels = new int[width * height];
|
||||||
|
pixelBuffer = new PixelBuffer<>(width, height, IntBuffer.wrap(pixels), PixelFormat.getIntArgbPreInstance());
|
||||||
|
image = new WritableImage(pixelBuffer);
|
||||||
|
invalidate();
|
||||||
|
ensureGlyphs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureGlyphs() {
|
||||||
|
int cellWidth = cellWidth();
|
||||||
|
int lineHeight = lineHeight();
|
||||||
|
double baseline = metrics.baselineOffset();
|
||||||
|
if (glyphs == null || glyphs.font != metrics.font()
|
||||||
|
|| glyphs.cellWidth != cellWidth || glyphs.lineHeight != lineHeight || glyphs.baseline != baseline) {
|
||||||
|
glyphs = new GlyphCache(metrics.font(), cellWidth, lineHeight, baseline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void present(GraphicsContext gc, double px, double py) {
|
||||||
|
// Only re-upload the rows that actually changed; the unchanged remainder of the pane
|
||||||
|
// texture is already correct on the GPU from the previous frame.
|
||||||
|
Rectangle2D dirty = dirtyRegion();
|
||||||
|
resetDirty();
|
||||||
|
pixelBuffer.updateBuffer(ignored -> dirty);
|
||||||
|
gc.drawImage(image, px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canDiff(RenderStateSnapshot snapshot) {
|
||||||
|
return rowHashes.length == snapshot.rows() && snapshot.renderRows().size() == snapshot.rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rememberSnapshot(RenderStateSnapshot snapshot) {
|
||||||
|
if (rowHashes.length != snapshot.rows()) {
|
||||||
|
rowHashes = new long[snapshot.rows()];
|
||||||
|
}
|
||||||
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
|
rowHashes[row.row()] = rowHash(row);
|
||||||
|
}
|
||||||
|
lastCursor = CursorState.from(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] hashes(RenderStateSnapshot snapshot) {
|
||||||
|
long[] hashes = new long[snapshot.rows()];
|
||||||
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
|
hashes[row.row()] = rowHash(row);
|
||||||
|
}
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int inferScroll(long[] currentHashes) {
|
||||||
|
int rows = currentHashes.length;
|
||||||
|
int bestDelta = 0;
|
||||||
|
int bestScore = 0;
|
||||||
|
int maxDelta = Math.min(8, Math.max(0, rows - 1));
|
||||||
|
for (int delta = -maxDelta; delta <= maxDelta; delta++) {
|
||||||
|
if (delta == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int score = 0;
|
||||||
|
int overlap = 0;
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
int previous = row - delta;
|
||||||
|
if (previous < 0 || previous >= rows) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
overlap++;
|
||||||
|
if (currentHashes[row] == rowHashes[previous]) {
|
||||||
|
score++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (score > bestScore || (score == bestScore && Math.abs(delta) < Math.abs(bestDelta))) {
|
||||||
|
bestScore = score;
|
||||||
|
bestDelta = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int threshold = Math.max(3, (int) Math.ceil(rows * 0.55));
|
||||||
|
return bestScore >= threshold ? bestDelta : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int shiftedCursorRow(CursorState cursor, int scroll) {
|
||||||
|
int row = cursor.viewportRow();
|
||||||
|
if (row < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
row += scroll;
|
||||||
|
return row >= 0 && row < rowHashes.length ? row : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollHashes(int rows) {
|
||||||
|
long[] shifted = new long[rowHashes.length];
|
||||||
|
for (int row = 0; row < shifted.length; row++) {
|
||||||
|
int previous = row - rows;
|
||||||
|
if (previous >= 0 && previous < rowHashes.length) {
|
||||||
|
shifted[row] = rowHashes[previous];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowHashes = shifted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollContentPixels(int rows) {
|
||||||
|
int dy = rows * lineHeight();
|
||||||
|
int top = contentTop();
|
||||||
|
int contentHeight = rowHashes.length * lineHeight();
|
||||||
|
// The whole content region shifts; the arraycopy below moves pixels that the
|
||||||
|
// per-strip fillRect calls don't touch, so mark the full content band for upload.
|
||||||
|
markDirtyRows(top, top + contentHeight);
|
||||||
|
if (dy == 0 || Math.abs(dy) >= contentHeight) {
|
||||||
|
fillRect(0, top, width, contentHeight, argbPre(PANE_BACKGROUND));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dy < 0) {
|
||||||
|
int srcY = top - dy;
|
||||||
|
int dstY = top;
|
||||||
|
int copyHeight = contentHeight + dy;
|
||||||
|
for (int y = 0; y < copyHeight; y++) {
|
||||||
|
System.arraycopy(pixels, (srcY + y) * width, pixels, (dstY + y) * width, width);
|
||||||
|
}
|
||||||
|
fillRect(0, top + copyHeight, width, -dy, argbPre(PANE_BACKGROUND));
|
||||||
|
} else {
|
||||||
|
int srcY = top;
|
||||||
|
int dstY = top + dy;
|
||||||
|
int copyHeight = contentHeight - dy;
|
||||||
|
for (int y = copyHeight - 1; y >= 0; y--) {
|
||||||
|
System.arraycopy(pixels, (srcY + y) * width, pixels, (dstY + y) * width, width);
|
||||||
|
}
|
||||||
|
fillRect(0, top, width, dy, argbPre(PANE_BACKGROUND));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintSnapshot(RenderStateSnapshot snapshot) {
|
||||||
|
List<RenderRow> rows = snapshot.renderRows();
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int top = contentTop();
|
||||||
|
int contentBottom = top + snapshot.rows() * lineHeight();
|
||||||
|
fillRect(0, 0, width, top, argbPre(rowEdgeBackground(rows.get(0), true)));
|
||||||
|
fillRect(0, contentBottom, width, height - contentBottom, argbPre(rowEdgeBackground(rows.get(rows.size() - 1), true)));
|
||||||
|
for (RenderRow row : rows) {
|
||||||
|
paintRow(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintRow(RenderRow row) {
|
||||||
|
int rowTop = contentTop() + (row.row() * lineHeight());
|
||||||
|
int rowHeight = lineHeight();
|
||||||
|
fillRect(0, rowTop, width, rowHeight, argbPre(PANE_BACKGROUND));
|
||||||
|
if (row.row() == 0) {
|
||||||
|
fillRect(0, 0, width, contentTop(), argbPre(rowEdgeBackground(row, true)));
|
||||||
|
}
|
||||||
|
if (row.row() == rowHashes.length - 1) {
|
||||||
|
int bottom = contentTop() + (rowHashes.length * lineHeight());
|
||||||
|
fillRect(0, bottom, width, height - bottom, argbPre(rowEdgeBackground(row, true)));
|
||||||
|
}
|
||||||
|
paintRowSidePadding(row, rowTop, rowHeight);
|
||||||
|
paintRowBackgrounds(row, rowTop, rowHeight);
|
||||||
|
paintRowText(row, rowTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintRowSidePadding(RenderRow row, int rowTop, int rowHeight) {
|
||||||
|
List<RenderCell> cells = row.cells();
|
||||||
|
if (cells.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int left = contentLeft();
|
||||||
|
int contentRight = left + (cells.size() * cellWidth());
|
||||||
|
fillRect(0, rowTop, left, rowHeight, argbPre(rowEdgeBackground(row, true)));
|
||||||
|
fillRect(contentRight, rowTop, width - contentRight, rowHeight, argbPre(rowEdgeBackground(row, false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintRowBackgrounds(RenderRow row, int rowTop, int rowHeight) {
|
||||||
|
int cellWidth = cellWidth();
|
||||||
|
Color runBackground = null;
|
||||||
|
int runStartColumn = 0;
|
||||||
|
int previousColumn = -1;
|
||||||
|
for (RenderCell cell : row.cells()) {
|
||||||
|
if (cell.kittyPlaceholder().isPresent()) {
|
||||||
|
flushBackground(runBackground, runStartColumn, previousColumn, rowTop, rowHeight);
|
||||||
|
runBackground = null;
|
||||||
|
previousColumn = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color bg = cell.selected() ? SELECTED_BACKGROUND : cellBackgroundOverride(cell);
|
||||||
|
if (bg == null) {
|
||||||
|
flushBackground(runBackground, runStartColumn, previousColumn, rowTop, rowHeight);
|
||||||
|
runBackground = null;
|
||||||
|
previousColumn = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (runBackground == null || bg != runBackground || cell.column() != previousColumn + 1) {
|
||||||
|
flushBackground(runBackground, runStartColumn, previousColumn, rowTop, rowHeight);
|
||||||
|
runBackground = bg;
|
||||||
|
runStartColumn = cell.column();
|
||||||
|
}
|
||||||
|
previousColumn = cell.column();
|
||||||
|
}
|
||||||
|
flushBackground(runBackground, runStartColumn, previousColumn, rowTop, rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushBackground(Color background, int startColumn, int endColumn, int rowTop, int rowHeight) {
|
||||||
|
if (background == null || endColumn < startColumn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fillRect(contentLeft() + (startColumn * cellWidth()), rowTop,
|
||||||
|
(endColumn - startColumn + 1) * cellWidth(), rowHeight, argbPre(background));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintRowText(RenderRow row, int rowTop) {
|
||||||
|
int cellWidth = cellWidth();
|
||||||
|
int x0 = contentLeft();
|
||||||
|
for (RenderCell cell : row.cells()) {
|
||||||
|
if (cell.kittyPlaceholder().isPresent() || cell.codepoints().length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Glyph glyph = glyphs.glyph(cell.text());
|
||||||
|
int color = rgb(cellForegroundColor(cell));
|
||||||
|
blitGlyph(glyph, x0 + (cell.column() * cellWidth), rowTop, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void blitGlyph(Glyph glyph, int x, int y, int rgb) {
|
||||||
|
int red = (rgb >> 16) & 0xff;
|
||||||
|
int green = (rgb >> 8) & 0xff;
|
||||||
|
int blue = rgb & 0xff;
|
||||||
|
// Clamp the glyph rectangle to the buffer once, so the inner loops carry no
|
||||||
|
// per-pixel bounds check (this is the hottest pixel loop on a text repaint).
|
||||||
|
int gyStart = Math.max(0, -y);
|
||||||
|
int gyEnd = Math.min(glyph.height, height - y);
|
||||||
|
int gxStart = Math.max(0, -x);
|
||||||
|
int gxEnd = Math.min(glyph.width, width - x);
|
||||||
|
if (gyStart >= gyEnd || gxStart >= gxEnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int gy = gyStart; gy < gyEnd; gy++) {
|
||||||
|
int rowOffset = (y + gy) * width;
|
||||||
|
int glyphOffset = gy * glyph.width;
|
||||||
|
for (int gx = gxStart; gx < gxEnd; gx++) {
|
||||||
|
int alpha = glyph.alpha[glyphOffset + gx] & 0xff;
|
||||||
|
if (alpha == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int index = rowOffset + x + gx;
|
||||||
|
pixels[index] = blendOpaque(pixels[index], red, green, blue, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markDirtyRows(y + gyStart, y + gyEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int blendOpaque(int dst, int red, int green, int blue, int alpha) {
|
||||||
|
int inv = 255 - alpha;
|
||||||
|
int dstRed = (dst >> 16) & 0xff;
|
||||||
|
int dstGreen = (dst >> 8) & 0xff;
|
||||||
|
int dstBlue = dst & 0xff;
|
||||||
|
int outRed = ((red * alpha) + (dstRed * inv) + 127) / 255;
|
||||||
|
int outGreen = ((green * alpha) + (dstGreen * inv) + 127) / 255;
|
||||||
|
int outBlue = ((blue * alpha) + (dstBlue * inv) + 127) / 255;
|
||||||
|
return 0xff000000 | (outRed << 16) | (outGreen << 8) | outBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCursor(RenderStateSnapshot snapshot) {
|
||||||
|
if (!snapshot.cursorVisible() || !snapshot.cursorViewportHasValue()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int x = contentLeft() + ((int) snapshot.cursorViewportX() * cellWidth());
|
||||||
|
int y = contentTop() + ((int) snapshot.cursorViewportY() * lineHeight());
|
||||||
|
int cw = cellWidth();
|
||||||
|
int lh = lineHeight();
|
||||||
|
RenderCursorStyle style = snapshot.cursorStyle();
|
||||||
|
if (style == RenderCursorStyle.BAR) {
|
||||||
|
fillRect(x, y + 2, 1, Math.max(1, lh - 4), argbPre(DEFAULT_FOREGROUND));
|
||||||
|
} else if (style == RenderCursorStyle.UNDERLINE) {
|
||||||
|
fillRect(x + 1, y + lh - 2, Math.max(1, cw - 2), 1, argbPre(DEFAULT_FOREGROUND));
|
||||||
|
} else if (style == RenderCursorStyle.BLOCK) {
|
||||||
|
fillRectAlpha(x, y + 1, Math.max(1, cw - 1), Math.max(1, lh - 2), CURSOR_FILL);
|
||||||
|
} else {
|
||||||
|
strokeRect(x, y + 1, Math.max(1, cw - 1), Math.max(1, lh - 2), argbPre(DEFAULT_FOREGROUND), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBorder(boolean active) {
|
||||||
|
strokeRect(0, 0, width, height, argbPre(active ? ACTIVE_BORDER : INACTIVE_BORDER), active ? 2 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void strokeRect(int x, int y, int w, int h, int color, int lineWidth) {
|
||||||
|
// The border is redrawn every frame to restore the side edges over the rows we
|
||||||
|
// repaint, but its pixels never change between incremental frames. Write it without
|
||||||
|
// marking the dirty band: the segments inside a repainted row's band are already
|
||||||
|
// covered by that band (and so re-uploaded), and the segments outside it are
|
||||||
|
// identical to what is already on the GPU, so they need no upload.
|
||||||
|
for (int i = 0; i < lineWidth; i++) {
|
||||||
|
fillRectRaw(x + i, y + i, w - (2 * i), 1, color);
|
||||||
|
fillRectRaw(x + i, y + h - 1 - i, w - (2 * i), 1, color);
|
||||||
|
fillRectRaw(x + i, y + i, 1, h - (2 * i), color);
|
||||||
|
fillRectRaw(x + w - 1 - i, y + i, 1, h - (2 * i), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillRectAlpha(int x, int y, int w, int h, Color color) {
|
||||||
|
int alpha = (int) Math.round(color.getOpacity() * 255.0);
|
||||||
|
int rgb = rgb(color);
|
||||||
|
int red = (rgb >> 16) & 0xff;
|
||||||
|
int green = (rgb >> 8) & 0xff;
|
||||||
|
int blue = rgb & 0xff;
|
||||||
|
int x0 = Math.max(0, x);
|
||||||
|
int y0 = Math.max(0, y);
|
||||||
|
int x1 = Math.min(width, x + w);
|
||||||
|
int y1 = Math.min(height, y + h);
|
||||||
|
for (int py = y0; py < y1; py++) {
|
||||||
|
int offset = py * width;
|
||||||
|
for (int px = x0; px < x1; px++) {
|
||||||
|
int index = offset + px;
|
||||||
|
pixels[index] = blendOpaque(pixels[index], red, green, blue, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markDirtyRows(y0, y1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillRect(int x, int y, int w, int h, int color) {
|
||||||
|
int y0 = Math.max(0, y);
|
||||||
|
int y1 = Math.min(height, y + h);
|
||||||
|
if (fillRectRaw(x, y, w, h, color)) {
|
||||||
|
markDirtyRows(y0, y1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw fill with no dirty-band tracking; returns whether any pixels were written.
|
||||||
|
private boolean fillRectRaw(int x, int y, int w, int h, int color) {
|
||||||
|
int x0 = Math.max(0, x);
|
||||||
|
int y0 = Math.max(0, y);
|
||||||
|
int x1 = Math.min(width, x + w);
|
||||||
|
int y1 = Math.min(height, y + h);
|
||||||
|
if (x0 >= x1 || y0 >= y1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int py = y0; py < y1; py++) {
|
||||||
|
Arrays.fill(pixels, (py * width) + x0, (py * width) + x1, color);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int contentLeft() {
|
||||||
|
return (int) Math.round(TerminalMetrics.PADDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int contentTop() {
|
||||||
|
return (int) Math.round(TerminalMetrics.PADDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cellWidth() {
|
||||||
|
return Math.max(1, (int) Math.round(metrics.cellWidth()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lineHeight() {
|
||||||
|
return Math.max(1, (int) Math.round(metrics.lineHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class GlyphCache {
|
||||||
|
private final javafx.scene.text.Font font;
|
||||||
|
private final int cellWidth;
|
||||||
|
private final int lineHeight;
|
||||||
|
private final double baseline;
|
||||||
|
private final Map<String, Glyph> glyphs = new HashMap<>();
|
||||||
|
|
||||||
|
private GlyphCache(javafx.scene.text.Font font, int cellWidth, int lineHeight, double baseline) {
|
||||||
|
this.font = font;
|
||||||
|
this.cellWidth = cellWidth;
|
||||||
|
this.lineHeight = lineHeight;
|
||||||
|
this.baseline = baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Glyph glyph(String text) {
|
||||||
|
return glyphs.computeIfAbsent(text, this::renderGlyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Glyph renderGlyph(String value) {
|
||||||
|
Text measured = new Text(value);
|
||||||
|
measured.setFont(font);
|
||||||
|
int glyphWidth = Math.max(cellWidth, (int) Math.ceil(measured.getLayoutBounds().getWidth()) + 2);
|
||||||
|
Canvas canvas = new Canvas(glyphWidth, lineHeight);
|
||||||
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
|
gc.setFontSmoothingType(FontSmoothingType.GRAY);
|
||||||
|
gc.setFont(font);
|
||||||
|
gc.setFill(Color.WHITE);
|
||||||
|
gc.fillText(value, 0.0, baseline);
|
||||||
|
|
||||||
|
SnapshotParameters parameters = new SnapshotParameters();
|
||||||
|
parameters.setFill(Color.TRANSPARENT);
|
||||||
|
WritableImage snapshot = canvas.snapshot(parameters, null);
|
||||||
|
PixelReader reader = snapshot.getPixelReader();
|
||||||
|
byte[] alpha = new byte[glyphWidth * lineHeight];
|
||||||
|
for (int y = 0; y < lineHeight; y++) {
|
||||||
|
int offset = y * glyphWidth;
|
||||||
|
for (int x = 0; x < glyphWidth; x++) {
|
||||||
|
alpha[offset + x] = (byte) ((reader.getArgb(x, y) >>> 24) & 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Glyph(glyphWidth, lineHeight, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Glyph(int width, int height, byte[] alpha) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CursorState(boolean visible, boolean hasViewport, int column, int row, RenderCursorStyle style) {
|
||||||
|
private static CursorState none() {
|
||||||
|
return new CursorState(false, false, -1, -1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CursorState from(RenderStateSnapshot snapshot) {
|
||||||
|
if (snapshot == null || !snapshot.cursorVisible() || !snapshot.cursorViewportHasValue()) {
|
||||||
|
return none();
|
||||||
|
}
|
||||||
|
return new CursorState(true, true, (int) snapshot.cursorViewportX(), (int) snapshot.cursorViewportY(), snapshot.cursorStyle());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int viewportRow() {
|
||||||
|
return visible && hasViewport ? row : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long rowHash(RenderRow row) {
|
||||||
|
long hash = 0xcbf29ce484222325L;
|
||||||
|
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) {
|
||||||
|
return color == null ? mix(hash, -1) : mix(hash, (color.red() << 16) | (color.green() << 8) | color.blue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long hash, long value) {
|
||||||
|
hash ^= value;
|
||||||
|
return hash * 0x100000001b3L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int rgb(Color color) {
|
||||||
|
int red = (int) Math.round(color.getRed() * 255.0);
|
||||||
|
int green = (int) Math.round(color.getGreen() * 255.0);
|
||||||
|
int blue = (int) Math.round(color.getBlue() * 255.0);
|
||||||
|
return (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int argbPre(Color color) {
|
||||||
|
int alpha = (int) Math.round(color.getOpacity() * 255.0);
|
||||||
|
int red = (int) Math.round(color.getRed() * alpha);
|
||||||
|
int green = (int) Math.round(color.getGreen() * alpha);
|
||||||
|
int blue = (int) Math.round(color.getBlue() * alpha);
|
||||||
|
return (alpha << 24) | (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
// A kitty image is immutable for a given (id, number); re-transmitting under the same id
|
// A kitty image is immutable for a given (id, number); re-transmitting under the same id
|
||||||
// changes the number (and the snapshot below evicts stale entries by id anyway). So the
|
// changes the number (and the snapshot below evicts stale entries by id anyway). So the
|
||||||
// identity + dimensions + payload length are enough to key the decoded-image cache, and
|
// identity + dimensions + payload length are enough to key the decoded-image cache, and
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final class KeyEncoder {
|
|||||||
return switch (code) {
|
return switch (code) {
|
||||||
case ENTER -> "\r";
|
case ENTER -> "\r";
|
||||||
case BACK_SPACE -> "\u007f";
|
case BACK_SPACE -> "\u007f";
|
||||||
case TAB -> "\t";
|
case TAB -> event.isShiftDown() ? "\u001b[Z" : "\t";
|
||||||
case ESCAPE -> "\u001b";
|
case ESCAPE -> "\u001b";
|
||||||
case UP -> "\u001b[A";
|
case UP -> "\u001b[A";
|
||||||
case DOWN -> "\u001b[B";
|
case DOWN -> "\u001b[B";
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ public final class LinuxPty implements AutoCloseable {
|
|||||||
|
|
||||||
private final Arena arena = Arena.ofShared();
|
private final Arena arena = Arena.ofShared();
|
||||||
private final MemorySegment readBuffer = arena.allocate(65536);
|
private final MemorySegment readBuffer = arena.allocate(65536);
|
||||||
|
private final MemorySegment writeBuffer = arena.allocate(65536);
|
||||||
private final Object writeLock = new Object();
|
private final Object writeLock = new Object();
|
||||||
private final int masterFd;
|
private final int masterFd;
|
||||||
private final int pid;
|
private final int pid;
|
||||||
@@ -186,17 +187,20 @@ public final class LinuxPty implements AutoCloseable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (writeLock) {
|
synchronized (writeLock) {
|
||||||
try (Arena a = Arena.ofConfined()) {
|
int offset = 0;
|
||||||
MemorySegment buf = a.allocate(data.length);
|
while (offset < data.length) {
|
||||||
MemorySegment.copy(data, 0, buf, ValueLayout.JAVA_BYTE, 0, data.length);
|
int chunk = (int) Math.min(writeBuffer.byteSize(), data.length - offset);
|
||||||
long offset = 0;
|
MemorySegment.copy(data, offset, writeBuffer, ValueLayout.JAVA_BYTE, 0, chunk);
|
||||||
while (offset < data.length) {
|
|
||||||
long n = callLong(WRITE, masterFd, buf.asSlice(offset), data.length - offset);
|
long written = 0;
|
||||||
if (n < 0) {
|
while (written < chunk) {
|
||||||
|
long n = callLong(WRITE, masterFd, writeBuffer.asSlice(written), chunk - written);
|
||||||
|
if (n <= 0) {
|
||||||
throw new IllegalStateException("write to pty failed");
|
throw new IllegalStateException("write to pty failed");
|
||||||
}
|
}
|
||||||
offset += n;
|
written += n;
|
||||||
}
|
}
|
||||||
|
offset += chunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import javafx.scene.shape.Shape;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +34,7 @@ final class Tab implements AutoCloseable {
|
|||||||
private double lastTopInset;
|
private double lastTopInset;
|
||||||
// Bumped whenever one of this tab's panes changes content; the compositor reads the current
|
// Bumped whenever one of this tab's panes changes content; the compositor reads the current
|
||||||
// tab's value each frame as an O(1) "anything to repaint?" check.
|
// tab's value each frame as an O(1) "anything to repaint?" check.
|
||||||
private long contentVersion;
|
private final AtomicLong contentVersion = new AtomicLong();
|
||||||
|
|
||||||
Tab(AppConfig config, TerminalMetrics metrics) {
|
Tab(AppConfig config, TerminalMetrics metrics) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
@@ -54,7 +55,7 @@ final class Tab implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long contentVersion() {
|
long contentVersion() {
|
||||||
return contentVersion;
|
return contentVersion.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,7 +292,7 @@ final class Tab implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void markContentChanged() {
|
private void markContentChanged() {
|
||||||
contentVersion++;
|
contentVersion.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TerminalPane openPane(boolean asFloating) {
|
private TerminalPane openPane(boolean asFloating) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import javafx.scene.canvas.GraphicsContext;
|
|||||||
import javafx.scene.shape.Shape;
|
import javafx.scene.shape.Shape;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One terminal: owns its ghostty {@link Terminal}, the {@link ShellSession}/pty driving it,
|
* One terminal: owns its ghostty {@link Terminal}, the {@link ShellSession}/pty driving it,
|
||||||
@@ -49,7 +50,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
private int rows;
|
private int rows;
|
||||||
private int pixelWidth;
|
private int pixelWidth;
|
||||||
private int pixelHeight;
|
private int pixelHeight;
|
||||||
private long contentVersion;
|
private final AtomicLong contentVersion = new AtomicLong();
|
||||||
private long snapshotVersion = -1;
|
private long snapshotVersion = -1;
|
||||||
|
|
||||||
private TerminalPane(Terminal terminal, TerminalMetrics metrics, boolean kittyEnabled,
|
private TerminalPane(Terminal terminal, TerminalMetrics metrics, boolean kittyEnabled,
|
||||||
@@ -169,16 +170,17 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
|
|
||||||
private RenderStateSnapshot takeSnapshot(boolean full) {
|
private RenderStateSnapshot takeSnapshot(boolean full) {
|
||||||
synchronized (terminal) {
|
synchronized (terminal) {
|
||||||
|
long version = contentVersion.get();
|
||||||
if (full) {
|
if (full) {
|
||||||
renderState.update(terminal);
|
renderState.update(terminal);
|
||||||
cachedSnapshot = renderState.snapshot();
|
cachedSnapshot = renderState.snapshot();
|
||||||
renderState.resetDirty();
|
renderState.resetDirty();
|
||||||
snapshotVersion = contentVersion;
|
snapshotVersion = version;
|
||||||
} else if (snapshotVersion != contentVersion) {
|
} else if (snapshotVersion != version) {
|
||||||
renderState.update(terminal);
|
renderState.update(terminal);
|
||||||
cachedSnapshot = renderState.snapshotIncremental();
|
cachedSnapshot = renderState.snapshotIncremental();
|
||||||
renderState.resetDirty();
|
renderState.resetDirty();
|
||||||
snapshotVersion = contentVersion;
|
snapshotVersion = version;
|
||||||
}
|
}
|
||||||
return cachedSnapshot;
|
return cachedSnapshot;
|
||||||
}
|
}
|
||||||
@@ -192,7 +194,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
|
|
||||||
/** This pane's own content revision, bumped on every change (see {@link #refresh()}). */
|
/** This pane's own content revision, bumped on every change (see {@link #refresh()}). */
|
||||||
public long contentVersion() {
|
public long contentVersion() {
|
||||||
return contentVersion;
|
return contentVersion.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -276,18 +278,20 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
// Mark this pane's content dirty (the snapshot is computed lazily in the paint path,
|
// Mark this pane's content dirty (the snapshot is computed lazily in the paint path,
|
||||||
// so a burst of writes collapses into one snapshot per frame) and tell the owning tab
|
// so a burst of writes collapses into one snapshot per frame) and tell the owning tab
|
||||||
// one of its panes changed.
|
// one of its panes changed.
|
||||||
contentVersion++;
|
contentVersion.incrementAndGet();
|
||||||
onContentChange.run();
|
onContentChange.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Paint the whole pane; see {@link TerminalRenderer#paintFull}. */
|
/** Paint the whole pane; see {@link TerminalRenderer#paintFull}. */
|
||||||
public void paintFull(GraphicsContext gc, boolean active) {
|
public long paintFull(GraphicsContext gc, boolean active) {
|
||||||
renderer.paintFull(gc, this, active);
|
renderer.paintFull(gc, this, active);
|
||||||
|
return snapshotVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Repaint what changed; see {@link TerminalRenderer#paintIncremental}. */
|
/** Repaint what changed; see {@link TerminalRenderer#paintIncremental}. */
|
||||||
public void paintIncremental(GraphicsContext gc, boolean active) {
|
public long paintIncremental(GraphicsContext gc, boolean active) {
|
||||||
renderer.paintIncremental(gc, this, active);
|
renderer.paintIncremental(gc, this, active);
|
||||||
|
return snapshotVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user