lib/ui/src/abi/envelope.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const scalar = @import("scalar.zig");
4
5 const Rect = scalar.Rect;
6 const Span = scalar.Span;
7
8 /// `magic` is 0x31495554. Its four bytes read "TUI1".
9 /// A publish whose header carries another value is rejected with `error.MagicMismatch`.
10 pub const magic: u32 = 0x31_49_55_54;
11
12 /// `abi_version` is 1. A publish whose header carries another value is rejected with
13 /// `error.AbiVersionMismatch`.
14 pub const abi_version: u32 = 1;
15
16 /// `Header` is the 152 byte prefix of every publish buffer.
17 /// Each `Span` field locates one table in the same buffer.
18 pub const Header = extern struct {
19 magic: u32 = magic,
20 abi_version: u32 = abi_version,
21 /// `Header.buffer_bytes` must equal the length of the slice passed to publish,
22 /// or the publish is rejected with `error.BufferLengthMismatch`.
23 buffer_bytes: u32 = 0,
24 /// `Header.node_bytes` must equal the size of `Node` in this build, or the publish is
25 /// rejected with `error.NodeSizeMismatch`.
26 node_bytes: u32 = @sizeOf(Node),
27 /// A publish whose header revision equals the retained revision is accepted after reading
28 /// only the header and dirties nothing.
29 revision: u64 = 0,
30 /// A node flagged `retained` is admitted only when this field equals the revision of the
31 /// tree the store holds, otherwise `error.StaleRetainedSubtree`.
32 base_revision: u64 = 0,
33 viewport: Rect = .{},
34 /// `Header.root_font_size` is layout units per rem.
35 root_font_size: f32 = 0,
36 /// `Header.unit_scale` is informational and never a cascade input.
37 unit_scale: f32 = 0,
38 nodes: Span = .{},
39 declarations: Span = .{},
40 classes: Span = .{},
41 texts: Span = .{},
42 runs: Span = .{},
43 relations: Span = .{},
44 atoms: Span = .{},
45 strings: Span = .{},
46 solved_roots: Span = .{},
47 solved_rects: Span = .{},
48 reserved: [2]u64 = .{ 0, 0 },
49 };
50
51 /// `Node` is one 64 byte record. Nodes sit in one flat array in preorder.
52 pub const Node = extern struct {
53 /// `Node.id` must be unique within one publish, or `error.DuplicateNodeId`.
54 /// The splice finds a retained node by this id.
55 id: u64 = 0,
56 /// `Node.revision` stamps the content of the node's whole subtree.
57 /// The splice compares `Node.revision` with the revision of the retained node
58 /// that has the same `id`. The comparison is for equality only, never for order.
59 /// A producer that changes a descendant must also change the revision of every ancestor.
60 revision: u64 = 0,
61 /// `Node.parent` is the array index of the parent. The root stores 0, or the publish is
62 /// rejected with `error.RootParentMismatch`. The parent must be the nearest enclosing
63 /// ancestor, or `error.ParentNotAncestor`.
64 parent: u32 = 0,
65 /// `Node.subtree_count` is the number of descendants.
66 /// They occupy the next `subtree_count` entries of the array.
67 subtree_count: u32 = 0,
68 /// `Node.identifier` is an index into the atom table, and 0 means absent.
69 identifier: u32 = 0,
70 declaration_first: u32 = 0,
71 /// `Node.class_first` is the index of the node's first entry in the class table.
72 /// Each entry is an atom index.
73 class_first: u32 = 0,
74 /// `Node.text` is an index into the text table, and 0 means absent.
75 text: u32 = 0,
76 /// `Node.name` is an index into the atom table, and 0 means absent.
77 name: u32 = 0,
78 /// `Node.action` is an index into the atom table, and 0 means absent.
79 action: u32 = 0,
80 declaration_count: u16 = 0,
81 /// `Node.class_count` is the number of the node's entries in the class table.
82 class_count: u16 = 0,
83 /// `Node.kind` holds a `Kind`.
84 kind: u16 = 0,
85 /// `Node.role` holds a `Role`.
86 role: u16 = 0,
87 /// `Node.flags` holds the bits of `Flag`.
88 flags: u16 = 0,
89 focus_order: u16 = 0,
90 /// `Node.state` holds the bits of `State`.
91 state: u32 = 0,
92 };
93
94 /// `TextRecord.content` names a UTF-8 atom. The admission survey checks its
95 /// full content before layout and returns `TextNotUtf8` for malformed encoding,
96 /// including overlong, lone continuation, and truncated sequences.
97 /// `run_first` and `run_count` locate its runs.
98 pub const TextRecord = extern struct {
99 content: u32 = 0,
100 run_first: u32 = 0,
101 run_count: u32 = 0,
102 flags: u32 = 0,
103 };
104
105 /// A `TextRun` is checked in the admission survey. `byte_start` and `byte_end`
106 /// are offsets into the UTF-8 bytes of its `TextRecord.content` atom: the start
107 /// is inclusive and the end exclusive. A run must be nonempty (`TextRunEmpty`)
108 /// and fit inside those bytes (`TextRunOutOfBounds`). Runs for one text record
109 /// are sorted by start (`TextRunsUnordered`) and cannot overlap
110 /// (`TextRunsOverlap`). Gaps are accepted and use the node's computed style.
111 ///
112 /// The survey validates the record's UTF-8 content before checking runs, but
113 /// both endpoints must still be scalar boundaries: a continuation byte is not
114 /// an endpoint (`TextRunSplitsScalar`). A run may end inside a grapheme
115 /// cluster, and that cluster takes the style of the run holding its first byte.
116 ///
117 /// `declaration_first` and `declaration_count` locate declarations in the
118 /// publish declaration table. The allowed properties are `background-color`;
119 /// each border side's color, width, and style (`border-top-color`,
120 /// `border-top-width`, `border-top-style`, `border-right-color`,
121 /// `border-right-width`, `border-right-style`, `border-bottom-color`,
122 /// `border-bottom-width`, `border-bottom-style`, `border-left-color`,
123 /// `border-left-width`, `border-left-style`); each border corner's radius
124 /// (`border-top-left-radius`, `border-top-right-radius`,
125 /// `border-bottom-right-radius`, `border-bottom-left-radius`); `color`,
126 /// `opacity`, `outline-color`, `outline-style`, `outline-width`,
127 /// `outline-offset`, `font-family`, `font-size`, `font-style`, `font-weight`,
128 /// `letter-spacing`, `line-height`, `word-spacing`, `text-decoration-color`,
129 /// `text-decoration-line`, `text-decoration-style`,
130 /// `text-decoration-thickness`, `text-transform`, `text-underline-offset`, and
131 /// `user-select`. All others, including `text-align`, `text-indent`,
132 /// `white-space`, `word-break`, `overflow-wrap`, and `display`, are rejected
133 /// (`TextRunPropertyNotAllowed`).
134 pub const TextRun = extern struct {
135 byte_start: u32 = 0,
136 byte_end: u32 = 0,
137 declaration_first: u32 = 0,
138 declaration_count: u32 = 0,
139 };
140
141 /// `Relation` rows must be sorted by `subject`, or `error.RelationsUnsorted`.
142 /// `subject` and `object` are node indices below the node count, or
143 /// `error.RelationNodeOutOfBounds`.
144 pub const Relation = extern struct {
145 subject: u32 = 0,
146 object: u32 = 0,
147 kind: u8 = 0,
148 _pad: [3]u8 = .{ 0, 0, 0 },
149 };
150
151 /// A solved root names a node by its `node` array index, and the admission survey rejects a
152 /// `node` index at or past the node count with `error.SolvedRootNodeOutOfBounds`.
153 /// Solved roots must be sorted and must not overlap, or `error.SolvedRootsUnordered`.
154 /// Every node with `flags.solved` requires exactly one matching `SolvedRoot` entry, and
155 /// admission returns `error.SolvedNodeWithoutRoot` when one is missing.
156 /// Admission rejects a retained subtree that contains a solved node with
157 /// `error.RetainedSolvedSubtree`, because splice does not carry solved rectangles from
158 /// the old publish.
159 ///
160 /// A `SolvedRoot` points to the first rectangle for one contiguous preorder subtree through
161 /// `rect_first`. A node carrying `flags.solved` names the subtree: its descendants remain
162 /// real UI nodes for hit testing, facts, and accessibility.
163 ///
164 /// The supplied rectangles are in the publisher's own frame, in layout units, because
165 /// their absolute positions in the UI viewport are not known at publish time. The root
166 /// rectangle's width and height are the size arrange uses when it treats the solved
167 /// subtree as one leaf, and its x and y define the publisher frame's origin for the
168 /// subtree. After arrange places that leaf, each descendant moves by the difference
169 /// between the arranged root origin and the supplied root origin. Every supplied width,
170 /// height, and relative offset remains the same.
171 ///
172 /// A descendant can have a negative relative offset or extend beyond the root box: this is
173 /// visible overflow and belongs in subtree bounds. Scrolling uses the descendant's own
174 /// scroll and clip flags. Supplied rectangles are positions before scroll offset is applied.
175 pub const SolvedRoot = extern struct {
176 /// `SolvedRoot.node` must carry the `solved` flag, or `error.SolvedRootNotSolved`.
177 node: u32 = 0,
178 /// `rect_first` is the first of `subtree_count + 1` rects, where `subtree_count` is that
179 /// node's, and they must fit the rect table, or `error.SolvedRectsOutOfBounds`.
180 rect_first: u32 = 0,
181 };
182
183 /// `text_absent` is 0. Entry 0 of the text table is reserved and never dereferenced.
184 /// `View.textOf` returns null for it, and the survey reads a text record only for a
185 /// nonzero index.
186 pub const text_absent: u32 = 0;
187
188 pub const header_bytes: u32 = @sizeOf(Header);
189
190 comptime {
191 std.debug.assert(@sizeOf(Header) == 152);
192 std.debug.assert(@alignOf(Header) == 8);
193 std.debug.assert(@offsetOf(Header, "magic") == 0);
194 std.debug.assert(@offsetOf(Header, "abi_version") == 4);
195 std.debug.assert(@offsetOf(Header, "buffer_bytes") == 8);
196 std.debug.assert(@offsetOf(Header, "node_bytes") == 12);
197 std.debug.assert(@offsetOf(Header, "revision") == 16);
198 std.debug.assert(@offsetOf(Header, "base_revision") == 24);
199 std.debug.assert(@offsetOf(Header, "viewport") == 32);
200 std.debug.assert(@offsetOf(Header, "root_font_size") == 48);
201 std.debug.assert(@offsetOf(Header, "unit_scale") == 52);
202 std.debug.assert(@offsetOf(Header, "nodes") == 56);
203 std.debug.assert(@offsetOf(Header, "declarations") == 64);
204 std.debug.assert(@offsetOf(Header, "classes") == 72);
205 std.debug.assert(@offsetOf(Header, "texts") == 80);
206 std.debug.assert(@offsetOf(Header, "runs") == 88);
207 std.debug.assert(@offsetOf(Header, "relations") == 96);
208 std.debug.assert(@offsetOf(Header, "atoms") == 104);
209 std.debug.assert(@offsetOf(Header, "strings") == 112);
210 std.debug.assert(@offsetOf(Header, "solved_roots") == 120);
211 std.debug.assert(@offsetOf(Header, "solved_rects") == 128);
212 std.debug.assert(@offsetOf(Header, "reserved") == 136);
213 }
214
215 comptime {
216 std.debug.assert(@sizeOf(Node) == 64);
217 std.debug.assert(@alignOf(Node) == 8);
218 std.debug.assert(@offsetOf(Node, "id") == 0);
219 std.debug.assert(@offsetOf(Node, "revision") == 8);
220 std.debug.assert(@offsetOf(Node, "parent") == 16);
221 std.debug.assert(@offsetOf(Node, "subtree_count") == 20);
222 std.debug.assert(@offsetOf(Node, "identifier") == 24);
223 std.debug.assert(@offsetOf(Node, "declaration_first") == 28);
224 std.debug.assert(@offsetOf(Node, "class_first") == 32);
225 std.debug.assert(@offsetOf(Node, "text") == 36);
226 std.debug.assert(@offsetOf(Node, "name") == 40);
227 std.debug.assert(@offsetOf(Node, "action") == 44);
228 std.debug.assert(@offsetOf(Node, "declaration_count") == 48);
229 std.debug.assert(@offsetOf(Node, "class_count") == 50);
230 std.debug.assert(@offsetOf(Node, "kind") == 52);
231 std.debug.assert(@offsetOf(Node, "role") == 54);
232 std.debug.assert(@offsetOf(Node, "flags") == 56);
233 std.debug.assert(@offsetOf(Node, "focus_order") == 58);
234 std.debug.assert(@offsetOf(Node, "state") == 60);
235 std.debug.assert(!@hasField(Node, "first_child"));
236 std.debug.assert(!@hasField(Node, "child_count"));
237 }
238
239 comptime {
240 std.debug.assert(@sizeOf(TextRecord) == 16);
241 std.debug.assert(@alignOf(TextRecord) == 4);
242 std.debug.assert(@offsetOf(TextRecord, "run_first") == 4);
243 std.debug.assert(@offsetOf(TextRecord, "flags") == 12);
244 std.debug.assert(@sizeOf(TextRun) == 16);
245 std.debug.assert(@alignOf(TextRun) == 4);
246 std.debug.assert(@offsetOf(TextRun, "declaration_first") == 8);
247 std.debug.assert(@sizeOf(Relation) == 12);
248 std.debug.assert(@alignOf(Relation) == 4);
249 std.debug.assert(@offsetOf(Relation, "kind") == 8);
250 std.debug.assert(@sizeOf(SolvedRoot) == 8);
251 std.debug.assert(@alignOf(SolvedRoot) == 4);
252 std.debug.assert(@offsetOf(SolvedRoot, "rect_first") == 4);
253 std.debug.assert(header_bytes == 152);
254 }
255
256 test "a default header names this abi and the node size it was compiled against" {
257 const header = Header{};
258 try std.testing.expectEqual(@as(u32, 0x31_49_55_54), header.magic);
259 try std.testing.expectEqual(@as(u32, 1), header.abi_version);
260 try std.testing.expectEqual(@as(u32, 64), header.node_bytes);
261 try std.testing.expectEqual(@as(u32, @sizeOf(Node)), header.node_bytes);
262 try std.testing.expectEqual(@as(u64, 0), header.revision);
263 try std.testing.expectEqual(@as(u32, 0), header.nodes.count);
264 try std.testing.expectEqual([2]u64{ 0, 0 }, header.reserved);
265 }
266
267 test "the magic word spells the four ascii bytes of this envelope" {
268 const bytes = std.mem.toBytes(magic);
269 try std.testing.expectEqualStrings("TUI1", &bytes);
270 }
271
272 test "every envelope record defaults to the absent encoding" {
273 try std.testing.expectEqual(@as(u32, 0), (Node{}).identifier);
274 try std.testing.expectEqual(@as(u32, 0), (Node{}).text);
275 try std.testing.expectEqual(@as(u32, 0), (TextRecord{}).content);
276 try std.testing.expectEqual(@as(u32, 0), (TextRun{}).byte_start);
277 try std.testing.expectEqual([3]u8{ 0, 0, 0 }, (Relation{})._pad);
278 try std.testing.expectEqual(@as(u32, 0), (SolvedRoot{}).rect_first);
279 }
280
281 test "one node occupies exactly one cache line" {
282 try std.testing.expectEqual(@as(usize, 64), @sizeOf(Node));
283 try std.testing.expectEqual(@as(usize, 1_048_576), 16_384 * @sizeOf(Node));
284 }