lib/ui/src/style/share.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const css = @import("css");
3 const computed = @import("computed.zig");
4 const asset = @import("../asset/root.zig");
5
6 pub const Key = extern struct {
7 matched_rules: u32,
8 inline_first: u32,
9 inline_count: u32,
10 parent_style: u32,
11 };
12
13 pub const KeySlot = struct {
14 key: Key = .{ .matched_rules = 0, .inline_first = 0, .inline_count = 0, .parent_style = 0 },
15 style: u32 = 0,
16 matched: [4]u64 = @splat(0),
17 matched_count: u8 = 0,
18 };
19
20 comptime {
21 std.debug.assert(@sizeOf(Key) == 16);
22 std.debug.assert(@alignOf(Key) == 4);
23 std.debug.assert(@offsetOf(Key, "parent_style") == 12);
24 }
25
26 pub const Pool = struct {
27 records: []computed.Record,
28 slots: []u32,
29 keys: []KeySlot = &.{},
30 chains: []asset.AssetHandle = &.{},
31 font_faces: u32 = 0,
32 used: u32 = 0,
33
34 pub fn reset(self: *Pool) void {
35 self.used = 0;
36 @memset(self.slots, 0);
37 for (self.keys) |*slot| slot.* = .{};
38 }
39
40 pub fn truncate(self: *Pool, retained: u32) void {
41 std.debug.assert(retained <= self.used);
42 self.used = retained;
43 @memset(self.slots, 0);
44 for (self.keys) |*slot| slot.* = .{};
45 const mask = self.slots.len - 1;
46 for (self.records[0..retained], 0..) |*record, index| {
47 const hash = std.hash.Wyhash.hash(0, std.mem.asBytes(&record.computed));
48 var at: usize = @intCast(hash & @as(u64, @intCast(mask)));
49 while (self.slots[at] != 0) at = (at + 1) & mask;
50 self.slots[at] = @intCast(index + 1);
51 }
52 }
53
54 fn sameMatches(slot: KeySlot, matches: []const css.match.Match) bool {
55 if (matches.len != slot.matched_count) return false;
56 for (matches, 0..) |item, at| {
57 const identity = (@as(u64, item.rule) << 32) | item.selector;
58 if (slot.matched[at] != identity) return false;
59 }
60 return true;
61 }
62
63 fn remember(slot: *KeySlot, key: Key, matches: []const css.match.Match, style: u32) void {
64 slot.* = .{ .key = key, .style = style, .matched_count = @intCast(matches.len) };
65 for (matches, 0..) |item, at| {
66 slot.matched[at] = (@as(u64, item.rule) << 32) | item.selector;
67 }
68 }
69
70 pub fn lookup(self: *const Pool, key: Key, matches: []const css.match.Match) ?u32 {
71 if (self.keys.len == 0 or matches.len > 4) return null;
72 const key_hash = std.hash.Wyhash.hash(0, std.mem.asBytes(&key));
73 const mask = self.keys.len - 1;
74 var at: usize = @intCast(key_hash & @as(u64, @intCast(mask)));
75 var probe: usize = 0;
76 while (probe < self.keys.len) : (probe += 1) {
77 const slot = self.keys[at];
78 if (slot.style == 0) return null;
79 if (std.meta.eql(slot.key, key) and sameMatches(slot, matches)) return slot.style;
80 at = (at + 1) & mask;
81 }
82 return null;
83 }
84
85 pub fn intern(self: *Pool, key: Key, matches: []const css.match.Match, style: css.cascade.Computed, metrics: computed.Metrics, fonts: ?computed.FontSource) error{DistinctStyleQuota}!u32 {
86 var key_slot: ?*KeySlot = null;
87 if (self.keys.len != 0 and matches.len <= 4) {
88 std.debug.assert(std.math.isPowerOfTwo(self.keys.len));
89 const key_hash = std.hash.Wyhash.hash(0, std.mem.asBytes(&key));
90 const key_mask = self.keys.len - 1;
91 var key_at: usize = @intCast(key_hash & @as(u64, @intCast(key_mask)));
92 var key_probe: usize = 0;
93 while (key_probe < self.keys.len) : (key_probe += 1) {
94 const slot = &self.keys[key_at];
95 if (slot.style == 0) {
96 key_slot = slot;
97 break;
98 }
99 if (std.meta.eql(slot.key, key) and sameMatches(slot.*, matches) and std.mem.eql(u8, std.mem.asBytes(&self.records[slot.style - 1].computed), std.mem.asBytes(&style))) return slot.style;
100 key_at = (key_at + 1) & key_mask;
101 }
102 }
103 std.debug.assert(std.math.isPowerOfTwo(self.slots.len));
104 const hash = std.hash.Wyhash.hash(0, std.mem.asBytes(&style));
105 const mask = self.slots.len - 1;
106 var probe: usize = 0;
107 var at: usize = @intCast(hash & @as(u64, @intCast(mask)));
108 while (probe < self.slots.len) : (probe += 1) {
109 const id = self.slots[at];
110 if (id == 0) {
111 if (self.used == self.records.len) return error.DistinctStyleQuota;
112 const font_environment: ?computed.FontEnvironment = if (fonts) |source| blk: {
113 std.debug.assert(source.registry.font_used <= self.font_faces);
114 const first = @as(usize, self.used) * self.font_faces;
115 break :blk .{
116 .source = source,
117 .fallback_out = self.chains[first..][0..self.font_faces],
118 .first = @intCast(first),
119 };
120 } else null;
121 self.used += 1;
122 self.records[self.used - 1] = computed.lower(style, metrics, font_environment);
123 self.slots[at] = self.used;
124 if (key_slot) |slot| remember(slot, key, matches, self.used);
125 return self.used;
126 }
127 if (std.mem.eql(u8, std.mem.asBytes(&self.records[id - 1].computed), std.mem.asBytes(&style))) {
128 if (key_slot) |slot| remember(slot, key, matches, id);
129 return id;
130 }
131 at = (at + 1) & mask;
132 }
133 return error.DistinctStyleQuota;
134 }
135
136 pub fn get(self: *const Pool, id: u32) *const computed.Record {
137 std.debug.assert(id > 0);
138 std.debug.assert(id <= self.used);
139 return &self.records[id - 1];
140 }
141 };
142
143 test "style sharing interns equal independent cascade results" {
144 var records: [3]computed.Record = undefined;
145 var slots: [8]u32 = @splat(0);
146 var pool = Pool{ .records = &records, .slots = &slots };
147 const initial = css.cascade.Computed.initial();
148 const left = try pool.intern(.{ .matched_rules = 1, .inline_first = 0, .inline_count = 0, .parent_style = 0 }, &.{}, initial, .{}, null);
149 const right = try pool.intern(.{ .matched_rules = 2, .inline_first = 1, .inline_count = 0, .parent_style = 0 }, &.{}, initial, .{}, null);
150 try std.testing.expectEqual(left, right);
151 var changed = initial;
152 changed.set(.color, css.value.Value.color(0xFF0000FF));
153 const other = try pool.intern(.{ .matched_rules = 2, .inline_first = 1, .inline_count = 1, .parent_style = 0 }, &.{}, changed, .{}, null);
154 try std.testing.expect(other != left);
155 try std.testing.expectEqual(@as(u32, 2), pool.used);
156 }