images work
This commit is contained in:
@@ -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