diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 16:28:57 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 1ab4c071be58033d5452f69797ca8e2c32240cff (patch) | |
| tree | afa108a1c372276da1d2c62b6c05d8f167cc81b8 /src/detached | |
| parent | c6ab9db0fdbcb8d16f666749194bb111218b96a8 (diff) | |
| download | pardes-1ab4c071be58033d5452f69797ca8e2c32240cff.tar.gz pardes-1ab4c071be58033d5452f69797ca8e2c32240cff.zip | |
Separate tag text geometry from physical pane grips
Diffstat (limited to 'src/detached')
| -rw-r--r-- | src/detached/client.zig | 4 | ||||
| -rw-r--r-- | src/detached/server.zig | 3 | ||||
| -rw-r--r-- | src/detached/wire.zig | 131 |
3 files changed, 121 insertions, 17 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig index 42404bca..9481cd54 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -103,6 +103,7 @@ pub const Client = struct { cursor: ?wire.Cursor = null, pointer_shape: pardes.Surface.PointerShape = .arrow, body_layers: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{}), + tag_layers: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = @splat(.{}), in: std.ArrayListUnmanaged(u8) = .empty, out: std.ArrayListUnmanaged(u8) = .empty, /// Bytes of `in` belonging to the message `next` returned last. Compacted at @@ -179,6 +180,7 @@ pub const Client = struct { c.gpa.free(layer.cells); layer.* = .{}; } + for (&c.tag_layers) |*layer| c.gpa.free(layer.cells); c.in.deinit(c.gpa); c.out.deinit(c.gpa); } @@ -316,7 +318,7 @@ pub const Client = struct { try c.reshape(f.cols, f.rows); } try f.apply(c.grid.items); - f.applyLayers(c.gpa, &c.body_layers) catch |err| return switch (err) { + f.applyLayers(c.gpa, &c.body_layers, &c.tag_layers) catch |err| return switch (err) { error.OutOfMemory => error.Closed, else => @as(wire.Error, @errorCast(err)), }; diff --git a/src/detached/server.zig b/src/detached/server.zig index 82abc432..6b82cd7b 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -707,7 +707,7 @@ pub const Session = struct { fn sendFrame(s: *Session, c: *Client, surface: *const pardes.Surface) void { const cells = surface.cells; - const want = wire.frameBound(surface.cols, surface.rows) + wire.layersBound(&surface.body_layers); + const want = wire.frameBound(surface.cols, surface.rows) + wire.layersBound(&surface.body_layers, &surface.tag_layers); s.scratch.ensureTotalCapacity(s.gpa, want) catch return s.close(c, .oom); const prev: []const pardes.Cell = if (c.need_full or c.mirror.items.len != cells.len) &.{} @@ -726,6 +726,7 @@ pub const Session = struct { cells, prev, &surface.body_layers, + &surface.tag_layers, ) catch |err| { log.debug("frame {d}x{d} not encodable: {t}", .{ surface.cols, surface.rows, err }); return; diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 416330e8..245bc9f4 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -4,7 +4,7 @@ const std = @import("std"); const pardes = @import("../pardes.zig"); const limits = @import("../memory.zig").limits; -pub const version: u16 = 5; +pub const version: u16 = 6; pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; @@ -164,15 +164,25 @@ pub const Frame = struct { nruns: u32, runs: []const u8, - pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.BodyLayer) !void { + pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.BodyLayer, tags: *[pardes.MAX_TAG_LAYERS]pardes.TagLayer) !void { for (layers) |*layer| { gpa.free(layer.cells); layer.* = .{}; } - errdefer for (layers) |*layer| { + for (tags) |*layer| { gpa.free(layer.cells); layer.* = .{}; - }; + } + errdefer { + for (layers) |*layer| { + gpa.free(layer.cells); + layer.* = .{}; + } + for (tags) |*layer| { + gpa.free(layer.cells); + layer.* = .{}; + } + } if (f.layers.len == 0) return; var r: Reader = .init(f.layers); const count = try r.getU16(); @@ -196,6 +206,27 @@ pub const Frame = struct { layer.cells = try gpa.alloc(pardes.Cell, @as(usize, layer.cols) * layer.rows); for (layer.cells) |*cell| cell.* = try decodeCell(&r); } + const tag_count = try r.getU16(); + if (tag_count > tags.len) return error.Overlong; + for (tags[0..tag_count]) |*layer| { + layer.kind = try r.getTag(pardes.TagKind); + layer.id = try r.getU16(); + layer.serial = try r.getU32(); + layer.viewport = .{ .x = try r.getU16(), .y = try r.getU16(), .w = try r.getU16(), .h = try r.getU16() }; + layer.cols = try r.getU16(); + for (&layer.bg) |*channel| channel.* = try r.getByte(); + if (layer.cols == 0 or layer.viewport.w == 0 or layer.viewport.h != 1 or + layer.viewport.x > f.cols or layer.viewport.w > f.cols - layer.viewport.x or + layer.viewport.y >= f.rows or (switch (layer.kind) { + .workspace => layer.id != 0, + .column => layer.id >= pardes.MAX_COLS, + .pane => layer.id >= pardes.MAX_PANES, + })) return error.BadValue; + const cursor = try getCursor(&r, layer.cols, 1); + layer.cursor = if (cursor) |c| .{ .col = c.x, .bar = c.bar } else null; + layer.cells = try gpa.alloc(pardes.Cell, layer.cols); + for (layer.cells) |*cell| cell.* = try decodeCell(&r); + } try r.end(); } @@ -638,18 +669,21 @@ pub fn encodeFramePointer( cells: []const pardes.Cell, prev: []const pardes.Cell, ) Error![]const u8 { - return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}); + return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}, &.{}); } -pub fn layersBound(layers: []const pardes.BodyLayer) usize { - var n: usize = 2; +pub fn layersBound(layers: []const pardes.BodyLayer, tags: []const pardes.TagLayer) usize { + var n: usize = 4; for (layers) |layer| if (layer.rows != 0) { n += 26 + layer.context_rows + layer.cells.len * cell_max; }; + for (tags) |layer| if (layer.cols != 0) { + n += 26 + layer.cells.len * cell_max; + }; return n; } -pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, pointer_shape: pardes.Surface.PointerShape, cells: []const pardes.Cell, prev: []const pardes.Cell, layers: []const pardes.BodyLayer) Error![]const u8 { +pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, pointer_shape: pardes.Surface.PointerShape, cells: []const pardes.Cell, prev: []const pardes.Cell, layers: []const pardes.BodyLayer, tags: []const pardes.TagLayer) Error![]const u8 { // The protocol's ceiling, enforced by the ENCODER too, and BEFORE the // assert below so a caller can be told rather than tripped. `max_payload` // is derived from these two, so a larger grid is a frame this decoder @@ -671,7 +705,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point const layers_len_at = w.n; try w.putU32(0); const layers_start = w.n; - if (layers.len != 0) { + if (layers.len != 0 or tags.len != 0) { var count: u16 = 0; for (layers) |layer| if (layer.rows != 0) { count += 1; @@ -693,6 +727,26 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point 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); } + var tag_count: u16 = 0; + for (tags) |layer| if (layer.cols != 0) { + tag_count += 1; + }; + try w.putU16(tag_count); + for (tags) |layer| { + if (layer.cols == 0) continue; + if (layer.cells.len != layer.cols) return error.BadValue; + try w.putByte(@intFromEnum(layer.kind)); + try w.putU16(layer.id); + try w.putU32(layer.serial); + try w.putU16(layer.viewport.x); + try w.putU16(layer.viewport.y); + try w.putU16(layer.viewport.w); + try w.putU16(layer.viewport.h); + try w.putU16(layer.cols); + for (layer.bg) |channel| try w.putByte(channel); + try putCursor(&w, if (layer.cursor) |c| .{ .x = c.col, .y = 0, .bar = c.bar } else null, layer.cols, 1); + for (layer.cells) |*cell| try putCell(&w, cell); + } } std.mem.writeInt(u32, w.buf[layers_len_at..][0..4], @intCast(w.n - layers_start), .little); const nruns_at = w.n; @@ -810,6 +864,34 @@ fn getBodyHit(r: *Reader) Error!?pardes.Mouse.BodyHit { return hit; } +fn putTagHit(w: *Writer, hit: ?pardes.TagHit) Error!void { + try w.putBool(hit != null); + if (hit) |h| { + try w.putByte(@intFromEnum(h.kind)); + try w.putU16(h.id); + try w.putU32(h.serial); + try w.putU16(h.col); + try w.putF32(h.pixel_x); + try w.putF32(h.pixel_y); + try putMetrics(w, h.metrics); + } +} + +fn getTagHit(r: *Reader) Error!?pardes.TagHit { + if (!try r.getBool()) return null; + const hit: pardes.TagHit = .{ + .kind = try r.getTag(pardes.TagKind), + .id = try r.getU16(), + .serial = try r.getU32(), + .col = try r.getU16(), + .pixel_x = try r.getF32(), + .pixel_y = try r.getF32(), + .metrics = (try getMetrics(r)) orelse return error.BadValue, + }; + if (!std.math.isFinite(hit.pixel_x) or !std.math.isFinite(hit.pixel_y)) return error.BadValue; + return hit; +} + pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 { var w: Writer = .init(out); const at = try beginMessage(&w, @intFromEnum(clientTag(msg))); @@ -839,6 +921,7 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 { try w.putU16(m.row); try w.putBool(m.ctrl); try putBodyHit(&w, m.body_hit); + try putTagHit(&w, m.tag_hit); }, .resize => |rs| { try w.putU16(rs.cols); @@ -901,6 +984,7 @@ pub fn decodeClient(tag: u8, payload: []const u8) Error!ClientMsg { .row = try r.getU16(), .ctrl = try r.getBool(), .body_hit = try getBodyHit(&r), + .tag_hit = try getTagHit(&r), } } }, .resize => blk: { var ev: pardes.Event = .{ .resize = .{ .cols = try r.getCols(), .rows = try r.getRows() } }; @@ -1735,15 +1819,20 @@ test "detached wire carries compact body rows and exact pointer metrics" { logical[10] = .{ .text = "z".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false }; 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})); + var tag_cells: [3]pardes.Cell = @splat(.{}); + tag_cells[2] = logical[10]; + const tag: pardes.TagLayer = .{ .kind = .column, .id = 1, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 1 }, .cols = 3, .bg = .{ 11, 22, 33 }, .cells = &tag_cells, .cursor = .{ .col = 2, .bar = true } }; + const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag})); defer gpa.free(out); - const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}); + const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}); const packet = (try framed(encoded)).?; const frame = (try decodeServer(packet.tag, packet.payload)).frame; var decoded: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{}); + var tags: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = @splat(.{}); + defer for (&tags) |*item| gpa.free(item.cells); defer for (&decoded) |*item| gpa.free(item.cells); try frame.apply(&cells); - try frame.applyLayers(gpa, &decoded); + try frame.applyLayers(gpa, &decoded, &tags); 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)); @@ -1751,25 +1840,37 @@ test "detached wire carries compact body rows and exact pointer metrics" { 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); + try std.testing.expectEqual(tag.kind, tags[0].kind); + try std.testing.expectEqual(tag.id, tags[0].id); + try std.testing.expectEqual(tag.bg, tags[0].bg); + try std.testing.expectEqual(tag.cols, tags[0].cols); + try std.testing.expectEqual(@as(u16, 2), tags[0].cursor.?.col); + try std.testing.expectEqualStrings("z", tags[0].cells[2].grapheme()); const pointer = layer.hitAt(4, 27, 8, 16, 6, 8).?; - const message: ClientMsg = .{ .event = .{ .mouse = .{ .col = 0, .row = 1, .button = .left, .kind = .press, .body_hit = pointer } } }; + const tag_pointer = tag.hitAt(11, 8, 8, 16, 4, 8).?; + const message: ClientMsg = .{ .event = .{ .mouse = .{ .col = 0, .row = 1, .button = .left, .kind = .press, .body_hit = pointer, .tag_hit = tag_pointer } } }; var mouse_bytes: [256]u8 = undefined; const mouse_encoded = try encodeClient(&mouse_bytes, message); const mouse_packet = (try framed(mouse_encoded)).?; const mouse = (try decodeClient(mouse_packet.tag, mouse_packet.payload)).event.mouse; try std.testing.expectEqualDeep(pointer, mouse.body_hit.?); + try std.testing.expectEqualDeep(tag_pointer, mouse.tag_hit.?); try std.testing.expect(mouse_encoded.len <= clientBound(message)); var truncated = frame; truncated.layers = frame.layers[0 .. frame.layers.len - 1]; - try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded)); + try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded, &tags)); 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()); } + for (tags) |item| { + try std.testing.expectEqual(@as(u16, 0), item.cols); + try std.testing.expectEqual(@as(usize, 0), item.cells.len); + } const no_layers = try encodeFramePointer(out, 2, 4, null, .arrow, &cells, &cells); const cleared_packet = (try framed(no_layers)).?; const cleared = (try decodeServer(cleared_packet.tag, cleared_packet.payload)).frame; - try cleared.applyLayers(gpa, &decoded); + try cleared.applyLayers(gpa, &decoded, &tags); try std.testing.expectEqual(@as(u16, 0), decoded[0].rows); } |
