images work
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,8 @@ 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.KeyModifiers;
|
||||
import dev.jlibghostty.MouseButton;
|
||||
@@ -30,6 +32,7 @@ import javafx.scene.text.Text;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class TerminalCanvasView {
|
||||
@@ -39,9 +42,15 @@ public final class TerminalCanvasView {
|
||||
private final Canvas canvas = new Canvas();
|
||||
private final TerminalWorkspace workspace;
|
||||
private final AppConfig config;
|
||||
private final Map<Long, Image> kittyImageCache = new HashMap<>();
|
||||
private final Map<KittyImageKey, Image> kittyImageCache = new HashMap<>();
|
||||
private final Map<TerminalPane, PaneRenderCache> paneRenderCache = new HashMap<>();
|
||||
private String fontFamily;
|
||||
private double fontSize;
|
||||
private Font cachedFont;
|
||||
private FontMetrics cachedMetrics;
|
||||
private String cachedFontFamily;
|
||||
private double cachedFontSize;
|
||||
private String lastRenderKey;
|
||||
private boolean mouseButtonPressed;
|
||||
private MouseButton pressedButton = MouseButton.UNKNOWN;
|
||||
|
||||
@@ -65,53 +74,111 @@ public final class TerminalCanvasView {
|
||||
public void setFont(String family, double size) {
|
||||
this.fontFamily = family;
|
||||
this.fontSize = size;
|
||||
cachedFont = null;
|
||||
cachedMetrics = null;
|
||||
paneRenderCache.clear();
|
||||
lastRenderKey = null;
|
||||
}
|
||||
|
||||
public void render() {
|
||||
double width = canvas.getWidth();
|
||||
double height = canvas.getHeight();
|
||||
workspace.layout(width, height);
|
||||
Font font = currentFont();
|
||||
FontMetrics metrics = currentFontMetrics();
|
||||
List<TerminalPane> panes = workspace.panes();
|
||||
|
||||
String renderKey = renderKey(width, height, metrics, panes);
|
||||
if (renderKey.equals(lastRenderKey)) {
|
||||
return;
|
||||
}
|
||||
lastRenderKey = renderKey;
|
||||
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
gc.setFill(Color.rgb(16, 16, 18));
|
||||
gc.fillRect(0, 0, width, height);
|
||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||
|
||||
for (TerminalPane pane : workspace.panes()) {
|
||||
drawPane(gc, pane);
|
||||
paneRenderCache.keySet().removeIf(pane -> !panes.contains(pane));
|
||||
for (TerminalPane pane : panes) {
|
||||
drawPane(gc, pane, font, metrics);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPane(GraphicsContext gc, TerminalPane pane) {
|
||||
gc.save();
|
||||
gc.beginPath();
|
||||
gc.rect(pane.x(), pane.y(), pane.width(), pane.height());
|
||||
gc.clip();
|
||||
private void drawPane(GraphicsContext gc, TerminalPane pane, Font font, FontMetrics metrics) {
|
||||
if (config.kittyGraphics() && paneHasKittyGraphics(pane)) {
|
||||
paneRenderCache.remove(pane);
|
||||
gc.save();
|
||||
gc.beginPath();
|
||||
gc.rect(pane.x(), pane.y(), pane.width(), pane.height());
|
||||
gc.clip();
|
||||
drawPaneContent(gc, pane, font, metrics, pane.x(), pane.y(), pane.width(), pane.height(), false);
|
||||
gc.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
PaneRenderCache cache = paneRenderCache.computeIfAbsent(pane, ignored -> new PaneRenderCache());
|
||||
String cacheKey = paneCacheKey(pane, metrics);
|
||||
int imageWidth = Math.max(1, (int) Math.ceil(pane.width()));
|
||||
int imageHeight = Math.max(1, (int) Math.ceil(pane.height()));
|
||||
if (cache.image == null || cache.canvas == null || cache.imageWidth != imageWidth || cache.imageHeight != imageHeight || !cacheKey.equals(cache.key)) {
|
||||
cache.canvas = new Canvas(imageWidth, imageHeight);
|
||||
drawPaneContent(cache.canvas.getGraphicsContext2D(), pane, font, metrics, 0.0, 0.0, imageWidth, imageHeight, true);
|
||||
cache.image = new WritableImage(imageWidth, imageHeight);
|
||||
cache.canvas.snapshot(null, cache.image);
|
||||
cache.imageWidth = imageWidth;
|
||||
cache.imageHeight = imageHeight;
|
||||
cache.key = cacheKey;
|
||||
}
|
||||
|
||||
gc.drawImage(cache.image, pane.x(), pane.y());
|
||||
}
|
||||
|
||||
private void drawPaneContent(
|
||||
GraphicsContext gc,
|
||||
TerminalPane pane,
|
||||
Font font,
|
||||
FontMetrics metrics,
|
||||
double x,
|
||||
double y,
|
||||
double width,
|
||||
double height,
|
||||
boolean clear
|
||||
) {
|
||||
if (clear) {
|
||||
gc.clearRect(x, y, width, height);
|
||||
}
|
||||
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||
if (pane.floating()) {
|
||||
gc.setGlobalAlpha(0.96);
|
||||
}
|
||||
gc.setFill(Color.rgb(9, 10, 12));
|
||||
gc.fillRect(pane.x(), pane.y(), pane.width(), pane.height());
|
||||
gc.fillRect(x, y, width, height);
|
||||
gc.setGlobalAlpha(1.0);
|
||||
|
||||
gc.setStroke(workspace.isActive(pane) ? Color.rgb(87, 166, 255) : Color.rgb(52, 57, 65));
|
||||
gc.setLineWidth(workspace.isActive(pane) ? 2.0 : 1.0);
|
||||
gc.strokeRect(pane.x() + 0.5, pane.y() + 0.5, pane.width() - 1.0, pane.height() - 1.0);
|
||||
gc.strokeRect(x + 0.5, y + 0.5, width - 1.0, height - 1.0);
|
||||
|
||||
Font font = Font.font(fontFamily, fontSize);
|
||||
gc.setFont(font);
|
||||
|
||||
FontMetrics metrics = measureFontMetrics(font);
|
||||
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth));
|
||||
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
|
||||
int columns = Math.max(1, (int) ((width - 24.0) / metrics.cellWidth));
|
||||
int rows = Math.max(1, (int) ((height - 24.0) / metrics.lineHeight));
|
||||
pane.resize(columns, rows, (int) Math.round(metrics.cellWidth), (int) Math.round(metrics.lineHeight));
|
||||
|
||||
double left = pane.x() + 12.0;
|
||||
double top = pane.y() + 12.0;
|
||||
double left = x + 12.0;
|
||||
double top = y + 12.0;
|
||||
double baseline = top + metrics.baselineOffset;
|
||||
|
||||
RenderStateSnapshot snapshot = pane.renderSnapshot();
|
||||
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds = config.kittyGraphics()
|
||||
? kittyPlaceholderBounds(snapshot)
|
||||
: Map.of();
|
||||
|
||||
if (config.kittyGraphics()) {
|
||||
drawKittyGraphics(gc, pane, KittyPlacementLayer.BELOW_TEXT, placeholderBounds, left, top, metrics.cellWidth, metrics.lineHeight);
|
||||
}
|
||||
|
||||
if (snapshot != null) {
|
||||
for (RenderRow row : snapshot.renderRows()) {
|
||||
drawRow(gc, row, left, top, baseline, metrics.cellWidth, metrics.lineHeight);
|
||||
@@ -123,9 +190,8 @@ public final class TerminalCanvasView {
|
||||
}
|
||||
|
||||
if (config.kittyGraphics()) {
|
||||
drawKittyGraphics(gc, pane, left, top, metrics.cellWidth, metrics.lineHeight);
|
||||
drawKittyGraphics(gc, pane, KittyPlacementLayer.ABOVE_TEXT, placeholderBounds, left, top, metrics.cellWidth, metrics.lineHeight);
|
||||
}
|
||||
gc.restore();
|
||||
}
|
||||
|
||||
private static FontMetrics measureFontMetrics(Font font) {
|
||||
@@ -134,13 +200,80 @@ public final class TerminalCanvasView {
|
||||
double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight());
|
||||
double baselineOffset = -text.getLayoutBounds().getMinY();
|
||||
|
||||
String sample = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
Text cell = new Text(sample);
|
||||
Text cell = new Text("M");
|
||||
cell.setFont(font);
|
||||
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth() / sample.length());
|
||||
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth());
|
||||
return new FontMetrics(cellWidth, lineHeight, baselineOffset);
|
||||
}
|
||||
|
||||
private Font currentFont() {
|
||||
if (cachedFont == null || !fontFamily.equals(cachedFontFamily) || fontSize != cachedFontSize) {
|
||||
cachedFont = Font.font(fontFamily, fontSize);
|
||||
cachedMetrics = null;
|
||||
cachedFontFamily = fontFamily;
|
||||
cachedFontSize = fontSize;
|
||||
}
|
||||
return cachedFont;
|
||||
}
|
||||
|
||||
private FontMetrics currentFontMetrics() {
|
||||
if (cachedMetrics == null) {
|
||||
cachedMetrics = measureFontMetrics(currentFont());
|
||||
}
|
||||
return cachedMetrics;
|
||||
}
|
||||
|
||||
private String renderKey(double width, double height, FontMetrics metrics, List<TerminalPane> panes) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(width).append(':')
|
||||
.append(height).append(':')
|
||||
.append(workspace.version()).append(':')
|
||||
.append(fontFamily).append(':')
|
||||
.append(fontSize).append(':')
|
||||
.append(metrics.cellWidth).append(':')
|
||||
.append(metrics.lineHeight);
|
||||
for (TerminalPane pane : panes) {
|
||||
builder.append('|')
|
||||
.append(System.identityHashCode(pane)).append(',')
|
||||
.append(pane.renderVersion()).append(',')
|
||||
.append(workspace.isActive(pane)).append(',')
|
||||
.append(pane.x()).append(',')
|
||||
.append(pane.y()).append(',')
|
||||
.append(pane.width()).append(',')
|
||||
.append(pane.height());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String paneCacheKey(TerminalPane pane, FontMetrics metrics) {
|
||||
return pane.renderVersion()
|
||||
+ ":" + workspace.isActive(pane)
|
||||
+ ":" + pane.width()
|
||||
+ ":" + pane.height()
|
||||
+ ":" + fontFamily
|
||||
+ ":" + fontSize
|
||||
+ ":" + metrics.cellWidth
|
||||
+ ":" + metrics.lineHeight
|
||||
+ ":" + config.kittyGraphics();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void handleMousePressed(MouseEvent event) {
|
||||
canvas.requestFocus();
|
||||
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||
@@ -244,7 +377,7 @@ public final class TerminalCanvasView {
|
||||
return null;
|
||||
}
|
||||
|
||||
FontMetrics metrics = measureFontMetrics(Font.font(fontFamily, fontSize));
|
||||
FontMetrics metrics = currentFontMetrics();
|
||||
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth));
|
||||
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
|
||||
long cellWidth = Math.max(1L, Math.round(metrics.cellWidth));
|
||||
@@ -325,15 +458,19 @@ public final class TerminalCanvasView {
|
||||
double lineHeight
|
||||
) {
|
||||
for (RenderCell cell : row.cells()) {
|
||||
if (cell.kittyPlaceholder().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double x = left + (cell.column() * cellWidth);
|
||||
double cellTop = top + (row.row() * lineHeight);
|
||||
cell.background().ifPresent(background -> {
|
||||
gc.setFill(toFxColor(background));
|
||||
fillCellRect(gc, x, cellTop, cellWidth, lineHeight);
|
||||
gc.fillRect(x, cellTop, cellWidth, lineHeight);
|
||||
});
|
||||
if (cell.selected()) {
|
||||
gc.setFill(SELECTED_BACKGROUND);
|
||||
fillCellRect(gc, x, cellTop, cellWidth, lineHeight);
|
||||
gc.fillRect(x, cellTop, cellWidth, lineHeight);
|
||||
}
|
||||
if (cell.codepoints().length == 0) {
|
||||
continue;
|
||||
@@ -346,14 +483,6 @@ public final class TerminalCanvasView {
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillCellRect(GraphicsContext gc, double x, double y, double width, double height) {
|
||||
double x1 = Math.floor(x);
|
||||
double y1 = Math.floor(y);
|
||||
double x2 = Math.ceil(x + width);
|
||||
double y2 = Math.ceil(y + height);
|
||||
gc.fillRect(x1, y1, Math.max(1.0, x2 - x1), Math.max(1.0, y2 - y1));
|
||||
}
|
||||
|
||||
private static Color toFxColor(RenderColor color) {
|
||||
return Color.rgb(color.red(), color.green(), color.blue());
|
||||
}
|
||||
@@ -381,54 +510,174 @@ public final class TerminalCanvasView {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawKittyGraphics(GraphicsContext gc, TerminalPane pane, double originX, double originY, double cellWidth, double lineHeight) {
|
||||
private void drawKittyGraphics(
|
||||
GraphicsContext gc,
|
||||
TerminalPane pane,
|
||||
KittyPlacementLayer layer,
|
||||
Map<KittyPlaceholderKey, KittyPlaceholderBounds> placeholderBounds,
|
||||
double originX,
|
||||
double originY,
|
||||
double cellWidth,
|
||||
double lineHeight
|
||||
) {
|
||||
pane.kittyGraphics().ifPresent(graphics -> {
|
||||
for (KittyPlacement placement : graphics.placements()) {
|
||||
for (KittyPlacement placement : graphics.placements(layer)) {
|
||||
Image image = imageFor(placement);
|
||||
if (image == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KittyRenderInfo renderInfo = placement.renderInfo().orElse(null);
|
||||
double x = originX;
|
||||
double y = originY;
|
||||
double width = image.getWidth();
|
||||
double height = image.getHeight();
|
||||
|
||||
if (renderInfo != null) {
|
||||
x += renderInfo.viewportColumn() * cellWidth;
|
||||
y += renderInfo.viewportRow() * lineHeight;
|
||||
width = renderInfo.gridColumns() > 0 ? renderInfo.gridColumns() * cellWidth : renderInfo.pixelWidth();
|
||||
height = renderInfo.gridRows() > 0 ? renderInfo.gridRows() * lineHeight : renderInfo.pixelHeight();
|
||||
if (placement.virtual()) {
|
||||
drawVirtualKittyPlacement(gc, placement, image, placeholderBounds, originX, originY, cellWidth, lineHeight);
|
||||
} else {
|
||||
width = placement.columns() > 0 ? placement.columns() * cellWidth : width;
|
||||
height = placement.rows() > 0 ? placement.rows() * lineHeight : height;
|
||||
drawPinnedKittyPlacement(gc, placement, image, originX, originY, cellWidth, lineHeight);
|
||||
}
|
||||
|
||||
gc.drawImage(image, x + placement.xOffset(), y + placement.yOffset(), width, height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Image imageFor(KittyPlacement placement) {
|
||||
return placement.image()
|
||||
.map(snapshot -> kittyImageCache.computeIfAbsent(snapshot.id(), ignored -> decodeImage(snapshot)))
|
||||
.orElse(null);
|
||||
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 Image decodeImage(KittyImageSnapshot snapshot) {
|
||||
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 width = bounds.columns() * cellWidth;
|
||||
double height = bounds.rows() * lineHeight;
|
||||
|
||||
if (sourceWidth <= 0.0 || sourceHeight <= 0.0 || width <= 0.0 || height <= 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 boolean paneHasKittyGraphics(TerminalPane pane) {
|
||||
return pane.kittyGraphics()
|
||||
.map(graphics -> !graphics.placements().isEmpty())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private Image decodeImage(KittyImageSnapshot snapshot, byte[] data) {
|
||||
if (snapshot.compression() != KittyImageCompression.NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (snapshot.format() == KittyImageFormat.PNG) {
|
||||
return new Image(new ByteArrayInputStream(snapshot.data()));
|
||||
return new Image(new ByteArrayInputStream(data));
|
||||
}
|
||||
|
||||
int width = Math.toIntExact(snapshot.width());
|
||||
int height = Math.toIntExact(snapshot.height());
|
||||
WritableImage image = new WritableImage(width, height);
|
||||
byte[] data = snapshot.data();
|
||||
|
||||
if (snapshot.format() == KittyImageFormat.RGBA) {
|
||||
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), rgbaToBgra(data), 0, width * 4);
|
||||
@@ -454,4 +703,83 @@ public final class TerminalCanvasView {
|
||||
|
||||
private record MouseTarget(MouseEncoderSize size, long screenWidth, long screenHeight) {
|
||||
}
|
||||
|
||||
private record KittyImageKey(long id, long number, long width, long height, KittyImageFormat format, int dataLength, long fingerprint) {
|
||||
private static KittyImageKey of(KittyImageSnapshot snapshot, byte[] data) {
|
||||
return new KittyImageKey(
|
||||
snapshot.id(),
|
||||
snapshot.number(),
|
||||
snapshot.width(),
|
||||
snapshot.height(),
|
||||
snapshot.format(),
|
||||
data.length,
|
||||
fingerprint(data)
|
||||
);
|
||||
}
|
||||
|
||||
private static long fingerprint(byte[] data) {
|
||||
long hash = 0xcbf29ce484222325L;
|
||||
for (byte value : data) {
|
||||
hash ^= Byte.toUnsignedInt(value);
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PaneRenderCache {
|
||||
private Canvas canvas;
|
||||
private WritableImage image;
|
||||
private int imageWidth;
|
||||
private int imageHeight;
|
||||
private String key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ public final class TerminalPane implements AutoCloseable {
|
||||
private int rows;
|
||||
private int pixelWidth;
|
||||
private int pixelHeight;
|
||||
private long renderVersion;
|
||||
|
||||
private TerminalPane(Terminal terminal, int columns, int rows) {
|
||||
this.terminal = terminal;
|
||||
@@ -114,6 +115,10 @@ public final class TerminalPane implements AutoCloseable {
|
||||
return renderSnapshot.get();
|
||||
}
|
||||
|
||||
public long renderVersion() {
|
||||
return renderVersion;
|
||||
}
|
||||
|
||||
public Optional<KittyGraphics> kittyGraphics() {
|
||||
synchronized (terminal) {
|
||||
return terminal.kittyGraphics();
|
||||
@@ -182,6 +187,7 @@ public final class TerminalPane implements AutoCloseable {
|
||||
|
||||
private void refresh() {
|
||||
renderSnapshot.set(terminal.renderSnapshot());
|
||||
renderVersion++;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,6 +9,7 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
private final List<TerminalPane> panes = new ArrayList<>();
|
||||
private int activeIndex;
|
||||
private int hiddenFloatingFocusIndex = -1;
|
||||
private long version;
|
||||
|
||||
public TerminalWorkspace(AppConfig config) {
|
||||
this.config = config;
|
||||
@@ -38,10 +39,15 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
return activePane() == pane;
|
||||
}
|
||||
|
||||
public long version() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void focus(TerminalPane pane) {
|
||||
int index = panes.indexOf(pane);
|
||||
if (index >= 0 && pane.visible()) {
|
||||
if (index >= 0 && pane.visible() && activeIndex != index) {
|
||||
activeIndex = index;
|
||||
version++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +85,7 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
public void navigate(Direction direction) {
|
||||
TerminalPane current = activePane();
|
||||
if (current.floating() && navigateFloatingStack(direction)) {
|
||||
version++;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +94,10 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
.filter(pane -> pane != current)
|
||||
.filter(pane -> directionFilter(direction, current, pane))
|
||||
.min(Comparator.comparingDouble(pane -> distance(current, pane)))
|
||||
.ifPresent(pane -> activeIndex = panes.indexOf(pane));
|
||||
.ifPresent(pane -> {
|
||||
activeIndex = panes.indexOf(pane);
|
||||
version++;
|
||||
});
|
||||
}
|
||||
|
||||
public void toggleFloating() {
|
||||
@@ -105,10 +115,12 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex();
|
||||
floating.forEach(pane -> pane.setVisible(false));
|
||||
activeIndex = firstVisibleNonFloatingIndex();
|
||||
version++;
|
||||
} else {
|
||||
floating.forEach(pane -> pane.setVisible(true));
|
||||
activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1)));
|
||||
hiddenFloatingFocusIndex = -1;
|
||||
version++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,12 +128,14 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
TerminalPane pane = openPane(true);
|
||||
panes.add(pane);
|
||||
activeIndex = panes.size() - 1;
|
||||
version++;
|
||||
}
|
||||
|
||||
public void nextFloatingPane() {
|
||||
TerminalPane next = nextFloatingAfter(activeIndex);
|
||||
next.setVisible(true);
|
||||
activeIndex = panes.indexOf(next);
|
||||
version++;
|
||||
}
|
||||
|
||||
public void closeActivePane() {
|
||||
@@ -136,6 +150,7 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
active.close();
|
||||
activeIndex = adjustIndexAfterRemoval(previous, removed);
|
||||
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
|
||||
version++;
|
||||
}
|
||||
|
||||
private TerminalPane nextFloatingAfter(int index) {
|
||||
|
||||
Reference in New Issue
Block a user