diff --git a/src/terminal/c/kitty_graphics.zig b/src/terminal/c/kitty_graphics.zig index 3ee0f7b73..a509d23cf 100644 --- a/src/terminal/c/kitty_graphics.zig +++ b/src/terminal/c/kitty_graphics.zig @@ -10,6 +10,7 @@ const grid_ref = @import("grid_ref.zig"); const selection_c = @import("selection.zig"); const terminal_c = @import("terminal.zig"); const Terminal = @import("../Terminal.zig"); +const PageList = @import("../PageList.zig"); const Result = @import("result.zig").Result; /// C: GhosttyKittyGraphics @@ -600,10 +601,35 @@ fn computeViewportPos( ) struct { col: i32, row: i32, visible: bool } { // Virtual placements use unicode placeholders and don't have a // screen position — they are rendered inline by the text layout. - const pin = switch (p.location) { - .pin => |pin| pin, - .virtual, .relative => return .{ .col = 0, .row = 0, .visible = false }, + // Relative placements are anchored at the root of their parent + // chain, offset by the accumulated chain offsets. A chain rooted + // at a virtual placement has no resolvable position here: its + // origin is the parent's placeholder cells, which only a renderer + // scanning the screen can locate. + const origin: struct { + pin: *const PageList.Pin, + col_offset: i32 = 0, + row_offset: i32 = 0, + } = switch (p.location) { + .pin => |pin| .{ .pin = pin }, + .virtual => return .{ .col = 0, .row = 0, .visible = false }, + .relative => |rel| origin: { + const storage = &t.screens.active.kitty_images; + const chain = storage.resolveChain(rel) orelse + return .{ .col = 0, .row = 0, .visible = false }; + switch (chain.root.location) { + .pin => |root_pin| break :origin .{ + .pin = root_pin, + .col_offset = chain.horizontal_offset, + .row_offset = chain.vertical_offset, + }, + .virtual => return .{ .col = 0, .row = 0, .visible = false }, + .relative => unreachable, // resolveChain roots are never relative + } + }, }; + + const pin = origin.pin; if (pin.garbage) return .{ .col = 0, .row = 0, .visible = false }; // Convert both the placement's pin and the viewport's top-left @@ -618,17 +644,23 @@ fn computeViewportPos( // Subtracting viewport origin from the pin gives us viewport- // relative coordinates. The row can be negative when the - // placement has partially scrolled above the viewport. - const vp_row: i32 = @as(i32, @intCast(pin_screen.screen.y)) - - @as(i32, @intCast(vp_screen.screen.y)); - const vp_col: i32 = @intCast(pin_screen.screen.x); + // placement has partially scrolled above the viewport, and both + // can be negative for relative placements with negative offsets. + const vp_row: i32 = (@as(i32, @intCast(pin_screen.screen.y)) - + @as(i32, @intCast(vp_screen.screen.y))) +| origin.row_offset; + const vp_col: i32 = @as(i32, @intCast(pin_screen.screen.x)) +| + origin.col_offset; - // A placement is invisible if its bottom edge (row + height) - // is above the viewport, or its top edge is at or below the - // viewport's last row. + // A placement is invisible if its bottom edge (row + height) is + // above the viewport, or its top edge is at or below the viewport's + // last row. The same applies horizontally: a pin's column is always + // in bounds, but a relative placement's offsets can push it fully + // off either side. const grid_size = p.gridSize(image.*, t); const bottom_row = @as(i64, vp_row) + @as(i64, grid_size.rows); - const visible = bottom_row > 0 and vp_row < @as(i32, t.rows); + const right_col = @as(i64, vp_col) + @as(i64, grid_size.cols); + const visible = bottom_row > 0 and vp_row < @as(i32, t.rows) and + right_col > 0 and vp_col < @as(i32, t.cols); return .{ .col = vp_col, .row = vp_row, .visible = visible }; }