summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/body_layer.zig29
-rw-r--r--src/detached/wire.zig20
-rw-r--r--src/gui/gui.zig41
-rw-r--r--src/macos.zig2
-rw-r--r--src/macos/Sources/PardesView.swift13
-rw-r--r--src/pardes.zig1
-rw-r--r--src/web.zig2
-rw-r--r--src/web/app.mjs12
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);
}