lib/ui/src/style/element.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const css = @import("css");
3 const abi = @import("../abi/root.zig");
4 const tree = @import("../tree/root.zig");
5
6 pub const Interaction = struct {
7 hovered: u64 = 0,
8 focused: u64 = 0,
9 active: u64 = 0,
10 keyboard_focus: bool = false,
11 };
12
13 pub const Adapter = struct {
14 view: tree.View,
15 sheet: *const css.StyleSheet,
16 interaction: Interaction = .{},
17 hovered_index: ?u32 = null,
18 focused_index: ?u32 = null,
19 active_index: ?u32 = null,
20 resolved: bool = false,
21 ancestor_steps: u64 = 0,
22
23 pub fn element(self: *Adapter) css.match.Element {
24 if (!self.resolved) {
25 self.hovered_index = self.find(self.interaction.hovered);
26 self.focused_index = self.find(self.interaction.focused);
27 self.active_index = self.find(self.interaction.active);
28 self.resolved = true;
29 }
30 return .{
31 .context = self,
32 .parent = parent,
33 .previous = previous,
34 .kind = kind,
35 .role = role,
36 .identifier = identifier,
37 .classes = classes,
38 .pseudo = pseudo,
39 .sibling_index = siblingIndex,
40 };
41 }
42
43 fn selfOf(context: ?*anyopaque) *Adapter {
44 return @ptrCast(@alignCast(context.?));
45 }
46
47 fn parent(context: ?*anyopaque, index: u32) callconv(.c) u32 {
48 const self = selfOf(context);
49 self.ancestor_steps += 1;
50 std.debug.assert(index < self.view.nodes.len);
51 return self.view.nodes[index].parent;
52 }
53
54 fn previous(context: ?*anyopaque, index: u32) callconv(.c) u32 {
55 const self = selfOf(context);
56 self.ancestor_steps += 1;
57 std.debug.assert(index < self.view.nodes.len);
58 if (index == 0) return index;
59 const wanted = self.view.nodes[index].parent;
60 var candidate = index;
61 while (candidate > 0) {
62 candidate -= 1;
63 if (self.view.nodes[candidate].parent == wanted and candidate != wanted and
64 candidate + self.view.nodes[candidate].subtree_count < index) return candidate;
65 if (candidate == wanted) break;
66 }
67 return index;
68 }
69
70 fn kind(context: ?*anyopaque, index: u32) callconv(.c) u16 {
71 const self = selfOf(context);
72 const value: abi.Kind = @fromBackingInt(@intCast(self.view.nodes[index].kind));
73 return @intCast(self.sheet.token(@tagName(value)));
74 }
75
76 fn role(context: ?*anyopaque, index: u32) callconv(.c) u16 {
77 const self = selfOf(context);
78 const value: abi.Role = @fromBackingInt(@intCast(self.view.nodes[index].role));
79 return @intCast(self.sheet.token(@tagName(value)));
80 }
81
82 fn identifier(context: ?*anyopaque, index: u32) callconv(.c) u32 {
83 const self = selfOf(context);
84 return self.sheet.token(self.view.text(self.view.nodes[index].identifier));
85 }
86
87 fn classes(context: ?*anyopaque, index: u32, out: [*]u32, capacity: u32) callconv(.c) u32 {
88 const self = selfOf(context);
89 const source = self.view.classesOf(index);
90 std.debug.assert(source.len <= capacity);
91 for (source, 0..) |atom, at| out[at] = self.sheet.token(self.view.text(atom));
92 return @intCast(source.len);
93 }
94
95 fn hasAncestor(self: *const Adapter, index: u32, start: ?u32) bool {
96 var candidate = start orelse return false;
97 while (true) {
98 if (candidate == index) return true;
99 if (candidate == 0) return false;
100 candidate = self.view.nodes[candidate].parent;
101 }
102 }
103
104 fn pseudo(context: ?*anyopaque, index: u32) callconv(.c) u32 {
105 const self = selfOf(context);
106 const node = self.view.nodes[index];
107 var word: u32 = 0;
108 inline for (@typeInfo(abi.State).@"enum".field_names, 0..) |name, at| {
109 const state: abi.State = @fromBackingInt(@intCast(at));
110 const class = @field(css.selector.PseudoClass, name);
111 if (abi.active(node.state, state)) word |= css.selector.bit(class);
112 }
113 if (index == 0) word |= css.selector.bit(.root);
114 if (node.subtree_count == 0 and node.text == abi.text_absent) word |= css.selector.bit(.empty);
115 if (!abi.active(node.state, .disabled)) word |= css.selector.bit(.enabled);
116 if (abi.holds(node.flags, .modal)) word |= css.selector.bit(.modal);
117 if (self.hasAncestor(index, self.hovered_index)) word |= css.selector.bit(.hover);
118 if (self.hasAncestor(index, self.active_index)) word |= css.selector.bit(.active);
119 if (self.view.nodes[index].id == self.interaction.focused and self.interaction.focused != 0) {
120 word |= css.selector.bit(.focus);
121 if (self.interaction.keyboard_focus) word |= css.selector.bit(.focus_visible);
122 }
123 if (self.focused_index) |focused| {
124 if (self.isAncestor(index, focused)) word |= css.selector.bit(.focus_within);
125 }
126 return word;
127 }
128
129 pub fn find(self: *const Adapter, id: u64) ?u32 {
130 if (id == 0) return null;
131 if (self.view.identity) |identity| return identity.lookup(self.view.nodes, id);
132 for (self.view.nodes, 0..) |node, at| if (node.id == id) return @intCast(at);
133 return null;
134 }
135
136 fn isAncestor(self: *const Adapter, ancestor: u32, child: u32) bool {
137 var index = child;
138 while (true) {
139 if (index == ancestor) return true;
140 if (index == 0) return false;
141 index = self.view.nodes[index].parent;
142 }
143 }
144
145 fn siblingIndex(context: ?*anyopaque, index: u32, of_type: bool) callconv(.c) u32 {
146 const self = selfOf(context);
147 const node = self.view.nodes[index];
148 var first: u32 = if (index == 0) 0 else node.parent + 1;
149 var ordinal: u32 = 0;
150 var total: u32 = 0;
151 while (first < self.view.nodes.len and
152 (index == 0 or first <= node.parent + self.view.nodes[node.parent].subtree_count))
153 {
154 const candidate = self.view.nodes[first];
155 if (!of_type or candidate.kind == node.kind) {
156 total += 1;
157 if (first <= index) ordinal = total;
158 }
159 first += candidate.subtree_count + 1;
160 }
161 return css.match.packSibling(ordinal, total);
162 }
163 };