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 /src | |
| parent | c1787ff868ee723629c1d500098d36dc419f77c3 (diff) | |
| download | pardes-8ac4a48a2e8e66a9e72bcbc61c114be8db9215fd.tar.gz pardes-8ac4a48a2e8e66a9e72bcbc61c114be8db9215fd.zip | |
Separate discontinuous pinned source context with thin borders
Diffstat (limited to 'src')
| -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 |
8 files changed, 92 insertions, 28 deletions
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); } |
