Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/flterm/lib/src/rendering/cell_content_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'codepoint_classification.dart';
/// row rendering and block cursor glyph rendering. It does not decide where
/// or how the entry is painted; row and cursor builders own the output lane.
final class CellContentResolver {
static const _kittyUnicodePlaceholder = 0x10EEEE;

final Atlas _atlas;
var _lastCodepoint = -1;
var _lastSpan = 0;
Expand All @@ -29,6 +31,7 @@ final class CellContentResolver {
if (graphemeLength == 0) return null;

final codepoint = cell.codepoint;
if (codepoint == _kittyUnicodePlaceholder) return null;
if (borrowedCell && graphemeLength == 1) {
return resolveCodepoint(
codepoint,
Expand Down Expand Up @@ -68,6 +71,7 @@ final class CellContentResolver {
required int span,
}) {
if (content.isEmpty) return null;
if (codepoint == _kittyUnicodePlaceholder) return null;

if (_usesCodepointEntry(
codepoint: codepoint,
Expand Down
56 changes: 55 additions & 1 deletion packages/flterm/lib/src/rendering/kitty_placement_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ final class KittyPlacementCache {
final nextSnapshots = <KittyPlacementSnapshot>[];
final nextLiveImageIds = <int>{};
var replacementPending = false;
var hasVirtualPlacements = false;
for (final placement in graphics.placements()) {
nextLiveImageIds.add(placement.imageId);
if (placement.isVirtual) {
hasVirtualPlacements = true;
continue;
}

final info = placement.renderInfo;
if (!info.viewportVisible) continue;
Expand Down Expand Up @@ -88,6 +93,50 @@ final class KittyPlacementCache {
info.sourceHeight.toDouble(),
),
z: placement.z,
paintOrder: nextSnapshots.length,
),
);
}

final unicodePlacements = hasVirtualPlacements
? graphics.unicodePlacements()
: const <KittyUnicodePlacement>[];
for (final placement in unicodePlacements) {
nextLiveImageIds.add(placement.imageId);

final info = placement.renderInfo;
if (info == null || info.pixelWidth == 0 || info.pixelHeight == 0) {
continue;
}

final image = graphics.image(placement.imageId);
if (image == null) continue;

final imageGeneration = image.generation;
final entry = _images.lookup(image, generation: imageGeneration);
replacementPending |=
entry is KittyImagePending ||
(entry is KittyImageReady && entry.generation != imageGeneration);
nextSnapshots.add(
KittyPlacementSnapshot(
imageId: placement.imageId,
imageGeneration: imageGeneration,
dst: Rect.fromLTWH(
info.viewportCol * key.cellWidth +
info.cellOffsetX / key.devicePixelRatio,
info.viewportRow * key.cellHeight +
info.cellOffsetY / key.devicePixelRatio,
info.pixelWidth / key.devicePixelRatio,
info.pixelHeight / key.devicePixelRatio,
),
src: Rect.fromLTWH(
info.sourceX.toDouble(),
info.sourceY.toDouble(),
info.sourceWidth.toDouble(),
info.sourceHeight.toDouble(),
),
z: info.z,
paintOrder: nextSnapshots.length,
),
);
}
Expand Down Expand Up @@ -143,7 +192,9 @@ final class KittyPlacementCache {

static int _compareZ(KittyPlacementSnapshot a, KittyPlacementSnapshot b) {
final z = a.z.compareTo(b.z);
return z != 0 ? z : a.imageId.compareTo(b.imageId);
if (z != 0) return z;
final imageId = a.imageId.compareTo(b.imageId);
return imageId != 0 ? imageId : a._paintOrder.compareTo(b._paintOrder);
}
}

Expand All @@ -169,12 +220,15 @@ final class KittyPlacementSnapshot {
/// Signed z-index from the Kitty graphics protocol.
final int z;

final int _paintOrder;

const KittyPlacementSnapshot({
required this.imageId,
this.imageGeneration = 0,
required this.dst,
required this.src,
required this.z,
this._paintOrder = 0,
});
}

Expand Down
34 changes: 34 additions & 0 deletions packages/flterm/test/rendering/cell_content_resolver_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:ui';

import 'package:flterm/src/foundation/cell_metrics.dart';
Expand Down Expand Up @@ -43,6 +44,39 @@ void main() {
expect(entry, isNull);
});

test('returns null for Kitty Unicode placeholders', () {
final entry = resolver.resolve(
content: '\u{10EEEE}\u0305\u0305',
codepoint: 0x10EEEE,
graphemeLength: 3,
style: const Style(),
span: 1,
);

expect(entry, isNull);
});

test('returns null for a terminal cell containing a Kitty placeholder', () {
final terminal = Terminal(cols: 1, rows: 1);
addTearDown(terminal.dispose);
terminal.write(utf8.encode('\u{10EEEE}\u0305\u0305'));
final renderState = RenderState();
addTearDown(renderState.dispose);
final rows = RowIterator();
addTearDown(rows.dispose);
final cells = CellIterator();
addTearDown(cells.dispose);
renderState.update(terminal);
rows.reset(renderState);
rows.next();
cells.reset(rows);
cells.next();

final entry = resolver.resolveCell(cells, style: cells.style, span: 1);

expect(entry, isNull);
});

test('routes narrow single codepoints through the text lane', () {
final entry = resolver.resolve(
content: 'A',
Expand Down
58 changes: 57 additions & 1 deletion packages/flterm/test/rendering/kitty_placement_cache_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ library;
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui' show Image, ImageDecoderCallback, Size, decodeImageFromPixels;
import 'dart:ui'
show Image, ImageDecoderCallback, Rect, Size, decodeImageFromPixels;

import 'package:flterm/src/foundation/cell_metrics.dart';
import 'package:flterm/src/foundation/terminal_theme.dart';
Expand Down Expand Up @@ -38,6 +39,24 @@ void main() {
);
}

void writeUnicodePlacements(Terminal terminal) {
final pixels = base64Encode([0xff, 0x00, 0x00]);
terminal.write(
Uint8List.fromList(
utf8.encode(
'\x1b_Ga=T,t=d,f=24,i=23,s=1,v=1,C=1,U=1;'
'$pixels\x1b\\'
'\x1b[2;4H'
'\x1b[38;2;0;0;23m'
'\u{10EEEE}\u0305\u0305'
'x'
'\u{10EEEE}\u0305\u0305'
'\x1b[39m',
),
),
);
}

Future<Image> testImage() {
final completer = Completer<Image>();
decodeImageFromPixels(
Expand Down Expand Up @@ -103,6 +122,43 @@ void main() {
});

group('sync', () {
test(
'renders discontinuous Unicode placements away from their definition',
() {
final unicodeTerminal = Terminal(cols: 8, rows: 3)
..kittyImageStorageLimit = 1 << 20;
final unicodeState = PaintState(TerminalTheme.dark(), metrics)
..cols = 8
..rows = 3;
final unicodeImages = KittyImageCache(onImageReady: () {});
final unicodePlacements = KittyPlacementCache(
state: unicodeState,
images: unicodeImages,
);
addTearDown(unicodeImages.dispose);
addTearDown(unicodeTerminal.dispose);
unicodeTerminal.resize(
cols: 8,
rows: 3,
cellWidthPx: 8,
cellHeightPx: 16,
);
writeUnicodePlacements(unicodeTerminal);

unicodePlacements.sync(unicodeTerminal, geometryDirty: true);

expect(unicodePlacements.snapshots, hasLength(2));
expect(unicodePlacements.snapshots.map((snapshot) => snapshot.dst), [
const Rect.fromLTWH(24, 20, 8, 8),
const Rect.fromLTWH(40, 20, 8, 8),
]);
expect(unicodePlacements.snapshots.map((snapshot) => snapshot.src), [
const Rect.fromLTWH(0, 0, 1, 1),
const Rect.fromLTWH(0, 0, 1, 1),
]);
},
);

test('returns false when generation and geometry are unchanged', () {
final rebuilt = placements.sync(terminal, geometryDirty: false);

Expand Down
2 changes: 2 additions & 0 deletions packages/libghostty/lib/libghostty.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export 'src/types/types.dart'
IoException,
KittyPlacement,
KittyPlacementRenderInfo,
KittyUnicodePlacement,
KittyUnicodePlacementRenderInfo,
LibGhosttyException,
LimitExceededException,
MouseEncoderSize,
Expand Down
60 changes: 60 additions & 0 deletions packages/libghostty/lib/src/api/terminal/kitty_graphics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,66 @@ final class KittyGraphics {
}
}

/// Snapshots decoded Kitty Unicode placeholder occurrences in the active
/// viewport.
///
/// Occurrences are returned as row-major, single-row runs, including runs
/// whose image or virtual definition is unavailable. The returned values are
/// copied and remain stable after terminal mutations.
/// [KittyUnicodePlacement.renderInfo] is null for an unresolved or otherwise
/// non-drawable run. Image ids can be resolved through [image] while the
/// corresponding image remains stored.
///
/// ```dart
/// for (final placement in kitty.unicodePlacements()) {
/// final geometry = placement.renderInfo;
/// if (geometry == null) continue;
/// // Draw `placement.imageId` using `geometry`.
/// }
/// ```
List<KittyUnicodePlacement> unicodePlacements() {
final iterator = bindings.kittyGraphics
.kittyGraphicsUnicodePlacementIteratorNew();
try {
bindings.kittyGraphics.terminalGetKittyGraphicsUnicodePlacementIterator(
_terminal._terminalHandle,
iterator,
);
final out = <KittyUnicodePlacement>[];
while (bindings.kittyGraphics.kittyGraphicsUnicodePlacementNext(
iterator,
)) {
final raw = bindings.kittyGraphics.kittyGraphicsUnicodePlacementGet(
iterator,
_terminal._terminalHandle,
);
final topLeft = bindings.render.terminalPointFromGridRef(
_terminal._terminalHandle,
raw.topLeft,
.viewport,
);
if (topLeft == null) throw const InvalidValueException();
out.add(
KittyUnicodePlacement(
topLeft: topLeft,
imageId: raw.imageId,
placementId: raw.placementId,
column: raw.column,
row: raw.row,
columns: raw.columns,
rows: raw.rows,
renderInfo: raw.renderInfo,
),
);
}
return out;
} finally {
bindings.kittyGraphics.kittyGraphicsUnicodePlacementIteratorFree(
iterator,
);
}
}

/// Returns the Kitty graphics image storage for [terminal]'s active
/// screen, or null when Kitty graphics are disabled in the native
/// library build.
Expand Down
Loading
Loading