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