cleanup
This commit is contained in:
@@ -1,631 +0,0 @@
|
|||||||
package com.gregor.jprototerm;
|
|
||||||
|
|
||||||
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.RenderCell;
|
|
||||||
import dev.jlibghostty.RenderColor;
|
|
||||||
import dev.jlibghostty.RenderCursorStyle;
|
|
||||||
import dev.jlibghostty.RenderRow;
|
|
||||||
import dev.jlibghostty.RenderStateSnapshot;
|
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.PixelFormat;
|
|
||||||
import javafx.scene.image.WritableImage;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.text.FontSmoothingType;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The real terminal renderer: paints a pane's background, cell rows, cursor, border, padding
|
|
||||||
* and (when enabled) kitty graphics. One instance per pane, since it caches that pane's
|
|
||||||
* decoded kitty images.
|
|
||||||
*/
|
|
||||||
final class GhosttyTerminalRenderer extends TerminalRenderer {
|
|
||||||
// GhosttyRenderStateDirty values (stable C ABI; see ghostty/vt/render.h).
|
|
||||||
private static final int DIRTY_PARTIAL = 1;
|
|
||||||
private static final int DIRTY_FULL = 2;
|
|
||||||
|
|
||||||
private static final Color DEFAULT_FOREGROUND = Color.rgb(225, 229, 235);
|
|
||||||
private static final Color SELECTED_BACKGROUND = Color.rgb(52, 92, 140);
|
|
||||||
// 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).
|
|
||||||
private static final Color PANE_BACKGROUND = Color.rgb(9, 10, 12);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// truecolor gradient can't grow it without limit.
|
|
||||||
private static final Map<Integer, Color> COLOR_CACHE = new HashMap<>();
|
|
||||||
|
|
||||||
private final TerminalMetrics metrics;
|
|
||||||
// Decoded kitty images for this renderer's pane (kitty graphics state is per-terminal).
|
|
||||||
private final Map<KittyImageKey, Image> kittyImageCache = new HashMap<>();
|
|
||||||
|
|
||||||
GhosttyTerminalRenderer(TerminalMetrics metrics) {
|
|
||||||
this.metrics = metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void paintFull(GraphicsContext gc, RenderTarget target, boolean active) {
|
|
||||||
double px = Math.round(target.x());
|
|
||||||
double py = Math.round(target.y());
|
|
||||||
double width = target.width();
|
|
||||||
double height = target.height();
|
|
||||||
gc.save();
|
|
||||||
clip(gc, px, py, width, height, target.clip());
|
|
||||||
drawContent(gc, target, target.snapshotFull(), px, py, width, height, active,
|
|
||||||
target.kittyEnabled() && hasKittyGraphics(target));
|
|
||||||
gc.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void paintIncremental(GraphicsContext gc, RenderTarget target, boolean active) {
|
|
||||||
double px = Math.round(target.x());
|
|
||||||
double py = Math.round(target.y());
|
|
||||||
double width = target.width();
|
|
||||||
double height = target.height();
|
|
||||||
gc.save();
|
|
||||||
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.
|
|
||||||
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) {
|
|
||||||
drawContent(gc, target, snapshot, px, py, width, height, active, false);
|
|
||||||
} else if (dirty == DIRTY_PARTIAL) {
|
|
||||||
drawDirtyRows(gc, snapshot, px, py, width, height, active);
|
|
||||||
}
|
|
||||||
// dirty == FALSE: nothing visible changed.
|
|
||||||
}
|
|
||||||
gc.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
|
||||||
GraphicsContext gc,
|
|
||||||
RenderTarget target,
|
|
||||||
RenderStateSnapshot snapshot,
|
|
||||||
double x,
|
|
||||||
double y,
|
|
||||||
double width,
|
|
||||||
double height,
|
|
||||||
boolean active,
|
|
||||||
boolean withKitty
|
|
||||||
) {
|
|
||||||
double cellWidth = metrics.cellWidth();
|
|
||||||
double lineHeight = metrics.lineHeight();
|
|
||||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
|
||||||
gc.setFill(PANE_BACKGROUND);
|
|
||||||
gc.fillRect(x, y, width, height);
|
|
||||||
gc.setFont(metrics.font());
|
|
||||||
|
|
||||||
double left = x + TerminalMetrics.PADDING;
|
|
||||||
double top = y + TerminalMetrics.PADDING;
|
|
||||||
double baseline = top + metrics.baselineOffset();
|
|
||||||
|
|
||||||
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds = withKitty
|
|
||||||
? kittyPlaceholderBounds(snapshot)
|
|
||||||
: Map.of();
|
|
||||||
|
|
||||||
if (withKitty) {
|
|
||||||
drawKittyGraphics(gc, target, KittyPlacementLayer.BELOW_TEXT, placeholderBounds, left, top, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot != null) {
|
|
||||||
double contentBottom = top + snapshot.rows() * lineHeight;
|
|
||||||
fillVerticalPadding(gc, snapshot, x, y, width, height, top, contentBottom);
|
|
||||||
for (RenderRow row : snapshot.renderRows()) {
|
|
||||||
double y0 = Math.floor(top + (row.row() * lineHeight));
|
|
||||||
double y1 = Math.ceil(top + ((row.row() + 1) * lineHeight));
|
|
||||||
paintSidePadding(gc, row, x, width, left, cellWidth, y0, y1 - y0);
|
|
||||||
drawRow(gc, row, left, top, baseline, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
drawCursor(gc, snapshot, left, top, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withKitty) {
|
|
||||||
drawKittyGraphics(gc, target, KittyPlacementLayer.ABOVE_TEXT, placeholderBounds, left, top, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawBorder(gc, x, y, width, height, active);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Incremental render: repaint only the rows ghostty flagged dirty, then restore the
|
|
||||||
// cursor and border. The local band tracks the repainted span only so the border redraw
|
|
||||||
// can be limited to it.
|
|
||||||
private void drawDirtyRows(
|
|
||||||
GraphicsContext gc,
|
|
||||||
RenderStateSnapshot snapshot,
|
|
||||||
double px,
|
|
||||||
double py,
|
|
||||||
double pw,
|
|
||||||
double ph,
|
|
||||||
boolean active
|
|
||||||
) {
|
|
||||||
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;
|
|
||||||
boolean cursorRowDirty = false;
|
|
||||||
double bandMin = Double.POSITIVE_INFINITY;
|
|
||||||
double bandMax = Double.NEGATIVE_INFINITY;
|
|
||||||
for (RenderRow row : snapshot.renderRows()) {
|
|
||||||
if (!row.dirty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Snap the row band to integer pixels and paint opaque: a fractional-height fill
|
|
||||||
// would leave sub-pixel seams between rows.
|
|
||||||
double y0 = Math.floor(top + (row.row() * lineHeight));
|
|
||||||
double y1 = Math.ceil(top + ((row.row() + 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);
|
|
||||||
bandMin = Math.min(bandMin, y0);
|
|
||||||
bandMax = Math.max(bandMax, y1);
|
|
||||||
// Edge rows also own the top/bottom padding strip; repaint it and extend the
|
|
||||||
// band so panes stacked above get restored over it too.
|
|
||||||
if (row.row() == 0) {
|
|
||||||
gc.setFill(rowEdgeBackground(row, true));
|
|
||||||
gc.fillRect(px, py, pw, top - py);
|
|
||||||
bandMin = Math.min(bandMin, py);
|
|
||||||
}
|
|
||||||
if (row.row() == lastRow) {
|
|
||||||
gc.setFill(rowEdgeBackground(row, true));
|
|
||||||
gc.fillRect(px, contentBottom, pw, py + ph - contentBottom);
|
|
||||||
bandMax = Math.max(bandMax, py + ph);
|
|
||||||
}
|
|
||||||
if (snapshot.cursorViewportHasValue() && row.row() == snapshot.cursorViewportY()) {
|
|
||||||
cursorRowDirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bandMin > bandMax) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The cursor overlays its cell; redraw it only when its row was repainted, so we
|
|
||||||
// neither leave a stale cursor nor stack the translucent overlay on itself.
|
|
||||||
if (cursorRowDirty) {
|
|
||||||
drawCursor(gc, snapshot, left, top, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
// 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();
|
|
||||||
clipRect(gc, px, bandMin, pw, bandMax - bandMin);
|
|
||||||
drawBorder(gc, px, py, pw, ph, active);
|
|
||||||
gc.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.setLineWidth(active ? 2.0 : 1.0);
|
|
||||||
gc.strokeRect(x + 0.5, y + 0.5, width - 1.0, height - 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
if (cell.inverse()) {
|
|
||||||
var fg = cell.foreground();
|
|
||||||
return fg.isPresent() ? toFxColor(fg.get()) : DEFAULT_FOREGROUND;
|
|
||||||
}
|
|
||||||
var bg = cell.background();
|
|
||||||
return bg.isPresent() ? toFxColor(bg.get()) : PANE_BACKGROUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Color rowEdgeBackground(RenderRow row, boolean firstCell) {
|
|
||||||
List<RenderCell> cells = row.cells();
|
|
||||||
if (cells.isEmpty()) {
|
|
||||||
return PANE_BACKGROUND;
|
|
||||||
}
|
|
||||||
return cellBackgroundColor(firstCell ? cells.get(0) : cells.get(cells.size() - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend the row's edge-cell backgrounds into the left/right padding (the margin and the
|
|
||||||
// right-edge rounding sliver), so the unused space matches the rendered content.
|
|
||||||
private void paintSidePadding(GraphicsContext gc, RenderRow row, double paneX, double paneWidth,
|
|
||||||
double contentLeft, double cellWidth, double yTop, double bandHeight) {
|
|
||||||
int columns = row.cells().size();
|
|
||||||
if (columns == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double contentRight = contentLeft + (columns * cellWidth);
|
|
||||||
gc.setFill(rowEdgeBackground(row, true));
|
|
||||||
gc.fillRect(paneX, yTop, contentLeft - paneX, bandHeight);
|
|
||||||
gc.setFill(rowEdgeBackground(row, false));
|
|
||||||
gc.fillRect(contentRight, yTop, paneX + paneWidth - contentRight, bandHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill the top/bottom padding strips with the top/bottom row's edge colour.
|
|
||||||
private void fillVerticalPadding(GraphicsContext gc, RenderStateSnapshot snapshot,
|
|
||||||
double paneX, double paneY, double paneWidth, double paneHeight, double contentTop, double contentBottom) {
|
|
||||||
List<RenderRow> rows = snapshot.renderRows();
|
|
||||||
if (rows.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gc.setFill(rowEdgeBackground(rows.get(0), true));
|
|
||||||
gc.fillRect(paneX, paneY, paneWidth, contentTop - paneY);
|
|
||||||
gc.setFill(rowEdgeBackground(rows.get(rows.size() - 1), true));
|
|
||||||
gc.fillRect(paneX, contentBottom, paneWidth, paneY + paneHeight - contentBottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void drawRow(
|
|
||||||
GraphicsContext gc,
|
|
||||||
RenderRow row,
|
|
||||||
double left,
|
|
||||||
double top,
|
|
||||||
double baseline,
|
|
||||||
double cellWidth,
|
|
||||||
double lineHeight
|
|
||||||
) {
|
|
||||||
for (RenderCell cell : row.cells()) {
|
|
||||||
if (cell.kittyPlaceholder().isPresent()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
double x = left + (cell.column() * cellWidth);
|
|
||||||
double cellTop = top + (row.row() * lineHeight);
|
|
||||||
|
|
||||||
// Resolve fg/bg (null bg = terminal default, painted by the pane background).
|
|
||||||
// Avoid Optional.map's allocation on this hot path.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
double y = baseline + (row.row() * lineHeight);
|
|
||||||
gc.setFill(fg);
|
|
||||||
gc.fillText(cell.text(), x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Color toFxColor(RenderColor color) {
|
|
||||||
int key = (color.red() << 16) | (color.green() << 8) | color.blue();
|
|
||||||
Color cached = COLOR_CACHE.get(key);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
if (COLOR_CACHE.size() >= 4096) {
|
|
||||||
COLOR_CACHE.clear();
|
|
||||||
}
|
|
||||||
Color created = Color.rgb(color.red(), color.green(), color.blue());
|
|
||||||
COLOR_CACHE.put(key, created);
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void drawCursor(GraphicsContext gc, RenderStateSnapshot snapshot, double left, double top, double cellWidth, double lineHeight) {
|
|
||||||
if (!snapshot.cursorVisible() || !snapshot.cursorViewportHasValue()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double x = left + (snapshot.cursorViewportX() * cellWidth);
|
|
||||||
double y = top + (snapshot.cursorViewportY() * lineHeight);
|
|
||||||
gc.setStroke(Color.rgb(225, 229, 235));
|
|
||||||
gc.setFill(Color.rgb(225, 229, 235, 0.28));
|
|
||||||
gc.setLineWidth(1.5);
|
|
||||||
|
|
||||||
RenderCursorStyle style = snapshot.cursorStyle();
|
|
||||||
if (style == RenderCursorStyle.BAR) {
|
|
||||||
gc.strokeLine(x + 0.5, y + 2.0, x + 0.5, y + lineHeight - 2.0);
|
|
||||||
} else if (style == RenderCursorStyle.UNDERLINE) {
|
|
||||||
gc.strokeLine(x + 1.0, y + lineHeight - 2.0, x + cellWidth - 1.0, y + lineHeight - 2.0);
|
|
||||||
} else if (style == RenderCursorStyle.BLOCK) {
|
|
||||||
gc.fillRect(x + 0.5, y + 1.0, Math.max(1.0, cellWidth - 1.0), Math.max(1.0, lineHeight - 2.0));
|
|
||||||
} else {
|
|
||||||
gc.strokeRect(x + 0.5, y + 1.0, Math.max(1.0, cellWidth - 1.0), Math.max(1.0, lineHeight - 2.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Kitty graphics --------------------------------------------------------------
|
|
||||||
|
|
||||||
private static boolean hasKittyGraphics(RenderTarget target) {
|
|
||||||
return target.kittyGraphics()
|
|
||||||
.map(graphics -> !graphics.placements().isEmpty())
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawKittyGraphics(
|
|
||||||
GraphicsContext gc,
|
|
||||||
RenderTarget target,
|
|
||||||
KittyPlacementLayer layer,
|
|
||||||
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds,
|
|
||||||
double originX,
|
|
||||||
double originY,
|
|
||||||
double cellWidth,
|
|
||||||
double lineHeight
|
|
||||||
) {
|
|
||||||
target.kittyGraphics().ifPresent(graphics -> {
|
|
||||||
for (KittyPlacement placement : graphics.placements(layer)) {
|
|
||||||
Image image = imageFor(placement);
|
|
||||||
if (image == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (placement.virtual()) {
|
|
||||||
drawVirtualKittyPlacement(gc, placement, image, placeholderBounds, originX, originY, cellWidth, lineHeight);
|
|
||||||
} else {
|
|
||||||
drawPinnedKittyPlacement(gc, placement, image, originX, originY, cellWidth, lineHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 static void drawVirtualKittyPlacement(
|
|
||||||
GraphicsContext gc,
|
|
||||||
KittyPlacement placement,
|
|
||||||
Image image,
|
|
||||||
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds,
|
|
||||||
double originX,
|
|
||||||
double originY,
|
|
||||||
double cellWidth,
|
|
||||||
double lineHeight
|
|
||||||
) {
|
|
||||||
KittyPlaceholderBounds bounds = placeholderBounds.get(new KittyPlaceholderKey(placement.imageId(), placement.placementId()));
|
|
||||||
if (bounds == null) {
|
|
||||||
bounds = placeholderBounds.get(new KittyPlaceholderKey(placement.imageId(), 0));
|
|
||||||
}
|
|
||||||
if (bounds == null && placement.placementId() == 0) {
|
|
||||||
bounds = placeholderBounds.entrySet().stream()
|
|
||||||
.filter(entry -> entry.getKey().imageId() == placement.imageId())
|
|
||||||
.map(Map.Entry::getValue)
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
if (bounds == null || bounds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SourceRect source = sourceRect(placement, image);
|
|
||||||
if (source.width() <= 0.0 || source.height() <= 0.0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long gridColumns = gridColumns(placement, bounds);
|
|
||||||
long gridRows = gridRows(placement, bounds);
|
|
||||||
double sourceCellWidth = source.width() / Math.max(1L, gridColumns);
|
|
||||||
double sourceCellHeight = source.height() / Math.max(1L, gridRows);
|
|
||||||
|
|
||||||
double sourceX = source.x() + (bounds.minSourceColumn * sourceCellWidth);
|
|
||||||
double sourceY = source.y() + (bounds.minSourceRow * sourceCellHeight);
|
|
||||||
double sourceWidth = bounds.sourceColumns() * sourceCellWidth;
|
|
||||||
double sourceHeight = bounds.sourceRows() * sourceCellHeight;
|
|
||||||
double x = originX + (bounds.minColumn * cellWidth);
|
|
||||||
double y = originY + (bounds.minRow * lineHeight);
|
|
||||||
double availableWidth = bounds.columns() * cellWidth;
|
|
||||||
double availableHeight = bounds.rows() * lineHeight;
|
|
||||||
|
|
||||||
if (sourceWidth <= 0.0 || sourceHeight <= 0.0 || availableWidth <= 0.0 || availableHeight <= 0.0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double scale = Math.min(availableWidth / sourceWidth, availableHeight / sourceHeight);
|
|
||||||
double width = sourceWidth * scale;
|
|
||||||
double height = sourceHeight * scale;
|
|
||||||
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 Image decodeImage(KittyImageSnapshot snapshot, byte[] data) {
|
|
||||||
if (snapshot.compression() != KittyImageCompression.NONE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.format() == KittyImageFormat.PNG) {
|
|
||||||
return new Image(new ByteArrayInputStream(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = Math.toIntExact(snapshot.width());
|
|
||||||
int height = Math.toIntExact(snapshot.height());
|
|
||||||
WritableImage image = new WritableImage(width, height);
|
|
||||||
|
|
||||||
if (snapshot.format() == KittyImageFormat.RGBA) {
|
|
||||||
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), rgbaToBgra(data), 0, width * 4);
|
|
||||||
} else if (snapshot.format() == KittyImageFormat.RGB) {
|
|
||||||
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteRgbInstance(), data, 0, width * 3);
|
|
||||||
}
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] rgbaToBgra(byte[] rgba) {
|
|
||||||
byte[] bgra = new byte[rgba.length];
|
|
||||||
for (int i = 0; i + 3 < rgba.length; i += 4) {
|
|
||||||
bgra[i] = rgba[i + 2];
|
|
||||||
bgra[i + 1] = rgba[i + 1];
|
|
||||||
bgra[i + 2] = rgba[i];
|
|
||||||
bgra[i + 3] = rgba[i + 3];
|
|
||||||
}
|
|
||||||
return bgra;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<KittyPlaceholderKey, KittyPlaceholderBounds> kittyPlaceholderBounds(RenderStateSnapshot snapshot) {
|
|
||||||
if (snapshot == null) {
|
|
||||||
return Map.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<KittyPlaceholderKey, KittyPlaceholderBounds> result = new HashMap<>();
|
|
||||||
for (RenderRow row : snapshot.renderRows()) {
|
|
||||||
for (RenderCell cell : row.cells()) {
|
|
||||||
cell.kittyPlaceholder().ifPresent(placeholder -> {
|
|
||||||
KittyPlaceholderKey key = new KittyPlaceholderKey(placeholder.imageId(), placeholder.placementId());
|
|
||||||
result.computeIfAbsent(key, ignored -> new KittyPlaceholderBounds()).include(row.row(), cell.column(), placeholder);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// identity + dimensions + payload length are enough to key the decoded-image cache, and
|
|
||||||
// we avoid fingerprinting the whole payload — which previously ran once per frame per
|
|
||||||
// placement (O(image size)) just to look the image up.
|
|
||||||
private record KittyImageKey(long id, long number, long width, long height, KittyImageFormat format, int dataLength) {
|
|
||||||
private static KittyImageKey of(KittyImageSnapshot snapshot, byte[] data) {
|
|
||||||
return new KittyImageKey(
|
|
||||||
snapshot.id(),
|
|
||||||
snapshot.number(),
|
|
||||||
snapshot.width(),
|
|
||||||
snapshot.height(),
|
|
||||||
snapshot.format(),
|
|
||||||
data.length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.gregor.jprototerm;
|
|
||||||
|
|
||||||
import dev.jlibghostty.KittyGraphics;
|
|
||||||
import dev.jlibghostty.RenderStateSnapshot;
|
|
||||||
import javafx.scene.shape.Shape;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The read-only view of a pane that a {@link TerminalRenderer} draws: its on-screen rect, its
|
|
||||||
* current render snapshot, and its kitty-graphics state. Decoupling the renderer from
|
|
||||||
* {@link TerminalPane} through this interface lets the renderer be swapped (e.g. a debug
|
|
||||||
* renderer that just outlines bounds and clip bands) and unit-tested against a synthetic
|
|
||||||
* target without a real terminal.
|
|
||||||
*/
|
|
||||||
interface RenderTarget {
|
|
||||||
double x();
|
|
||||||
|
|
||||||
double y();
|
|
||||||
|
|
||||||
double width();
|
|
||||||
|
|
||||||
double height();
|
|
||||||
|
|
||||||
/** Whether kitty graphics should be drawn for this target at all. */
|
|
||||||
boolean kittyEnabled();
|
|
||||||
|
|
||||||
Optional<KittyGraphics> kittyGraphics();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Incremental snapshot: only rows that changed since the last frame are populated. May be
|
|
||||||
* {@code null} before the first snapshot exists.
|
|
||||||
*/
|
|
||||||
RenderStateSnapshot snapshot();
|
|
||||||
|
|
||||||
/** Full snapshot with every row populated, regardless of dirty state. */
|
|
||||||
RenderStateSnapshot snapshotFull();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The region this target may draw into, or {@code null} to clip to its plain rect. Set at
|
|
||||||
* layout time (a tiled pane gets its rect minus the floating panes that cover it), so the
|
|
||||||
* renderer can clip its own output and never paint over a pane on top.
|
|
||||||
*/
|
|
||||||
Shape clip();
|
|
||||||
}
|
|
||||||
@@ -108,9 +108,6 @@ final class Tab implements AutoCloseable {
|
|||||||
floatingWidth,
|
floatingWidth,
|
||||||
floatingHeight);
|
floatingHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
tiled.forEach(pane -> pane.setClip(null));
|
|
||||||
floating.forEach(pane -> pane.setClip(null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean navigate(Direction direction) {
|
boolean navigate(Direction direction) {
|
||||||
|
|||||||
@@ -12,35 +12,27 @@ import dev.jlibghostty.RenderStateSnapshot;
|
|||||||
import dev.jlibghostty.ScrollViewport;
|
import dev.jlibghostty.ScrollViewport;
|
||||||
import dev.jlibghostty.Terminal;
|
import dev.jlibghostty.Terminal;
|
||||||
import dev.jlibghostty.TerminalOptions;
|
import dev.jlibghostty.TerminalOptions;
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
|
||||||
import javafx.scene.shape.Shape;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,
|
||||||
* and its on-screen geometry and grid. It does not draw itself — it is a {@link RenderTarget}
|
* and its on-screen geometry and grid. It does not draw itself; {@link TerminalPaneNode}
|
||||||
* that a {@link TerminalRenderer} paints. {@link #paintFull}/{@link #paintIncremental} are the
|
* reads snapshots from it and represents the visible rows and kitty graphics as JavaFX nodes.
|
||||||
* only rendering API exposed to the {@link Compositor}, and they just delegate to that
|
|
||||||
* renderer; the compositor decides z-order and which rect each pane occupies.
|
|
||||||
*/
|
*/
|
||||||
public final class TerminalPane implements AutoCloseable, RenderTarget {
|
public final class TerminalPane implements AutoCloseable {
|
||||||
private final Terminal terminal;
|
private final Terminal terminal;
|
||||||
private final TerminalMetrics metrics;
|
private final TerminalMetrics metrics;
|
||||||
private final boolean kittyEnabled;
|
private final boolean kittyEnabled;
|
||||||
// Run on every content change so the owning tab can bump its content version — the
|
// Run on every content change so the owning tab can bump its content version — the
|
||||||
// compositor's O(1) "did the current tab change?" gate.
|
// compositor's O(1) "did the current tab change?" gate.
|
||||||
private final Runnable onContentChange;
|
private final Runnable onContentChange;
|
||||||
private final TerminalRenderer renderer;
|
|
||||||
private final MouseEncoder mouseEncoder = new MouseEncoder();
|
private final MouseEncoder mouseEncoder = new MouseEncoder();
|
||||||
// A persistent render state (reused across frames) is what makes ghostty's per-row dirty
|
// A persistent render state (reused across frames) is what makes ghostty's per-row dirty
|
||||||
// tracking meaningful: update() accumulates dirty since the last resetDirty().
|
// tracking meaningful: update() accumulates dirty since the last resetDirty().
|
||||||
private final RenderState renderState = new RenderState();
|
private final RenderState renderState = new RenderState();
|
||||||
private RenderStateSnapshot cachedSnapshot;
|
private RenderStateSnapshot cachedSnapshot;
|
||||||
private ShellSession session;
|
private ShellSession session;
|
||||||
// Clip region for rendering (rect minus the panes covering this one), set at layout time;
|
|
||||||
// null means clip to the plain bounds. See RenderTarget#clip().
|
|
||||||
private Shape clip;
|
|
||||||
private double x;
|
private double x;
|
||||||
private double y;
|
private double y;
|
||||||
private double width;
|
private double width;
|
||||||
@@ -53,12 +45,11 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
private long snapshotVersion = -1;
|
private long snapshotVersion = -1;
|
||||||
|
|
||||||
private TerminalPane(Terminal terminal, TerminalMetrics metrics, boolean kittyEnabled,
|
private TerminalPane(Terminal terminal, TerminalMetrics metrics, boolean kittyEnabled,
|
||||||
Runnable onContentChange, TerminalRenderer renderer, int columns, int rows) {
|
Runnable onContentChange, int columns, int rows) {
|
||||||
this.terminal = terminal;
|
this.terminal = terminal;
|
||||||
this.metrics = metrics;
|
this.metrics = metrics;
|
||||||
this.kittyEnabled = kittyEnabled;
|
this.kittyEnabled = kittyEnabled;
|
||||||
this.onContentChange = onContentChange;
|
this.onContentChange = onContentChange;
|
||||||
this.renderer = renderer;
|
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
this.rows = rows;
|
this.rows = rows;
|
||||||
}
|
}
|
||||||
@@ -75,8 +66,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
int rows = heightPx > 0 ? metrics.rowsFor(heightPx) : config.rows();
|
int rows = heightPx > 0 ? metrics.rowsFor(heightPx) : config.rows();
|
||||||
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, config.maxScrollback()));
|
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, config.maxScrollback()));
|
||||||
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
|
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
|
||||||
TerminalPane pane = new TerminalPane(terminal, metrics, config.kittyGraphics(), onContentChange,
|
TerminalPane pane = new TerminalPane(terminal, metrics, config.kittyGraphics(), onContentChange, columns, rows);
|
||||||
new GhosttyTerminalRenderer(metrics), columns, rows);
|
|
||||||
pane.refresh();
|
pane.refresh();
|
||||||
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, columns, rows));
|
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, columns, rows));
|
||||||
return pane;
|
return pane;
|
||||||
@@ -153,7 +143,6 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
* Snapshotting is deferred here rather than done in refresh(), so a burst of writes
|
* Snapshotting is deferred here rather than done in refresh(), so a burst of writes
|
||||||
* between two frames collapses into a single snapshot.
|
* between two frames collapses into a single snapshot.
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
public RenderStateSnapshot snapshot() {
|
public RenderStateSnapshot snapshot() {
|
||||||
return takeSnapshot(false);
|
return takeSnapshot(false);
|
||||||
}
|
}
|
||||||
@@ -162,7 +151,6 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
* Full snapshot with every row's cells populated. Used where the whole pane is redrawn
|
* Full snapshot with every row's cells populated. Used where the whole pane is redrawn
|
||||||
* regardless of dirty state (the kitty-graphics path).
|
* regardless of dirty state (the kitty-graphics path).
|
||||||
*/
|
*/
|
||||||
@Override
|
|
||||||
public RenderStateSnapshot snapshotFull() {
|
public RenderStateSnapshot snapshotFull() {
|
||||||
return takeSnapshot(true);
|
return takeSnapshot(true);
|
||||||
}
|
}
|
||||||
@@ -195,34 +183,28 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
return contentVersion;
|
return contentVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean kittyEnabled() {
|
public boolean kittyEnabled() {
|
||||||
return kittyEnabled;
|
return kittyEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<KittyGraphics> kittyGraphics() {
|
public Optional<KittyGraphics> kittyGraphics() {
|
||||||
synchronized (terminal) {
|
synchronized (terminal) {
|
||||||
return terminal.kittyGraphics();
|
return terminal.kittyGraphics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public double x() {
|
public double x() {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public double y() {
|
public double y() {
|
||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public double width() {
|
public double width() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public double height() {
|
public double height() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@@ -234,16 +216,6 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
this.height = height;
|
this.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the clip region applied on the next paints (see {@link RenderTarget#clip()}). */
|
|
||||||
public void setClip(Shape clip) {
|
|
||||||
this.clip = clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Shape clip() {
|
|
||||||
return clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Recompute the ghostty grid from the current bounds and the shared cell metrics. */
|
/** Recompute the ghostty grid from the current bounds and the shared cell metrics. */
|
||||||
public void fitToBounds() {
|
public void fitToBounds() {
|
||||||
int columns = metrics.columnsFor(width);
|
int columns = metrics.columnsFor(width);
|
||||||
@@ -280,16 +252,6 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
onContentChange.run();
|
onContentChange.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Paint the whole pane; see {@link TerminalRenderer#paintFull}. */
|
|
||||||
public void paintFull(GraphicsContext gc, boolean active) {
|
|
||||||
renderer.paintFull(gc, this, active);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Repaint what changed; see {@link TerminalRenderer#paintIncremental}. */
|
|
||||||
public void paintIncremental(GraphicsContext gc, boolean active) {
|
|
||||||
renderer.paintIncremental(gc, this, active);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package com.gregor.jprototerm;
|
|
||||||
|
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
|
||||||
import javafx.scene.shape.ClosePath;
|
|
||||||
import javafx.scene.shape.LineTo;
|
|
||||||
import javafx.scene.shape.MoveTo;
|
|
||||||
import javafx.scene.shape.Path;
|
|
||||||
import javafx.scene.shape.PathElement;
|
|
||||||
import javafx.scene.shape.Shape;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a {@link RenderTarget} onto a JavaFX canvas. The {@link Compositor} owns positioning
|
|
||||||
* and z-order; a renderer only fills the target's rect, clipped to the target's {@link
|
|
||||||
* RenderTarget#clip() clip region} so a repaint can never bleed over a pane on top.
|
|
||||||
* Implementations can change the look entirely — {@link GhosttyTerminalRenderer} is the real
|
|
||||||
* terminal renderer; a debug renderer could outline pane bounds instead.
|
|
||||||
*
|
|
||||||
* <p>A renderer may hold per-target state (e.g. a decoded-image cache), so an instance belongs
|
|
||||||
* to a single {@link TerminalPane}.
|
|
||||||
*/
|
|
||||||
abstract class TerminalRenderer {
|
|
||||||
/** Paint the whole target into its rect, clipped to its clip region. */
|
|
||||||
abstract void paintFull(GraphicsContext gc, RenderTarget target, boolean active);
|
|
||||||
|
|
||||||
/** Repaint only what changed since the last frame, clipped to the target's clip region. */
|
|
||||||
abstract void paintIncremental(GraphicsContext gc, RenderTarget target, boolean active);
|
|
||||||
|
|
||||||
protected static void clipRect(GraphicsContext gc, double x, double y, double width, double height) {
|
|
||||||
gc.beginPath();
|
|
||||||
gc.rect(x, y, width, height);
|
|
||||||
gc.clip();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clip to {@code region} if given (the pane's rect minus the panes covering it, computed by
|
|
||||||
* {@code Shape.subtract} at layout), otherwise to the plain rect. The region is a rectilinear
|
|
||||||
* path, so it replays onto the canvas as move/line/close segments.
|
|
||||||
*/
|
|
||||||
protected static void clip(GraphicsContext gc, double x, double y, double width, double height, Shape region) {
|
|
||||||
if (region == null) {
|
|
||||||
clipRect(gc, x, y, width, height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var elements = ((Path) region).getElements();
|
|
||||||
gc.beginPath();
|
|
||||||
if (elements.isEmpty()) {
|
|
||||||
gc.rect(x, y, 0.0, 0.0); // fully covered: clip to nothing
|
|
||||||
}
|
|
||||||
for (PathElement element : elements) {
|
|
||||||
if (element instanceof MoveTo moveTo) {
|
|
||||||
gc.moveTo(moveTo.getX(), moveTo.getY());
|
|
||||||
} else if (element instanceof LineTo lineTo) {
|
|
||||||
gc.lineTo(lineTo.getX(), lineTo.getY());
|
|
||||||
} else if (element instanceof ClosePath) {
|
|
||||||
gc.closePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gc.clip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user