From 1ab4c071be58033d5452f69797ca8e2c32240cff Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 16:28:57 -0300 Subject: Separate tag text geometry from physical pane grips --- test/panes.zig | 145 +++++++++++++++++++++++++++++++++++++++++++ test/web-snapshots/touch.mjs | 20 ++++++ 2 files changed, 165 insertions(+) (limited to 'test') diff --git a/test/panes.zig b/test/panes.zig index bf8a98a2..71127ea3 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -12,6 +12,150 @@ const image = pardes.image; const syntax = pardes.syntax; const filesystem = pardes.filesystem; +const TagLayerTests = struct { + const Kind = @FieldType(pardes.TagLayer, "kind"); + fn findLayer(p: *Pardes, kind: Kind, id: usize) !*pardes.TagLayer { + for (&p.surface.tag_layers) |*layer| { + if (layer.cols > 0 and layer.kind == kind and layer.id == id) return layer; + } + return error.MissingTagLayer; + } + + fn mouse(layer: *const pardes.TagLayer, x: f32, kind: pardes.Mouse.Kind) pardes.Mouse { + const y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * 20; + return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .tag_hit = layer.hitAt(x, y, 10, 20, 5, 10) }; + } + + test "compact tag layers render and edit Unicode text beyond body column capacity" { + for ([_]bool{ false, true }) |bottom| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 16 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.presentation.enabled = false; + p.settings.tag_bottom = bottom; + p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 }; + p.gpa.free(pane.file.?.path); + pane.file.?.path = try p.gpa.dupe(u8, "/x"); + const tail = " " ++ "a" ** 38 ++ "\t界 END"; + @memcpy(pane.tag_tail[0..tail.len], tail); + pane.tag_tail_len = tail.len; + pane.tag_init = true; + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const layer = try findLayer(p, .pane, 0); + const text = try p.tagText(frame.allocator(), pane); + const at = std.mem.indexOf(u8, text, "END").?; + const col = panes.File.displayWidth(text[0..at]); + try std.testing.expect(col > p.rects[0].w); + try std.testing.expect(col + 3 < layer.cols); + try std.testing.expectEqualStrings("E", layer.cells[col].grapheme()); + try std.testing.expectEqual(@as(u16, 0), pane.tag_scroll); + const x = @as(f32, @floatFromInt(layer.viewport.x)) * 10 + (@as(f32, @floatFromInt(col)) + 0.5) * 5; + var stale = mouse(layer, x, .press); + stale.tag_hit.?.serial +%= 1; + p.update(.{ .mouse = stale }); + try std.testing.expect(!pane.tag_edit); + p.update(.{ .mouse = mouse(layer, x, .press) }); + p.update(.{ .mouse = mouse(layer, x, .release) }); + try std.testing.expect(pane.tag_edit); + try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col); + p.update(.{ .key = .{ .cp = 'X', .text = "X" } }); + _ = try p.render(frame.allocator()); + const edited = try p.tagText(frame.allocator(), pane); + try std.testing.expect(std.mem.endsWith(u8, edited, "界 XEND")); + try std.testing.expectEqual(@as(u16, 0), pane.tag_scroll); + try std.testing.expectEqualStrings("body\n", pane.file.?.content); + p.update(.{ .key = .{ .cp = Key.escape } }); + const rect = p.rects[0]; + const track: layout.Track = .{ + .serial = pane.serial, + .pane = 0, + .effect = .zoom, + .frame = 4, + .frame_count = 10, + .from = .{ .x = @as(f32, @floatFromInt(rect.x)) + 2, .y = @floatFromInt(rect.y), .w = @floatFromInt(rect.w - 4), .h = @floatFromInt(rect.h) }, + .to = .{ .x = @floatFromInt(rect.x), .y = @floatFromInt(rect.y), .w = @floatFromInt(rect.w), .h = @floatFromInt(rect.h) }, + }; + p.presentation.shown_tracks[0] = track; + p.presentation.pending = false; + const shown = track.presented(); + const source_y = @as(f32, @floatFromInt(layer.viewport.y)) + 0.5; + const painted_x = shown.x + (x / 10 - track.to.x) / track.to.w * shown.w; + const painted_y = shown.y + (source_y - track.to.y) / track.to.h * shown.h; + for ([_]pardes.Mouse.Kind{ .press, .release }) |kind| { + var event = mouse(layer, x, kind); + // Hosts inverse the exact painted pixel into the tag envelope; + // physical routing still receives the original body-grid cell. + event.col = @intFromFloat(painted_x); + event.row = @intFromFloat(painted_y); + p.update(.{ .mouse = event }); + } + try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col); + } + } + + test "compact tag pointer keeps physical grips and adjacent header ownership" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("body\n"); + p.presentation.enabled = false; + p.settings.column_tags = true; + p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 }; + try std.testing.expect(p.executeBuiltinLine(0, "New")); + const second = p.active; + try std.testing.expect(layout.splitColumn(p, second, second, false)); + p.update(.tick); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const layer = try findLayer(p, .pane, second); + const edge = @as(f32, @floatFromInt(layer.viewport.x)) * 10; + p.update(.{ .mouse = mouse(layer, edge - 0.1, .press) }); + try std.testing.expect(p.drag == .move); + try std.testing.expectEqual(second, p.drag.move.id); + p.update(.{ .mouse = mouse(layer, edge - 0.1, .release) }); + p.update(.{ .mouse = mouse(layer, edge + 0.1, .press) }); + p.update(.{ .mouse = mouse(layer, edge + 0.1, .release) }); + try std.testing.expectEqual(second, p.active); + try std.testing.expect(p.panes[second].?.tag_edit); + try std.testing.expectEqual(@as(u16, 0), p.panes[second].?.tag_col); + p.update(.{ .key = .{ .cp = Key.escape } }); + const left = p.panes[0].?; + const tail = " " ++ "a" ** 110; + @memcpy(left.tag_tail[0..tail.len], tail); + left.tag_tail_len = tail.len; + left.tag_init = true; + _ = try p.render(frame.allocator()); + const left_layer = try findLayer(p, .pane, 0); + const left_text = try p.tagText(frame.allocator(), left); + const start_x = @as(f32, @floatFromInt(left_layer.viewport.x)) * 10 + 12.5; + const beyond_x = edge + 12.5; + p.update(.{ .mouse = mouse(left_layer, start_x, .press) }); + p.update(.{ .mouse = mouse(layer, beyond_x, .drag) }); + p.update(.{ .mouse = mouse(layer, beyond_x, .release) }); + try std.testing.expectEqual(@as(usize, 0), p.active); + const end_col = left_layer.capacity(p.row_metrics) - 1; + try std.testing.expectEqual(@as(u16, @intCast(panes.File.rawAtDisplay(left_text, end_col))), left.tag_col); + try std.testing.expect(left.tag_sel); + p.update(.{ .key = .{ .cp = Key.escape } }); + const long = "a" ** 60 ++ "界END"; + try std.testing.expect(p.global_tag.set(long)); + for (p.col_tags[0..p.ncol]) |*tag| try std.testing.expect(tag.set(long)); + _ = try p.render(frame.allocator()); + for ([_]Kind{ .workspace, .column }) |kind| { + const id: usize = if (kind == .column) 1 else 0; + const header = try findLayer(p, kind, id); + const x = @as(f32, @floatFromInt(header.viewport.x)) * 10 + 62.5 * 5; + p.update(.{ .mouse = mouse(header, x, .press) }); + p.update(.{ .mouse = mouse(header, x, .release) }); + try std.testing.expectEqual(@as(?u16, 63), p.topbar_col); + if (kind == .column) try std.testing.expectEqual(@as(?usize, 1), p.header_column) else try std.testing.expect(p.header_column == null); + p.update(.{ .key = .{ .cp = Key.escape } }); + } + } +}; + const TagNavigationTests = struct { fn key(p: *Pardes, cp: u21) void { p.update(.{ .key = .{ .cp = cp } }); @@ -3776,6 +3920,7 @@ const TerminalTests = struct { test { _ = TagNavigationTests; + _ = TagLayerTests; _ = TagNameTintTests; _ = TreeContextTests; _ = BodyHitTests; diff --git a/test/web-snapshots/touch.mjs b/test/web-snapshots/touch.mjs index ecd8a9a3..e65fdb80 100644 --- a/test/web-snapshots/touch.mjs +++ b/test/web-snapshots/touch.mjs @@ -79,6 +79,22 @@ export default async function ({ grid, touch, wait, stable, delay, evaluate, mou renderer.render(); result.closedGapBorders=borders(); result.borderChangeInvalidates=renderer.layerRoot.firstChild!==withGap; + const tagValues=[2,2,0,42,1,0,1,1,1,1,0x334455]; + wasm.pardes_tag_layer_limit=()=>1; + wasm.pardes_tag_layer_value=(_,field)=>tagValues[field]; + wasm.pardes_tag_layer_cells=()=>1024; + wasm.pardes_tagline_band_offset=()=>2560; + renderer.render(); + const tag=renderer.tagRoot.firstChild; + result.tag={left:parseFloat(tag.style.left),top:parseFloat(tag.style.top),width:parseFloat(tag.style.width), + text:tag.textContent,cursor:tag.querySelector('.is-cursor')?.textContent, + gripVisible:renderer.cells[0].style.visibility,covered:renderer.cells[1].style.visibility, + background:tag.style.backgroundColor}; + renderer.render(); + result.tag.cached=renderer.tagRoot.firstChild===tag; + tagValues[10]=0x556677; + renderer.render(); + result.tag.bgInvalidates=renderer.tagRoot.firstChild!==tag; root.remove();return result; })()`); assert.deepEqual(compactGeometry.bands, [[0,10],[10,20],[30,20]], "compact headers release real height to additional body rows"); @@ -90,6 +106,10 @@ export default async function ({ grid, touch, wait, stable, delay, evaluate, mou assert.deepEqual(compactGeometry.closedGapBorders,[30]); assert(compactGeometry.borderChangeInvalidates,"separator changes invalidate an otherwise unchanged layer"); + assert.deepEqual(compactGeometry.tag, {left:10,top:10,width:10,text:"AB",cursor:"B", + gripVisible:"",covered:"hidden",background:"rgb(51, 68, 85)",cached:true,bgInvalidates:true}, + "tag text uses its own columns after a fixed body-width grip, with cursor and cached base background"); + let frame = await grid(); assert.equal(tags(frame).length, 1); styled(frame); -- cgit v1.3