diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 15:33:30 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 8ac4a48a2e8e66a9e72bcbc61c114be8db9215fd (patch) | |
| tree | 4c48ecd42a800b12daed0712a4620639d1631f7e | |
| parent | c1787ff868ee723629c1d500098d36dc419f77c3 (diff) | |
| download | pardes-8ac4a48a2e8e66a9e72bcbc61c114be8db9215fd.tar.gz pardes-8ac4a48a2e8e66a9e72bcbc61c114be8db9215fd.zip | |
Separate discontinuous pinned source context with thin borders
| -rw-r--r-- | docs/config.md | 3 | ||||
| -rw-r--r-- | docs/detached.md | 5 | ||||
| -rw-r--r-- | src/body_layer.zig | 29 | ||||
| -rw-r--r-- | src/detached/wire.zig | 20 | ||||
| -rw-r--r-- | src/gui/gui.zig | 41 | ||||
| -rw-r--r-- | src/macos.zig | 2 | ||||
| -rw-r--r-- | src/macos/Sources/PardesView.swift | 13 | ||||
| -rw-r--r-- | src/pardes.zig | 1 | ||||
| -rw-r--r-- | src/web.zig | 2 | ||||
| -rw-r--r-- | src/web/app.mjs | 12 | ||||
| -rw-r--r-- | test/panes.zig | 30 | ||||
| -rw-r--r-- | test/web-snapshots/touch.mjs | 16 |
12 files changed, 141 insertions, 33 deletions
diff --git a/docs/config.md b/docs/config.md index b0065ad2..d0bf63da 100644 --- a/docs/config.md +++ b/docs/config.md @@ -130,7 +130,8 @@ their text; the command can still be executed from any source pane. `TreeContextTagStyle` toggles the experimental tagline treatment for those headers and is on by default. In graphical frontends it uses the tagline font, -line height and thin border. Turning it off restores body-sized context rows. +line height and thin border. A matching separator marks gaps between the +headers' source lines. Turning it off restores body-sized context rows. `TreeContext` itself still defaults off; this appearance option does not enable it. `Config` reports the appearance option, and dumps preserve it. diff --git a/docs/detached.md b/docs/detached.md index 1f56d24f..af81547a 100644 --- a/docs/detached.md +++ b/docs/detached.md @@ -49,8 +49,9 @@ write session files or install file watches. `src/detached/wire.zig` owns the versioned frontend protocol. Frames are full grids or changes relative to each frontend's last queued frame. A new attachment receives a full grid. Output queues and per-poll work are bounded; -a lagging frontend cannot hold the session's event loop. Protocol version 4 -also carries the pointer shape and compact source-context body layers. Pointer +a lagging frontend cannot hold the session's event loop. Protocol version 5 +also carries the pointer shape, compact source-context body layers and their +source-gap separators. Pointer updates work even when no grid cells change, and graphical clients use the same compact row geometry for drawing and mouse input. Terminal clients retain the ordinary fixed grid. diff --git a/src/body_layer.zig b/src/body_layer.zig index 3b5ee795..14fb91ec 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -3,6 +3,7 @@ const std = @import("std"); const pardes = @import("pardes.zig"); const config = @import("config.zig"); +const limits = @import("memory.zig").limits; pub const Metrics = struct { body_w: u16 = 1, @@ -18,9 +19,24 @@ pub const Layer = struct { cols: u16 = 0, rows: u16 = 0, context_rows: u16 = 0, + context_separators: std.bit_set.StaticBitSet(limits.wrap_rows) = .initEmpty(), cells: []pardes.Cell = &.{}, cursor: ?struct { x: u16, y: u16, bar: bool = false } = null, + pub fn setContextLines(layer: *Layer, lines: []const i32) void { + layer.context_separators = .initEmpty(); + const count = @min(lines.len, layer.context_rows); + var row: usize = 0; + while (row + 1 < count and row < limits.wrap_rows) : (row += 1) { + if (lines[row + 1] != lines[row] +| 1) layer.context_separators.set(row); + } + } + + pub fn hasContextBorderAfter(layer: *const Layer, row: usize) bool { + if (row >= layer.context_rows or row >= limits.wrap_rows) return false; + return row + 1 == layer.context_rows or layer.context_separators.isSet(row); + } + pub fn rowTop(layer: *const Layer, row: u16, body_h: f32, tagline_h: f32) f32 { const body = @max(1, body_h); const tag = std.math.clamp(tagline_h, 1, body); @@ -78,3 +94,16 @@ test "compact body layer rows fill reclaimed height and invert exact boundaries" try std.testing.expect(layer.hitAt(39.99, 80, 10, 20, 6, 12) == null); try std.testing.expect(layer.hitAt(std.math.nan(f32), 80, 10, 20, 6, 12) == null); } + +test "compact context borders separate source gaps and preserve multiline headers" { + var layer: Layer = .{ .context_rows = 6 }; + layer.setContextLines(&.{ 3, 4, 8, 9, 10, 17 }); + const expected = [_]bool{ false, true, false, false, true, true, false }; + for (expected, 0..) |border, row| try std.testing.expectEqual(border, layer.hasContextBorderAfter(row)); + layer.context_rows = 2; + layer.setContextLines(&.{ 9, 10 }); + try std.testing.expect(!layer.hasContextBorderAfter(0)); + try std.testing.expect(layer.hasContextBorderAfter(1)); + try std.testing.expect(!layer.hasContextBorderAfter(4)); + try std.testing.expect(!layer.context_separators.isSet(4)); +} diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 5d52e2df..416330e8 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -2,8 +2,9 @@ //! Native struct layout never reaches the wire. const std = @import("std"); const pardes = @import("../pardes.zig"); +const limits = @import("../memory.zig").limits; -pub const version: u16 = 4; +pub const version: u16 = 5; pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; @@ -184,9 +185,12 @@ pub const Frame = struct { layer.rows = try r.getU16(); layer.context_rows = try r.getU16(); if (layer.pane >= pardes.MAX_PANES or layer.cols == 0 or layer.cols > f.cols or - layer.rows == 0 or layer.rows > 2 * max_rows or layer.context_rows > layer.rows or + layer.rows == 0 or layer.rows > 2 * max_rows or layer.context_rows > layer.rows or layer.context_rows > limits.wrap_rows or layer.viewport.x > f.cols or layer.viewport.w > f.cols - layer.viewport.x or layer.viewport.y > f.rows or layer.viewport.h > f.rows - layer.viewport.y) return error.BadValue; + for (0..layer.context_rows) |row| { + if (try r.getBool()) layer.context_separators.set(row); + } const cursor = try getCursor(&r, layer.cols, layer.rows); layer.cursor = if (cursor) |c| .{ .x = c.x, .y = c.y, .bar = c.bar } else null; layer.cells = try gpa.alloc(pardes.Cell, @as(usize, layer.cols) * layer.rows); @@ -640,7 +644,7 @@ pub fn encodeFramePointer( pub fn layersBound(layers: []const pardes.BodyLayer) usize { var n: usize = 2; for (layers) |layer| if (layer.rows != 0) { - n += 26 + layer.cells.len * cell_max; + n += 26 + layer.context_rows + layer.cells.len * cell_max; }; return n; } @@ -675,6 +679,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point try w.putU16(count); for (layers) |layer| { if (layer.rows == 0) continue; + if (layer.context_rows > limits.wrap_rows or layer.context_rows > layer.rows) return error.BadValue; try w.putU16(layer.pane); try w.putU32(layer.serial); try w.putU16(layer.viewport.x); @@ -684,6 +689,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point try w.putU16(layer.cols); try w.putU16(layer.rows); try w.putU16(layer.context_rows); + for (0..layer.context_rows) |row| try w.putBool(layer.context_separators.isSet(row)); try putCursor(&w, if (layer.cursor) |c| .{ .x = c.x, .y = c.y, .bar = c.bar } else null, layer.cols, layer.rows); for (layer.cells) |*cell| try putCell(&w, cell); } @@ -1727,7 +1733,8 @@ test "detached wire carries compact body rows and exact pointer metrics" { var cells: [8]pardes.Cell = @splat(.{}); var logical: [12]pardes.Cell = @splat(.{}); logical[10] = .{ .text = "z".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false }; - const layer: pardes.BodyLayer = .{ .pane = 0, .serial = 42, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 4 }, .cols = 2, .rows = 6, .context_rows = 2, .cells = &logical, .cursor = .{ .x = 0, .y = 5, .bar = true } }; + var layer: pardes.BodyLayer = .{ .pane = 0, .serial = 42, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 4 }, .cols = 2, .rows = 6, .context_rows = 2, .cells = &logical, .cursor = .{ .x = 0, .y = 5, .bar = true } }; + layer.context_separators.set(0); const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer})); defer gpa.free(out); const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}); @@ -1738,6 +1745,10 @@ test "detached wire carries compact body rows and exact pointer metrics" { try frame.apply(&cells); try frame.applyLayers(gpa, &decoded); try std.testing.expectEqual(@as(u16, 6), decoded[0].rows); + try std.testing.expect(decoded[0].context_separators.isSet(0)); + try std.testing.expect(!decoded[0].context_separators.isSet(1)); + try std.testing.expect(decoded[0].hasContextBorderAfter(0)); + try std.testing.expect(decoded[0].hasContextBorderAfter(1)); try std.testing.expectEqualStrings("z", decoded[0].cells[10].grapheme()); try std.testing.expectEqual(@as(u16, 5), decoded[0].cursor.?.y); const pointer = layer.hitAt(4, 27, 8, 16, 6, 8).?; @@ -1754,6 +1765,7 @@ test "detached wire carries compact body rows and exact pointer metrics" { for (decoded) |item| { try std.testing.expectEqual(@as(u16, 0), item.rows); try std.testing.expectEqual(@as(usize, 0), item.cells.len); + try std.testing.expectEqual(@as(usize, 0), item.context_separators.count()); } const no_layers = try encodeFramePointer(out, 2, 4, null, .arrow, &cells, &cells); const cleared_packet = (try framed(no_layers)).?; diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 91176a63..0c0a5651 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -4937,7 +4937,7 @@ fn renderFrame( const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h; for (0..layer.rows) |lr| { if (layer.rowTop(@intCast(lr), layout.h, @floatFromInt(g.tagline_height)) >= bottom) break; - batch.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0); + batch.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0) + @as(u32, @intFromBool(layer.hasContextBorderAfter(lr))); } } for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| { @@ -4950,7 +4950,7 @@ fn renderFrame( const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h; for (0..layer.rows) |lr| { if (layer.rowTop(@intCast(lr), layout.h, @floatFromInt(g.tagline_height)) >= bottom) break; - destination.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0); + destination.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0) + @as(u32, @intFromBool(layer.hasContextBorderAfter(lr))); } } } @@ -5027,7 +5027,7 @@ fn renderFrame( for (surface.bodyLayers()) |*layer| { if (layer.rows == 0) continue; const batch_index = paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y); - emitBodyLayer(g, instances, &cell_next[batch_index], layer, win_w, win_h, paint_plan.batches[batch_index].track, page, paint_plan.len == 1, false); + emitBodyLayer(g, instances, &cell_next[batch_index], layer, win_w, win_h, paint_plan.batches[batch_index].track, page, paint_plan.len == 1, false, topbar_pane_border_rgb); } for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| { const track = batch.track.?; @@ -5036,7 +5036,7 @@ fn renderFrame( for (surface.previous_body_layers) |*layer| { if (layer.rows == 0 or layer.serial != track.serial) continue; const destination: usize = if (under) 0 else batch_index; - emitBodyLayer(g, instances, &cell_next[destination], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve); + emitBodyLayer(g, instances, &cell_next[destination], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve, topbar_pane_border_rgb); } } for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| { @@ -5357,7 +5357,7 @@ fn clipBodyInstance(instance: *CellInstance, bottom: f32, win_h: f32) void { instance.y1 = limit; } -fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const pardes.BodyLayer, win_w: f32, win_h: f32, track: ?pardes.layout.Track, page: Ground, show_cursor: bool, old_layer: bool) void { +fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const pardes.BodyLayer, win_w: f32, win_h: f32, track: ?pardes.layout.Track, page: Ground, show_cursor: bool, old_layer: bool, border_rgb: [3]u8) void { const bw: f32 = @floatFromInt(g.cell_w); const bh: f32 = @floatFromInt(g.cell_h); const tw: f32 = @floatFromInt(g.tagline_width); @@ -5387,6 +5387,17 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const clipBodyInstance(&instances[next.*], bottom, win_h); next.* += 1; } + if (layer.hasContextBorderAfter(row)) { + const border: pardes.Cell = .{ .default = false, .style = .{ .bg = .{ .rgb = border_rgb } } }; + emitInstance(g, instances, next.*, 0, 0, .{ + .w = @as(f32, @floatFromInt(layer.viewport.w)) * bw, + .h = 1, + .x_off = left, + .y_off = top + height - 1, + }, win_w, win_h, track, .body, &border, old_layer, false, page); + clipBodyInstance(&instances[next.*], bottom, win_h); + next.* += 1; + } } } @@ -6243,10 +6254,7 @@ fn buildOverlay( if (transient_on) for (surface.bodyLayers()) |*layer| { if (layer.rows == 0) continue; const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w; - const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w; const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h; - const border = layer.rowTop(layer.context_rows, layout.h, @floatFromInt(g.tagline_height)); - if (border <= bottom) builder.addRect(left, border - 1, right, border, overlayRgb(topbar_pane_border_rgb)); if (layer.cursor) |cursor| { if (!cursor.bar or cursor.y >= layer.rows or cursor.x >= layer.cols) continue; const top = layer.rowTop(cursor.y, layout.h, @floatFromInt(g.tagline_height)); @@ -6812,21 +6820,26 @@ test "compact body layer rendering fills recovered rows and clips the final glyp g.tagline_height = 12; g.space_slot = .{ .u = 0, .v = 0 }; var cells: [24]pardes.Cell = @splat(.{}); - const layer: pardes.BodyLayer = .{ + var layer: pardes.BodyLayer = .{ .viewport = .{ .x = 3, .y = 2, .w = 2, .h = 5 }, .cols = 2, .rows = 12, .context_rows = 4, .cells = &cells, }; + layer.setContextLines(&.{ 0, 4, 5, 6 }); var instances: [32]CellInstance = undefined; var next: u32 = 0; - emitBodyLayer(&g, &instances, &next, &layer, 200, 200, null, Ground.opaqueRgb(bg_default), false, false); + emitBodyLayer(&g, &instances, &next, &layer, 200, 200, null, Ground.opaqueRgb(bg_default), false, false, .{ 80, 90, 100 }); // Four compact header rows (48px), then three body rows: two complete // and a third clipped to the remaining12px of the100px viewport. - try std.testing.expectEqual(@as(u32, 18), next); + try std.testing.expectEqual(@as(u32, 20), next); + try std.testing.expectApproxEqAbs(@as(f32, 0.49), instances[3].y0, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, 0.48), instances[3].y1, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, 80.0 / 255.0), instances[3].br, 0.0001); + try std.testing.expectApproxEqAbs(instances[3].br, instances[13].br, 0.0001); try std.testing.expectApproxEqAbs(@as(f32, 0.6), instances[0].y0, 0.0001); - try std.testing.expectApproxEqAbs(@as(f32, 0.12), instances[12].y0, 0.0001); - try std.testing.expectApproxEqAbs(@as(f32, -0.4), instances[17].y1, 0.0001); - try std.testing.expectApproxEqAbs(instances[17].v0 + @as(f32, 12) / atlas_h, instances[17].v1, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, 0.12), instances[14].y0, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, -0.4), instances[19].y1, 0.0001); + try std.testing.expectApproxEqAbs(instances[19].v0 + @as(f32, 12) / atlas_h, instances[19].v1, 0.0001); } diff --git a/src/macos.zig b/src/macos.zig index bb5b6bf6..3a1926b5 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -1862,7 +1862,7 @@ export fn pardes_body_layer_value(index: u32, field: u32) u32 { 8 => if (layer.cursor) |c| c.x else 65535, 9 => if (layer.cursor) |c| c.y else 65535, 10 => if (layer.cursor) |c| @intFromBool(c.bar) else 0, - else => 0, + else => @intFromBool(layer.hasContextBorderAfter(field - 11)), }; } diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index b14e1aa0..2d43cc38 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -1185,7 +1185,8 @@ final class PardesView: NSView { private func snapshotBodyLayers() -> [FrozenBodyLayer] { var result: [FrozenBodyLayer] = [] for index in 0..<pardes_body_layer_limit() { - let values = (0..<11).map { Int(pardes_body_layer_value(index, UInt32($0))) } + let count = 11 + Int(pardes_body_layer_value(index, 2)) + let values = (0..<count).map { Int(pardes_body_layer_value(index, UInt32($0))) } if values[0] == 0 { continue } if let cells = pardes_body_layer_cells(index) { result.append(FrozenBodyLayer(values:values, cells:Array(UnsafeBufferPointer(start:cells,count:values[0]*values[1])))) @@ -1196,7 +1197,8 @@ final class PardesView: NSView { private func drawBodyLayers(_ ctx: CGContext, hideCursor:Bool) { for index in 0..<pardes_body_layer_limit() { - let values = (0..<11).map { Int(pardes_body_layer_value(index, UInt32($0))) } + let count = 11 + Int(pardes_body_layer_value(index, 2)) + let values = (0..<count).map { Int(pardes_body_layer_value(index, UInt32($0))) } if values[0] == 0 { continue } if let cells = pardes_body_layer_cells(index) { drawBodyLayer(ctx, values:values, cells:cells, hideCursor:hideCursor) } } @@ -1256,8 +1258,11 @@ final class PardesView: NSView { } let border = pardes_topbar_pane_border_rgb() if border != UInt32(PARDES_COLOR_DEFAULT) { - fill(ctx, CGRect(x: left, y: top+CGFloat(contexts)*taglines.height, - width: viewport.width, height: 1/taglines.scale), border, 1) + for row in 0..<contexts { + if value(11 + row) == 0 { continue } + fill(ctx, CGRect(x: left, y: top + CGFloat(row + 1)*taglines.height, + width: viewport.width, height: 1/taglines.scale), border, 1) + } } ctx.restoreGState() } diff --git a/src/pardes.zig b/src/pardes.zig index 40312770..eb0a442f 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -13467,6 +13467,7 @@ pub const Pardes = struct { layer.cols = width; layer.rows = @intCast(logical_rows); layer.context_rows = pane.context_rows; + layer.setContextLines(pane.wrap_line[0..pane.context_rows]); layer.cursor = null; if (temporary.cursor) |cursor| { if (cursor.x >= config.GUTTER and cursor.y >= first_row) diff --git a/src/web.zig b/src/web.zig index a944a4a5..46e8c3b4 100644 --- a/src/web.zig +++ b/src/web.zig @@ -339,7 +339,7 @@ export fn pardes_body_layer_value(index: u32, field: u32) u32 { 8 => if (layer.cursor) |c| c.x else 65535, 9 => if (layer.cursor) |c| c.y else 65535, 10 => if (layer.cursor) |c| @intFromBool(c.bar) else 0, - else => 0, + else => @intFromBool(layer.hasContextBorderAfter(field - 11)), }; } diff --git a/src/web/app.mjs b/src/web/app.mjs index 8e08c9d4..312627df 100644 --- a/src/web/app.mjs +++ b/src/web/app.mjs @@ -99,7 +99,7 @@ export class DomRenderer { if (!ptr) continue; const view = new DataView(this.wasm.memory.buffer, ptr, rows * cols * CELL_SIZE); const bytes = new Uint8Array(this.wasm.memory.buffer); - const geometry = [bw,bh,tw,th,...Array.from({length:11},(_,field)=>val(field))].join(":"); + const geometry = [bw,bh,tw,th,...Array.from({length:11+contexts},(_,field)=>val(field))].join(":"); const encoded = bytes.subarray(ptr,ptr+rows*cols*CELL_SIZE); const cached = this.layerNodes.get(i); if (cached && cached.geometry===geometry && cached.bytes.length===encoded.length && cached.bytes.every((byte,index)=>byte===encoded[index])) { @@ -130,9 +130,13 @@ export class DomRenderer { } layer.append(line); } - const border = this.root.ownerDocument.createElement("div"); - border.style.cssText = `position:absolute;left:0;right:0;top:${contexts*th}px;height:1px;background:currentColor;opacity:.45`; - layer.append(border); + for (let row = 0; row < contexts; row++) { + if (!val(11 + row)) continue; + const border = this.root.ownerDocument.createElement("div"); + border.dataset.contextBorder = String(row); + border.style.cssText = `position:absolute;left:0;right:0;top:${(row+1)*th}px;height:1px;background:currentColor;opacity:.45`; + layer.append(border); + } this.layerNodes.set(i,{geometry,bytes:encoded.slice(),node:layer}); nodes.push(layer); } diff --git a/test/panes.zig b/test/panes.zig index 8c140001..bf8a98a2 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -267,6 +267,36 @@ const TreeContextTests = struct { return pane; } + test "TreeContext separators mark source gaps without splitting multiline signatures" { + if (!syntax.enabled) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 64, .rows = 18 }); + defer p.deinit(); + const pane = try setZigFile(p, "const Box = struct {\n\n // omitted member comment\n\n pub fn work(\n value: usize,\n ) void {\n" ++ " // body\n" ** 30 ++ " }\n};\n"); + p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 8, .tagline_h = 10 }; + try std.testing.expect(p.executeBuiltinLine(0, "TreeContext")); + pane.scrollBy(10); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const layer = &p.surface.body_layers[0]; + try std.testing.expectEqual(@as(u16, 4), layer.context_rows); + for ([_]i32{ 0, 4, 5, 6 }, 0..) |source_row, index| { + const row: u16 = @intCast(index); + try std.testing.expectEqual(source_row, pane.wrapAt(row).line); + try std.testing.expectEqual(index == 0 or index == 3, layer.hasContextBorderAfter(row)); + const y = layer.rowTop(row, 20, 10) + 9.5; + const x = @as(f32, @floatFromInt(layer.viewport.x)) * 10 + 5; + // A one-pixel separator remains inside its existing header row. + try std.testing.expectEqual(row, layer.hitAt(x, y, 10, 20, 8, 10).?.row); + } + pane.scrollBy(-5); + _ = try p.render(frame.allocator()); + try std.testing.expectEqual(@as(u16, 2), layer.context_rows); + try std.testing.expect(layer.hasContextBorderAfter(0)); + try std.testing.expect(layer.hasContextBorderAfter(1)); + for (2..layer.rows) |row| try std.testing.expect(!layer.hasContextBorderAfter(@intCast(row))); + } + test "TreeContext Look to an open file discards the old viewport extent" { for ([_]bool{ false, true }) |context| { if (context and !syntax.enabled) continue; diff --git a/test/web-snapshots/touch.mjs b/test/web-snapshots/touch.mjs index 4dde2fa4..ecd8a9a3 100644 --- a/test/web-snapshots/touch.mjs +++ b/test/web-snapshots/touch.mjs @@ -49,12 +49,12 @@ export default async function ({ grid, touch, wait, stable, delay, evaluate, mou document.body.append(root); const memory = new WebAssembly.Memory({initial:1}); const bytes = new Uint8Array(memory.buffer), data = new DataView(memory.buffer); - for (const [base,count] of [[0,4],[1024,6]]) for(let i=0;i<count;i++) { + for (const [base,count] of [[0,4],[1024,10]]) for(let i=0;i<count;i++) { const off=base+i*20; bytes[off]=65+i; bytes[off+18]=1; data.setUint32(off+8,0xffffff,true); data.setUint32(off+12,0x123456,true); if(base && i<2) data.setUint16(off+16,128,true); } - const values=[3,2,1,0,0,2,2,42,0,2,1]; + const values=[3,2,1,0,0,2,2,42,0,2,1,1]; const wasm={memory,pardes_gui_tagline_font_percent:()=>50,pardes_frame:()=>4, pardes_pointer_shape:()=>0,pardes_frame_cols:()=>2,pardes_frame_rows:()=>2, pardes_frame_ptr:()=>0,pardes_cursor_x:()=>65535,pardes_cursor_y:()=>65535,pardes_cursor_bar:()=>0, @@ -70,6 +70,15 @@ export default async function ({ grid, touch, wait, stable, delay, evaluate, mou renderer.render(); const result={bands,height:rect.height,clipped:getComputedStyle(layer).overflow, cursor:cursor?.textContent,cached:renderer.layerRoot.firstChild===layer}; + values[0]=5;values[2]=3;values[6]=4;values[11]=0;values[12]=1;values[13]=1; + renderer.render(); + const borders=()=>[...renderer.layerRoot.querySelectorAll('[data-context-border]')].map(node=>parseFloat(node.style.top)); + result.gapBorders=borders(); + const withGap=renderer.layerRoot.firstChild; + values[12]=0; + renderer.render(); + result.closedGapBorders=borders(); + result.borderChangeInvalidates=renderer.layerRoot.firstChild!==withGap; root.remove();return result; })()`); assert.deepEqual(compactGeometry.bands, [[0,10],[10,20],[30,20]], "compact headers release real height to additional body rows"); @@ -77,6 +86,9 @@ export default async function ({ grid, touch, wait, stable, delay, evaluate, mou assert.equal(compactGeometry.clipped,"hidden"); assert.equal(compactGeometry.cursor,"E"); assert(compactGeometry.cached,"unchanged compact layers reuse their DOM nodes"); + assert.deepEqual(compactGeometry.gapBorders,[20,30],"source gaps and final header have separators, consecutive headers do not"); + assert.deepEqual(compactGeometry.closedGapBorders,[30]); + assert(compactGeometry.borderChangeInvalidates,"separator changes invalidate an otherwise unchanged layer"); let frame = await grid(); assert.equal(tags(frame).length, 1); |
