tiny.ui.style.generation
Defined in style.
API (25)
Actions
Public operations.
Capacity.deriveEngine.activateEngine.authoredFallbacksEngine.cascadeEngine.cascadeViewEngine.deinitEngine.initEngine.recordEngine.setFonts: The registry outlives the engine.Engine.styleEpoch: Returns a u64 counter that advances only when a full cascade flips the computed-style pool and may renumber IDs: a fast cascade that appends in the current pool keeps the epoch.Engine.styleId: Returns the interned computed Record ID for an admitted node.
Types and contracts
Public types and contracts.
CapacityEngineEngine.CapacityEngine.ExhaustionEngine.InitErrorEngine.LimitsEngine.StorageErrorExhaustionLimitsResult
Values and defaults
Public values and defaults.
Source
Source: lib/ui/src/style/generation.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const css = @import("css");const abi = @import("../abi/root.zig");const tree = @import("../tree/root.zig");const asset = @import("../asset/root.zig");const computed = @import("computed.zig");const element = @import("element.zig");const share = @import("share.zig");const Allocator = std.mem.Allocator;pub const Limits = struct { nodes: u32 = 16_384, distinct_styles: u32 = 224, selectors: u32 = 512, font_faces: u32 = 32, bytes: usize = 2_097_152,};pub const Capacity = struct { required_bytes: usize, total_bytes: usize, storage_bytes: usize, pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity { const nodes = std.math.mul(usize, limits.nodes, 2 * @sizeOf(NodeState)) catch return error.CapacityOverflow; const records = std.math.mul(usize, limits.distinct_styles, 2 * @sizeOf(computed.Record)) catch return error.CapacityOverflow; const chain_slots = std.math.mul(usize, limits.distinct_styles, limits.font_faces) catch return error.CapacityOverflow; if (chain_slots > std.math.maxInt(u32)) return error.CapacityOverflow; const chains = std.math.mul(usize, chain_slots, 2 * @sizeOf(asset.AssetHandle)) catch return error.CapacityOverflow; const slots = std.math.mul(usize, std.math.ceilPowerOfTwo(usize, @as(usize, limits.distinct_styles) * 2) catch return error.CapacityOverflow, 2 * @sizeOf(u32)) catch return error.CapacityOverflow; const key_slots = std.math.mul(usize, std.math.ceilPowerOfTwo(usize, limits.distinct_styles) catch return error.CapacityOverflow, 2 * @sizeOf(share.KeySlot)) catch return error.CapacityOverflow; const entries = std.math.mul(usize, limits.selectors, @sizeOf(css.bucket.Entry) + @sizeOf(css.match.Match)) catch return error.CapacityOverflow; const flags = @as(usize, limits.nodes) + @as(usize, limits.distinct_styles) * @sizeOf(u32); const word_count = std.math.divCeil(usize, limits.nodes, 64) catch return error.CapacityOverflow; const key_words = std.math.mul(usize, word_count, (2 * key_columns + 1) * @sizeOf(u64)) catch return error.CapacityOverflow; const node_keys = std.math.mul(usize, limits.nodes, 2 * @sizeOf(u16)) catch return error.CapacityOverflow; const sum = nodes; const sum2 = std.math.add(usize, sum, records) catch return error.CapacityOverflow; const sum3 = std.math.add(usize, sum2, slots) catch return error.CapacityOverflow; const sum4 = std.math.add(usize, sum3, entries) catch return error.CapacityOverflow; const sum5 = std.math.add(usize, sum4, key_slots) catch return error.CapacityOverflow; const tail = std.math.add(usize, flags, key_words) catch return error.CapacityOverflow; const tail_with_keys = std.math.add(usize, tail, node_keys) catch return error.CapacityOverflow; const tail_with_chains = std.math.add(usize, tail_with_keys, chains) catch return error.CapacityOverflow; const tail_with_alignment = std.math.add(usize, tail_with_chains, 80) catch return error.CapacityOverflow; const required = std.math.add(usize, sum5, tail_with_alignment) catch return error.CapacityOverflow; if (required > limits.bytes) return error.CapacityOverflow; return .{ .required_bytes = required, .total_bytes = limits.bytes, .storage_bytes = limits.bytes }; }};const NodeState = extern struct { id: u64 = 0, ancestor_classes: u64 = 0, style: u32 = 0, computed_generation: u32 = 0,};const hover_sensitive: u32 = 0x8000_0000;const style_mask: u32 = ~hover_sensitive;const key_columns: usize = 14;const ChainSet = struct { ids: [128]u64 = undefined, occupied: [128]bool = @splat(false), fn position(id: u64) usize { const mixed = id *% 0x9e3779b97f4a7c15; return @intCast((mixed >> 57) & 127); } fn insert(self: *ChainSet, id: u64) void { var at = position(id); for (0..self.ids.len) |_| { if (!self.occupied[at]) { self.ids[at] = id; self.occupied[at] = true; return; } if (self.ids[at] == id) return; at = (at + 1) & 127; } unreachable; } fn contains(self: *const ChainSet, id: u64) bool { var at = position(id); for (0..self.ids.len) |_| { if (!self.occupied[at]) return false; if (self.ids[at] == id) return true; at = (at + 1) & 127; } return false; }};fn bucketBit(space: css.bucket.Space, name: []const u8) u16 { const digest = css.atom.hash(name) ^ (@as(u32, @backingInt(space)) *% 0x9e3779b9); return @as(u16, 1) << @intCast(digest % key_columns);}const SelectorKeys = struct { bits: u16, universal: bool };fn selectorKeys(index: css.bucket.Index) SelectorKeys { var bits: u16 = 0; var universal = false; for (index.entries) |entry| { const space: css.bucket.Space = @fromBackingInt(@intCast(entry.space)); if (space == .universal) { universal = true; } else { bits |= bucketBit(space, index.source.name(entry.key)); } } return .{ .bits = bits, .universal = universal };}pub const Error = error{ DistinctStyleQuota, MatchedRuleQuota, SelectorQuota, CapacityOverflow } || Allocator.Error;pub const Exhaustion = error{ DistinctStyleQuota, MatchedRuleQuota, SelectorQuota, CapacityOverflow };const LimitsType = Limits;const CapacityType = Capacity;const ExhaustionType = Exhaustion;pub const Result = struct { restyled: u32 = 0, visited: u32 = 0, carried: u32 = 0, carried_bytes: u64 = 0, shared: u32 = 0, candidates: u64 = 0, ancestor_walks: u64 = 0, combinator_candidates: u64 = 0,};pub const Engine = struct { pub const Limits = LimitsType; pub const Capacity = CapacityType; pub const Storage = []align(8) u8; pub const storage_alignment: usize = 8; pub const InitError = error{CapacityOverflow} || Allocator.Error; pub const Exhaustion = ExhaustionType; pub const work_limits: alloc_phase.capacity.WorkLimits = .{ .transition_steps_max = 1, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "ui.style_engine", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "computed_style_pools", .lifetime = .steady, .detail = "two bounded record pools, value hash slots, and sixteen-byte sharing keys" }, .{ .id = "node_style_generations", .lifetime = .steady, .detail = "two node-state arrays and invalidation occupancy words" }, .{ .id = "rule_buckets_and_invalidation", .lifetime = .steady, .detail = "rule entries, match scratch, flags, and style remap" }, .{ .id = "authored_font_chains", .lifetime = .steady, .detail = "two bounded chains with one font slot per configured face and distinct style" }, }, .excluded = &.{ "the caller-owned stylesheet source and parsed sheet placement", "the retained publish envelope owned by ui.publish_store", }, }, .capacity = .{ .inputs = &.{alloc_phase.capacity.bindInput(LimitsType, "bytes", "bytes")}, .type_selectors = &.{}, .nodes = &.{.{ .input = 0 }}, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 0, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "Capacity.derive rejects an undersized byte budget, font binding rejects a face quota above the chain slots, and a cascade above the distinct style quota leaves the live pool sealed", }, .risks = .{ .transitive = .{ .status = .open, .detail = "the matcher and resolver accept no allocator, while a machine certificate of their call graphs remains open" }, .foreign = .{ .status = .excluded, .detail = "cascade and lowering call no foreign runtime" }, }, .obligations = &.{ .{ .key = "ui_style_capacity", .role = .capacity_model }, .{ .key = "ui_style_acquisition", .role = .acquisition }, .{ .key = "ui_style_sharing_quota", .role = .overload }, .{ .key = "ui_style_font_chain", .role = .overload }, .{ .key = "ui_style_oom", .role = .initialization_failure }, .{ .key = "ui_style_teardown", .role = .teardown }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker }, }, }, }; phase: alloc_phase.capacity.Phase, capacity: CapacityType, bytes: []align(8) u8, limits: LimitsType, states: [2][]NodeState, key_index: [2][]u64, key_bits: [2][]u16, touched: []u64, flags: []u8, entries: []css.bucket.Entry, matches: []css.match.Match, pools: [2]share.Pool, remap: []u32, live: u1 = 0, pool_live: u1 = 0, style_epoch: u64 = 0, count: u32 = 0, node_buffer_addr: usize = 0, view_revision: u64 = 0, last_view: tree.View = .{}, selector_count: u32 = 0, stylesheet_generation: u32 = 0, sheet: ?*const css.StyleSheet = null, summary: css.bucket.Summary = .{}, selector_key_bits: u16 = 0, universal_selector: bool = false, custom_sheet: bool = false, direct_hover: bool = false, hover_filter_enabled: bool = true, interaction: element.Interaction = .{}, metrics: computed.Metrics = .{}, fonts: ?*const asset.Registry = null, font_revision: u64 = 0, applied_font_revision: u64 = 0, applied_font_epoch: u64 = 0, pub fn init(allocator: Allocator, limits: LimitsType) InitError!Engine { const capacity = try CapacityType.derive(limits); const bytes = try allocator.alignedAlloc(u8, .of(u64), capacity.total_bytes); errdefer allocator.free(bytes); var arena = std.heap.FixedBufferAllocator.init(bytes); const a = arena.allocator(); const states_a = a.alloc(NodeState, limits.nodes) catch unreachable; const states_b = a.alloc(NodeState, limits.nodes) catch unreachable; const word_count = std.math.divCeil(usize, limits.nodes, 64) catch unreachable; const key_index_a = a.alloc(u64, key_columns * word_count) catch unreachable; const key_index_b = a.alloc(u64, key_columns * word_count) catch unreachable; const key_bits_a = a.alloc(u16, limits.nodes) catch unreachable; const key_bits_b = a.alloc(u16, limits.nodes) catch unreachable; const touched = a.alloc(u64, word_count) catch unreachable; const flags = a.alloc(u8, limits.nodes) catch unreachable; const entries = a.alloc(css.bucket.Entry, limits.selectors) catch unreachable; const matches = a.alloc(css.match.Match, limits.selectors) catch unreachable; const records_a = a.alloc(computed.Record, limits.distinct_styles) catch unreachable; const records_b = a.alloc(computed.Record, limits.distinct_styles) catch unreachable; const chain_slots = @as(usize, limits.distinct_styles) * limits.font_faces; const chains_a = a.alloc(asset.AssetHandle, chain_slots) catch unreachable; const chains_b = a.alloc(asset.AssetHandle, chain_slots) catch unreachable; const slot_count = std.math.ceilPowerOfTwo(usize, @as(usize, limits.distinct_styles) * 2) catch unreachable; const slots_a = a.alloc(u32, slot_count) catch unreachable; const slots_b = a.alloc(u32, slot_count) catch unreachable; const key_count = std.math.ceilPowerOfTwo(usize, limits.distinct_styles) catch unreachable; const keys_a = a.alloc(share.KeySlot, key_count) catch unreachable; const keys_b = a.alloc(share.KeySlot, key_count) catch unreachable; const remap = a.alloc(u32, limits.distinct_styles) catch unreachable; @memset(slots_a, 0); @memset(slots_b, 0); return .{ .phase = .initialization, .capacity = capacity, .bytes = bytes, .limits = limits, .states = .{ states_a, states_b }, .key_index = .{ key_index_a, key_index_b }, .key_bits = .{ key_bits_a, key_bits_b }, .touched = touched, .flags = flags, .entries = entries, .matches = matches, .pools = .{ .{ .records = records_a, .slots = slots_a, .keys = keys_a, .chains = chains_a, .font_faces = limits.font_faces }, .{ .records = records_b, .slots = slots_b, .keys = keys_b, .chains = chains_b, .font_faces = limits.font_faces }, }, .remap = remap, }; } pub fn activate(self: *Engine) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.bytes.len == self.capacity.storage_bytes); self.phase = .steady; } /// The registry outlives the engine. A changed registry epoch recomputes /// every style that may hold a resolved face handle. pub fn setFonts(self: *Engine, registry: ?*const asset.Registry) error{FontChainQuota}!void { if (registry) |value| { if (value.limits.fonts > self.limits.font_faces) return error.FontChainQuota; } self.fonts = registry; self.font_revision +%= 1; } pub fn authoredFallbacks(self: *const Engine, node: u32) []const asset.AssetHandle { const text = self.record(node).text; return self.pools[self.pool_live].chains[text.fallback_first..][0..text.fallback_count]; } pub fn deinit(self: *Engine, allocator: Allocator) void { std.debug.assert(self.phase != .teardown); self.phase = .teardown; allocator.free(self.bytes); self.bytes = &.{}; } pub fn record(self: *const Engine, node: u32) *const computed.Record { std.debug.assert(node < self.count); return self.pools[self.pool_live].get(self.states[self.live][node].style & style_mask); } /// Returns the interned computed Record ID for an admitted node. /// Two nodes sharing the same computed style have the same ID within one style epoch. pub fn styleId(self: *const Engine, node: u32) u32 { std.debug.assert(node < self.count); return self.states[self.live][node].style & style_mask; } /// Returns a u64 counter that advances only when a full cascade flips the /// computed-style pool and may renumber IDs: a fast cascade that appends in /// the current pool keeps the epoch. A cache retained across cascades keys /// by both epoch and ID, while a script fallback used only within one layout /// pass can key by ID alone. pub fn styleEpoch(self: *const Engine) u64 { std.debug.assert(self.phase == .steady); return self.style_epoch; } fn oldAt(self: *const Engine, id: u64) ?NodeState { for (self.states[self.live][0..self.count]) |state| { if (state.id == id) return state; } return null; } fn oldAtIndex(self: *const Engine, view: tree.View, at: u32) ?NodeState { if (self.count == view.nodes.len and self.node_buffer_addr == @intFromPtr(view.nodes.ptr) and self.view_revision == view.header.revision and self.states[self.live][at].id == view.nodes[at].id) { return self.states[self.live][at]; } if (view.prior.len == view.nodes.len) { const prior = view.prior[at] & ~tree.prior_dirty; if (prior != tree.prior_absent and prior < self.count) return self.states[self.live][prior]; return null; } if (at < self.count and self.states[self.live][at].id == view.nodes[at].id) { return self.states[self.live][at]; } return self.oldAt(view.nodes[at].id); } fn classBloom(adapter: *const element.Adapter, index: u32) u64 { var result: u64 = 0; for (adapter.view.classesOf(index)) |atom| { const token = adapter.sheet.token(adapter.view.text(atom)); result |= @as(u64, 1) << @truncate(token); } return result; } fn sameMatchInputs(before: tree.View, old: u32, after: tree.View, at: u32) bool { const left = before.nodes[old]; const right = after.nodes[at]; if (left.kind != right.kind or left.role != right.role or left.state != right.state or left.flags != right.flags or (old == 0) != (at == 0) or (left.subtree_count == 0 and left.text == abi.text_absent) != (right.subtree_count == 0 and right.text == abi.text_absent)) return false; if (old != 0 and before.nodes[left.parent].id != after.nodes[right.parent].id) return false; if (!std.mem.eql(u8, before.text(left.identifier), after.text(right.identifier))) return false; const old_classes = before.classesOf(old); const new_classes = after.classesOf(at); if (old_classes.len != new_classes.len) return false; for (old_classes, new_classes) |left_class, right_class| { if (!std.mem.eql(u8, before.text(left_class), after.text(right_class))) return false; } return std.mem.eql(u8, std.mem.sliceAsBytes(before.declarationsOf(old)), std.mem.sliceAsBytes(after.declarationsOf(at))); } fn tokenHit(word: u64, token: u32) bool { return token != 0 and word & (@as(u64, 1) << @truncate(token)) != 0; } fn hoverAncestorKeyHit(self: *const Engine, callbacks: css.match.Element, index: u32, classes: u64) bool { const summary = self.summary; if (summary.ancestor_kinds == 0 and summary.ancestor_roles == 0 and summary.ancestor_classes == 0 and summary.ancestor_ids == 0) return true; return classes & summary.ancestor_classes != 0 or tokenHit(summary.ancestor_kinds, callbacks.kind(callbacks.context, index)) or tokenHit(summary.ancestor_roles, callbacks.role(callbacks.context, index)) or tokenHit(summary.ancestor_ids, callbacks.identifier(callbacks.context, index)); } fn nodeKeys(view: tree.View, at: u32) u16 { const node = view.nodes[at]; const kind: abi.Kind = @fromBackingInt(@intCast(node.kind)); const role: abi.Role = @fromBackingInt(@intCast(node.role)); var bits = bucketBit(.kind, @tagName(kind)) | bucketBit(.role, @tagName(role)); if (node.identifier != 0) bits |= bucketBit(.identifier, view.text(node.identifier)); for (view.classesOf(at)) |class| bits |= bucketBit(.class, view.text(class)); return bits; } fn putKeyBits(self: *Engine, target: u1, at: u32, bits: u16) void { const stride = std.math.divCeil(usize, self.limits.nodes, 64) catch unreachable; self.key_bits[target][at] = bits; for (0..key_columns) |column| { if (bits & (@as(u16, 1) << @intCast(column)) != 0) { self.key_index[target][column * stride + at / 64] |= @as(u64, 1) << @intCast(at % 64); } } } fn updateKeyBits(self: *Engine, view: tree.View, at: u32) void { const bits = nodeKeys(view, at); const old = self.key_bits[self.live][at]; if (bits == old) return; const stride = std.math.divCeil(usize, self.limits.nodes, 64) catch unreachable; for (0..key_columns) |column| { const key = @as(u16, 1) << @intCast(column); if ((old ^ bits) & key == 0) continue; const word = &self.key_index[self.live][column * stride + at / 64]; const member = @as(u64, 1) << @intCast(at % 64); if (bits & key != 0) word.* |= member else word.* &= ~member; } self.key_bits[self.live][at] = bits; } fn markBucketKeys(self: *Engine, count: usize, bits: u16) void { const stride = std.math.divCeil(usize, self.limits.nodes, 64) catch unreachable; for (0..key_columns) |column| { if (bits & (@as(u16, 1) << @intCast(column)) == 0) continue; for (0..std.math.divCeil(usize, count, 64) catch unreachable) |word| { var members = self.key_index[self.live][column * stride + word]; while (members != 0) { const bit: usize = @intCast(@ctz(members)); self.flags[word * 64 + bit] = 1; members &= members - 1; } } } } fn markFastBucketKeys(self: *Engine, target: u1, count: usize, bits: u16, universal: bool) void { if (universal) { for (0..count) |at| self.markFast(@intCast(at)); return; } const stride = std.math.divCeil(usize, self.limits.nodes, 64) catch unreachable; for (0..key_columns) |column| { if (bits & (@as(u16, 1) << @intCast(column)) == 0) continue; for (0..std.math.divCeil(usize, count, 64) catch unreachable) |word| { var members = self.key_index[target][column * stride + word]; while (members != 0) { self.markFast(@intCast(word * 64 + @ctz(members))); members &= members - 1; } } } } fn markSubtree(self: *Engine, view: tree.View, index: u32) void { const end = index + view.nodes[index].subtree_count + 1; @memset(self.flags[index..end], 1); } fn countGroup(index: css.bucket.Index, group: []const css.bucket.Entry, result: *Result) void { result.candidates += group.len; for (group) |entry| { const selector = index.source.rules[entry.rule].selectors[entry.selector]; result.combinator_candidates += @intFromBool(selector.combinators.len != 0); } } fn countCandidates(index: css.bucket.Index, callbacks: css.match.Element, node: u32, result: *Result) void { countGroup(index, index.group(.universal, 0), result); const kind = callbacks.kind(callbacks.context, node); if (kind != 0) countGroup(index, index.group(.kind, kind), result); const role = callbacks.role(callbacks.context, node); if (role != 0) countGroup(index, index.group(.role, role), result); const identifier = callbacks.identifier(callbacks.context, node); if (identifier != 0) countGroup(index, index.group(.identifier, identifier), result); var held: [css.match.max_element_classes]u32 = undefined; const count = callbacks.classes(callbacks.context, node, &held, css.match.max_element_classes); for (held[0..count]) |class| { if (class != 0) countGroup(index, index.group(.class, class), result); } } fn markInteraction(self: *Engine, adapter: *const element.Adapter, id: u64, class: css.selector.PseudoClass) void { if (id == 0) return; const found = adapter.find(id) orelse return; var at = found; while (true) { const old = self.oldAt(adapter.view.nodes[at].id); const sensitive = if (class == .hover and self.hover_filter_enabled) if (old) |state| state.style & hover_sensitive != 0 else true else true; if (sensitive) { self.flags[at] = 1; if (self.summary.ancestorPseudoSensitive(class) or (class == .focus and self.summary.ancestorPseudoSensitive(.focus_within))) { self.markSubtree(adapter.view, at); } } if (at == 0) break; at = adapter.view.nodes[at].parent; } } fn markFast(self: *Engine, at: u32) void { const word = at / 64; const bit = @as(u64, 1) << @intCast(at % 64); if (self.touched[word] & bit == 0) self.flags[at] = 1; self.touched[word] |= bit; } fn markFastSubtree(self: *Engine, view: tree.View, at: u32) void { const end = at + view.nodes[at].subtree_count + 1; var index = at; while (index < end) : (index += 1) self.markFast(index); } fn markFastDescendants(self: *Engine, view: tree.View, at: u32) void { const end = at + view.nodes[at].subtree_count + 1; var index = at + 1; while (index < end) : (index += 1) self.markFast(index); } fn markFastChildren(self: *Engine, view: tree.View, at: u32) void { const end = at + view.nodes[at].subtree_count + 1; var index = at + 1; while (index < end) : (index += view.nodes[index].subtree_count + 1) self.markFast(index); } fn findInView(view: tree.View, id: u64) ?u32 { if (id == 0) return null; if (view.identity) |identity| return identity.lookup(view.nodes, id); for (view.nodes, 0..) |node, at| if (node.id == id) return @intCast(at); return null; } fn chain(view: tree.View, id: u64, held: *[65]u32) []const u32 { var at = findInView(view, id) orelse return held[0..0]; var used: usize = 0; while (true) { std.debug.assert(used < held.len); held[used] = at; used += 1; if (at == 0) break; at = view.nodes[at].parent; } return held[0..used]; } fn markFastPseudoNode(self: *Engine, adapter: *element.Adapter, at: u32, class: css.selector.PseudoClass) void { const sensitive = class != .hover or !self.hover_filter_enabled or self.direct_hover or (self.summary.ancestorPseudoSensitive(.hover) and self.hoverAncestorKeyHit(adapter.element(), at, classBloom(adapter, at))); if (!sensitive) return; self.markFast(at); if (self.summary.ancestorPseudoSensitive(class) or (class == .focus and self.summary.ancestorPseudoSensitive(.focus_within))) { self.markFastSubtree(adapter.view, at); } } fn markFastTransition(self: *Engine, adapter: *element.Adapter, old_id: u64, new_id: u64, class: css.selector.PseudoClass) void { var old_buffer: [65]u32 = undefined; var new_buffer: [65]u32 = undefined; const old_chain = chain(self.last_view, old_id, &old_buffer); const new_chain = chain(adapter.view, new_id, &new_buffer); if (class == .hover and !self.hover_filter_enabled) { for (old_chain) |at| { const current = adapter.find(self.last_view.nodes[at].id) orelse continue; self.markFastPseudoNode(adapter, current, class); } for (new_chain) |at| self.markFastPseudoNode(adapter, at, class); return; } var old_set = ChainSet{}; var new_set = ChainSet{}; for (old_chain) |at| old_set.insert(self.last_view.nodes[at].id); for (new_chain) |at| new_set.insert(adapter.view.nodes[at].id); for (old_chain) |at| { const id = self.last_view.nodes[at].id; if (new_set.contains(id)) continue; const current = adapter.find(id) orelse continue; self.markFastPseudoNode(adapter, current, class); } for (new_chain) |at| { if (old_set.contains(adapter.view.nodes[at].id)) continue; self.markFastPseudoNode(adapter, at, class); } } fn fastCascade( self: *Engine, view: tree.View, dirty: []const u32, sheet: *const css.StyleSheet, generation: u32, interaction: element.Interaction, index: css.bucket.Index, metrics: computed.Metrics, font_environment: ?computed.FontSource, carry: bool, carried_count: u32, changed_selector_bits: u16, changed_universal: bool, ancestor_classes: u64, ) Error!Result { const words = std.math.divCeil(usize, view.nodes.len, 64) catch unreachable; if (!carry) @memset(self.touched[0..words], 0); if (changed_selector_bits != 0 or changed_universal) { self.markFastBucketKeys(if (carry) self.live ^ 1 else self.live, view.nodes.len, changed_selector_bits, changed_universal); } var adapter = element.Adapter{ .view = view, .sheet = sheet, .interaction = interaction }; for (dirty) |at| { const old_index = if (carry) view.prior[at] & ~tree.prior_dirty else tree.prior_absent; if (!carry or old_index == tree.prior_absent or !sameMatchInputs(self.last_view, old_index, view, at)) self.markFast(at); const bloom = classBloom(&adapter, at); const old = self.states[if (carry) self.live ^ 1 else self.live][at]; if ((old.ancestor_classes ^ bloom) & ancestor_classes != 0) { self.markFastSubtree(view, at); } if (self.summary.sibling_sensitive or self.summary.positional) { self.markFastChildren(view, at); if (at != 0) self.markFastChildren(view, view.nodes[at].parent); } } if (carry or interaction.hovered != self.interaction.hovered) { self.markFastTransition(&adapter, self.interaction.hovered, interaction.hovered, .hover); } if (carry or interaction.focused != self.interaction.focused) { self.markFastTransition(&adapter, self.interaction.focused, interaction.focused, .focus); } if (interaction.focused != self.interaction.focused) { if (adapter.find(self.interaction.focused)) |old| self.markFastPseudoNode(&adapter, old, .focus); if (adapter.find(interaction.focused)) |new| self.markFastPseudoNode(&adapter, new, .focus); } if (interaction.keyboard_focus != self.interaction.keyboard_focus) { if (adapter.find(interaction.focused)) |focused| self.markFastPseudoNode(&adapter, focused, .focus_visible); } if (carry or interaction.active != self.interaction.active) { self.markFastTransition(&adapter, self.interaction.active, interaction.active, .active); } const stage: u1 = self.live ^ 1; const pool = &self.pools[self.pool_live]; var result = Result{ .carried = carried_count, .carried_bytes = @as(u64, carried_count) * @sizeOf(NodeState) }; var resolver = css.cascade.Resolver{}; for (0..words) |word| { var processed: u64 = 0; while (self.touched[word] & ~processed != 0) { const bit: u6 = @intCast(@ctz(self.touched[word] & ~processed)); const at: u32 = @intCast(word * 64 + bit); processed |= @as(u64, 1) << bit; self.flags[at] = 0; result.visited += 1; const node = view.nodes[at]; const prior = self.states[if (carry) stage else self.live][at]; const bloom = classBloom(&adapter, at); const parent_style: u32 = if (at == 0) 0 else blk: { const parent = node.parent; if (carry) break :blk self.states[stage][parent].style & style_mask; const parent_bit = @as(u64, 1) << @intCast(parent % 64); const staged = self.touched[parent / 64] & parent_bit != 0 and self.flags[parent] == 0; break :blk (if (staged) self.states[stage][parent].style else self.states[self.live][parent].style) & style_mask; }; const callbacks = adapter.element(); countCandidates(index, callbacks, at, &result); const before_steps = adapter.ancestor_steps; const matched = css.bucket.matchElement(index, callbacks, at, self.matches); if (matched.rejected != 0) return error.MatchedRuleQuota; result.ancestor_walks += adapter.ancestor_steps - before_steps; var rule_hash: u32 = 2166136261; for (self.matches[0..matched.count]) |item| { rule_hash = (rule_hash ^ item.rule) *% 16777619; rule_hash = (rule_hash ^ item.selector) *% 16777619; } const key = share.Key{ .matched_rules = rule_hash, .inline_first = node.declaration_first, .inline_count = node.declaration_count, .parent_style = parent_style, }; const sensitive = self.direct_hover or (self.summary.ancestorPseudoSensitive(.hover) and self.hoverAncestorKeyHit(callbacks, at, bloom)); resolver.begin(sheet.source, &sheet.atoms, null); for (self.matches[0..matched.count]) |item| { const rule = sheet.rules[item.rule]; for (rule.lowered) |declaration| resolver.add(declaration, .author, item.specificity, item.order); } for (view.declarationsOf(at)) |declaration| resolver.add(declaration, .author, css.Specificity.inlineStyle(), std.math.maxInt(u32)); const inherited = if (parent_style == 0) null else &pool.get(parent_style).computed; var style: css.cascade.Computed = undefined; resolver.finish(inherited, &style); const style_id = try pool.intern(key, self.matches[0..matched.count], style, metrics, font_environment); self.states[stage][at] = .{ .id = node.id, .ancestor_classes = bloom, .style = style_id | (if (sensitive) hover_sensitive else 0), .computed_generation = generation, }; result.restyled += 1; if (prior.style & style_mask == 0 or !std.mem.eql(u8, std.mem.asBytes(&pool.get(prior.style & style_mask).computed), std.mem.asBytes(&pool.get(style_id).computed))) { self.markFastDescendants(view, at); } } } if (carry) { self.live = stage; self.count = @intCast(view.nodes.len); self.node_buffer_addr = @intFromPtr(view.nodes.ptr); self.view_revision = view.header.revision; } else { for (self.touched[0..words], 0..) |held, word| { var members = held; while (members != 0) { const bit: u6 = @intCast(@ctz(members)); const at = word * 64 + bit; self.states[self.live][at] = self.states[stage][at]; self.updateKeyBits(view, @intCast(at)); members &= members - 1; } } } self.interaction = interaction; self.last_view = view; return result; } pub fn cascade( self: *Engine, store: *const tree.Store, sheet: *const css.StyleSheet, generation: u32, interaction: element.Interaction, ) Error!Result { std.debug.assert(self.phase == .steady); return self.cascadeView(store.retained(), store.dirty(), sheet, generation, interaction); } pub fn cascadeView( self: *Engine, view: tree.View, dirty: []const u32, sheet: *const css.StyleSheet, generation: u32, interaction: element.Interaction, ) Error!Result { std.debug.assert(self.phase == .steady); if (view.nodes.len > self.limits.nodes) return error.CapacityOverflow; const selector_count = css.bucket.Limits.inspect(sheet).entries; if (selector_count > self.entries.len) return error.SelectorQuota; var adapter = element.Adapter{ .view = view, .sheet = sheet, .interaction = interaction }; const metrics = computed.Metrics{ .root_font_size = view.header.root_font_size, .viewport_width = view.header.viewport.width, .viewport_height = view.header.viewport.height, }; const changed_sheet = self.sheet != sheet or self.stylesheet_generation != generation; const changed_fonts = self.applied_font_revision != self.font_revision or (if (self.fonts) |registry| self.applied_font_epoch != registry.epoch else false); const changed_inputs = !std.meta.eql(self.metrics, metrics) or changed_fonts or (changed_sheet and self.fonts != null); const font_environment: ?computed.FontSource = if (self.fonts) |registry| .{ .registry = registry, .source = sheet.source } else null; const previous_summary = self.summary; const previous_hover = self.direct_hover; const previous_custom = self.custom_sheet; errdefer if (changed_sheet) { self.summary = previous_summary; self.direct_hover = previous_hover; self.custom_sheet = previous_custom; if (self.sheet) |retained_sheet| _ = css.bucket.build(self.entries, retained_sheet); }; const index: css.bucket.Index = if (changed_sheet) css.bucket.build(self.entries, sheet) else .{ .entries = self.entries[0..self.selector_count], .source = sheet }; const keys: SelectorKeys = if (changed_sheet) selectorKeys(index) else .{ .bits = self.selector_key_bits, .universal = self.universal_selector }; if (changed_sheet) { self.summary = css.bucket.summarize(sheet); self.direct_hover = false; self.custom_sheet = false; for (sheet.rules) |rule| { if (rule.customs.len != 0) self.custom_sheet = true; for (rule.selectors) |selector| { if (selector.rightmost().pseudo & css.selector.bit(.hover) != 0) self.direct_hover = true; } } } const changed_selector_bits = if (changed_sheet) self.selector_key_bits | keys.bits else @as(u16, 0); const changed_universal = changed_sheet and (self.universal_selector or keys.universal); const ancestor_classes = previous_summary.ancestor_classes | self.summary.ancestor_classes; const complete_refresh = blk: { if (dirty.len != view.nodes.len) break :blk false; for (dirty, 0..) |at, index_at| if (at != index_at) break :blk false; break :blk true; }; if (!complete_refresh and self.count != 0 and !changed_inputs and !self.custom_sheet and !previous_custom and self.count == view.nodes.len and self.node_buffer_addr == @intFromPtr(view.nodes.ptr) and self.view_revision == view.header.revision) { var stable_ids = true; for (dirty) |at| { if (at >= self.count or self.states[self.live][at].id != view.nodes[at].id) { stable_ids = false; break; } } if (stable_ids) { const retained_styles = self.pools[self.pool_live].used; if (self.fastCascade(view, dirty, sheet, generation, interaction, index, metrics, font_environment, false, 0, changed_selector_bits, changed_universal, ancestor_classes)) |fast| { self.sheet = sheet; self.stylesheet_generation = generation; self.selector_key_bits = keys.bits; self.universal_selector = keys.universal; self.selector_count = @intCast(index.entries.len); return fast; } else |err| { self.pools[self.pool_live].truncate(retained_styles); @memset(self.touched[0 .. std.math.divCeil(usize, view.nodes.len, 64) catch unreachable], 0); switch (err) { error.DistinctStyleQuota => {}, else => return err, } } } } if (!complete_refresh and self.count != 0 and !changed_inputs and !self.custom_sheet and !previous_custom and view.prior.len == view.nodes.len and self.last_view.nodes.len == self.count and (view.header.base_revision == 0 or view.header.base_revision == self.view_revision) and (self.node_buffer_addr != @intFromPtr(view.nodes.ptr) or self.view_revision != view.header.revision)) { const stage: u1 = self.live ^ 1; @memset(self.touched[0 .. std.math.divCeil(usize, view.nodes.len, 64) catch unreachable], 0); @memset(self.key_index[stage], 0); var carried: u32 = 0; var valid = true; for (view.nodes, 0..) |node, at| { const old = view.prior[at] & ~tree.prior_dirty; if (old == tree.prior_absent) { self.states[stage][at] = .{ .id = node.id }; self.putKeyBits(stage, @intCast(at), nodeKeys(view, @intCast(at))); continue; } if (old >= self.count or self.states[self.live][old].id != node.id) { valid = false; break; } self.states[stage][at] = self.states[self.live][old]; const bits = if (view.prior[at] & tree.prior_dirty != 0) nodeKeys(view, @intCast(at)) else self.key_bits[self.live][old]; self.putKeyBits(stage, @intCast(at), bits); carried += 1; if (at != 0 and self.last_view.nodes[old].parent < self.last_view.nodes.len and self.last_view.nodes[self.last_view.nodes[old].parent].id != view.nodes[node.parent].id) { self.markFastSubtree(view, @intCast(at)); if (self.summary.sibling_sensitive or self.summary.positional) { self.markFastChildren(view, node.parent); const old_parent_id = self.last_view.nodes[self.last_view.nodes[old].parent].id; if (view.identity) |identity| { if (identity.lookup(view.nodes, old_parent_id)) |old_parent| { self.markFastChildren(view, old_parent); } } } } } if (valid) { const retained_styles = self.pools[self.pool_live].used; if (self.fastCascade(view, dirty, sheet, generation, interaction, index, metrics, font_environment, true, carried, changed_selector_bits, changed_universal, ancestor_classes)) |fast| { self.sheet = sheet; self.stylesheet_generation = generation; self.selector_key_bits = keys.bits; self.universal_selector = keys.universal; self.selector_count = @intCast(index.entries.len); return fast; } else |err| { self.pools[self.pool_live].truncate(retained_styles); @memset(self.touched[0 .. std.math.divCeil(usize, view.nodes.len, 64) catch unreachable], 0); switch (err) { error.DistinctStyleQuota => {}, else => return err, } } } } @memset(self.flags[0..view.nodes.len], if (self.count == 0 or changed_inputs or self.custom_sheet or (changed_sheet and (self.universal_selector or keys.universal or self.node_buffer_addr != @intFromPtr(view.nodes.ptr) or self.view_revision != view.header.revision))) 1 else 0); if (changed_sheet and self.count != 0 and !changed_inputs and !self.universal_selector and !keys.universal and self.node_buffer_addr == @intFromPtr(view.nodes.ptr) and self.view_revision == view.header.revision) { self.markBucketKeys(view.nodes.len, self.selector_key_bits | keys.bits); } for (dirty) |at| self.flags[at] = 1; if (self.summary.sibling_sensitive or self.summary.positional) { for (dirty) |at| { const parent = view.nodes[at].parent; self.markSubtree(view, parent); } } if (interaction.hovered != self.interaction.hovered) { self.markInteraction(&adapter, self.interaction.hovered, .hover); self.markInteraction(&adapter, interaction.hovered, .hover); } if (interaction.focused != self.interaction.focused or interaction.keyboard_focus != self.interaction.keyboard_focus) { self.markInteraction(&adapter, self.interaction.focused, .focus); self.markInteraction(&adapter, interaction.focused, .focus); } if (interaction.active != self.interaction.active) { self.markInteraction(&adapter, self.interaction.active, .active); self.markInteraction(&adapter, interaction.active, .active); } const next: u1 = self.live ^ 1; const next_pool = &self.pools[self.pool_live ^ 1]; next_pool.reset(); @memset(self.key_index[next], 0); @memset(self.remap, 0); var result = Result{}; var resolver = css.cascade.Resolver{}; var custom_stack: [65]css.cascade.CustomTable = undefined; var custom_nodes: [65]u32 = undefined; var custom_depth: usize = 0; for (view.nodes, 0..) |node, i| { result.visited += 1; const at: u32 = @intCast(i); self.putKeyBits(next, at, nodeKeys(view, at)); const prior = self.oldAtIndex(view, at); const bloom = classBloom(&adapter, at); const parent_style: u32 = if (at == 0) 0 else self.states[next][node.parent].style & style_mask; const ancestor_change = if (prior) |state| (state.ancestor_classes ^ bloom) & self.summary.ancestor_classes != 0 else false; if (ancestor_change) self.markSubtree(view, at); const same_view = self.count == view.nodes.len and self.node_buffer_addr == @intFromPtr(view.nodes.ptr) and self.view_revision == view.header.revision; const shifted = prior == null or if (same_view) self.states[self.live][at].id != node.id else if (view.prior.len == view.nodes.len) (view.prior[at] & ~tree.prior_dirty) != at else at >= self.count or self.states[self.live][at].id != node.id; const should_compute = changed_inputs or self.flags[at] != 0 or prior == null or shifted; if (!should_compute) { var same = prior.?; const old_id = same.style & style_mask; var new_id = self.remap[old_id - 1]; if (new_id == 0) { const inherited = self.pools[self.pool_live].get(old_id); new_id = try next_pool.intern(.{ .matched_rules = 0, .inline_first = 0, .inline_count = 0, .parent_style = parent_style, }, &.{}, inherited.computed, metrics, font_environment); self.remap[old_id - 1] = new_id; } const old_hover = same.style & hover_sensitive; const next_hover = if (changed_sheet) if (self.direct_hover or (self.summary.ancestorPseudoSensitive(.hover) and self.hoverAncestorKeyHit(adapter.element(), at, bloom))) hover_sensitive else @as(u32, 0) else old_hover; same.style = new_id | next_hover; same.ancestor_classes = bloom; same.computed_generation = generation; self.states[next][at] = same; result.shared += 1; continue; } const callbacks = adapter.element(); countCandidates(index, callbacks, at, &result); const before_steps = adapter.ancestor_steps; const matched = css.bucket.matchElement(index, callbacks, at, self.matches); if (matched.rejected != 0) return error.MatchedRuleQuota; result.ancestor_walks += adapter.ancestor_steps - before_steps; var rule_hash: u32 = 2166136261; for (self.matches[0..matched.count]) |item| { rule_hash = (rule_hash ^ item.rule) *% 16777619; rule_hash = (rule_hash ^ item.selector) *% 16777619; } const key = share.Key{ .matched_rules = rule_hash, .inline_first = node.declaration_first, .inline_count = node.declaration_count, .parent_style = parent_style, }; const sensitive = self.direct_hover or (self.summary.ancestorPseudoSensitive(.hover) and self.hoverAncestorKeyHit(callbacks, at, bloom)); if (if (self.custom_sheet) null else next_pool.lookup(key, self.matches[0..matched.count])) |style_id| { self.states[next][at] = .{ .id = node.id, .ancestor_classes = bloom, .style = style_id | (if (sensitive) hover_sensitive else 0), .computed_generation = generation, }; result.restyled += 1; result.shared += 1; if (prior) |old| { const old_style = self.pools[self.pool_live].get(old.style & style_mask); const new_style = next_pool.get(style_id); if (!std.mem.eql(u8, std.mem.asBytes(&old_style.computed), std.mem.asBytes(&new_style.computed))) { self.markSubtree(view, at); } } continue; } var parent_customs: ?*const css.cascade.CustomTable = null; if (self.custom_sheet and at != 0) { while (custom_depth > 0 and custom_nodes[custom_depth - 1] != node.parent) custom_depth -= 1; std.debug.assert(custom_depth > 0); parent_customs = &custom_stack[custom_depth - 1]; } resolver.begin(sheet.source, &sheet.atoms, parent_customs); for (self.matches[0..matched.count]) |item| { const rule = sheet.rules[item.rule]; for (rule.lowered) |declaration| resolver.add(declaration, .author, item.specificity, item.order); for (rule.customs) |custom| resolver.addCustom(custom, .author, item.specificity, item.order); } for (view.declarationsOf(at)) |declaration| resolver.add(declaration, .author, css.Specificity.inlineStyle(), std.math.maxInt(u32)); const inherited = if (parent_style == 0) null else &next_pool.get(parent_style).computed; var style: css.cascade.Computed = undefined; resolver.finish(inherited, &style); if (self.custom_sheet) { if (custom_depth == custom_stack.len) return error.CapacityOverflow; custom_stack[custom_depth] = resolver.customs; custom_nodes[custom_depth] = at; custom_depth += 1; } const style_id = try next_pool.intern(key, self.matches[0..matched.count], style, metrics, font_environment); self.states[next][at] = .{ .id = node.id, .ancestor_classes = bloom, .style = style_id | (if (sensitive) hover_sensitive else 0), .computed_generation = generation, }; result.restyled += 1; if (prior) |old| { const old_style = self.pools[self.pool_live].get(old.style & style_mask); if (!std.mem.eql(u8, std.mem.asBytes(&old_style.computed), std.mem.asBytes(&style))) { self.markSubtree(view, at); } } } self.live = next; self.pool_live ^= 1; std.debug.assert(self.style_epoch < std.math.maxInt(u64)); self.style_epoch += 1; self.count = @intCast(view.nodes.len); self.node_buffer_addr = @intFromPtr(view.nodes.ptr); self.view_revision = view.header.revision; self.last_view = view; self.selector_count = @intCast(index.entries.len); self.sheet = sheet; self.stylesheet_generation = generation; self.selector_key_bits = keys.bits; self.universal_selector = keys.universal; self.interaction = interaction; self.metrics = metrics; self.applied_font_revision = self.font_revision; self.applied_font_epoch = if (self.fonts) |registry| registry.epoch else 0; return result; }};comptime { alloc_phase.capacity.requireAllocatorExactOwnerShape(Engine);}test "incremental cascade agrees with a cold cascade after one dirty node" { const allocator = std.testing.allocator; var workspace = css.Workspace.init(allocator); defer workspace.deinit(); const sheet = try workspace.parse("box { color: #ff0000; } text { color: #0000ff; }", .default()); const nodes = [_]@import("../abi/root.zig").Node{ .{ .id = 1, .revision = 1, .kind = @backingInt(@import("../abi/root.zig").Kind.box), .subtree_count = 1 }, .{ .id = 2, .revision = 1, .kind = @backingInt(@import("../abi/root.zig").Kind.text), .parent = 0 }, }; const view = tree.View{ .header = .{ .root_font_size = 16 }, .nodes = &nodes }; const limits = Limits{ .nodes = 2, .distinct_styles = 4, .selectors = 4, .bytes = 16 * 1024 }; var incremental = try Engine.init(allocator, limits); defer incremental.deinit(allocator); incremental.activate(); const first = try incremental.cascadeView(view, &.{ 0, 1 }, &sheet, 1, .{}); try std.testing.expectEqual(@as(u32, 2), first.restyled); const next = try incremental.cascadeView(view, &.{1}, &sheet, 1, .{}); try std.testing.expectEqual(@as(u32, 1), next.restyled); var cold = try Engine.init(allocator, limits); defer cold.deinit(allocator); cold.activate(); _ = try cold.cascadeView(view, &.{ 0, 1 }, &sheet, 1, .{}); for (nodes, 0..) |_, at| { try std.testing.expect(std.meta.eql(incremental.record(@intCast(at)).computed, cold.record(@intCast(at)).computed)); }}test "stylesheet generation visits only indexed bucket keys" { const allocator = std.testing.allocator; var first_workspace = css.Workspace.init(allocator); defer first_workspace.deinit(); var second_workspace = css.Workspace.init(allocator); defer second_workspace.deinit(); const first = try first_workspace.parse(".alpha { color: red; }", .default()); const second = try second_workspace.parse(".beta { color: blue; }", .default()); var nodes: [128]abi.Node = undefined; nodes[0] = .{ .id = 1, .subtree_count = 127 }; for (1..nodes.len) |at| nodes[at] = .{ .id = @intCast(at + 1), .parent = 0 }; nodes[1].class_first = 0; nodes[1].class_count = 1; nodes[2].class_first = 1; nodes[2].class_count = 1; const classes = [_]u32{ 1, 2 }; const atoms = [_]abi.Atom{ .{}, .{ .offset = 0, .len = 5 }, .{ .offset = 5, .len = 4 } }; const view = tree.View{ .nodes = &nodes, .classes = &classes, .atoms = &atoms, .strings = "alphabeta" }; var incremental = try Engine.init(allocator, .{ .nodes = 128, .distinct_styles = 8, .selectors = 4, .bytes = 64 * 1024 }); defer incremental.deinit(allocator); incremental.activate(); _ = try incremental.cascadeView(view, &.{}, &first, 1, .{}); const changed = try incremental.cascadeView(view, &.{}, &second, 2, .{}); try std.testing.expectEqual(@as(u32, 2), changed.visited); try std.testing.expectEqual(@as(u32, 2), changed.restyled); var cold = try Engine.init(allocator, .{ .nodes = 128, .distinct_styles = 8, .selectors = 4, .bytes = 64 * 1024 }); defer cold.deinit(allocator); cold.activate(); _ = try cold.cascadeView(view, &.{}, &second, 2, .{}); for (nodes, 0..) |_, at| { try std.testing.expect(std.meta.eql(incremental.record(@intCast(at)).computed, cold.record(@intCast(at)).computed)); }}test "ui_style_capacity derives the caller bound" { comptime { @stardustClaim(alloc_phase.capacity.witness(Engine, "ui_style_capacity"), null, null, null, null, null, null); } const capacity = try Capacity.derive(.{}); try std.testing.expectEqual(@as(usize, 2_097_152), capacity.total_bytes); try std.testing.expect(capacity.required_bytes <= capacity.total_bytes);}test "ui_style_acquisition and ui_style_teardown own one block" { comptime { @stardustClaim(alloc_phase.capacity.witness(Engine, "ui_style_acquisition"), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness(Engine, "ui_style_teardown"), null, null, null, null, null, null); } var engine = try Engine.init(std.testing.allocator, .{}); try std.testing.expectEqual(@as(usize, 2_097_152), engine.bytes.len); engine.activate(); engine.deinit(std.testing.allocator); try std.testing.expectEqual(alloc_phase.capacity.Phase.teardown, engine.phase);}fn openAndClose(allocator: Allocator, limits: LimitsType) !void { var engine = try Engine.init(allocator, limits); engine.activate(); engine.deinit(allocator);}test "ui_style_oom refuses acquisition without a partial block" { comptime { @stardustClaim(alloc_phase.capacity.witness(Engine, "ui_style_oom"), null, null, null, null, null, null); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); try std.testing.expectError(error.OutOfMemory, Engine.init(failing.allocator(), .{})); try std.testing.expectEqual(@as(usize, 0), failing.allocated_bytes - failing.freed_bytes); try std.testing.checkAllAllocationFailures(std.testing.allocator, openAndClose, .{LimitsType{}});}test "ui_style_sharing_quota rejects before changing the live generation" { comptime { @stardustClaim(alloc_phase.capacity.witness(Engine, "ui_style_sharing_quota"), null, null, null, null, null, null); } const allocator = std.testing.allocator; var workspace = css.Workspace.init(allocator); defer workspace.deinit(); const sheet = try workspace.parse("box { color: red; } text { color: blue; }", .default()); const nodes = [_]abi.Node{ .{ .id = 1, .subtree_count = 1, .kind = @backingInt(abi.Kind.box) }, .{ .id = 2, .parent = 0, .kind = @backingInt(abi.Kind.text) }, }; var engine = try Engine.init(allocator, .{ .nodes = 2, .distinct_styles = 1, .selectors = 2, .bytes = 8 * 1024 }); defer engine.deinit(allocator); engine.activate(); const view = tree.View{ .header = .{ .root_font_size = 16 }, .nodes = &nodes }; try std.testing.expectError(error.DistinctStyleQuota, engine.cascadeView(view, &.{ 0, 1 }, &sheet, 1, .{})); try std.testing.expectEqual(@as(u32, 0), engine.count); try std.testing.expectEqual(@as(u32, 0), engine.stylesheet_generation);}test "ui_style_font_chain rejects a face quota before binding" { comptime { @stardustClaim(alloc_phase.capacity.witness(Engine, "ui_style_font_chain"), null, null, null, null, null, null); } const allocator = std.testing.allocator; var registry = try asset.Registry.init(allocator, .{ .fonts = 2, .images = 0, .owned_bytes = 0 }); defer registry.deinit(allocator); registry.activate(); var engine = try Engine.init(allocator, .{ .nodes = 1, .distinct_styles = 1, .selectors = 1, .font_faces = 1, .bytes = 8 * 1024 }); defer engine.deinit(allocator); engine.activate(); try std.testing.expectError(error.FontChainQuota, engine.setFonts(®istry)); try std.testing.expect(engine.fonts == null);}test "sparse quota failure preserves the live pool and node states" { const allocator = std.testing.allocator; var workspace = css.Workspace.init(allocator); defer workspace.deinit(); const sheet = try workspace.parse(".red { color: red; } .blue { color: blue; }", .default()); const nodes = [_]abi.Node{ .{ .id = 1, .subtree_count = 2 }, .{ .id = 2, .parent = 0, .class_first = 0, .class_count = 1 }, .{ .id = 3, .parent = 0, .class_first = 1, .class_count = 1 }, }; var classes = [_]u32{ 1, 1 }; const atoms = [_]abi.Atom{ .{}, .{ .offset = 0, .len = 3 }, .{ .offset = 3, .len = 4 } }; const view = tree.View{ .nodes = &nodes, .classes = &classes, .atoms = &atoms, .strings = "redblue" }; var engine = try Engine.init(allocator, .{ .nodes = 3, .distinct_styles = 2, .selectors = 4, .bytes = 64 * 1024 }); defer engine.deinit(allocator); engine.activate(); _ = try engine.cascadeView(view, &.{ 0, 1, 2 }, &sheet, 1, .{}); const original = engine.record(2).computed; const original_epoch = engine.styleEpoch(); const retained_styles = engine.pools[engine.pool_live].used; classes[1] = 2; try std.testing.expectError(error.DistinctStyleQuota, engine.cascadeView(view, &.{2}, &sheet, 1, .{})); try std.testing.expectEqual(retained_styles, engine.pools[engine.pool_live].used); try std.testing.expectEqual(original_epoch, engine.styleEpoch()); try std.testing.expect(std.meta.eql(original, engine.record(2).computed)); classes[1] = 1; const restored = try engine.cascadeView(view, &.{2}, &sheet, 1, .{}); try std.testing.expectEqual(@as(u32, 1), restored.visited); try std.testing.expect(std.meta.eql(original, engine.record(2).computed));}test "style ids share within an epoch and a full cascade advances it" { const allocator = std.testing.allocator; var workspace = css.Workspace.init(allocator); defer workspace.deinit(); const red = try workspace.parse("box { color: red; }", .default()); const blue = try workspace.parse("box { color: blue; }", .default()); const nodes = [_]abi.Node{ .{ .id = 1, .subtree_count = 1, .kind = @backingInt(abi.Kind.box) }, .{ .id = 2, .parent = 0, .kind = @backingInt(abi.Kind.box) }, }; const view = tree.View{ .header = .{ .root_font_size = 16 }, .nodes = &nodes, }; var engine = try Engine.init(allocator, .{ .nodes = 2, .distinct_styles = 4, .selectors = 2, .bytes = 16 * 1024, }); defer engine.deinit(allocator); engine.activate(); try std.testing.expectEqual(@as(u64, 0), engine.styleEpoch()); _ = try engine.cascadeView(view, &.{ 0, 1 }, &red, 1, .{}); const first_epoch = engine.styleEpoch(); const first_id = engine.styleId(0); try std.testing.expectEqual(@as(u64, 1), first_epoch); try std.testing.expectEqual(first_id, engine.styleId(1)); _ = try engine.cascadeView(view, &.{1}, &red, 1, .{}); try std.testing.expectEqual(first_epoch, engine.styleEpoch()); try std.testing.expectEqual(first_id, engine.styleId(0)); _ = try engine.cascadeView(view, &.{ 0, 1 }, &blue, 2, .{}); try std.testing.expectEqual(first_epoch + 1, engine.styleEpoch());}Source: lib/ui/src/style/root.zig:5
zig
pub const generation = @import("generation.zig");Complete caller list for style.Engine.activate
12 direct callers.
lib.ui.src.style.generation.openAndClose[function] — private source atlib/ui/src/style/generation.zig:1171in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_incremental_cascade_agrees_with_a_cold_cascade_after_one_dirty_node[function] — test source atlib/ui/src/style/generation.zig:1089in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_sparse_quota_failure_preserves_the_live_pool_and_node_states[function] — test source atlib/ui/src/style/generation.zig:1223in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_style_ids_share_within_an_epoch_and_a_full_cascade_advances_it[function] — test source atlib/ui/src/style/generation.zig:1254in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_stylesheet_generation_visits_only_indexed_bucket_keys[function] — test source atlib/ui/src/style/generation.zig:1116in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_acquisition_and_ui_style_teardown_own_one_block[function] — test source atlib/ui/src/style/generation.zig:1159in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_font_chain_rejects_a_face_quota_before_binding[function] — test source atlib/ui/src/style/generation.zig:1208in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_sharing_quota_rejects_before_changing_the_live_generation[function] — test source atlib/ui/src/style/generation.zig:1187in nearest public ownertiny.ui.style.generationlib.ui.src.style.test.test_style_lowering_reads_an_inline_font_handle_from_the_admitted_declaration[function] — test source atlib/ui/src/style/test.zig:115in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_lowering_resolves_a_registered_primary_face_and_refreshes_on_release[function] — test source atlib/ui/src/style/test.zig:64in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_media_environment_filters_sheets_before_cascade[function] — test source atlib/ui/src/style/test.zig:140in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_stylesheet_replacement_resolves_a_new_family_with_equal_cascade_bytes[function] — test source atlib/ui/src/style/test.zig:90in nearest public ownerlib.ui.src.style.test
Complete caller list for style.Engine.cascadeView
10 direct callers.
tiny.ui.style.Engine.cascade[method] atlib/ui/src/style/generation.zig:749lib.ui.src.style.generation.test_incremental_cascade_agrees_with_a_cold_cascade_after_one_dirty_node[function] — test source atlib/ui/src/style/generation.zig:1089in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_sparse_quota_failure_preserves_the_live_pool_and_node_states[function] — test source atlib/ui/src/style/generation.zig:1223in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_style_ids_share_within_an_epoch_and_a_full_cascade_advances_it[function] — test source atlib/ui/src/style/generation.zig:1254in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_stylesheet_generation_visits_only_indexed_bucket_keys[function] — test source atlib/ui/src/style/generation.zig:1116in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_sharing_quota_rejects_before_changing_the_live_generation[function] — test source atlib/ui/src/style/generation.zig:1187in nearest public ownertiny.ui.style.generationlib.ui.src.style.test.test_style_lowering_reads_an_inline_font_handle_from_the_admitted_declaration[function] — test source atlib/ui/src/style/test.zig:115in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_lowering_resolves_a_registered_primary_face_and_refreshes_on_release[function] — test source atlib/ui/src/style/test.zig:64in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_media_environment_filters_sheets_before_cascade[function] — test source atlib/ui/src/style/test.zig:140in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_stylesheet_replacement_resolves_a_new_family_with_equal_cascade_bytes[function] — test source atlib/ui/src/style/test.zig:90in nearest public ownerlib.ui.src.style.test
Complete call list for style.Engine.cascadeView
13 direct calls.
lib.ui.src.style.generation.Engine.classBloom[function] — private source atlib/ui/src/style/generation.zig:368in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.countCandidates[function] — private source atlib/ui/src/style/generation.zig:490in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.fastCascade[method] — private source atlib/ui/src/style/generation.zig:612in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.hoverAncestorKeyHit[method] — private source atlib/ui/src/style/generation.zig:399in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.markBucketKeys[method] — private source atlib/ui/src/style/generation.zig:444in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.markFastChildren[method] — private source atlib/ui/src/style/generation.zig:544in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.markFastSubtree[method] — private source atlib/ui/src/style/generation.zig:532in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.markInteraction[method] — private source atlib/ui/src/style/generation.zig:505in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.markSubtree[method] — private source atlib/ui/src/style/generation.zig:477in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.nodeKeys[function] — private source atlib/ui/src/style/generation.zig:409in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.oldAtIndex[method] — private source atlib/ui/src/style/generation.zig:351in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.Engine.putKeyBits[method] — private source atlib/ui/src/style/generation.zig:419in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.selectorKeys[function] — private source atlib/ui/src/style/generation.zig:106in nearest public ownertiny.ui.style.generation
Complete caller list for style.Engine.deinit
12 direct callers.
lib.ui.src.style.generation.openAndClose[function] — private source atlib/ui/src/style/generation.zig:1171in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_incremental_cascade_agrees_with_a_cold_cascade_after_one_dirty_node[function] — test source atlib/ui/src/style/generation.zig:1089in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_sparse_quota_failure_preserves_the_live_pool_and_node_states[function] — test source atlib/ui/src/style/generation.zig:1223in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_style_ids_share_within_an_epoch_and_a_full_cascade_advances_it[function] — test source atlib/ui/src/style/generation.zig:1254in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_stylesheet_generation_visits_only_indexed_bucket_keys[function] — test source atlib/ui/src/style/generation.zig:1116in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_acquisition_and_ui_style_teardown_own_one_block[function] — test source atlib/ui/src/style/generation.zig:1159in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_font_chain_rejects_a_face_quota_before_binding[function] — test source atlib/ui/src/style/generation.zig:1208in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_sharing_quota_rejects_before_changing_the_live_generation[function] — test source atlib/ui/src/style/generation.zig:1187in nearest public ownertiny.ui.style.generationlib.ui.src.style.test.test_style_lowering_reads_an_inline_font_handle_from_the_admitted_declaration[function] — test source atlib/ui/src/style/test.zig:115in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_lowering_resolves_a_registered_primary_face_and_refreshes_on_release[function] — test source atlib/ui/src/style/test.zig:64in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_media_environment_filters_sheets_before_cascade[function] — test source atlib/ui/src/style/test.zig:140in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_stylesheet_replacement_resolves_a_new_family_with_equal_cascade_bytes[function] — test source atlib/ui/src/style/test.zig:90in nearest public ownerlib.ui.src.style.test
Complete caller list for style.Engine.init
13 direct callers.
lib.ui.src.style.generation.openAndClose[function] — private source atlib/ui/src/style/generation.zig:1171in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_incremental_cascade_agrees_with_a_cold_cascade_after_one_dirty_node[function] — test source atlib/ui/src/style/generation.zig:1089in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_sparse_quota_failure_preserves_the_live_pool_and_node_states[function] — test source atlib/ui/src/style/generation.zig:1223in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_style_ids_share_within_an_epoch_and_a_full_cascade_advances_it[function] — test source atlib/ui/src/style/generation.zig:1254in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_stylesheet_generation_visits_only_indexed_bucket_keys[function] — test source atlib/ui/src/style/generation.zig:1116in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_acquisition_and_ui_style_teardown_own_one_block[function] — test source atlib/ui/src/style/generation.zig:1159in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_font_chain_rejects_a_face_quota_before_binding[function] — test source atlib/ui/src/style/generation.zig:1208in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_oom_refuses_acquisition_without_a_partial_block[function] — test source atlib/ui/src/style/generation.zig:1177in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_ui_style_sharing_quota_rejects_before_changing_the_live_generation[function] — test source atlib/ui/src/style/generation.zig:1187in nearest public ownertiny.ui.style.generationlib.ui.src.style.test.test_style_lowering_reads_an_inline_font_handle_from_the_admitted_declaration[function] — test source atlib/ui/src/style/test.zig:115in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_lowering_resolves_a_registered_primary_face_and_refreshes_on_release[function] — test source atlib/ui/src/style/test.zig:64in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_media_environment_filters_sheets_before_cascade[function] — test source atlib/ui/src/style/test.zig:140in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_stylesheet_replacement_resolves_a_new_family_with_equal_cascade_bytes[function] — test source atlib/ui/src/style/test.zig:90in nearest public ownerlib.ui.src.style.test
Complete caller list for style.Engine.record
8 direct callers.
tiny.ui.style.Engine.authoredFallbacks[method] atlib/ui/src/style/generation.zig:310lib.ui.src.style.generation.test_incremental_cascade_agrees_with_a_cold_cascade_after_one_dirty_node[function] — test source atlib/ui/src/style/generation.zig:1089in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_sparse_quota_failure_preserves_the_live_pool_and_node_states[function] — test source atlib/ui/src/style/generation.zig:1223in nearest public ownertiny.ui.style.generationlib.ui.src.style.generation.test_stylesheet_generation_visits_only_indexed_bucket_keys[function] — test source atlib/ui/src/style/generation.zig:1116in nearest public ownertiny.ui.style.generationlib.ui.src.style.test.test_style_lowering_reads_an_inline_font_handle_from_the_admitted_declaration[function] — test source atlib/ui/src/style/test.zig:115in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_lowering_resolves_a_registered_primary_face_and_refreshes_on_release[function] — test source atlib/ui/src/style/test.zig:64in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_style_media_environment_filters_sheets_before_cascade[function] — test source atlib/ui/src/style/test.zig:140in nearest public ownerlib.ui.src.style.testlib.ui.src.style.test.test_stylesheet_replacement_resolves_a_new_family_with_equal_cascade_bytes[function] — test source atlib/ui/src/style/test.zig:90in nearest public ownerlib.ui.src.style.test
Audit
| Definitions | 26 |
|---|---|
| Public names | 45 |
| Members | 55 |
| Version | 26.7.0 |
| Revision | daab053ee433 |