From 9c98d8778355c4d83f4912068249a2f345f962d4 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Sun, 31 May 2026 16:21:38 +0200 Subject: [PATCH] recover abstract terminal renderer --- .../gregor/jprototerm/TerminalRenderer.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/main/java/com/gregor/jprototerm/TerminalRenderer.java diff --git a/src/main/java/com/gregor/jprototerm/TerminalRenderer.java b/src/main/java/com/gregor/jprototerm/TerminalRenderer.java new file mode 100644 index 0000000..286de53 --- /dev/null +++ b/src/main/java/com/gregor/jprototerm/TerminalRenderer.java @@ -0,0 +1,60 @@ +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. + * + *

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(); + } +}