summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig128
1 files changed, 126 insertions, 2 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index a01df94b..a067ffce 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -795,6 +795,63 @@ fn updateCoreTerminalSize(core: *pardes.Pardes, cols: u16, rows: u16, pixel_w: u
} });
}
+/// A terminal pane's image fragment (`native.exact`): its `src` into its
+/// `dst`, inside one cell row. Kitty scales a source rectangle to whole
+/// cells, so the source is trimmed to the cells' scale rather than the
+/// picture stretched: a fragment loses under a cell at its right edge.
+/// A full row is declared one row high; a part of one (an image's top or
+/// bottom edge) only its columns, its height then following the source.
+fn exactKittyPlacement(
+ place: pardes.ImagePlace,
+ screen_cols: u16,
+ screen_rows: u16,
+ screen_pixel_w: u16,
+ screen_pixel_h: u16,
+) ?KittyPlacement {
+ if (comptime !pardes.pdf_enabled) return null;
+ const g = place.native.geometry orelse return null;
+ const cell_w = terminalCellPixels(screen_pixel_w, screen_cols, 8);
+ const cell_h = terminalCellPixels(screen_pixel_h, screen_rows, 16);
+ if (g.dst.w == 0 or g.dst.h == 0 or g.src.w == 0 or g.src.h == 0) return null;
+ const cell_x = g.dst.x / cell_w;
+ const pixel_x = g.dst.x % cell_w;
+ const pixel_y = @min(g.dst.y, cell_h - 1);
+ const cols: u32 = @max(1, (g.dst.w + cell_w / 2) / cell_w);
+ if (cell_x >= place.w) return null;
+ const declared_w = cols * cell_w;
+ // At the cells' scale, the source that fills `cols` cells.
+ var src_w = g.src.w;
+ if (declared_w < g.dst.w) src_w = @max(1, @as(u32, @intCast(@as(u64, g.src.w) * declared_w / g.dst.w)));
+ var src_h = g.src.h;
+ const full = g.dst.y == 0 and g.dst.h >= cell_h;
+ if (!full) {
+ // As tall as it is at that scale, and no taller than the row.
+ const room = cell_h - pixel_y;
+ const fits: u32 = @intCast(@as(u64, room) * src_w / declared_w);
+ if (fits == 0) return null;
+ src_h = @min(src_h, fits);
+ }
+ return .{
+ .cell_x = std.math.cast(u16, cell_x) orelse return null,
+ .cell_y = 0,
+ .cell_cols = std.math.cast(u16, @min(@as(u32, place.w) - cell_x, (pixel_x + declared_w + cell_w - 1) / cell_w)) orelse return null,
+ .cell_rows = 1,
+ .options = .{
+ .clip_region = .{
+ .x = std.math.cast(u16, g.src.x) orelse return null,
+ .y = std.math.cast(u16, g.src.y) orelse return null,
+ .width = std.math.cast(u16, src_w) orelse return null,
+ .height = std.math.cast(u16, src_h) orelse return null,
+ },
+ .pixel_offset = if (pixel_x != 0 or pixel_y != 0) .{ .x = @intCast(pixel_x), .y = @intCast(pixel_y) } else null,
+ .size = if (full)
+ .{ .cols = std.math.cast(u16, cols) orelse return null, .rows = 1 }
+ else
+ .{ .cols = std.math.cast(u16, cols) orelse return null },
+ },
+ };
+}
+
fn kittyPlacement(
place: pardes.ImagePlace,
screen_cols: u16,
@@ -901,6 +958,61 @@ fn kittyPlacement(
}
}
+test "a terminal's image fragment goes to kitty cropped to its cells' scale, a full row one row high" {
+ if (comptime !pardes.pdf_enabled) return;
+ var place = pardes.ImagePlace{
+ .pane = 0,
+ .serial = 3,
+ .native = .{ .page = 7, .exact = true, .geometry = .{
+ .src = .{ .x = 0, .y = 40, .w = 240, .h = 20 },
+ .dst = .{ .x = 0, .y = 0, .w = 240, .h = 20 },
+ } },
+ .x = 99,
+ .y = 4,
+ .w = 24,
+ .h = 1,
+ .rgba = &.{},
+ .iw = 240,
+ .ih = 160,
+ };
+ // 10x20 pixel cells: a whole row, one to one.
+ const full = exactKittyPlacement(place, 100, 30, 1000, 600).?;
+ try std.testing.expectEqual(@as(u16, 0), full.cell_x);
+ try std.testing.expectEqual(@as(?u16, 24), full.options.size.?.cols);
+ try std.testing.expectEqual(@as(?u16, 1), full.options.size.?.rows);
+ try std.testing.expectEqual(@as(?u16, 40), full.options.clip_region.?.y);
+ try std.testing.expectEqual(@as(?u16, 240), full.options.clip_region.?.width);
+ try std.testing.expect(full.options.pixel_offset == null);
+
+ // The top of a letterboxed picture: 7 pixels down into the row, 13
+ // tall, 35 pixels in, 205 wide, which rounds to 21 cells. Its columns
+ // only, its source no taller than the 13 pixels left in the row at
+ // that scale.
+ place.native.geometry = .{
+ .src = .{ .x = 0, .y = 0, .w = 205, .h = 13 },
+ .dst = .{ .x = 35, .y = 7, .w = 205, .h = 13 },
+ };
+ const top = exactKittyPlacement(place, 100, 30, 1000, 600).?;
+ try std.testing.expectEqual(@as(u16, 3), top.cell_x);
+ try std.testing.expectEqual(@as(u16, 5), top.options.pixel_offset.?.x);
+ try std.testing.expectEqual(@as(u16, 7), top.options.pixel_offset.?.y);
+ try std.testing.expectEqual(@as(?u16, 21), top.options.size.?.cols);
+ try std.testing.expectEqual(@as(?u16, null), top.options.size.?.rows);
+ const w = top.options.clip_region.?.width.?;
+ const h = top.options.clip_region.?.height.?;
+ try std.testing.expectEqual(@as(u16, 205), w);
+ try std.testing.expect(h > 0 and @as(u32, h) * 210 / w <= 13);
+
+ // Narrower than its cells round to: the source is cut to their scale.
+ place.native.geometry = .{
+ .src = .{ .x = 0, .y = 0, .w = 400, .h = 40 },
+ .dst = .{ .x = 0, .y = 0, .w = 204, .h = 20 },
+ };
+ const cut = exactKittyPlacement(place, 100, 30, 1000, 600).?;
+ try std.testing.expectEqual(@as(?u16, 20), cut.options.size.?.cols);
+ try std.testing.expectEqual(@as(?u16, 392), cut.options.clip_region.?.width);
+}
+
test "Kitty PDF fragments never declare pixels beyond their clipped bottom" {
if (comptime !pardes.pdf_enabled) return;
@@ -1696,7 +1808,19 @@ const Shell = struct {
const child = win.child(.{ .x_off = place.x, .y_off = place.y, .width = place.w, .height = place.h });
cached.draw(child, .{ .scale = .contain }) catch {};
} else {
- if (place.native.fit == .contain) {
+ if (place.native.exact) {
+ if (exactKittyPlacement(place, vx.screen.width, vx.screen.height, vx.screen.width_pix, vx.screen.height_pix)) |placement| {
+ // The cell keeps its own glyph and colours
+ // (Image.draw would reset them): a fragment
+ // short of the cell shows the pane under it.
+ const x = place.x + placement.cell_x;
+ if (win.readCell(x, place.y)) |under| {
+ var cell = under;
+ cell.image = .{ .img_id = cached.id, .options = placement.options };
+ win.writeCell(x, place.y, cell);
+ }
+ }
+ } else if (place.native.fit == .contain) {
const child = win.child(.{ .x_off = place.x, .y_off = place.y, .width = place.w, .height = place.h });
cached.draw(child, .{ .scale = .contain }) catch {};
} else if (kittyPlacement(
@@ -1778,7 +1902,7 @@ const Shell = struct {
fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
const s = of(ctx);
if (s.ptys[pane]) |pt| {
- const ws: posix.winsize = .{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
+ const ws = host_io.ptyWinsize(s.core, cols, rows);
_ = posix.system.ioctl(pt.file.handle, TIOCSWINSZ, @intFromPtr(&ws));
}
}