summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/render-pipeline.md2
-rw-r--r--src/Layer.zig198
-rw-r--r--src/Messages.zig6
-rw-r--r--src/Presentation.zig12
-rw-r--r--src/body_layer.zig117
-rw-r--r--src/detached/client.zig30
-rw-r--r--src/detached/server.zig3
-rw-r--r--src/detached/wire.zig118
-rw-r--r--src/draw.zig187
-rw-r--r--src/edit.zig2
-rw-r--r--src/gui/gui.zig139
-rw-r--r--src/look.zig2
-rw-r--r--src/macos.zig12
-rw-r--r--src/pardes.zig40
-rw-r--r--src/surface.zig15
-rw-r--r--src/tag_layer.zig87
-rw-r--r--src/web.zig12
-rw-r--r--src/web/app.mjs38
-rw-r--r--test/column_drag.zig10
-rw-r--r--test/panes.zig30
20 files changed, 590 insertions, 470 deletions
diff --git a/docs/render-pipeline.md b/docs/render-pipeline.md
index f63b9a0b..a8b3f019 100644
--- a/docs/render-pipeline.md
+++ b/docs/render-pipeline.md
@@ -809,7 +809,7 @@ stripped. No visual change until stage 9 unless stated.
| 4 | PLACE: Region list built once; renderPane/tags/notices read their rects from it | wait for tag→Text; the refactor is already replacing BOX_H with `pane.tag_rows` and `p.tagTop/bodyTop(pane, r)` — PLACE absorbs those into the region rects |
| 5 | JOIN: paint tags/notices/headers once into layers, copy into grid; the grid wins where the copies disagree, and each disagreement is listed for a later decision | goldens are the oracle; wide-grapheme re-clip at the edge (§3.2) |
| 6 | body layer joined the same way (paint once, copy visible rows) | riskiest join; A/B the old double paint in a temporary test with tag_bottom on and off, then delete it |
-| 7 | Layer merge (TagLayer + BodyLayer), wire v8, web accessors | after the other agent lands; touches mouse hit paths; breaks the macOS shell's layer ABI (accepted: macOS build ignored for now) |
+| 7 | Layer merge (TagLayer + BodyLayer), wire v8, web accessors | after the other agent lands; touches mouse hit paths; breaks the macOS shell's layer ABI (accepted: macOS build ignored for now); done: one `Layer` (src/Layer.zig) with `rows` (0 = no layer) and a `cursor{x, y}`; a tag of N rows is ONE layer of N grid rows (`tagHit` answers the row as `line`, `bodyHit` keeps its meaning), so the per-line layer bases are gone. Wire v8 ships rows, cursor y and the region list in the one bump; v7 and v9 peers are refused in both directions (tests). web: `tag_layer_value` 11 = rows, 12 = cursor y, and app.mjs lays every row. macOS: its Zig side compiles against `Layer`, but pardes.h still sees one row per tag layer (a taller tag shows its first row there) |
| 8 | GUI draws from regions: role tiers with track groups in tiers 2-3, page cover, hard-edged snapped decor for rules/rails/grips (pane chrome in the pane's tier), per-instance clip; delete inference functions, `transient_on` and `mark_hover` (breaks macOS glass hover; accepted, macOS ignored for now) | stage-0 PPM goldens byte-identical (possible only because hard decor is snapped and not anti-aliased); pane chrome now also shows during transitions, which is the one allowed visible delta, listed |
| 9 | post chain: glslang, Shadertoy prefix, ping-pong, ShaderAnimation, redraw levels A/B (§5.5); bundled CRT without barrel; delete scene_effects/crt.zig/crt.frag | first visual change (the CRT look) |
| 10+ | `fx` bookmark: G1–G3 → feel review → G4 bundled → G5 lapis theme → P2s; tty T1–T2 → feel review → removals of audited effects (after user decision) → T3–T5 | each effect its own change, default off |
diff --git a/src/Layer.zig b/src/Layer.zig
new file mode 100644
index 00000000..e979eab7
--- /dev/null
+++ b/src/Layer.zig
@@ -0,0 +1,198 @@
+//! A band of cells a pixel shell draws over the grid at its own pitch: a
+//! pane's, a column's or the workspace's tag rows, a notice chip, or a file
+//! body whose compact tree-sitter context rows outnumber its grid rows. The
+//! grid under it holds a copy of what fits there (draw.zig joins them), so a
+//! terminal client, which has only the grid, loses nothing but the pitch.
+//!
+//! Its viewport is in grid cells, so compact text never moves a hit target.
+//! A tag's rows are each one grid row, their glyphs at the tagline's size; a
+//! body's first `context_rows` rows are stacked at the tagline's height, the
+//! rest at the body's. `rows == 0` is no layer.
+const std = @import("std");
+const pardes = @import("pardes.zig");
+const config = @import("config.zig");
+const limits = @import("memory.zig").limits;
+
+const Layer = @This();
+
+/// `notice` is a pane's transient line -- a builtin's name, a pending leader
+/// chord -- carried as a tagline-shaped band so it gets the tagline's height,
+/// small-font metrics, band offset and border instead of being a body-grid row
+/// wearing a tagline font.
+pub const Kind = enum(u8) { workspace, column, pane, notice, body };
+
+pub const Metrics = struct {
+ body_w: u16 = 1,
+ body_h: u16 = 1,
+ tagline_w: u16 = 1,
+ tagline_h: u16 = 1,
+};
+
+/// A point on a tag, notice or header layer: which row of it and which
+/// column at the tagline's pitch.
+pub const TagHit = struct {
+ kind: Kind,
+ id: u16,
+ serial: u32,
+ col: u16,
+ /// Which row of a taller tag.
+ line: u16 = 0,
+ pixel_x: f32,
+ pixel_y: f32,
+ metrics: Metrics,
+};
+
+kind: Kind = .body,
+/// The pane slot, or the column, it belongs to.
+id: u16 = 0,
+serial: u32 = 0,
+viewport: pardes.Rect = .{ .x = 0, .y = 0, .w = 0, .h = 0 },
+cols: u16 = 0,
+rows: u16 = 0,
+/// A body's compact rows, from its top.
+context_rows: u16 = 0,
+context_separators: std.bit_set.StaticBitSet(limits.wrap_rows) = .initEmpty(),
+/// `rows * cols`, row after row.
+cells: []pardes.Cell = &.{},
+cursor: ?struct { x: u16, y: u16, bar: bool = false } = null,
+/// A band's fill, where its cells run out.
+bg: [3]u8 = .{ 0, 0, 0 },
+/// Vertical offset in rows (negative: above its viewport), for a band
+/// that is moving into place. A pixel shell draws it offset and clipped
+/// to the rows below the viewport's top; the grid has no such thing.
+slide: f32 = 0,
+/// How present a notice band is (1 = fully) while it fades up or
+/// dissolves. `bg` and the cells are already blended by it; a pixel
+/// shell uses it for what it draws around the band, like its rule.
+fade: f32 = 1,
+
+/// Tag columns a viewport this wide holds at the tagline's pitch.
+pub fn capacity(layer: *const Layer, metrics: Metrics) u16 {
+ return @intCast(@min(std.math.maxInt(u16), @max(1, @as(u32, layer.viewport.w) * @max(1, metrics.body_w) / @max(1, metrics.tagline_w))));
+}
+
+pub fn setContextLines(layer: *Layer, lines: []const i32) void {
+ layer.context_separators = .initEmpty();
+ const count = @min(lines.len, layer.context_rows);
+ var index: usize = 0;
+ while (index + 1 < count and index < limits.wrap_rows) : (index += 1) {
+ if (lines[index + 1] != lines[index] +| 1) layer.context_separators.set(index);
+ }
+}
+
+pub fn hasContextBorderAfter(layer: *const Layer, index: usize) bool {
+ if (index >= layer.context_rows or index >= limits.wrap_rows) return false;
+ return index + 1 == layer.context_rows or layer.context_separators.isSet(index);
+}
+
+/// Pixel top of row `index`: a body's compact rows stacked at the tagline's
+/// height, every other row a grid row.
+pub fn rowTop(layer: *const Layer, index: u16, body_h: f32, tagline_h: f32) f32 {
+ const body = @max(1, body_h);
+ const tag = std.math.clamp(tagline_h, 1, body);
+ return @as(f32, @floatFromInt(layer.viewport.y)) * body +
+ @as(f32, @floatFromInt(@min(index, layer.context_rows))) * tag +
+ @as(f32, @floatFromInt(index -| layer.context_rows)) * body;
+}
+
+pub fn rowHeight(layer: *const Layer, index: u16, body_h: f32, tagline_h: f32) f32 {
+ return if (index < layer.context_rows) std.math.clamp(tagline_h, 1, @max(1, body_h)) else @max(1, body_h);
+}
+
+const Point = struct { index: u16, col: u16, metrics: Metrics };
+
+/// The row and column a window pixel lands on, each row at its own pitch:
+/// a tag's at the tagline's width, a body's compact rows too.
+fn pointAt(layer: *const Layer, x: f32, y: f32, body_w: f32, body_h: f32, tagline_w: f32, tagline_h: f32) ?Point {
+ if (layer.rows == 0 or !std.math.isFinite(x) or !std.math.isFinite(y)) return null;
+ // A band's hit is refused outright on metrics no host could have; a
+ // body's has always been floored to a pixel instead.
+ if (layer.kind != .body) for ([_]f32{ body_w, body_h, tagline_w, tagline_h }) |metric|
+ if (!std.math.isFinite(metric) or metric < 1 or metric > std.math.maxInt(u16)) return null;
+ const bw = @max(1, body_w);
+ const bh = @max(1, body_h);
+ const tw = std.math.clamp(tagline_w, 1, bw);
+ const th = std.math.clamp(tagline_h, 1, bh);
+ const dx = x - @as(f32, @floatFromInt(layer.viewport.x)) * bw;
+ const dy = y - @as(f32, @floatFromInt(layer.viewport.y)) * bh;
+ if (dx < 0 or dy < 0 or dx >= @as(f32, @floatFromInt(layer.viewport.w)) * bw or dy >= @as(f32, @floatFromInt(layer.viewport.h)) * bh) return null;
+ const headers = @as(f32, @floatFromInt(layer.context_rows)) * th;
+ const index: u16 = @intFromFloat(if (dy < headers) @floor(dy / th) else @as(f32, @floatFromInt(layer.context_rows)) + @floor((dy - headers) / bh));
+ if (index >= layer.rows) return null;
+ const pitch = if (layer.kind != .body or index < layer.context_rows) tw else bw;
+ return .{
+ .index = index,
+ .col = @intFromFloat(@min(@as(f32, @floatFromInt(layer.cols -| 1)), @floor(dx / pitch))),
+ .metrics = .{ .body_w = @intFromFloat(bw), .body_h = @intFromFloat(bh), .tagline_w = @intFromFloat(tw), .tagline_h = @intFromFloat(th) },
+ };
+}
+
+/// A body layer's pixel as the body cell under it (grid columns, from the
+/// pane's text edge).
+pub fn bodyHit(layer: *const Layer, x: f32, y: f32, body_w: f32, body_h: f32, tagline_w: f32, tagline_h: f32) ?pardes.Mouse.BodyHit {
+ if (layer.kind != .body) return null;
+ const point = layer.pointAt(x, y, body_w, body_h, tagline_w, tagline_h) orelse return null;
+ return .{ .serial = layer.serial, .row = point.index, .col = point.col + config.GUTTER, .pixel_x = x, .pixel_y = y, .metrics = point.metrics };
+}
+
+/// A band's pixel as the tag cell under it.
+pub fn tagHit(layer: *const Layer, x: f32, y: f32, body_w: f32, body_h: f32, tagline_w: f32, tagline_h: f32) ?TagHit {
+ if (layer.kind == .body) return null;
+ const point = layer.pointAt(x, y, body_w, body_h, tagline_w, tagline_h) orelse return null;
+ return .{ .kind = layer.kind, .id = layer.id, .serial = layer.serial, .line = point.index, .col = point.col, .pixel_x = x, .pixel_y = y, .metrics = point.metrics };
+}
+
+test "compact body layer rows fill reclaimed height and invert exact boundaries" {
+ const layer: Layer = .{ .serial = 7, .viewport = .{ .x = 4, .y = 3, .w = 40, .h = 10 }, .cols = 40, .rows = 15, .context_rows = 5 };
+ try std.testing.expectEqual(@as(f32, 120), layer.rowTop(5, 20, 12));
+ try std.testing.expectEqual(@as(f32, 240), layer.rowTop(11, 20, 12));
+ try std.testing.expectEqual(@as(f32, 12), layer.rowHeight(4, 20, 12));
+ try std.testing.expectEqual(@as(f32, 20), layer.rowHeight(5, 20, 12));
+ for (0..12) |index| {
+ const r: u16 = @intCast(index);
+ const y = layer.rowTop(r, 20, 12) + layer.rowHeight(r, 20, 12) / 2;
+ const width: f32 = if (r < 5) 6 else 10;
+ const hit = layer.bodyHit(40 + 3.5 * width, y, 10, 20, 6, 12).?;
+ try std.testing.expectEqual(r, hit.row);
+ try std.testing.expectEqual(@as(u16, 3 + config.GUTTER), hit.col);
+ }
+ try std.testing.expectEqual(@as(u16, 4), layer.bodyHit(45, 119.99, 10, 20, 6, 12).?.row);
+ try std.testing.expectEqual(@as(u16, 5), layer.bodyHit(45, 120, 10, 20, 6, 12).?.row);
+ try std.testing.expectEqual(@as(u16, 11), layer.bodyHit(45, 259.99, 10, 20, 6, 12).?.row);
+ try std.testing.expect(layer.bodyHit(45, 260, 10, 20, 6, 12) == null);
+ try std.testing.expect(layer.bodyHit(39.99, 80, 10, 20, 6, 12) == null);
+ try std.testing.expect(layer.bodyHit(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, index| try std.testing.expectEqual(border, layer.hasContextBorderAfter(index));
+ 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));
+}
+
+test "tag text capacity uses its own pitch and excludes the physical grip" {
+ const layer: Layer = .{ .kind = .pane, .id = 2, .serial = 7, .viewport = .{ .x = 12, .y = 4, .w = 38, .h = 1 }, .cols = 90, .rows = 1 };
+ try std.testing.expectEqual(@as(u16, 63), layer.capacity(.{ .body_w = 10, .tagline_w = 6 }));
+ try std.testing.expect(layer.tagHit(119.99, 85, 10, 20, 6, 12) == null);
+ try std.testing.expectEqual(@as(u16, 0), layer.tagHit(120, 85, 10, 20, 6, 12).?.col);
+ try std.testing.expectEqual(@as(u16, 50), layer.tagHit(423, 85, 10, 20, 6, 12).?.col);
+ try std.testing.expect(layer.tagHit(500, 85, 10, 20, 6, 12) == null);
+ try std.testing.expect(layer.tagHit(120, 85, std.math.nan(f32), 20, 6, 12) == null);
+ try std.testing.expect(layer.tagHit(120, 85, 10, 20, 0, 12) == null);
+}
+
+test "a taller tag's rows are grid rows, each hit on its own line" {
+ const layer: Layer = .{ .kind = .pane, .id = 2, .serial = 7, .viewport = .{ .x = 12, .y = 4, .w = 38, .h = 3 }, .cols = 90, .rows = 3 };
+ try std.testing.expectEqual(@as(u16, 0), layer.tagHit(120, 80, 10, 20, 6, 12).?.line);
+ try std.testing.expectEqual(@as(u16, 1), layer.tagHit(120, 100, 10, 20, 6, 12).?.line);
+ try std.testing.expectEqual(@as(u16, 2), layer.tagHit(120, 139.99, 10, 20, 6, 12).?.line);
+ try std.testing.expect(layer.tagHit(120, 140, 10, 20, 6, 12) == null);
+ try std.testing.expect(layer.bodyHit(120, 100, 10, 20, 6, 12) == null);
+}
diff --git a/src/Messages.zig b/src/Messages.zig
index 7334de95..b7153e5c 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -747,7 +747,7 @@ test "a builtin announces itself on the message row, and Verbose silences it" {
_ = try p.render(frame.allocator());
const r = p.rects[p.active];
const band = for (p.surface.tagLayers()) |*layer| {
- if (layer.cols != 0 and layer.kind == .notice) break layer;
+ if (layer.rows != 0 and layer.kind == .notice) break layer;
} else return error.NoNoticeBand;
try std.testing.expectEqual(@as(u16, 1), band.viewport.h);
try std.testing.expectEqual(if (p.settings.tag_bottom) r.y else r.y + BOX_H, band.viewport.y);
@@ -794,7 +794,7 @@ test "notices stack over the body's top rows without moving them" {
var seen: usize = 0;
for (p.surface.tagLayers()) |*layer| {
- if (layer.cols == 0 or layer.kind != .notice) continue;
+ if (layer.rows == 0 or layer.kind != .notice) continue;
try std.testing.expectEqual(@as(u16, 1), layer.viewport.h);
// Sized to its own message, right up against the pane's right edge --
// not the width of the pane.
@@ -814,7 +814,7 @@ test "notices stack over the body's top rows without moving them" {
_ = frame.reset(.retain_capacity);
_ = try p.render(frame.allocator());
for (p.surface.tagLayers()) |*layer|
- try std.testing.expect(layer.cols == 0 or layer.kind != .notice);
+ try std.testing.expect(layer.rows == 0 or layer.kind != .notice);
}
test "an idle core wants no wake and a lingering message sleeps to its linger's end" {
diff --git a/src/Presentation.zig b/src/Presentation.zig
index 45b196c8..e1f1b585 100644
--- a/src/Presentation.zig
+++ b/src/Presentation.zig
@@ -42,8 +42,8 @@ shown_closing_len: usize = 0,
acknowledged: bool = false,
pending: bool = false,
previous_cells: []pardes.Cell = &.{},
-previous_body_layers: [MAX_PANES]pardes.BodyLayer = @splat(.{}),
-previous_tag_layers: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = @splat(.{}),
+previous_body_layers: [MAX_PANES]pardes.Layer = @splat(.{}),
+previous_tag_layers: [pardes.MAX_TAG_LAYERS]pardes.Layer = @splat(.{}),
previous_cols: u16 = 0,
previous_rows: u16 = 0,
previous_valid: bool = false,
@@ -335,7 +335,7 @@ pub fn capturePrevious(self: *Presentation, gpa: std.mem.Allocator, surface: *co
old.cells = storage;
}
for (&self.previous_tag_layers, surface.tagLayers()) |*old, *current| {
- const count = if (current.cols > 0) current.cells.len else 0;
+ const count = if (current.rows > 0) current.cells.len else 0;
const storage = if (old.cells.len == count) old.cells else gpa.realloc(old.cells, count) catch {
self.previous_valid = false;
self.previous_layout = @splat(null);
@@ -684,7 +684,7 @@ test "presentation submission failure preserves shown geometry until retry is ac
}
test "fractional panel inverse preserves compact header rows through slide and zoom" {
- const layer: pardes.BodyLayer = .{ .serial = 7, .viewport = .{ .x = 4, .y = 3, .w = 30, .h = 10 }, .cols = 30, .rows = 15, .context_rows = 4 };
+ const layer: pardes.Layer = .{ .serial = 7, .viewport = .{ .x = 4, .y = 3, .w = 30, .h = 10 }, .cols = 30, .rows = 15, .context_rows = 4 };
for ([_]Transition{ .slide, .zoom, .vertical }) |effect| {
var presentation: Presentation = .{};
const track: Track = .{ .serial = 7, .effect = effect, .frame = 4, .frame_count = 10, .from = .{ .x = 2, .y = 2, .w = 20, .h = 8 }, .to = .{ .x = 3, .y = 2, .w = 32, .h = 12 } };
@@ -699,7 +699,7 @@ test "fractional panel inverse preserves compact header rows through slide and z
const point = presentation.pointerFractional(80, 30, x, y).?;
try std.testing.expectApproxEqAbs(source_x, point.x, 0.0001);
try std.testing.expectApproxEqAbs(source_y, point.y, 0.0001);
- const body_hit = layer.hitAt(point.x * 10, point.y * 20, 10, 20, 8, 10).?;
+ const body_hit = layer.bodyHit(point.x * 10, point.y * 20, 10, 20, 8, 10).?;
try std.testing.expectEqual(row, body_hit.row);
}
}
@@ -715,7 +715,7 @@ test "presentation snapshots own compact body and tag rows across later edits" {
surface.body_layers[0] = .{ .serial = 9, .cols = 1, .rows = 2, .context_rows = 1, .cells = &body };
var tag: [1]pardes.Cell = .{.{}};
tag[0].text[0] = 'T';
- surface.tag_layers[0] = .{ .kind = .pane, .serial = 9, .cols = 1, .cells = &tag };
+ surface.tag_layers[0] = .{ .kind = .pane, .serial = 9, .cols = 1, .rows = 1, .cells = &tag };
state.capturePrevious(std.testing.allocator, &surface);
try std.testing.expect(state.previous_valid);
tag[0].text[0] = 'U';
diff --git a/src/body_layer.zig b/src/body_layer.zig
index ac87a07b..583a2e98 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -1,9 +1,6 @@
-//! A pane body as the surface shows it. A file body's logical rows can
-//! outnumber its physical grid rows: hosts place the compact declaration rows
-//! first and clip the remaining body rows. After the layer types come the
-//! functions that draw a body into the surface: its text, colours, selections
-//! and cursor; then the tags drawn beside it, a pane's and the column and
-//! workspace ones, each line a tag layer of its own.
+//! Drawing a pane body into a surface: its text, colours, selections and
+//! cursor, and joining a body with compact context rows through its layer
+//! (Layer.zig).
const std = @import("std");
const pardes = @import("pardes.zig");
const normal = @import("normal.zig");
@@ -29,7 +26,7 @@ const tagline = @import("tagline.zig");
const Messages = @import("Messages.zig");
const Text = panes.Text;
const MAX_PANES = pardes.MAX_PANES;
-const TagKind = @import("tag_layer.zig").Kind;
+const TagKind = @import("Layer.zig").Kind;
const MAX_TAG_ROWS = pardes.MAX_TAG_ROWS;
const NOTICE_LAYER_BASE = pardes.NOTICE_LAYER_BASE;
const TAG_LINE_LAYER_BASE = pardes.TAG_LINE_LAYER_BASE;
@@ -40,108 +37,7 @@ const COLBAR_H = pardes.COLBAR_H;
const TAG_GAP = pardes.TAG_GAP;
const wordBoundsAtCol = pardes.wordBoundsAtCol;
-pub const Metrics = struct {
- body_w: u16 = 1,
- body_h: u16 = 1,
- tagline_w: u16 = 1,
- tagline_h: u16 = 1,
-};
-
-pub const Layer = struct {
- pane: u16 = 0,
- serial: u32 = 0,
- viewport: pardes.Rect = .{ .x = 0, .y = 0, .w = 0, .h = 0 },
- 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);
- return @as(f32, @floatFromInt(layer.viewport.y)) * body +
- @as(f32, @floatFromInt(@min(row, layer.context_rows))) * tag +
- @as(f32, @floatFromInt(row -| layer.context_rows)) * body;
- }
-
- pub fn rowHeight(layer: *const Layer, row: u16, body_h: f32, tagline_h: f32) f32 {
- return if (row < layer.context_rows) std.math.clamp(tagline_h, 1, @max(1, body_h)) else @max(1, body_h);
- }
-
- pub fn hitAt(layer: *const Layer, x: f32, y: f32, body_w: f32, body_h: f32, tagline_w: f32, tagline_h: f32) ?pardes.Mouse.BodyHit {
- if (layer.rows == 0 or !std.math.isFinite(x) or !std.math.isFinite(y)) return null;
- const bw = @max(1, body_w);
- const bh = @max(1, body_h);
- const tw = std.math.clamp(tagline_w, 1, bw);
- const th = std.math.clamp(tagline_h, 1, bh);
- const dx = x - @as(f32, @floatFromInt(layer.viewport.x)) * bw;
- const dy = y - @as(f32, @floatFromInt(layer.viewport.y)) * bh;
- if (dx < 0 or dy < 0 or dx >= @as(f32, @floatFromInt(layer.viewport.w)) * bw or dy >= @as(f32, @floatFromInt(layer.viewport.h)) * bh) return null;
- const headers = @as(f32, @floatFromInt(layer.context_rows)) * th;
- const row: u16 = @intFromFloat(if (dy < headers) @floor(dy / th) else @as(f32, @floatFromInt(layer.context_rows)) + @floor((dy - headers) / bh));
- if (row >= layer.rows) return null;
- const col: u16 = @intFromFloat(@min(@as(f32, @floatFromInt(layer.cols -| 1)), @floor(dx / (if (row < layer.context_rows) tw else bw))));
- return .{
- .serial = layer.serial,
- .row = row,
- .col = col + config.GUTTER,
- .pixel_x = x,
- .pixel_y = y,
- .metrics = .{ .body_w = @intFromFloat(bw), .body_h = @intFromFloat(bh), .tagline_w = @intFromFloat(tw), .tagline_h = @intFromFloat(th) },
- };
- }
-};
-
-test "compact body layer rows fill reclaimed height and invert exact boundaries" {
- const layer: Layer = .{ .serial = 7, .viewport = .{ .x = 4, .y = 3, .w = 40, .h = 10 }, .cols = 40, .rows = 15, .context_rows = 5 };
- try std.testing.expectEqual(@as(f32, 120), layer.rowTop(5, 20, 12));
- try std.testing.expectEqual(@as(f32, 240), layer.rowTop(11, 20, 12));
- try std.testing.expectEqual(@as(f32, 12), layer.rowHeight(4, 20, 12));
- try std.testing.expectEqual(@as(f32, 20), layer.rowHeight(5, 20, 12));
- for (0..12) |index| {
- const row: u16 = @intCast(index);
- const y = layer.rowTop(row, 20, 12) + layer.rowHeight(row, 20, 12) / 2;
- const width: f32 = if (row < 5) 6 else 10;
- const hit = layer.hitAt(40 + 3.5 * width, y, 10, 20, 6, 12).?;
- try std.testing.expectEqual(row, hit.row);
- try std.testing.expectEqual(@as(u16, 3 + config.GUTTER), hit.col);
- }
- try std.testing.expectEqual(@as(u16, 4), layer.hitAt(45, 119.99, 10, 20, 6, 12).?.row);
- try std.testing.expectEqual(@as(u16, 5), layer.hitAt(45, 120, 10, 20, 6, 12).?.row);
- try std.testing.expectEqual(@as(u16, 11), layer.hitAt(45, 259.99, 10, 20, 6, 12).?.row);
- try std.testing.expect(layer.hitAt(45, 260, 10, 20, 6, 12) == null);
- 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));
-}
+const Layer = @import("Layer.zig");
// ---- drawing a pane body into the surface ----
@@ -305,7 +201,8 @@ pub fn joinBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane,
const start = (row + first_row) * r.w + config.GUTTER;
@memcpy(layer.cells[row * width ..][0..width], temporary_cells[start..][0..width]);
}
- layer.pane = @intCast(id);
+ layer.kind = .body;
+ layer.id = @intCast(id);
layer.serial = pane.serial;
const body = s.region(.body, id).?;
layer.viewport = .{ .x = body.rect.x, .y = body.rect.y, .w = body.rect.w - body.clip, .h = body.rect.h };
diff --git a/src/detached/client.zig b/src/detached/client.zig
index 4468c961..3347f609 100644
--- a/src/detached/client.zig
+++ b/src/detached/client.zig
@@ -103,8 +103,10 @@ 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(.{}),
- tag_layers: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = @splat(.{}),
+ body_layers: [pardes.MAX_PANES]pardes.Layer = @splat(.{}),
+ tag_layers: [pardes.MAX_TAG_LAYERS]pardes.Layer = @splat(.{}),
+ /// The session's placed regions, as its last frame gave them.
+ regions: std.ArrayListUnmanaged(pardes.Region) = .empty,
in: std.ArrayListUnmanaged(u8) = .empty,
out: std.ArrayListUnmanaged(u8) = .empty,
/// Bytes of `in` belonging to the message `next` returned last. Compacted at
@@ -182,6 +184,7 @@ pub const Client = struct {
layer.* = .{};
}
for (&c.tag_layers) |*layer| c.gpa.free(layer.cells);
+ c.regions.deinit(c.gpa);
c.in.deinit(c.gpa);
c.out.deinit(c.gpa);
}
@@ -319,7 +322,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, &c.tag_layers) catch |err| return switch (err) {
+ f.applyLayers(c.gpa, &c.body_layers, &c.tag_layers, &c.regions) catch |err| return switch (err) {
error.OutOfMemory => error.Closed,
else => @as(wire.Error, @errorCast(err)),
};
@@ -995,6 +998,12 @@ test "detached session: a frontend that dies takes nothing with it" {
}
test "detached session: a frontend speaking another protocol is refused, loudly" {
+ // The protocol before this one (v7 carried one-row tags and no regions)
+ // and the one after.
+ for ([_]u16{ wire.version - 1, wire.version + 1 }) |claimed| try refusedHello(claimed);
+}
+
+fn refusedHello(claimed: u16) !void {
var h: Harness = undefined;
try h.init(60, 16);
defer h.deinit();
@@ -1006,7 +1015,7 @@ test "detached session: a frontend speaking another protocol is refused, loudly"
defer _ = libc.close(fd);
var buf: [64]u8 = undefined;
const hello = try wire.encodeClient(&buf, .{
- .hello = .{ .version = wire.version + 1, .cols = 60, .rows = 16 },
+ .hello = .{ .version = claimed, .cols = 60, .rows = 16 },
});
try testing.expectEqual(@as(isize, @intCast(hello.len)), libc.send(fd, hello.ptr, hello.len, nosignal));
@@ -1024,6 +1033,17 @@ test "detached session: a frontend speaking another protocol is refused, loudly"
try testing.expect(!h.core.quit);
}
+test "detached frontend: a session speaking another protocol is not attached to" {
+ for ([_]u16{ wire.version - 1, wire.version + 1 }) |claimed| {
+ var c: Client = .{ .gpa = testing.allocator };
+ defer c.deinit();
+ var buf: [64]u8 = undefined;
+ try c.in.appendSlice(testing.allocator, try wire.encodeServer(&buf, .{ .welcome = .{ .version = claimed, .slot = 0, .cols = 60, .rows = 16 } }));
+ try testing.expectError(error.Ungreeted, c.next());
+ try testing.expect(!c.attached());
+ }
+}
+
test "detached session: a peer that sends garbage is dropped, not obeyed" {
var h: Harness = undefined;
try h.init(60, 16);
@@ -1281,7 +1301,7 @@ fn pumpUntilNotice(h: *Harness, c: *Client, want: []const u8) !bool {
try c.wait(5);
while (try c.next()) |_| {}
for (&c.tag_layers) |*layer| {
- if (layer.cols == 0 or layer.kind != .notice) continue;
+ if (layer.rows == 0 or layer.kind != .notice) continue;
var n: usize = 0;
for (layer.cells[0..layer.cols]) |cell| {
const g = cell.grapheme();
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 212c9a3d..46fb8fcf 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -739,7 +739,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, &surface.tag_layers);
+ const want = wire.frameBound(surface.cols, surface.rows) + wire.layersBound(&surface.body_layers, &surface.tag_layers, surface.regionList());
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)
&.{}
@@ -759,6 +759,7 @@ pub const Session = struct {
prev,
&surface.body_layers,
&surface.tag_layers,
+ surface.regionList(),
) 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 94bd7f0b..684c869b 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -7,7 +7,9 @@ const limits = @import("../memory.zig").limits;
/// 7: tag layers gained the `.notice` kind, so a frame can carry a tag value
/// an older frontend would reject mid-session. The greeting is where that has
/// to be refused, which is what this number is for.
-pub const version: u16 = 7;
+/// 8: one layer type: a tag band carries all its rows (not a layer per
+/// line), and a frame carries the core's placed regions after its layers.
+pub const version: u16 = 8;
pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace };
@@ -167,7 +169,8 @@ pub const Frame = struct {
nruns: u32,
runs: []const u8,
- pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.BodyLayer, tags: *[pardes.MAX_TAG_LAYERS]pardes.TagLayer) !void {
+ pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.Layer, tags: *[pardes.MAX_TAG_LAYERS]pardes.Layer, regions: *std.ArrayListUnmanaged(pardes.Region)) !void {
+ regions.clearRetainingCapacity();
for (layers) |*layer| {
gpa.free(layer.cells);
layer.* = .{};
@@ -191,13 +194,14 @@ pub const Frame = struct {
const count = try r.getU16();
if (count > layers.len) return error.Overlong;
for (layers[0..count]) |*layer| {
- layer.pane = try r.getU16();
+ layer.kind = .body;
+ 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();
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
+ if (layer.id >= 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.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;
@@ -217,19 +221,40 @@ pub const Frame = struct {
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();
for (&layer.bg) |*channel| channel.* = try r.getByte();
- if (layer.cols == 0 or layer.viewport.w == 0 or layer.viewport.h != 1 or
+ if (layer.cols == 0 or layer.viewport.w == 0 or layer.rows == 0 or layer.rows > pardes.MAX_TAG_ROWS or
+ layer.viewport.h != layer.rows 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) {
+ layer.viewport.y >= f.rows or layer.viewport.h > f.rows - layer.viewport.y or (switch (layer.kind) {
.workspace => layer.id != 0,
.column => layer.id >= pardes.MAX_COLS,
.pane, .notice => layer.id >= pardes.MAX_PANES,
+ .body => true,
})) 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);
+ 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);
}
+ const region_count = try r.getU16();
+ if (region_count > pardes.MAX_REGIONS) return error.Overlong;
+ try regions.ensureTotalCapacity(gpa, region_count);
+ for (0..region_count) |_| {
+ var region: pardes.Region = .{ .kind = try r.getTag(pardes.Region.Kind), .rect = undefined };
+ region.owner = try r.getByte();
+ region.serial = try r.getU32();
+ region.rect = .{ .x = try r.getU16(), .y = try r.getU16(), .w = try r.getU16(), .h = try r.getU16() };
+ region.active = try r.getBool();
+ region.thumb_y = try r.getU16();
+ region.thumb_h = try r.getU16();
+ region.notice = try r.getByte();
+ region.clip = try r.getU16();
+ if (region.rect.x > f.cols or region.rect.w > f.cols - region.rect.x or
+ region.rect.y > f.rows or region.rect.h > f.rows - region.rect.y or
+ region.clip > region.rect.w or region.thumb_y > region.rect.h or region.thumb_h > region.rect.h - region.thumb_y) return error.BadValue;
+ regions.appendAssumeCapacity(region);
+ }
try r.end();
}
@@ -672,21 +697,23 @@ 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, tags: []const pardes.TagLayer) usize {
- var n: usize = 4;
+pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, regions: []const pardes.Region) usize {
+ var n: usize = 6;
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;
+ for (tags) |layer| if (layer.rows != 0) {
+ n += 28 + layer.cells.len * cell_max;
};
- return n;
+ return n + regions.len * region_len;
}
-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 {
+const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2;
+
+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.Layer, tags: []const pardes.Layer, regions: []const pardes.Region) 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
@@ -708,7 +735,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 or tags.len != 0) {
+ if (layers.len != 0 or tags.len != 0 or regions.len != 0) {
var count: u16 = 0;
for (layers) |layer| if (layer.rows != 0) {
count += 1;
@@ -717,7 +744,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
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.putU16(layer.id);
try w.putU32(layer.serial);
try w.putU16(layer.viewport.x);
try w.putU16(layer.viewport.y);
@@ -731,13 +758,13 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
for (layer.cells) |*cell| try putCell(&w, cell);
}
var tag_count: u16 = 0;
- for (tags) |layer| if (layer.cols != 0) {
+ for (tags) |layer| if (layer.rows != 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;
+ if (layer.rows == 0) continue;
+ if (layer.cells.len != @as(usize, layer.cols) * layer.rows) return error.BadValue;
try w.putByte(@intFromEnum(layer.kind));
try w.putU16(layer.id);
try w.putU32(layer.serial);
@@ -746,10 +773,26 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
try w.putU16(layer.viewport.w);
try w.putU16(layer.viewport.h);
try w.putU16(layer.cols);
+ try w.putU16(layer.rows);
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);
+ 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);
}
+ try w.putU16(@intCast(regions.len));
+ for (regions) |region| {
+ try w.putByte(@intFromEnum(region.kind));
+ try w.putByte(region.owner);
+ try w.putU32(region.serial);
+ try w.putU16(region.rect.x);
+ try w.putU16(region.rect.y);
+ try w.putU16(region.rect.w);
+ try w.putU16(region.rect.h);
+ try w.putBool(region.active);
+ try w.putU16(region.thumb_y);
+ try w.putU16(region.thumb_h);
+ try w.putByte(region.notice);
+ try w.putU16(region.clip);
+ }
}
std.mem.writeInt(u32, w.buf[layers_len_at..][0..4], @intCast(w.n - layers_start), .little);
const nruns_at = w.n;
@@ -1820,22 +1863,26 @@ 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 };
- 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 } };
+ var layer: pardes.Layer = .{ .id = 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);
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}));
+ const tag: pardes.Layer = .{ .kind = .column, .id = 1, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 1 }, .cols = 3, .rows = 1, .bg = .{ 11, 22, 33 }, .cells = &tag_cells, .cursor = .{ .x = 2, .y = 0, .bar = true } };
+ const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .thumb_y = 1, .thumb_h = 2, .clip = 1 }};
+ const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag}, &placed));
defer gpa.free(out);
- const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag});
+ const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}, &placed);
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(.{});
+ var decoded: [pardes.MAX_PANES]pardes.Layer = @splat(.{});
+ var tags: [pardes.MAX_TAG_LAYERS]pardes.Layer = @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, &tags);
+ var regions: std.ArrayListUnmanaged(pardes.Region) = .empty;
+ defer regions.deinit(gpa);
+ try frame.applyLayers(gpa, &decoded, &tags, &regions);
+ try std.testing.expectEqualDeep(placed[0..], regions.items);
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));
@@ -1847,10 +1894,10 @@ test "detached wire carries compact body rows and exact pointer metrics" {
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.expectEqual(@as(u16, 2), tags[0].cursor.?.x);
try std.testing.expectEqualStrings("z", tags[0].cells[2].grapheme());
- const pointer = layer.hitAt(4, 27, 8, 16, 6, 8).?;
- const tag_pointer = tag.hitAt(11, 8, 8, 16, 4, 8).?;
+ const pointer = layer.bodyHit(4, 27, 8, 16, 6, 8).?;
+ const tag_pointer = tag.tagHit(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);
@@ -1861,19 +1908,20 @@ test "detached wire carries compact body rows and exact pointer metrics" {
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, &tags));
+ try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded, &tags, &regions));
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(u16, 0), item.rows);
try std.testing.expectEqual(@as(usize, 0), item.cells.len);
}
+ try std.testing.expectEqual(@as(usize, 0), regions.items.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, &tags);
+ try cleared.applyLayers(gpa, &decoded, &tags, &regions);
try std.testing.expectEqual(@as(u16, 0), decoded[0].rows);
}
diff --git a/src/draw.zig b/src/draw.zig
index 7d91ad31..7cfe7554 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -28,7 +28,7 @@ const Pane = panes.Pane;
const Text = panes.Text;
const Rect = layout.Rect;
const Region = @import("surface.zig").Region;
-const TagKind = @import("tag_layer.zig").Kind;
+const TagKind = @import("Layer.zig").Kind;
const pdf_enabled = panes.Pdf.enabled;
const wordBoundsAtCol = pardes.wordBoundsAtCol;
const BOX_H = pardes.BOX_H;
@@ -40,8 +40,6 @@ const MAX_PANES = pardes.MAX_PANES;
const MAX_COLS = pardes.MAX_COLS;
const MAX_TAG_ROWS = pardes.MAX_TAG_ROWS;
const NOTICE_LAYER_BASE = pardes.NOTICE_LAYER_BASE;
-const TAG_LINE_LAYER_BASE = pardes.TAG_LINE_LAYER_BASE;
-const HEADER_LINE_LAYER_BASE = pardes.HEADER_LINE_LAYER_BASE;
/// Where every part of the frame goes, decided once before anything is
/// painted: the page; each pane's grip, tag, body, rail (with its thumb)
@@ -172,7 +170,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
// never set.
for (s.cells) |*c| c.hover = false;
for (&s.body_layers) |*layer| layer.rows = 0;
- for (&s.tag_layers) |*layer| layer.cols = 0;
+ for (&s.tag_layers) |*layer| layer.rows = 0;
s.nimages = 0;
s.npanel_tracks = 0;
s.previous_cells = &.{};
@@ -505,11 +503,9 @@ pub fn paintHeaders(p: *Pardes, s: *Surface, arena: std.mem.Allocator) !void {
tagline.revealHeader(p);
if (s.region(.workspace_tag, Region.none)) |region| {
const top = if (p.header_focus and p.header_column == null) p.header_top else 0;
- for (0..region.rect.h) |line| {
- const index = if (line == 0) MAX_PANES + MAX_COLS else HEADER_LINE_LAYER_BASE + MAX_COLS * (MAX_TAG_ROWS - 1) + line - 1;
- try renderHeaderLayer(p, arena, index, .workspace, 0, @intCast(line), .{ .x = region.rect.x, .y = region.rect.y + @as(u16, @intCast(line)), .w = region.rect.w, .h = TOPBAR_H }, modal.lineSlice(tagline.topbar(p), line + top), chrome.tag_bg, chrome.tag_fg);
- joinHeaderLine(s, &s.tag_layers[index], chrome.tag_fg);
- }
+ const layer = &s.tag_layers[MAX_PANES + MAX_COLS];
+ try renderHeaderLayer(p, arena, layer, .workspace, 0, region.rect, tagline.topbar(p), top, chrome.tag_bg, chrome.tag_fg);
+ joinHeaderRows(s, layer, chrome.tag_fg);
}
const column_rows = p.columnBarHeight();
if (column_rows > 0) for (0..p.ncol) |c| {
@@ -531,23 +527,21 @@ pub fn paintHeaders(p: *Pardes, s: *Surface, arena: std.mem.Allocator) !void {
s.fill(p.col_x[c] + grip, region.rect.y, inset - grip, column_rows, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
}
const top = if (p.header_focus and p.header_column == c) p.header_top else 0;
- for (0..column_rows) |line| {
- const index = if (line == 0) MAX_PANES + c else HEADER_LINE_LAYER_BASE + c * (MAX_TAG_ROWS - 1) + line - 1;
- try renderHeaderLayer(p, arena, index, .column, @intCast(c), @intCast(line), .{ .x = region.rect.x, .y = region.rect.y + @as(u16, @intCast(line)), .w = region.rect.w, .h = COLBAR_H }, modal.lineSlice(tagline.columnTag(p, c), line + top), bg, fg);
- if (p.col_x[c] < s.cols) joinHeaderLine(s, &s.tag_layers[index], fg);
- // Laid out at its full width; only shown short of the rail.
- s.tag_layers[index].viewport.w -= region.clip;
- }
+ const layer = &s.tag_layers[MAX_PANES + c];
+ try renderHeaderLayer(p, arena, layer, .column, @intCast(c), region.rect, tagline.columnTag(p, c), top, bg, fg);
+ if (p.col_x[c] < s.cols) joinHeaderRows(s, layer, fg);
+ // Laid out at its full width; only shown short of the rail.
+ layer.viewport.w -= region.clip;
};
}
-/// Copy a header line's layer into the grid under it, and its caret.
-fn joinHeaderLine(s: *Surface, layer: *const pardes.TagLayer, fg: [3]u8) void {
+/// Copy a header's layer into the grid under it, row by row, and its caret.
+fn joinHeaderRows(s: *Surface, layer: *const pardes.Layer, fg: [3]u8) void {
const v = layer.viewport;
const w = @min(v.w, s.cols -| v.x);
- joinCells(s, v.x, v.y, layer.cells[0..layer.cols], 0, w, .{ .rgb = fg });
+ for (0..layer.rows) |line| joinCells(s, v.x, v.y + @as(u16, @intCast(line)), layer.cells[line * layer.cols ..][0..layer.cols], 0, w, .{ .rgb = fg });
if (layer.cursor) |caret| {
- if (caret.col < v.w) s.cursor = .{ .x = v.x + caret.col, .y = v.y, .bar = caret.bar };
+ if (caret.x < v.w) s.cursor = .{ .x = v.x + caret.x, .y = v.y + caret.y, .bar = caret.bar };
}
}
@@ -600,77 +594,85 @@ fn tagTintRange(pane: *const Pane, tag: []const u8, prefix_len: usize) ?struct {
return .{ .lo = start + basename, .hi = start + name.len };
}
-/// One row of a header or a notice band as a tag layer: `text` is line
-/// `line` of what it shows.
-fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: TagKind, id: u16, line: u16, viewport: Rect, text: []const u8, bg: [3]u8, fg: [3]u8) !void {
+/// A header or a notice band as a tag layer: `viewport.h` rows, line `top`
+/// of `text` and on, laid out at the tagline's pitch -- as wide as its
+/// widest line and never narrower than its viewport.
+fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer, kind: TagKind, id: u16, viewport: Rect, text: []const u8, top: usize, bg: [3]u8, fg: [3]u8) !void {
_ = arena;
- const layer = &p.surface.tag_layers[index];
- if (viewport.w == 0) {
- layer.cols = 0;
+ if (viewport.w == 0 or viewport.h == 0) {
+ layer.rows = 0;
return;
}
- const columns: u16 = @intCast(@min(std.math.maxInt(u16), @max(p.tagCapacity(viewport.w), panes.File.displayWidth(text) + 1)));
- if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
+ var widest: usize = 0;
+ for (0..viewport.h) |line| widest = @max(widest, panes.File.displayWidth(modal.lineSlice(text, line + top)));
+ const columns: u16 = @intCast(@min(std.math.maxInt(u16), @max(p.tagCapacity(viewport.w), widest + 1)));
+ const count = @as(usize, columns) * viewport.h;
+ if (layer.cells.len != count) layer.cells = try p.gpa.realloc(layer.cells, count);
layer.kind = kind;
layer.id = id;
- layer.line = line;
layer.serial = if (kind == .column) p.col_serial[id] else 0;
layer.viewport = viewport;
layer.cols = columns;
+ layer.rows = viewport.h;
+ layer.context_rows = 0;
layer.bg = bg;
layer.cursor = null;
layer.slide = 0;
layer.fade = 1;
- var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells };
// A notice band carries a PANE id, not a column index, so it must not
// answer to the column being edited -- that would scroll and select
// its text with the column tag's offsets and give it a second caret.
const editing: ?*Text = if (p.header_focus and switch (kind) {
.workspace => p.header_column == null,
.column => p.header_column == id,
- .pane, .notice => false,
+ .pane, .notice, .body => false,
}) tagline.focusedHeader(p) else null;
const scroll = if (editing != null) p.header_scroll else 0;
- surface.fill(0, 0, columns, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
- _ = surface.print(0, 0, columns, text[panes.File.rawAtDisplay(text, scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline });
- // The word under the pointer: on a header even while it is edited, but
- // not on a notice (a message is not a command line), nor on a column a
- // carried column's pointer is aiming at a place over.
- const hovered = p.pointer_inside and kind != .notice and !(kind == .column and p.drag == .column_move);
- if (hovered) {
- const tag_hit = p.pointer_tag_hit;
- const col: ?usize = if (tag_hit) |pointer|
- (if (pointer.kind == kind and pointer.id == id and pointer.line == line) pointer.col else null)
- else if (p.hover_row == viewport.y and p.hover_col >= viewport.x and p.hover_col - viewport.x < viewport.w)
- p.hover_col - viewport.x
- else
- null;
- if (col) |hover| if (wordBoundsAtCol(text, panes.File.rawAtDisplay(text, hover + scroll))) |bounds| {
- var cell = panes.File.rawDisplayCol(text, bounds.lo) -| scroll;
- const end = panes.File.rawDisplayCol(text, bounds.hi) -| scroll;
- while (cell < end and cell < columns) : (cell += 1) {
- surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg };
- surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg };
- }
- };
- }
- if (editing) |t| {
- if (tagline.headerSelection(t, try edit.flatSurface(p, t), line + p.header_top)) |span| {
- var col = panes.File.rawDisplayCol(text, span.lo) -| scroll;
- const end = panes.File.rawDisplayCol(text, span.hi) -| scroll;
- while (col < end and col < columns) : (col += 1) {
- surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg };
- surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ for (0..viewport.h) |index| {
+ const line: u16 = @intCast(index);
+ const shown = modal.lineSlice(text, index + top);
+ var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells[index * columns ..][0..columns] };
+ surface.fill(0, 0, columns, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ _ = surface.print(0, 0, columns, shown[panes.File.rawAtDisplay(shown, scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline });
+ // The word under the pointer: on a header even while it is edited,
+ // but not on a notice (a message is not a command line), nor on a
+ // column a carried column's pointer is aiming at a place over.
+ const hovered = p.pointer_inside and kind != .notice and !(kind == .column and p.drag == .column_move);
+ if (hovered) {
+ const tag_hit = p.pointer_tag_hit;
+ const col: ?usize = if (tag_hit) |pointer|
+ (if (pointer.kind == kind and pointer.id == id and pointer.line == line) pointer.col else null)
+ else if (p.hover_row == viewport.y + line and p.hover_col >= viewport.x and p.hover_col - viewport.x < viewport.w)
+ p.hover_col - viewport.x
+ else
+ null;
+ if (col) |hover| if (wordBoundsAtCol(shown, panes.File.rawAtDisplay(shown, hover + scroll))) |bounds| {
+ var cell = panes.File.rawDisplayCol(shown, bounds.lo) -| scroll;
+ const end = panes.File.rawDisplayCol(shown, bounds.hi) -| scroll;
+ while (cell < end and cell < columns) : (cell += 1) {
+ surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ }
+ };
+ }
+ if (editing) |t| {
+ if (tagline.headerSelection(t, try edit.flatSurface(p, t), line + p.header_top)) |span| {
+ var col = panes.File.rawDisplayCol(shown, span.lo) -| scroll;
+ const end = panes.File.rawDisplayCol(shown, span.hi) -| scroll;
+ while (col < end and col < columns) : (col += 1) {
+ surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ }
}
+ const caret = panes.File.rawDisplayCol(shown, @min(@as(usize, @intCast(@max(0, t.cur_col))), shown.len)) -| scroll;
+ const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
+ if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .x = @intCast(caret), .y = line, .bar = t.mode == .insert };
}
- const caret = panes.File.rawDisplayCol(text, @min(@as(usize, @intCast(@max(0, t.cur_col))), text.len)) -| scroll;
- const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
- if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .insert };
}
}
-/// A pane's tag rows, painted once into their layers, a layer to a line,
-/// and copied into the grid; the grid's caret where the layer has it.
+/// A pane's tag rows, painted once into their layer, a row to a line, and
+/// copied into the grid; the grid's caret where the layer has it.
fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool) !void {
const capacity = p.tagCapacity(r.w -| TAG_TEXT_INSET);
// Too narrow for a layer: only the grid's gap cell.
@@ -692,23 +694,26 @@ fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r
var temporary: Surface = .{ .cols = columns + TAG_TEXT_INSET, .rows = rows, .cells = storage };
try paintPaneTag(p, &temporary, arena, pane, .{ .x = 0, .y = 0, .w = columns + TAG_TEXT_INSET, .h = rows }, id, active, capacity);
const tag_fg: Color = .{ .rgb = if (active and p.settings.focus_tint) chrome.tag_active_fg else chrome.tag_fg };
- // A layer to a line: the first in the pane's own slot, the rest in the
- // slots kept for the lines of taller tags.
+ // One layer, a row to each line the tag shows.
+ const layer = &s.tag_layers[id];
+ const count = @as(usize, columns) * rows;
+ if (layer.cells.len != count) layer.cells = try p.gpa.realloc(layer.cells, count);
+ layer.kind = .pane;
+ layer.id = @intCast(id);
+ layer.serial = pane.serial;
+ layer.viewport = .{ .x = region.rect.x + TAG_GAP, .y = region.rect.y, .w = region.rect.w - TAG_GAP - region.clip, .h = rows };
+ layer.cols = columns;
+ layer.rows = rows;
+ layer.context_rows = 0;
+ layer.bg = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
+ layer.slide = 0;
+ layer.fade = 1;
+ layer.cursor = if (p.header_focus) null else if (temporary.cursor) |cursor|
+ .{ .x = cursor.x - TAG_TEXT_INSET, .y = cursor.y, .bar = cursor.bar }
+ else
+ null;
for (0..rows) |line| {
- const layer = &s.tag_layers[if (line == 0) id else TAG_LINE_LAYER_BASE + id * (MAX_TAG_ROWS - 1) + line - 1];
- if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
- @memcpy(layer.cells, storage[line * pitch + TAG_TEXT_INSET ..][0..columns]);
- layer.kind = .pane;
- layer.id = @intCast(id);
- layer.serial = pane.serial;
- layer.line = @intCast(line);
- layer.viewport = .{ .x = region.rect.x + TAG_GAP, .y = region.rect.y + @as(u16, @intCast(line)), .w = region.rect.w - TAG_GAP - region.clip, .h = BOX_H };
- layer.cols = columns;
- layer.bg = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
- layer.cursor = if (p.header_focus) null else if (temporary.cursor) |cursor|
- (if (cursor.y == line) .{ .col = cursor.x - TAG_TEXT_INSET, .bar = cursor.bar } else null)
- else
- null;
+ @memcpy(layer.cells[line * columns ..][0..columns], storage[line * pitch + TAG_TEXT_INSET ..][0..columns]);
// The grid's row: the gap cell and the text, as far as the pane.
joinCells(s, region.rect.x, region.rect.y + @as(u16, @intCast(line)), storage[line * pitch ..][0..pitch], config.GUTTER, region.rect.w, tag_fg);
}
@@ -765,9 +770,8 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region)
const line = try arena.alloc(u8, pad + kept.len);
@memset(line[0..pad], ' ');
@memcpy(line[pad..], kept);
- const index = NOTICE_LAYER_BASE + id * Pane.Notices.max + i;
- try renderHeaderLayer(p, arena, index, .notice, @intCast(id), 0, region.rect, line, bg, fg);
- const layer = &s.tag_layers[index];
+ const layer = &s.tag_layers[NOTICE_LAYER_BASE + id * Pane.Notices.max + i];
+ try renderHeaderLayer(p, arena, layer, .notice, @intCast(id), region.rect, line, 0, bg, fg);
layer.slide = motion.slide;
layer.fade = motion.alpha;
// The caret rides the band, where the prompt's glyphs are.
@@ -775,7 +779,7 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region)
const typed = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]);
const dropped = shown - panes.File.displayWidth(kept);
if (typed < dropped) break :caret;
- layer.cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.input.mode == .insert };
+ layer.cursor = .{ .x = @intCast(pad + typed - dropped), .y = 0, .bar = pane.input.mode == .insert };
}
// The grid shows the chip's last cells of the band, where the text ends
// (all of it where the tagline's pitch is the grid's).
@@ -785,7 +789,7 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region)
// no ink of its own (the layer prints it as spaces in the band's ink).
for (skip..@min(pad, skip + chip)) |col| s.at(region.rect.x + @as(u16, @intCast(col - skip)), region.rect.y).style.fg = .default;
if (layer.cursor) |caret| {
- if (caret.col >= skip) s.cursor = .{ .x = region.rect.x + caret.col - @as(u16, @intCast(skip)), .y = region.rect.y, .bar = caret.bar };
+ if (caret.x >= skip) s.cursor = .{ .x = region.rect.x + caret.x - @as(u16, @intCast(skip)), .y = region.rect.y, .bar = caret.bar };
}
// Laid out at its full width; only shown short of the rail.
layer.viewport.w -= region.clip;
@@ -960,10 +964,11 @@ test "place puts every part where the painters and layers put it, in every layou
try std.testing.expectEqual(FontRole.tagline, s.at(grip.rect.x, grip.rect.y).style.font_role);
const tag = s.region(.tag, id).?;
try std.testing.expectEqual(Rect{ .x = r.x + config.GUTTER, .y = p.tagTop(pane, r), .w = r.w - config.GUTTER, .h = pane.tag_rows }, tag.rect);
- if (r.w > TAG_TEXT_INSET) for (0..pane.tag_rows) |line| {
- const layer = &s.tag_layers[if (line == 0) id else TAG_LINE_LAYER_BASE + id * (MAX_TAG_ROWS - 1) + line - 1];
- try std.testing.expectEqual(Rect{ .x = r.x + TAG_TEXT_INSET, .y = p.tagTop(pane, r) + @as(u16, @intCast(line)), .w = r.w - TAG_TEXT_INSET, .h = BOX_H }, layer.viewport);
- };
+ // One layer, a row to each line of the tag.
+ if (r.w > TAG_TEXT_INSET) {
+ try std.testing.expectEqual(Rect{ .x = r.x + TAG_TEXT_INSET, .y = p.tagTop(pane, r), .w = r.w - TAG_TEXT_INSET, .h = pane.tag_rows }, s.tag_layers[id].viewport);
+ try std.testing.expectEqual(pane.tag_rows, s.tag_layers[id].rows);
+ }
if (pane.collapsed) {
try std.testing.expect(s.region(.body, id) == null);
try std.testing.expect(s.region(.rail, id) == null);
diff --git a/src/edit.zig b/src/edit.zig
index b2d71ed2..9044d7ed 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -17,7 +17,7 @@ const platform = pardes.platform;
const Pane = panes.Pane;
const Text = panes.Text;
const tag_layer = @import("tag_layer.zig");
-const TagHit = tag_layer.Hit;
+const TagHit = pardes.TagHit;
const BOX_H = pardes.BOX_H;
const TAG_TEXT_INSET = pardes.TAG_TEXT_INSET;
const Mouse = pardes.Mouse;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index a1e21dc0..ff99bd39 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1779,8 +1779,8 @@ const Gui = struct {
tagline_percent: u8,
tagline_width: u32,
tagline_height: u32,
- attached_layers: ?*const [pardes.MAX_PANES]pardes.BodyLayer = null,
- attached_tag_layers: ?*const [pardes.MAX_TAG_LAYERS]pardes.TagLayer = null,
+ attached_layers: ?*const [pardes.MAX_PANES]pardes.Layer = null,
+ attached_tag_layers: ?*const [pardes.MAX_TAG_LAYERS]pardes.Layer = null,
cell_w: u32,
cell_h: u32,
ascent: i32,
@@ -3517,7 +3517,7 @@ fn gridCellAtDimensions(
var point: MouseCell = .{ .col = @intFromFloat(@min(@floor(@max(x, 0) / @max(body_w, 1)), 10_000)), .row = row };
if (core) |p| if (p.presentation.pointerFractional(p.screen_w, p.screen_h, x / @max(body_w, 1), y / @max(body_h, 1))) |mapped| {
for (p.surface.tagLayers()) |*layer| {
- if (layer.hitAt(mapped.x * body_w, mapped.y * body_h, body_w, body_h, tagline_w, @floatFromInt(p.row_metrics.tagline_h))) |hit| {
+ if (layer.tagHit(mapped.x * body_w, mapped.y * body_h, body_w, body_h, tagline_w, @floatFromInt(p.row_metrics.tagline_h))) |hit| {
point.tag_hit = hit;
break;
}
@@ -3529,19 +3529,19 @@ fn gridCellAtDimensions(
fn gridCellAtPixels(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32) MouseCell {
var point = gridCellAtDimensions(core, x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width));
if (core) |p| if (p.presentation.pointerFractional(p.screen_w, p.screen_h, x / @as(f32, @floatFromInt(g.cell_w)), y / @as(f32, @floatFromInt(g.cell_h)))) |mapped| for (p.surface.bodyLayers()) |*layer| {
- if (layer.hitAt(mapped.x * @as(f32, @floatFromInt(g.cell_w)), mapped.y * @as(f32, @floatFromInt(g.cell_h)), @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |hit| {
+ if (layer.bodyHit(mapped.x * @as(f32, @floatFromInt(g.cell_w)), mapped.y * @as(f32, @floatFromInt(g.cell_h)), @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |hit| {
point.body_hit = hit;
break;
}
};
if (core == null) if (g.attached_layers) |layers| for (layers) |*layer| {
- if (layer.hitAt(x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |body_hit| {
+ if (layer.bodyHit(x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |body_hit| {
point.body_hit = body_hit;
break;
}
};
if (core == null) if (g.attached_tag_layers) |layers| for (layers) |*layer| {
- if (layer.hitAt(x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |hit| {
+ if (layer.tagHit(x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |hit| {
point.tag_hit = hit;
break;
}
@@ -5203,18 +5203,18 @@ fn renderFrame(
}
}
for (surface.tagLayers()) |*layer| {
- if (layer.cols == 0) continue;
+ if (layer.rows == 0) continue;
const batch = &paint_plan.batches[paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y)];
- batch.cell_count += tagLayerCellCount(g, layer) + 1;
+ batch.cell_count += (tagLayerCellCount(g, layer) + 1) * layer.rows;
}
for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
const track = batch.track.?;
const under = track.effect == .vertical and track.phase == .opening;
if (!under and track.phase != .closing and track.effect != .dissolve) continue;
for (surface.previous_tag_layers) |*layer| {
- if (layer.cols == 0 or layer.kind != .pane or layer.serial != track.serial) continue;
+ if (layer.rows == 0 or layer.kind != .pane or layer.serial != track.serial) continue;
const destination = if (under) &paint_plan.batches[0] else &paint_plan.batches[batch_index];
- destination.cell_count += tagLayerCellCount(g, layer) + 1;
+ destination.cell_count += (tagLayerCellCount(g, layer) + 1) * layer.rows;
}
}
var cell_next: [pardes.MAX_PANES * 2 + 1]u32 = @splat(0);
@@ -5303,7 +5303,7 @@ fn renderFrame(
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 (surface.tagLayers()) |*layer| {
- if (layer.cols == 0) continue;
+ if (layer.rows == 0) continue;
const batch_index = paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y);
emitTagLayer(g, instances, &cell_next[batch_index], layer, win_w, win_h, paint_plan.batches[batch_index].track, page, paint_plan.len == 1, false);
}
@@ -5312,7 +5312,7 @@ fn renderFrame(
const under = track.effect == .vertical and track.phase == .opening;
if (!under and track.phase != .closing and track.effect != .dissolve) continue;
for (surface.previous_tag_layers) |*layer| {
- if (layer.cols == 0 or layer.kind != .pane or layer.serial != track.serial) continue;
+ if (layer.rows == 0 or layer.kind != .pane or layer.serial != track.serial) continue;
const destination: usize = if (under) 0 else batch_index;
emitTagLayer(g, instances, &cell_next[destination], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve);
}
@@ -5621,10 +5621,10 @@ test "compact column and pane underlays share focus tint without tinting the glo
try std.testing.expectEqual(chrome.tag_bg, taglineBaseRgb(core, active.x, pardes.TOPBAR_H));
}
-fn tagLayerIn(layers: []const pardes.TagLayer, col: u16, row: u16) ?*const pardes.TagLayer {
+fn tagLayerIn(layers: []const pardes.Layer, col: u16, row: u16) ?*const pardes.Layer {
for (layers) |*layer| {
const r = layer.viewport;
- if (layer.cols > 0 and row == r.y and col >= r.x and col - r.x < r.w) return layer;
+ if (layer.rows > 0 and row >= r.y and row - r.y < layer.rows and col >= r.x and col - r.x < r.w) return layer;
}
return null;
}
@@ -5633,9 +5633,9 @@ fn tagLayerIn(layers: []const pardes.TagLayer, col: u16, row: u16) ?*const parde
/// themselves: the grid's cell loops skip them. Filling each layer's rectangle
/// once replaces scanning every layer for every cell, which was a third of the
/// gui's CPU on a full screen of text. It marks exactly the cells bodyLayerIn
-/// and tagLayerIn find a layer for: a tag layer covers only its viewport's
-/// first row, and a viewport past the grid covers the grid's part of it.
-fn coverLayers(cover: []bool, cols: u16, rows: u16, body_layers: []const pardes.BodyLayer, tag_layers: []const pardes.TagLayer) void {
+/// and tagLayerIn find a layer for: a tag layer covers its rows, and a
+/// viewport past the grid covers the grid's part of it.
+fn coverLayers(cover: []bool, cols: u16, rows: u16, body_layers: []const pardes.Layer, tag_layers: []const pardes.Layer) void {
@memset(cover, false);
for (body_layers) |*layer| {
const r = layer.viewport;
@@ -5647,12 +5647,13 @@ fn coverLayers(cover: []bool, cols: u16, rows: u16, body_layers: []const pardes.
for (tag_layers) |*layer| {
const r = layer.viewport;
const right = @min(cols, @as(u32, r.x) + r.w);
- if (layer.cols == 0 or r.y >= rows or r.x >= right) continue;
- @memset(cover[@as(usize, r.y) * cols ..][r.x..right], true);
+ if (layer.rows == 0 or r.x >= right) continue;
+ var y: u32 = r.y;
+ while (y < @min(rows, @as(u32, r.y) + layer.rows)) : (y += 1) @memset(cover[y * cols ..][r.x..right], true);
}
}
-fn expectCoverIsScan(cover: []const bool, cols: u16, rows: u16, body_layers: []const pardes.BodyLayer, tag_layers: []const pardes.TagLayer) !void {
+fn expectCoverIsScan(cover: []const bool, cols: u16, rows: u16, body_layers: []const pardes.Layer, tag_layers: []const pardes.Layer) !void {
for (0..rows) |row| for (0..cols) |col| {
const scan = bodyLayerIn(body_layers, @intCast(col), @intCast(row)) != null or tagLayerIn(tag_layers, @intCast(col), @intCast(row)) != null;
try std.testing.expectEqual(scan, cover[row * cols + col]);
@@ -5685,7 +5686,7 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
@memset(&cover, true); // what an earlier frame left
coverLayers(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers());
try expectCoverIsScan(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers());
- for (s.tagLayers()) |layer| notices = notices or (layer.cols != 0 and layer.kind == .notice);
+ for (s.tagLayers()) |layer| notices = notices or (layer.rows != 0 and layer.kind == .notice);
}
try std.testing.expect(notices);
@@ -5702,8 +5703,8 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
};
var prng = std.Random.DefaultPrng.init(0x6c61796572);
const random = prng.random();
- var body: [pardes.MAX_PANES]pardes.BodyLayer = undefined;
- var tag: [pardes.MAX_TAG_LAYERS]pardes.TagLayer = undefined;
+ var body: [pardes.MAX_PANES]pardes.Layer = undefined;
+ var tag: [pardes.MAX_TAG_LAYERS]pardes.Layer = undefined;
var grid: [64 * 40]bool = undefined;
for (0..3000) |_| {
const cols = random.intRangeAtMost(u16, 1, 64);
@@ -5720,12 +5721,12 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
}
}
-fn tagLayerCellCount(g: *const Gui, layer: *const pardes.TagLayer) u32 {
+fn tagLayerCellCount(g: *const Gui, layer: *const pardes.Layer) u32 {
const pixels = @as(u32, layer.viewport.w) * g.cell_w;
return @min(layer.cols, (pixels + g.tagline_width - 1) / g.tagline_width);
}
-fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const pardes.TagLayer, win_w: f32, win_h: f32, track: ?pardes.animation.Track, page: Ground, show_cursor: bool, old_layer: bool) void {
+fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const pardes.Layer, win_w: f32, win_h: f32, track: ?pardes.animation.Track, page: Ground, show_cursor: bool, old_layer: bool) void {
const bw: f32 = @floatFromInt(g.cell_w);
const bh: f32 = @floatFromInt(g.cell_h);
const tw: f32 = @floatFromInt(g.tagline_width);
@@ -5735,20 +5736,25 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p
// A band sliding into place is drawn offset and cut at its row's top, so
// it comes out from under whatever is above it.
const slide = layer.slide * bh;
- const top = @as(f32, @floatFromInt(layer.viewport.y)) * bh;
// A window is rarely a whole number of cells wide: a band that ends at
// the last column runs on through the leftover pixels to the edge.
const band_right = if (win_w - right < bw) @max(right, win_w) else right;
- emitInstance(g, instances, next.*, 0, layer.viewport.y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, (&background)[0..1], 0, null, old_layer, false, page);
- if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
- next.* += 1;
- const line_cursor: ?usize = if (layer.cursor) |cu| cu.col else null;
- for (0..tagLayerCellCount(g, layer)) |col| {
- const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.col == col else false;
- emitInstance(g, instances, next.*, @intCast(col), layer.viewport.y, .{ .w = tw, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .tagline, layer.cells[0..layer.cols], col, line_cursor, old_layer, cursor, page);
- clipInstanceRight(&instances[next.*], right, win_w);
+ // A row to each line, each a grid row with the tagline's glyphs.
+ for (0..layer.rows) |index| {
+ const y = layer.viewport.y + @as(u16, @intCast(index));
+ const top = @as(f32, @floatFromInt(y)) * bh;
+ emitInstance(g, instances, next.*, 0, y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, (&background)[0..1], 0, null, old_layer, false, page);
if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
next.* += 1;
+ const cells = layer.cells[index * layer.cols ..][0..layer.cols];
+ const line_cursor: ?usize = if (layer.cursor) |cu| (if (cu.y == index) cu.x else null) else null;
+ for (0..tagLayerCellCount(g, layer)) |col| {
+ const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.x == col and cu.y == index else false;
+ emitInstance(g, instances, next.*, @intCast(col), y, .{ .w = tw, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .tagline, cells, col, line_cursor, old_layer, cursor, page);
+ clipInstanceRight(&instances[next.*], right, win_w);
+ if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
+ next.* += 1;
+ }
}
}
@@ -5760,11 +5766,11 @@ fn clipInstanceTop(instance: *CellInstance, top: f32, win_h: f32) void {
instance.y0 = @max(instance.y1, limit);
}
-fn bodyLayerAt(surface: *const pardes.Surface, col: u16, row: u16) ?*const pardes.BodyLayer {
+fn bodyLayerAt(surface: *const pardes.Surface, col: u16, row: u16) ?*const pardes.Layer {
return bodyLayerIn(surface.bodyLayers(), col, row);
}
-fn bodyLayerIn(layers: []const pardes.BodyLayer, col: u16, row: u16) ?*const pardes.BodyLayer {
+fn bodyLayerIn(layers: []const pardes.Layer, col: u16, row: u16) ?*const pardes.Layer {
for (layers) |*layer| {
const r = layer.viewport;
if (layer.rows > 0 and col >= r.x and col - r.x < r.w and row >= r.y and row - r.y < r.h) return layer;
@@ -5788,7 +5794,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.animation.Track, page: Ground, show_cursor: bool, old_layer: bool, border_rgb: [3]u8) void {
+fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const pardes.Layer, win_w: f32, win_h: f32, track: ?pardes.animation.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);
@@ -5851,11 +5857,11 @@ fn gripCellChanged(surface: *const pardes.Surface, col: u16, row: u16) bool {
return false;
}
-fn paneGripCell(layers: []const pardes.TagLayer, cells: []const pardes.Cell, cols: u16, col: u16, row: u16) ?GripCell {
+fn paneGripCell(layers: []const pardes.Layer, cells: []const pardes.Cell, cols: u16, col: u16, row: u16) ?GripCell {
const at = @as(usize, row) * cols + col;
if (at >= cells.len or cells[at].default or cells[at].style.font_role != .tagline) return null;
for (layers) |layer| {
- if (layer.cols == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET or layer.viewport.y != row) continue;
+ if (layer.rows == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET or row < layer.viewport.y or row - layer.viewport.y >= layer.rows) continue;
const x = layer.viewport.x - pardes.TAG_TEXT_INSET;
if (col < x or col - x >= config.GUTTER) continue;
const index = @as(usize, row) * cols + x;
@@ -6607,7 +6613,7 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" {
cells[0].text[0] = '$';
var previous = cells;
previous[0].text[0] = '^';
- var layers = [_]pardes.TagLayer{.{ .kind = .pane, .serial = 1, .viewport = .{ .x = pardes.TAG_TEXT_INSET, .y = 0, .w = 3, .h = 1 }, .cols = 3 }};
+ var layers = [_]pardes.Layer{.{ .kind = .pane, .serial = 1, .viewport = .{ .x = pardes.TAG_TEXT_INSET, .y = 0, .w = 3, .h = 1 }, .cols = 3, .rows = 1 }};
var diffs: [12]pardes.PanelCellDiff = @splat(.unchanged);
diffs[0] = pardes.PanelCellDiff.between(&previous[0], &cells[0]);
var surface: pardes.Surface = .{ .cols = 6, .rows = 2, .cells = &cells, .previous_cells = &previous, .previous_tag_layers = &layers, .cell_diffs = &diffs };
@@ -7316,16 +7322,17 @@ fn buildOverlay(
}
};
if (transient_on) for (surface.tagLayers()) |*layer| {
- if (layer.cols == 0) continue;
+ if (layer.rows == 0) continue;
const cursor = layer.cursor orelse continue;
- if (!cursor.bar or cursor.col >= layer.cols) continue;
+ if (!cursor.bar or cursor.x >= layer.cols or cursor.y >= layer.rows) continue;
const width: f32 = @floatFromInt(g.tagline_width);
const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
- const x = left + @as(f32, @floatFromInt(cursor.col)) * width;
+ const x = left + @as(f32, @floatFromInt(cursor.x)) * width;
if (x >= right) continue;
- const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height)));
- builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[cursor.col], .tagline)));
+ const y = layer.viewport.y + cursor.y;
+ const top = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height)));
+ builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline)));
};
if (transient_on) appendTouchOverlay(g, &builder, debug_on);
return @intCast(builder.len);
@@ -7506,7 +7513,7 @@ fn appendPaneChrome(
// off from the text beneath it the way a tagline is: the same colour and
// thickness along its bottom, running to the window edge when it does.
for (surface.tagLayers()) |*layer| {
- if (layer.cols == 0 or layer.kind != .notice) continue;
+ if (layer.rows == 0 or layer.kind != .notice) continue;
const x0 = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
const x1 = x0 + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
const rule_x1 = if (builder.win_w - x1 < layout.w) @max(x1, builder.win_w) else x1;
@@ -7528,21 +7535,25 @@ fn appendPaneChrome(
fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout, builder: *OverlayBuilder) void {
if (g.tagline_height < 6 or g.tagline_width < 3) return;
for (surface.tagLayers()) |layer| {
- if (layer.cols == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET) continue;
+ if (layer.rows == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET) continue;
const x = layer.viewport.x - pardes.TAG_TEXT_INSET;
- const y = layer.viewport.y;
- if (x >= surface.cols or y >= surface.rows) continue;
- const cell = &surface.cells[@as(usize, y) * surface.cols + x];
- if (cell.default or cell.printableAscii() != ' ') continue;
- const rgb = switch (cell.style.fg) {
- .rgb => |rgb| rgb,
- else => continue,
- };
- const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, builder.win_h) / 2));
- const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
- const half_w = @max(1, @min(3, layout.w - 2));
- builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb));
- builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb));
+ // A mark on each of the tag's rows whose grip cell is blank: every
+ // row but the one with the mode's glyph.
+ for (0..layer.rows) |index| {
+ const y = layer.viewport.y + @as(u16, @intCast(index));
+ if (x >= surface.cols or y >= surface.rows) continue;
+ const cell = &surface.cells[@as(usize, y) * surface.cols + x];
+ if (cell.default or cell.printableAscii() != ' ') continue;
+ const rgb = switch (cell.style.fg) {
+ .rgb => |rgb| rgb,
+ else => continue,
+ };
+ const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, builder.win_h) / 2));
+ const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
+ const half_w = @max(1, @min(3, layout.w - 2));
+ builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb));
+ builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb));
+ }
}
}
@@ -7553,8 +7564,8 @@ test "column grips stay unmarked while pane marks retain physical centering" {
g.cell_h = 20;
var cells: [80]pardes.Cell = @splat(.{ .default = false, .style = .{ .fg = .{ .rgb = .{ 100, 120, 140 } } } });
var surface: pardes.Surface = .{ .cols = 20, .rows = 4, .cells = &cells };
- surface.tag_layers[0] = .{ .kind = .column, .viewport = .{ .x = 8, .y = 1, .w = 13, .h = 1 }, .cols = 20 };
- surface.tag_layers[1] = .{ .kind = .pane, .viewport = .{ .x = 8, .y = 3, .w = 13, .h = 1 }, .cols = 20 };
+ surface.tag_layers[0] = .{ .kind = .column, .viewport = .{ .x = 8, .y = 1, .w = 13, .h = 1 }, .cols = 20, .rows = 1 };
+ surface.tag_layers[1] = .{ .kind = .pane, .viewport = .{ .x = 8, .y = 3, .w = 13, .h = 1 }, .cols = 20, .rows = 1 };
var vertices: [24]OverlayVertex = undefined;
var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 200, .win_h = 80 };
appendTagGrips(&g, &surface, .{ .w = 10, .h = 20, .x_off = 0, .y_off = 0 }, &builder);
@@ -7927,7 +7938,7 @@ 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(.{});
- var layer: pardes.BodyLayer = .{
+ var layer: pardes.Layer = .{
.viewport = .{ .x = 3, .y = 2, .w = 2, .h = 5 },
.cols = 2,
.rows = 12,
@@ -7967,7 +7978,7 @@ test "compact tag layers fill available width and clip at their physical edge" {
g.tagline_height = 12;
g.space_slot = .{ .u = 0, .v = 0 };
var cells: [6]pardes.Cell = @splat(.{});
- const layer: pardes.TagLayer = .{ .kind = .pane, .id = 0, .serial = 7, .viewport = .{ .x = 2, .y = 3, .w = 3, .h = 1 }, .cols = 6, .cells = &cells, .bg = .{ 20, 30, 40 } };
+ const layer: pardes.Layer = .{ .kind = .pane, .id = 0, .serial = 7, .viewport = .{ .x = 2, .y = 3, .w = 3, .h = 1 }, .cols = 6, .rows = 1, .cells = &cells, .bg = .{ 20, 30, 40 } };
var instances: [8]CellInstance = undefined;
var next: u32 = 0;
emitTagLayer(&g, &instances, &next, &layer, 100, 100, null, Ground.opaqueRgb(bg_default), false, false);
diff --git a/src/look.zig b/src/look.zig
index 8f9e15f2..258cf8f5 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -21,7 +21,7 @@ const pdf = panes.Pdf.pdf;
const Pane = panes.Pane;
const MAX_PANES = pardes.MAX_PANES;
const tag_layer = @import("tag_layer.zig");
-const TagHit = tag_layer.Hit;
+const TagHit = pardes.TagHit;
const BOX_H = pardes.BOX_H;
const TAG_TEXT_INSET = pardes.TAG_TEXT_INSET;
const pathUnder = pardes.pathUnder;
diff --git a/src/macos.zig b/src/macos.zig
index fce0a53e..f7e03cd4 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -1861,9 +1861,9 @@ fn clearFrame(st: *State) void {
st.panel_diff_len = 0;
}
-fn blankTagCells(cells: []Cell, cols: u16, rows: u16, source: []const pardes.Cell, layers: []const pardes.TagLayer) void {
+fn blankTagCells(cells: []Cell, cols: u16, rows: u16, source: []const pardes.Cell, layers: []const pardes.Layer) void {
for (layers) |layer| {
- if (layer.cols == 0 or layer.viewport.y >= rows or layer.viewport.x >= cols) continue;
+ if (layer.rows == 0 or layer.viewport.y >= rows or layer.viewport.x >= cols) continue;
const start = @as(usize, layer.viewport.y) * cols + layer.viewport.x;
const count = @min(layer.viewport.w, cols - layer.viewport.x);
for (cells[start..][0..count], source[start..][0..count]) |*cell, was| {
@@ -1879,7 +1879,7 @@ fn blankTagCells(cells: []Cell, cols: u16, rows: u16, source: []const pardes.Cel
fn tagCursorCovered(core: *const pardes.Pardes) bool {
const cursor = core.surface.cursor orelse return false;
for (core.surface.tag_layers) |layer| {
- if (layer.cols != 0 and cursor.y == layer.viewport.y and cursor.x >= layer.viewport.x and
+ if (layer.rows != 0 and cursor.y == layer.viewport.y and cursor.x >= layer.viewport.x and
cursor.x - layer.viewport.x < layer.viewport.w) return true;
}
return false;
@@ -2121,7 +2121,7 @@ export fn pardes_tag_layer_value(index: u32, field: u32) u32 {
5 => layer.viewport.y,
6 => layer.viewport.w,
7 => layer.viewport.h,
- 8 => if (layer.cursor) |c| c.col else 65535,
+ 8 => if (layer.cursor) |c| c.x else 65535,
9 => if (layer.cursor) |c| @intFromBool(c.bar) else 0,
10 => @as(u32, layer.bg[0]) << 16 | @as(u32, layer.bg[1]) << 8 | layer.bg[2],
11 => @intFromBool(layer.kind == .pane and st.core.settings.tag_bottom),
@@ -2139,7 +2139,7 @@ export fn pardes_tag_layer_cells(index: u32) ?[*]const Cell {
const st = &(state orelse return null);
if (index >= pardes.MAX_TAG_LAYERS) return null;
const layer = &st.core.surface.tag_layers[index];
- if (layer.cols == 0) return null;
+ if (layer.rows == 0) return null;
const cells = &st.tag_cells[index];
if (cells.len != layer.cells.len) cells.* = st.gpa.realloc(cells.*, layer.cells.len) catch return null;
for (layer.cells, cells.*) |source, *out| out.* = encodeCell(source);
@@ -3110,7 +3110,7 @@ test "mac tag layer ABI preserves logical capacity and physical grip" {
logical[11].text[0] = 'Z';
logical[11].len = 1;
logical[11].default = false;
- core.surface.tag_layers[0] = .{ .kind = .pane, .id = 1, .serial = 42, .viewport = .{ .x = 2, .y = 1, .w = 6, .h = 1 }, .cols = 12, .cells = &logical, .bg = .{ 0x12, 0x34, 0x56 }, .cursor = .{ .col = 11, .bar = true } };
+ core.surface.tag_layers[0] = .{ .kind = .pane, .id = 1, .serial = 42, .viewport = .{ .x = 2, .y = 1, .w = 6, .h = 1 }, .cols = 12, .rows = 1, .cells = &logical, .bg = .{ 0x12, 0x34, 0x56 }, .cursor = .{ .x = 11, .y = 0, .bar = true } };
defer core.surface.tag_layers[0] = .{};
try std.testing.expectEqual(@as(u32, 12), pardes_tag_layer_value(0, 0));
try std.testing.expectEqual(@as(u32, 0x123456), pardes_tag_layer_value(0, 10));
diff --git a/src/pardes.zig b/src/pardes.zig
index 6946e123..f8087bdb 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -39,6 +39,7 @@ pub const ImagePatch = @import("surface.zig").ImagePatch;
pub const ImagePlace = @import("surface.zig").ImagePlace;
pub const Surface = @import("surface.zig").Surface;
pub const Region = @import("surface.zig").Region;
+pub const MAX_REGIONS = @import("surface.zig").MAX_REGIONS;
pub const builtins = @import("builtins.zig");
const limits = memory.limits;
const selection_pipe = @import("selection_pipe.zig");
@@ -247,21 +248,17 @@ pub const Pane = panes.Pane;
const Text = panes.Text;
pub const MAX_PANES = 16;
-pub const BodyLayer = @import("body_layer.zig").Layer;
-pub const RowMetrics = @import("body_layer.zig").Metrics;
-pub const TagLayer = @import("tag_layer.zig").Layer;
-pub const TagHit = @import("tag_layer.zig").Hit;
-pub const TagKind = @import("tag_layer.zig").Kind;
-/// Panes, then columns, then the workspace bar, then each pane's notice bands.
+pub const Layer = @import("Layer.zig");
+pub const RowMetrics = Layer.Metrics;
+pub const TagHit = Layer.TagHit;
+pub const TagKind = Layer.Kind;
/// A tag shows at most this many lines; past them it is cut, like a line
/// too long for its width.
pub const MAX_TAG_ROWS = 8;
-pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1 + MAX_PANES * Pane.Notices.max + (MAX_PANES + MAX_COLS + 1) * (MAX_TAG_ROWS - 1);
+/// Panes, then columns, then the workspace bar, then each pane's notice
+/// bands: a band to a tag, however many lines it has.
+pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1 + MAX_PANES * Pane.Notices.max;
pub const NOTICE_LAYER_BASE = MAX_PANES + MAX_COLS + 1;
-/// Where the layers for a pane tag's second and later lines start.
-pub const TAG_LINE_LAYER_BASE = NOTICE_LAYER_BASE + MAX_PANES * Pane.Notices.max;
-/// ...and those of a column's (by index) or the workspace's (after them).
-pub const HEADER_LINE_LAYER_BASE = TAG_LINE_LAYER_BASE + MAX_PANES * (MAX_TAG_ROWS - 1);
pub const PDF_PAGE_GAP_PX = panes.Pdf.page_gap_px;
pub const MAX_COLS = 6;
const column_weight_unit = layout.column_weight_unit;
@@ -787,7 +784,7 @@ test "TreeContext tag style preserves message rows and its opt-out survives rest
try std.testing.expectEqual(full_height, p.surface.body_layers[0].viewport.h);
const r = p.rects[0];
const band = for (p.surface.tagLayers()) |*layer| {
- if (layer.cols != 0 and layer.kind == .notice) break layer;
+ if (layer.rows != 0 and layer.kind == .notice) break layer;
} else return error.NoNoticeBand;
try std.testing.expectEqual(p.bodyTop(pane, r), band.viewport.y);
try std.testing.expectEqual(p.surface.body_layers[0].viewport.y, band.viewport.y);
@@ -5832,15 +5829,14 @@ pub const Pardes = struct {
pub fn reprojectTagHit(p: *const Pardes, tag_hit: TagHit) ?TagHit {
for (p.surface.tagLayers()) |*layer| {
- if (layer.cols == 0 or layer.kind != tag_hit.kind or layer.id != tag_hit.id or layer.serial != tag_hit.serial) continue;
+ if (layer.rows == 0 or layer.kind != tag_hit.kind or layer.id != tag_hit.id or layer.serial != tag_hit.serial) continue;
if (layer.kind == .pane) {
if (layer.id >= p.panes.len) return null;
const pane = p.panes[layer.id] orelse return null;
if (pane.serial != layer.serial) return null;
}
if (layer.kind == .column and (layer.id >= p.ncol or p.col_serial[layer.id] != layer.serial)) return null;
- // A taller tag has a layer per line; the point is on one of them.
- return layer.hitAt(tag_hit.pixel_x, tag_hit.pixel_y, @floatFromInt(tag_hit.metrics.body_w), @floatFromInt(tag_hit.metrics.body_h), @floatFromInt(tag_hit.metrics.tagline_w), @floatFromInt(tag_hit.metrics.tagline_h)) orelse continue;
+ return layer.tagHit(tag_hit.pixel_x, tag_hit.pixel_y, @floatFromInt(tag_hit.metrics.body_w), @floatFromInt(tag_hit.metrics.body_h), @floatFromInt(tag_hit.metrics.tagline_w), @floatFromInt(tag_hit.metrics.tagline_h)) orelse continue;
}
return null;
}
@@ -5858,14 +5854,14 @@ pub const Pardes = struct {
}
pub fn tagCapacity(p: *const Pardes, physical_width: u16) u16 {
- const layer: TagLayer = .{ .viewport = .{ .x = 0, .y = 0, .w = physical_width, .h = 1 } };
+ const layer: Layer = .{ .kind = .pane, .viewport = .{ .x = 0, .y = 0, .w = physical_width, .h = 1 } };
return layer.capacity(p.row_metrics);
}
pub fn reprojectBodyHit(p: *const Pardes, body_hit: Mouse.BodyHit) ?Mouse.BodyHit {
for (&p.surface.body_layers) |*layer| {
if (layer.rows == 0 or layer.serial != body_hit.serial) continue;
- return layer.hitAt(body_hit.pixel_x, body_hit.pixel_y, @floatFromInt(body_hit.metrics.body_w), @floatFromInt(body_hit.metrics.body_h), @floatFromInt(body_hit.metrics.tagline_w), @floatFromInt(body_hit.metrics.tagline_h));
+ return layer.bodyHit(body_hit.pixel_x, body_hit.pixel_y, @floatFromInt(body_hit.metrics.body_w), @floatFromInt(body_hit.metrics.body_h), @floatFromInt(body_hit.metrics.tagline_w), @floatFromInt(body_hit.metrics.tagline_h));
}
return null;
}
@@ -6111,12 +6107,12 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch
// A pixel shell draws the prompt from its band, in the tagline's pitch,
// with the caret right after the marker's last glyph.
const band = for (p.surface.tagLayers()) |*layer| {
- if (layer.cols != 0 and layer.kind == .notice) break layer;
+ if (layer.rows != 0 and layer.kind == .notice) break layer;
} else return error.NoPromptBand;
try std.testing.expectEqual(row, band.viewport.y);
const caret = band.cursor orelse return error.MissingBandCaret;
try std.testing.expect(caret.bar);
- try std.testing.expectEqualStrings("/", band.cells[caret.col - 1].grapheme());
+ try std.testing.expectEqualStrings("/", band.cells[caret.x - 1].grapheme());
// The chip covers only its own columns. Left of it is ordinary body, and
// a sweep there still selects.
@@ -6162,11 +6158,11 @@ test "a prompt is typed on its own line in the notice band and leaves the tag ro
try std.testing.expect(p.surface.tag_layers[0].cursor == null);
// The band carries the answer and the caret, one cell back from its end.
const band = for (p.surface.tagLayers()) |*layer| {
- if (layer.cols != 0 and layer.kind == .notice) break layer;
+ if (layer.rows != 0 and layer.kind == .notice) break layer;
} else return error.NoPromptBand;
const caret = band.cursor orelse return error.MissingBandCaret;
- try std.testing.expectEqualStrings("t", band.cells[caret.col - 1].grapheme());
- try std.testing.expectEqualStrings("w", band.cells[caret.col].grapheme());
+ try std.testing.expectEqualStrings("t", band.cells[caret.x - 1].grapheme());
+ try std.testing.expectEqualStrings("w", band.cells[caret.x].grapheme());
const grid = p.surface.cursor orelse return error.MissingPromptCursor;
try std.testing.expectEqual(p.bodyTop(pane, p.rects[0]), grid.y);
diff --git a/src/surface.zig b/src/surface.zig
index fec43e30..db7dd468 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -11,8 +11,7 @@ const config = @import("config.zig");
const pdf_enabled = panes.Pdf.enabled;
const image = @import("image.zig");
const MAX_PANES = pardes.MAX_PANES;
-const BodyLayer = @import("body_layer.zig").Layer;
-const TagLayer = @import("tag_layer.zig").Layer;
+const Layer = @import("Layer.zig");
const MAX_TAG_LAYERS = pardes.MAX_TAG_LAYERS;
const CellPixels = pardes.CellPixels;
@@ -380,8 +379,8 @@ pub const Surface = struct {
/// paint the quiet affordance can carry the hover bit through the ABI
/// without every selection painter learning a new parameter.
mark_hover: bool = false,
- body_layers: [MAX_PANES]BodyLayer = @splat(.{}),
- tag_layers: [MAX_TAG_LAYERS]TagLayer = @splat(.{}),
+ body_layers: [MAX_PANES]Layer = @splat(.{}),
+ tag_layers: [MAX_TAG_LAYERS]Layer = @splat(.{}),
cols: u16 = 0,
rows: u16 = 0,
regions: [MAX_REGIONS]Region = undefined,
@@ -394,15 +393,15 @@ pub const Surface = struct {
panel_tracks: [MAX_PANES * 2]animation.Track = undefined,
npanel_tracks: usize = 0,
previous_cells: []const Cell = &.{},
- previous_body_layers: []const BodyLayer = &.{},
- previous_tag_layers: []const TagLayer = &.{},
+ previous_body_layers: []const Layer = &.{},
+ previous_tag_layers: []const Layer = &.{},
cell_diffs: []const PanelCellDiff = &.{},
- pub fn tagLayers(s: *const Surface) []const TagLayer {
+ pub fn tagLayers(s: *const Surface) []const Layer {
return &s.tag_layers;
}
- pub fn bodyLayers(s: *const Surface) []const BodyLayer {
+ pub fn bodyLayers(s: *const Surface) []const Layer {
return &s.body_layers;
}
diff --git a/src/tag_layer.zig b/src/tag_layer.zig
index 7cfe5bbf..213ebc07 100644
--- a/src/tag_layer.zig
+++ b/src/tag_layer.zig
@@ -1,83 +1,10 @@
-//! Tag text has its own horizontal pitch. Its viewport stays in physical
-//! body-grid coordinates so compact text never changes pane hit targets.
+//! Pointer hits on tag bands (Layer.TagHit): comparing two, and bringing one
+//! that left its band back onto it.
const std = @import("std");
const pardes = @import("pardes.zig");
-
-/// `notice` is a pane's transient line -- a builtin's name, a pending leader
-/// chord -- carried as a tagline-shaped band so it gets the tagline's height,
-/// small-font metrics, band offset and border instead of being a body-grid row
-/// wearing a tagline font.
-pub const Kind = enum(u8) { workspace, column, pane, notice };
-pub const Hit = struct {
- kind: Kind,
- id: u16,
- serial: u32,
- col: u16,
- /// Which line of a taller tag the layer draws.
- line: u16 = 0,
- pixel_x: f32,
- pixel_y: f32,
- metrics: pardes.RowMetrics,
-};
-pub const Layer = struct {
- kind: Kind = .workspace,
- id: u16 = 0,
- serial: u32 = 0,
- /// A pane's tag taller than a line is drawn a layer to a line; this is
- /// which one, counted from its first.
- line: u16 = 0,
- viewport: pardes.Rect = .{ .x = 0, .y = 0, .w = 0, .h = 0 },
- cols: u16 = 0,
- bg: [3]u8 = .{ 0, 0, 0 },
- cells: []pardes.Cell = &.{},
- cursor: ?struct { col: u16, bar: bool = false } = null,
- /// Vertical offset in rows (negative: above its viewport), for a band
- /// that is moving into place. A pixel shell draws it offset and clipped
- /// to the rows below the viewport's top; the grid has no such thing.
- slide: f32 = 0,
- /// How present a notice band is (1 = fully) while it fades up or
- /// dissolves. `bg` and the cells are already blended by it; a pixel
- /// shell uses it for what it draws around the band, like its rule.
- fade: f32 = 1,
-
- pub fn capacity(layer: *const Layer, metrics: pardes.RowMetrics) u16 {
- return @intCast(@min(std.math.maxInt(u16), @max(1, @as(u32, layer.viewport.w) * @max(1, metrics.body_w) / @max(1, metrics.tagline_w))));
- }
-
- pub fn hitAt(layer: *const Layer, x: f32, y: f32, body_w: f32, body_h: f32, tagline_w: f32, tagline_h: f32) ?Hit {
- if (layer.cols == 0 or !std.math.isFinite(x) or !std.math.isFinite(y)) return null;
- for ([_]f32{ body_w, body_h, tagline_w, tagline_h }) |metric|
- if (!std.math.isFinite(metric) or metric < 1 or metric > std.math.maxInt(u16)) return null;
- const bw = @max(1, body_w);
- const bh = @max(1, body_h);
- const tw = std.math.clamp(tagline_w, 1, bw);
- const th = std.math.clamp(tagline_h, 1, bh);
- const dx = x - @as(f32, @floatFromInt(layer.viewport.x)) * bw;
- const dy = y - @as(f32, @floatFromInt(layer.viewport.y)) * bh;
- if (dx < 0 or dy < 0 or dx >= @as(f32, @floatFromInt(layer.viewport.w)) * bw or dy >= @as(f32, @floatFromInt(layer.viewport.h)) * bh) return null;
- return .{
- .kind = layer.kind,
- .id = layer.id,
- .serial = layer.serial,
- .line = layer.line,
- .col = @intFromFloat(@min(@as(f32, @floatFromInt(layer.cols - 1)), @floor(dx / tw))),
- .pixel_x = x,
- .pixel_y = y,
- .metrics = .{ .body_w = @intFromFloat(bw), .body_h = @intFromFloat(bh), .tagline_w = @intFromFloat(tw), .tagline_h = @intFromFloat(th) },
- };
- }
-};
-
-test "tag text capacity uses its own pitch and excludes the physical grip" {
- const layer: Layer = .{ .kind = .pane, .id = 2, .serial = 7, .viewport = .{ .x = 12, .y = 4, .w = 38, .h = 1 }, .cols = 90 };
- try std.testing.expectEqual(@as(u16, 63), layer.capacity(.{ .body_w = 10, .tagline_w = 6 }));
- try std.testing.expect(layer.hitAt(119.99, 85, 10, 20, 6, 12) == null);
- try std.testing.expectEqual(@as(u16, 0), layer.hitAt(120, 85, 10, 20, 6, 12).?.col);
- try std.testing.expectEqual(@as(u16, 50), layer.hitAt(423, 85, 10, 20, 6, 12).?.col);
- try std.testing.expect(layer.hitAt(500, 85, 10, 20, 6, 12) == null);
- try std.testing.expect(layer.hitAt(120, 85, std.math.nan(f32), 20, 6, 12) == null);
- try std.testing.expect(layer.hitAt(120, 85, 10, 20, 0, 12) == null);
-}
+const Layer = @import("Layer.zig");
+const Hit = Layer.TagHit;
+const Kind = Layer.Kind;
/// Whether two pointer hits name the same cell of the same tag line.
pub fn sameCell(a: ?Hit, b: ?Hit) bool {
@@ -92,13 +19,13 @@ pub fn columnAt(p: *const pardes.Pardes, kind: Kind, id: usize, supplied: ?Hit,
var point = supplied orelse return null;
if (point.kind != kind or point.id != id) return null;
if (clamp) for (p.surface.tagLayers()) |*layer| {
- if (layer.cols == 0 or layer.kind != kind or layer.id != id or layer.serial != point.serial or layer.line != line) continue;
+ if (layer.rows == 0 or layer.kind != kind or layer.id != id or layer.serial != point.serial or line >= layer.rows) continue;
const bw: f32 = @floatFromInt(point.metrics.body_w);
const bh: f32 = @floatFromInt(point.metrics.body_h);
const left = @as(f32, @floatFromInt(layer.viewport.x)) * bw;
const right = @as(f32, @floatFromInt(layer.viewport.x + layer.viewport.w)) * bw;
point.pixel_x = std.math.clamp(point.pixel_x, left, @max(left, right - 0.001));
- point.pixel_y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * bh;
+ point.pixel_y = (@as(f32, @floatFromInt(layer.viewport.y + line)) + 0.5) * bh;
break;
};
return if (p.reprojectTagHit(point)) |mapped| mapped.col else null;
diff --git a/src/web.zig b/src/web.zig
index 532e3794..ac0ecbbf 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -224,13 +224,13 @@ export fn pardes_mouse_pixel(button_arg: u32, kind_arg: u32, col_arg: u32, row_a
var hit: ?pardes.Mouse.BodyHit = null;
for (&s.core.surface.body_layers) |*layer| {
const point = source orelse break;
- hit = layer.hitAt(point.x * bw, point.y * bh, bw, bh, tw, th);
+ hit = layer.bodyHit(point.x * bw, point.y * bh, bw, bh, tw, th);
if (hit != null) break;
}
var tag_hit: ?pardes.TagHit = null;
for (&s.core.surface.tag_layers) |*layer| {
const point = source orelse break;
- tag_hit = layer.hitAt(point.x * bw, point.y * bh, bw, bh, tw, th);
+ tag_hit = layer.tagHit(point.x * bw, point.y * bh, bw, bh, tw, th);
if (tag_hit != null) break;
}
mouseWithHit(button_arg, kind_arg, col_arg, row_arg, mods, hit, tag_hit);
@@ -341,6 +341,7 @@ export fn pardes_tag_layer_value(index: u32, field: u32) u32 {
const s = &(state orelse return 0);
if (index >= pardes.MAX_TAG_LAYERS) return 0;
const layer = &s.core.surface.tag_layers[index];
+ if (layer.rows == 0) return 0;
return switch (field) {
0 => layer.cols,
1 => @intFromEnum(layer.kind),
@@ -350,9 +351,12 @@ export fn pardes_tag_layer_value(index: u32, field: u32) u32 {
5 => layer.viewport.y,
6 => layer.viewport.w,
7 => layer.viewport.h,
- 8 => if (layer.cursor) |c| c.col else 65535,
+ 8 => if (layer.cursor) |c| c.x else 65535,
9 => if (layer.cursor) |c| @intFromBool(c.bar) else 0,
10 => @as(u32, layer.bg[0]) << 16 | @as(u32, layer.bg[1]) << 8 | layer.bg[2],
+ // A band has a row to each line of its tag: `cols * rows` cells.
+ 11 => layer.rows,
+ 12 => if (layer.cursor) |c| c.y else 65535,
else => 0,
};
}
@@ -361,7 +365,7 @@ export fn pardes_tag_layer_cells(index: u32) ?[*]const WebCell {
const s = &(state orelse return null);
if (index >= pardes.MAX_TAG_LAYERS) return null;
const layer = &s.core.surface.tag_layers[index];
- if (layer.cols == 0) return null;
+ if (layer.rows == 0) return null;
const cells = &s.tag_cells[index];
if (cells.len != layer.cells.len) cells.* = gpa.realloc(cells.*, layer.cells.len) catch return null;
for (layer.cells, cells.*) |source, *out| out.* = encodeWebCell(source);
diff --git a/src/web/app.mjs b/src/web/app.mjs
index 280239d5..03829178 100644
--- a/src/web/app.mjs
+++ b/src/web/app.mjs
@@ -64,13 +64,16 @@ export class DomRenderer {
if (!val(0)) continue;
const gripCols = this.wasm.pardes_grip_columns?.() ?? 2;
const textInset = this.wasm.pardes_tag_text_inset?.() ?? gripCols;
- if (val(1) !== 0 && val(4) >= textInset && this.cellHeight) {
- const offset = this.wasm.pardes_tagline_band_offset(val(5),rows*this.cellHeight*256,Math.round(this.cellHeight*256),Math.round(this.taglineHeight*256))/256;
- for (let x = val(4)-textInset; x < val(4); x++) tagPrefixBands.set(val(5)*cols+x,offset);
- if (val(1) === 2) paneIndicators.add(val(5)*cols+val(4)-textInset);
- }
- for (let x = val(4); x < Math.min(cols, val(4) + val(6)); x++) {
- if (val(5) < rows) tagCovered[val(5) * cols + x] = 1;
+ // A row to each line of a taller tag.
+ for (let y = val(5); y < val(5) + Math.max(1, val(11)); y++) {
+ if (val(1) !== 0 && val(4) >= textInset && this.cellHeight) {
+ const offset = this.wasm.pardes_tagline_band_offset(y,rows*this.cellHeight*256,Math.round(this.cellHeight*256),Math.round(this.taglineHeight*256))/256;
+ for (let x = val(4)-textInset; x < val(4); x++) tagPrefixBands.set(y*cols+x,offset);
+ if (val(1) === 2) paneIndicators.add(y*cols+val(4)-textInset);
+ }
+ for (let x = val(4); x < Math.min(cols, val(4) + val(6)); x++) {
+ if (y < rows) tagCovered[y * cols + x] = 1;
+ }
}
}
}
@@ -118,27 +121,35 @@ export class DomRenderer {
const nodes = [];
const bw = this.cellWidth, bh = this.cellHeight, tw = this.taglineWidth, th = this.taglineHeight;
for (let i = 0; i < this.wasm.pardes_tag_layer_limit(); i++) {
- const values = Array.from({length:11}, (_,field) => this.wasm.pardes_tag_layer_value(i,field));
- const cols = values[0];
+ const layerValues = Array.from({length:13}, (_,field) => this.wasm.pardes_tag_layer_value(i,field));
+ const cols = layerValues[0];
if (!cols) continue;
const ptr = this.wasm.pardes_tag_layer_cells(i);
if (!ptr) continue;
- const bytes = new Uint8Array(this.wasm.memory.buffer,ptr,cols*CELL_SIZE);
+ // A row to each line of a taller tag, each drawn as a band of its own.
+ for (let line = 0; line < Math.max(1, layerValues[11]); line++) {
+ const key = i*64+line;
+ const values = layerValues.slice(0,11);
+ values[5] = layerValues[5] + line;
+ values[7] = 1;
+ values[8] = layerValues[12] === line ? layerValues[8] : 65535;
+ const bytes = new Uint8Array(this.wasm.memory.buffer,ptr+line*cols*CELL_SIZE,cols*CELL_SIZE);
const offset = this.wasm.pardes_tagline_band_offset(values[5],this.rowCount*bh*256,Math.round(bh*256),Math.round(th*256))/256;
const gripCols = this.wasm.pardes_grip_columns?.() ?? 2;
const gripX = values[4] - (this.wasm.pardes_tag_text_inset?.() ?? gripCols);
const gripCell = canonical[values[5]*this.cols+gripX];
const grip = values[1] === 2 && gripX >= 0 && gripCell && th >= 1 && tw >= 1;
const geometry = [bw,bh,tw,th,offset,...values,grip,gripCell?.fg,gripCell?.bg,gripCell?.attrs,gripCell?.text,globalThis.devicePixelRatio].join(":");
- const cached = this.tagNodes.get(i);
+ const cached = this.tagNodes.get(key);
if (cached && cached.geometry === geometry && cached.bytes.length === bytes.length && cached.bytes.every((byte,index)=>byte===bytes[index])) {
nodes.push(cached.node);
if (cached.grip) nodes.push(cached.grip);
continue;
}
- const view = new DataView(this.wasm.memory.buffer,ptr,cols*CELL_SIZE);
+ const view = new DataView(this.wasm.memory.buffer,ptr+line*cols*CELL_SIZE,cols*CELL_SIZE);
const layer = this.root.ownerDocument.createElement("div");
layer.dataset.tagLayer = String(i);
+ layer.dataset.tagLine = String(line);
layer.style.cssText = `position:absolute;overflow:hidden;white-space:pre;left:${values[4]*bw}px;top:${values[5]*bh+offset}px;width:${values[6]*bw}px;height:${th}px;background:${color(values[10],DEFAULT_BG)}`;
for (let col = 0; col < cols; col++) {
const off = col*CELL_SIZE;
@@ -171,9 +182,10 @@ export class DomRenderer {
gripNode.append(mark);
}
}
- this.tagNodes.set(i,{geometry,bytes:bytes.slice(),node:layer,grip:gripNode});
+ this.tagNodes.set(key,{geometry,bytes:bytes.slice(),node:layer,grip:gripNode});
nodes.push(layer);
if (gripNode) nodes.push(gripNode);
+ }
}
if (nodes.length !== this.tagRoot.childNodes.length || nodes.some((node,index)=>node!==this.tagRoot.childNodes[index])) this.tagRoot.replaceChildren(...nodes);
}
diff --git a/test/column_drag.zig b/test/column_drag.zig
index feea1bf7..3aea4286 100644
--- a/test/column_drag.zig
+++ b/test/column_drag.zig
@@ -8,17 +8,17 @@ fn mouse(p: *Pardes, kind: pardes.Mouse.Kind, col: u16) void {
p.update(.{ .mouse = .{ .button = .left, .kind = kind, .col = col, .row = pardes.TOPBAR_H } });
}
-fn tagLayer(p: *Pardes, column: usize) !*pardes.TagLayer {
+fn tagLayer(p: *Pardes, column: usize) !*pardes.Layer {
for (&p.surface.tag_layers) |*layer| {
- if (layer.cols > 0 and layer.kind == .column and layer.id == column) return layer;
+ if (layer.rows > 0 and layer.kind == .column and layer.id == column) return layer;
}
return error.MissingColumnTagLayer;
}
-fn headerMouse(layer: *const pardes.TagLayer, display_col: u16, kind: pardes.Mouse.Kind) pardes.Mouse {
+fn headerMouse(layer: *const pardes.Layer, display_col: u16, kind: pardes.Mouse.Kind) pardes.Mouse {
const x = @as(f32, @floatFromInt(layer.viewport.x)) * 10 + (@as(f32, @floatFromInt(display_col)) + 0.5) * 5;
const y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * 20;
- return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .tag_hit = layer.hitAt(x, y, 10, 20, 5, 10) };
+ return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .tag_hit = layer.tagHit(x, y, 10, 20, 5, 10) };
}
const Snapshot = struct {
@@ -144,7 +144,7 @@ test "column grip is separate from compact text and the resize seam" {
try std.testing.expectEqual(p.col_x[column], rect.x);
try std.testing.expectEqual(rect.x + pardes.TAG_TEXT_INSET, (try tagLayer(p, column)).viewport.x);
for (p.surface.tagLayers()) |pane_layer| {
- if (pane_layer.cols > 0 and pane_layer.kind == .pane and pane_layer.id == pane_id)
+ if (pane_layer.rows > 0 and pane_layer.kind == .pane and pane_layer.id == pane_id)
try std.testing.expectEqual((try tagLayer(p, column)).viewport.x, pane_layer.viewport.x);
}
const pane_tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
diff --git a/test/panes.zig b/test/panes.zig
index 11fb750a..5e26cdd0 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -19,17 +19,17 @@ fn setTag(pane: *Pane, text: []const u8) !void {
}
const TagLayerTests = struct {
- const Kind = @FieldType(pardes.TagLayer, "kind");
- fn findLayer(p: *Pardes, kind: Kind, id: usize) !*pardes.TagLayer {
+ const Kind = @FieldType(pardes.Layer, "kind");
+ fn findLayer(p: *Pardes, kind: Kind, id: usize) !*pardes.Layer {
for (&p.surface.tag_layers) |*layer| {
- if (layer.cols > 0 and layer.kind == kind and layer.id == id) return layer;
+ if (layer.rows > 0 and layer.kind == kind and layer.id == id) return layer;
}
return error.MissingTagLayer;
}
- fn mouse(layer: *const pardes.TagLayer, x: f32, kind: pardes.Mouse.Kind) pardes.Mouse {
+ fn mouse(layer: *const pardes.Layer, x: f32, kind: pardes.Mouse.Kind) pardes.Mouse {
const y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * 20;
- return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .tag_hit = layer.hitAt(x, y, 10, 20, 5, 10) };
+ return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .tag_hit = layer.tagHit(x, y, 10, 20, 5, 10) };
}
test "compact tag layers render and edit Unicode text beyond body column capacity" {
@@ -200,14 +200,16 @@ test "a tag with a newline in it is as tall as its lines, and each line is typed
const body_y = p.bodyTop(pane, r);
try std.testing.expectEqual(if (bottom) r.y else r.y + 2, body_y);
try std.testing.expectEqualStrings("b", surface.at(r.x + config.GUTTER + panes.File.gutterWidth(pane), body_y).grapheme());
- // a layer to a line, for a pixel shell
- var lines: usize = 0;
+ // one layer, a row to each line, for a pixel shell
+ var layers: usize = 0;
for (p.surface.tagLayers()) |*layer| {
- if (layer.cols == 0 or layer.kind != .pane or layer.id != 0) continue;
- try std.testing.expectEqual(tag_y + layer.line, layer.viewport.y);
- lines += 1;
+ if (layer.rows == 0 or layer.kind != .pane or layer.id != 0) continue;
+ try std.testing.expectEqual(tag_y, layer.viewport.y);
+ try std.testing.expectEqual(@as(u16, 2), layer.rows);
+ try std.testing.expectEqual(@as(u16, 2), layer.viewport.h);
+ layers += 1;
}
- try std.testing.expectEqual(@as(usize, 2), lines);
+ try std.testing.expectEqual(@as(usize, 1), layers);
// a click on the second line puts the tag's cursor on it
p.update(.{ .key = .{ .cp = Key.escape } });
p.update(.{ .key = .{ .cp = ':' } });
@@ -541,11 +543,11 @@ const TagNavigationTests = struct {
};
const BodyHitTests = struct {
- fn mouse(layer: *const pardes.BodyLayer, row: u16, col: u16, kind: pardes.Mouse.Kind) pardes.Mouse {
+ fn mouse(layer: *const pardes.Layer, row: u16, col: u16, kind: pardes.Mouse.Kind) pardes.Mouse {
const width: f32 = if (row < layer.context_rows) 8 else 10;
const x = @as(f32, @floatFromInt(layer.viewport.x)) * 10 + (@as(f32, @floatFromInt(col)) + 0.5) * width;
const y = layer.rowTop(row, 20, 10) + layer.rowHeight(row, 20, 10) / 2;
- return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .body_hit = layer.hitAt(x, y, 10, 20, 8, 10).? };
+ return .{ .button = .left, .kind = kind, .col = @intFromFloat(x / 10), .row = @intFromFloat(y / 20), .body_hit = layer.bodyHit(x, y, 10, 20, 8, 10).? };
}
test "compact TreeContext mouse follows source rows in reclaimed space" {
@@ -673,7 +675,7 @@ const TreeContextTests = struct {
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);
+ try std.testing.expectEqual(row, layer.bodyHit(x, y, 10, 20, 8, 10).?.row);
}
pane.scrollBy(-5);
_ = try p.render(frame.allocator());