diff options
Diffstat (limited to 'src/tty/tty.zig')
| -rw-r--r-- | src/tty/tty.zig | 128 |
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)); } } |
