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 }