summaryrefslogtreecommitdiff
path: root/src/detached
diff options
context:
space:
mode:
Diffstat (limited to 'src/detached')
-rw-r--r--src/detached/client.zig9
-rw-r--r--src/detached/server.zig6
-rw-r--r--src/detached/wire.zig164
3 files changed, 172 insertions, 7 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig
index affa070e..42404bca 100644
--- a/src/detached/client.zig
+++ b/src/detached/client.zig
@@ -102,6 +102,7 @@ pub const Client = struct {
grid: std.ArrayListUnmanaged(pardes.Cell) = .empty,
cursor: ?wire.Cursor = null,
pointer_shape: pardes.Surface.PointerShape = .arrow,
+ body_layers: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{}),
in: std.ArrayListUnmanaged(u8) = .empty,
out: std.ArrayListUnmanaged(u8) = .empty,
/// Bytes of `in` belonging to the message `next` returned last. Compacted at
@@ -174,6 +175,10 @@ pub const Client = struct {
if (c.fd >= 0) _ = libc.close(c.fd);
c.fd = -1;
c.grid.deinit(c.gpa);
+ for (&c.body_layers) |*layer| {
+ c.gpa.free(layer.cells);
+ layer.* = .{};
+ }
c.in.deinit(c.gpa);
c.out.deinit(c.gpa);
}
@@ -311,6 +316,10 @@ 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) {
+ error.OutOfMemory => error.Closed,
+ else => @as(wire.Error, @errorCast(err)),
+ };
c.cursor = f.cursor;
c.pointer_shape = f.pointer_shape;
},
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 9d83d3eb..82abc432 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);
+ const want = wire.frameBound(surface.cols, surface.rows) + wire.layersBound(&surface.body_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)
&.{}
@@ -717,7 +717,7 @@ pub const Session = struct {
.{ .x = cur.x, .y = cur.y, .bar = cur.bar }
else
null;
- const bytes = wire.encodeFramePointer(
+ const bytes = wire.encodeFrameLayers(
s.scratch.allocatedSlice()[0..want],
surface.cols,
surface.rows,
@@ -725,6 +725,7 @@ pub const Session = struct {
surface.pointer_shape,
cells,
prev,
+ &surface.body_layers,
) catch |err| {
log.debug("frame {d}x{d} not encodable: {t}", .{ surface.cols, surface.rows, err });
return;
@@ -987,6 +988,7 @@ pub const Session = struct {
.resize => |r| {
c.cols = r.cols;
c.rows = r.rows;
+ if (r.row_metrics) |metrics| s.core.row_metrics = metrics;
},
else => {
s.origin = slot;
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 5087cfcd..5d52e2df 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -3,7 +3,7 @@
const std = @import("std");
const pardes = @import("../pardes.zig");
-pub const version: u16 = 3;
+pub const version: u16 = 4;
pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace };
@@ -15,7 +15,7 @@ pub const header_len = 5; // tag:u8, payload length:u32le
const cell_max = 1 + 1 + 7 + 4 + 4 + 1 + 1 + 1;
const run_header = 4 + 2; // start:u32, count:u16
const run_max = std.math.maxInt(u16);
-const frame_head = 1 + 2 + 2 + 6 + 4;
+const frame_head = 1 + 2 + 2 + 6 + 4 + 4;
// Worst case: every cell changed, each in its own run.
pub fn frameBound(cols: u16, rows: u16) usize {
@@ -154,6 +154,7 @@ pub const Cursor = struct { x: u16, y: u16, bar: bool };
/// union is passed by value, and the receiver already owns the grid the runs
/// belong in. `apply` is the bounds-checked walk.
pub const Frame = struct {
+ layers: []const u8 = &.{},
pointer_shape: pardes.Surface.PointerShape = .arrow,
kind: FrameKind,
cols: u16,
@@ -162,6 +163,38 @@ 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 {
+ for (layers) |*layer| {
+ gpa.free(layer.cells);
+ layer.* = .{};
+ }
+ errdefer for (layers) |*layer| {
+ gpa.free(layer.cells);
+ layer.* = .{};
+ };
+ if (f.layers.len == 0) return;
+ var r: Reader = .init(f.layers);
+ const count = try r.getU16();
+ if (count > layers.len) return error.Overlong;
+ for (layers[0..count]) |*layer| {
+ layer.pane = 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();
+ 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.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;
+ 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);
+ for (layer.cells) |*cell| cell.* = try decodeCell(&r);
+ }
+ try r.end();
+ }
+
/// Paint this frame into `grid`, which must be exactly `cols * rows` cells
/// — the receiver resizes on a geometry change before applying, and a grid
/// of the wrong size is a receiver bug, not a wire condition.
@@ -601,6 +634,18 @@ pub fn encodeFramePointer(
cells: []const pardes.Cell,
prev: []const pardes.Cell,
) Error![]const u8 {
+ return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{});
+}
+
+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;
+ };
+ 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 {
// 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
@@ -619,6 +664,31 @@ pub fn encodeFramePointer(
try w.putU16(cols);
try w.putU16(rows);
try putCursor(&w, cursor, cols, rows);
+ const layers_len_at = w.n;
+ try w.putU32(0);
+ const layers_start = w.n;
+ if (layers.len != 0) {
+ var count: u16 = 0;
+ for (layers) |layer| if (layer.rows != 0) {
+ count += 1;
+ };
+ try w.putU16(count);
+ for (layers) |layer| {
+ if (layer.rows == 0) continue;
+ try w.putU16(layer.pane);
+ 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);
+ try w.putU16(layer.rows);
+ try w.putU16(layer.context_rows);
+ 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);
+ }
+ }
+ std.mem.writeInt(u32, w.buf[layers_len_at..][0..4], @intCast(w.n - layers_start), .little);
const nruns_at = w.n;
try w.putU32(0);
@@ -698,6 +768,42 @@ fn clientTag(msg: ClientMsg) ClientTag {
/// WIRE is not.
const has_cell_pixels = @hasField(pardes.CellPixels, "w");
+fn putMetrics(w: *Writer, metrics: ?pardes.RowMetrics) Error!void {
+ try w.putBool(metrics != null);
+ if (metrics) |m| {
+ try w.putU16(m.body_w);
+ try w.putU16(m.body_h);
+ try w.putU16(m.tagline_w);
+ try w.putU16(m.tagline_h);
+ }
+}
+
+fn getMetrics(r: *Reader) Error!?pardes.RowMetrics {
+ if (!try r.getBool()) return null;
+ const m: pardes.RowMetrics = .{ .body_w = try r.getU16(), .body_h = try r.getU16(), .tagline_w = try r.getU16(), .tagline_h = try r.getU16() };
+ if (m.body_w == 0 or m.body_h == 0 or m.tagline_w == 0 or m.tagline_h == 0) return error.BadValue;
+ return m;
+}
+
+fn putBodyHit(w: *Writer, hit: ?pardes.Mouse.BodyHit) Error!void {
+ try w.putBool(hit != null);
+ if (hit) |h| {
+ try w.putU32(h.serial);
+ try w.putU16(h.row);
+ try w.putU16(h.col);
+ try w.putF32(h.pixel_x);
+ try w.putF32(h.pixel_y);
+ try putMetrics(w, h.metrics);
+ }
+}
+
+fn getBodyHit(r: *Reader) Error!?pardes.Mouse.BodyHit {
+ if (!try r.getBool()) return null;
+ const hit: pardes.Mouse.BodyHit = .{ .serial = try r.getU32(), .row = try r.getU16(), .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)));
@@ -726,6 +832,7 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 {
try w.putU16(m.col);
try w.putU16(m.row);
try w.putBool(m.ctrl);
+ try putBodyHit(&w, m.body_hit);
},
.resize => |rs| {
try w.putU16(rs.cols);
@@ -735,6 +842,7 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 {
// carries where it exists.
try w.putU16(if (comptime has_cell_pixels) rs.cell_pixels.w else 8);
try w.putU16(if (comptime has_cell_pixels) rs.cell_pixels.h else 16);
+ try putMetrics(&w, rs.row_metrics);
},
.paste => |b| try w.putSlice32(b),
.command => |line| try w.putSlice16(line),
@@ -786,6 +894,7 @@ pub fn decodeClient(tag: u8, payload: []const u8) Error!ClientMsg {
.col = try r.getU16(),
.row = try r.getU16(),
.ctrl = try r.getBool(),
+ .body_hit = try getBodyHit(&r),
} } },
.resize => blk: {
var ev: pardes.Event = .{ .resize = .{ .cols = try r.getCols(), .rows = try r.getRows() } };
@@ -794,6 +903,7 @@ pub fn decodeClient(tag: u8, payload: []const u8) Error!ClientMsg {
// Read either way — the bytes are on the wire — and kept only by a
// build that has somewhere to keep them.
if (comptime has_cell_pixels) ev.resize.cell_pixels = .{ .w = px_w, .h = px_h };
+ ev.resize.row_metrics = try getMetrics(&r);
break :blk .{ .event = ev };
},
.paste => .{ .event = .{ .paste = try r.getSlice32() } },
@@ -849,7 +959,7 @@ pub fn encodeServer(out: []u8, msg: ServerMsg) Error![]const u8 {
/// field-by-field count: a bound that is 32 bytes generous costs one `memcpy`
/// worth of nothing, and a bound that is one byte tight is a bug that only
/// shows up on the message nobody tested.
-const msg_slack = header_len + 32;
+const msg_slack = header_len + 64;
/// An upper bound on `encodeServer`'s output, so a caller sizes its buffer
/// once instead of guessing and retrying.
@@ -897,6 +1007,7 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg {
const cols = try r.getCols();
const rows = try r.getRows();
const cursor = try getCursor(&r, cols, rows);
+ const layers = try r.getSlice32();
const nruns = try r.getU32();
// One run per cell is the worst an encoder emits, so a bigger
// count cannot be describing this grid. Checked HERE rather than
@@ -906,6 +1017,7 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg {
const runs = r.bytes[r.i..];
r.i = r.bytes.len;
break :blk .{ .frame = .{
+ .layers = layers,
.kind = kind,
.pointer_shape = if (header == .full_link or header == .diff_link) .link else .arrow,
.cols = cols,
@@ -1287,7 +1399,7 @@ test "detached wire: the diff is worth having, in bytes, on the board's grid" {
for (cells) |*c| c.* = .{ .text = "a".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false };
const full = (try encodeFrame(buf, cols, rows, null, cells, &.{})).len;
try testing.expectEqual(@as(usize, header_len + frame_head + run_header + n * 8), full);
- try testing.expectEqual(@as(usize, 6298), full);
+ try testing.expectEqual(@as(usize, 6302), full);
const before = try gpa.dupe(pardes.Cell, cells);
defer gpa.free(before);
@@ -1298,7 +1410,7 @@ test "detached wire: the diff is worth having, in bytes, on the board's grid" {
cells[500] = .{ .text = "d".* ++ @as([6]u8, @splat(0)), .len = 1, .default = false };
const diff = (try encodeFrame(buf, cols, rows, null, cells, before)).len;
try testing.expectEqual(@as(usize, header_len + frame_head + 2 * run_header + 3 * 8), diff);
- try testing.expectEqual(@as(usize, 56), diff);
+ try testing.expectEqual(@as(usize, 60), diff);
// The whole reason this codec exists rather than shipping the Surface.
try testing.expect(full / diff > 100);
}
@@ -1443,6 +1555,7 @@ test "detached wire: a frame that lies about its runs cannot walk out of the gri
try w.putU16(cols);
try w.putU16(rows);
try putCursor(&w, null, cols, rows);
+ try w.putU32(0); // no body layers
try w.putU32(9);
try testing.expectError(error.Overlong, decodeServer(@intFromEnum(ServerTag.frame), w.written()));
@@ -1508,6 +1621,7 @@ test "detached wire: a cursor outside the grid is refused, not painted" {
try w.putU16(cols);
try w.putU16(rows);
try putCursor(&w, .{ .x = cols - 1, .y = rows - 1, .bar = true }, cols, rows);
+ try w.putU32(0); // no body layers
try w.putU32(0);
const got = (try decodeServer(@intFromEnum(ServerTag.frame), w.written())).frame;
try testing.expectEqual(@as(u16, cols - 1), got.cursor.?.x);
@@ -1607,3 +1721,43 @@ test "detached wire: max_payload bounds every message this protocol can build" {
var small: [8]u8 = undefined;
try testing.expectError(error.NoSpace, encodeClient(&small, .{ .event = .{ .paste = "0123456789" } }));
}
+
+test "detached wire carries compact body rows and exact pointer metrics" {
+ const gpa = std.testing.allocator;
+ 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 } };
+ 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});
+ const packet = (try framed(encoded)).?;
+ const frame = (try decodeServer(packet.tag, packet.payload)).frame;
+ var decoded: [pardes.MAX_PANES]pardes.BodyLayer = @splat(.{});
+ defer for (&decoded) |*item| gpa.free(item.cells);
+ try frame.apply(&cells);
+ try frame.applyLayers(gpa, &decoded);
+ try std.testing.expectEqual(@as(u16, 6), decoded[0].rows);
+ 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).?;
+ const message: ClientMsg = .{ .event = .{ .mouse = .{ .col = 0, .row = 1, .button = .left, .kind = .press, .body_hit = 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.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));
+ for (decoded) |item| {
+ try std.testing.expectEqual(@as(u16, 0), item.rows);
+ 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 std.testing.expectEqual(@as(u16, 0), decoded[0].rows);
+}