diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index a5ed6f4e7..59d441515 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -46,21 +46,13 @@ const StyleManager = @This(); const Tag = Element.Tag; const Input = Element.Html.Input; -const RuleList = std.MultiArrayList(TrackedRule); frame: *Frame, arena: *lp.Arena, -// Bucketed rules for fast lookup - keyed by rightmost selector part -id_rules: std.StringHashMapUnmanaged(RuleList) = .empty, -class_rules: std.StringHashMapUnmanaged(RuleList) = .empty, -tag_rules: std.AutoHashMapUnmanaged(Tag, RuleList) = .empty, -other_rules: RuleList = .empty, // universal, attribute, pseudo-class endings - -// Own-element results, valid while Page.style_version == memo_version. -memo: Memo = .empty, -memo_version: usize = 0, +visibility: Group(Visibility) = .{}, +geometry: Group(Geometry) = .{}, // Keyed by property name, pruning to what is (hopefully) one or few rules custom_rules: std.StringHashMapUnmanaged(CustomProperty) = .empty, @@ -373,14 +365,14 @@ fn isValidLayerComponent(component: []const u8) bool { return true; } -/// Compute every layer's rank and apply it to every TrackedRule we have. +/// Compute every layer's rank and apply it to every group Rule we have. /// We can only do this now that we've parsed every parsed every sheet since /// @layer statement can change the ordering/ fn finalizeLayerRanks(self: *StyleManager, build_arena: Allocator) Allocator.Error!void { const layers = self.layers.items; if (layers.len == 0) { - // No layers. Every TrackedRule already has the correct layerless + // No layers. Every group Rule already has the correct layerless // priority, and with no layers, there's nothing to adjust. return; } @@ -441,21 +433,8 @@ fn finalizeLayerRanks(self: *StyleManager, build_arena: Allocator) Allocator.Err } } - self.stampRuleList(&self.other_rules); - - var id_it = self.id_rules.valueIterator(); - while (id_it.next()) |rules| { - self.stampRuleList(rules); - } - - var class_it = self.class_rules.valueIterator(); - while (class_it.next()) |rules| { - self.stampRuleList(rules); - } - - var tag_it = self.tag_rules.valueIterator(); - while (tag_it.next()) |rules| { - self.stampRuleList(rules); + inline for (group_fields) |field| { + @field(self, field).stampRules(self); } // Custom rules parse their selectors lazily, but the layer ranks are only @@ -468,12 +447,6 @@ fn finalizeLayerRanks(self: *StyleManager, build_arena: Allocator) Allocator.Err } } -fn stampRuleList(self: *StyleManager, rules: *RuleList) void { - for (rules.items(.priority)) |*priority| { - priority.* |= self.layerRank(priority.*) << RANK_SHIFT; - } -} - /// The rank finalizeLayerRanks still has to stamp for a packed priority, via /// its doc_order: 0 for an unlayered rule, which packed UNLAYERED_RANK at /// creation. @@ -491,18 +464,18 @@ fn addRawRule(self: *StyleManager, build_arena: Allocator, selector_text: []cons return; } var customs: CustomSink = .{ .allocator = build_arena }; - const props = try foldDeclarations(block_text, &customs); + const props = try foldDeclarations(Declarations, block_text, &customs); _ = try self.addSelectorRules(selector_text, props, customs.map.values(), layer); } -// Tracked rules get one TrackedRule per selector (not per selector list) +// Tracked rules get one group Rule per selector (not per selector list) // so each has correct specificity, bucketed by their rightmost selector part. // Custom properties are keyed by name instead, and lazily parse the selector. // Returns whether the rule set any tracked property. fn addSelectorRules( self: *StyleManager, selector_text: []const u8, - props: TrackedProperties, + props: Declarations, customs: []const CustomDeclaration, layer: u16, ) !bool { @@ -540,14 +513,22 @@ fn addSelectorRules( return self.next_doc_order != before; } -fn addTrackedRule(self: *StyleManager, selector: Selector.Selector, props: TrackedProperties, layer: u16) !void { +// One priority per selector, shared by every group it lands in: doc_order +// indexes rule_layers. +fn addTrackedRule(self: *StyleManager, selector: Selector.Selector, props: Declarations, layer: u16) !void { const key = getBucketKey(selector.rightmost()) orelse return; - const priority = try self.nextPriority(layer); - try self.bucket(key, .{ - .props = props, - .selector = selector, - .priority = priority | (@as(u64, computeSpecificity(selector)) << SPEC_SHIFT), - }); + const rank_and_order = try self.nextPriority(layer); + const priority = rank_and_order | (@as(u64, computeSpecificity(selector)) << SPEC_SHIFT); + inline for (group_fields) |field| { + const declared = @field(props, field); + if (declaresAny(declared)) { + try @field(self, field).bucket(self.arena.allocator(), key, .{ + .props = declared, + .selector = selector, + .priority = priority, + }); + } + } } /// Rank and document-order bits of the next rule. A rule outside any layer @@ -561,17 +542,6 @@ fn nextPriority(self: *StyleManager, layer: u16) !u64 { return priority; } -fn bucket(self: *StyleManager, key: BucketKey, rule: TrackedRule) !void { - const allocator = self.arena.allocator(); - const list: *RuleList = switch (key) { - .id => |id| (try self.id_rules.getOrPutValue(allocator, id, .empty)).value_ptr, - .class => |class| (try self.class_rules.getOrPutValue(allocator, class, .empty)).value_ptr, - .tag => |tag| (try self.tag_rules.getOrPutValue(allocator, tag, .empty)).value_ptr, - .other => &self.other_rules, - }; - try list.append(allocator, rule); -} - fn isCustomProperty(name: []const u8) bool { return std.mem.startsWith(u8, name, "--"); } @@ -606,33 +576,21 @@ fn rebuildIfDirty(self: *StyleManager) !void { self.dirty = false; errdefer self.dirty = true; - const id_rules_count = self.id_rules.count(); - const class_rules_count = self.class_rules.count(); - const tag_rules_count = self.tag_rules.count(); - const other_rules_count = self.other_rules.len; - const memo_count = self.memo.count(); + var caps: [group_fields.len]Capacities = undefined; + inline for (group_fields, &caps) |field, *c| { + c.* = @field(self, field).capacities(); + } const custom_rules_count = self.custom_rules.count(); self.arena.resetRetain(); - self.memo = .empty; - try self.memo.ensureTotalCapacity(self.arena.allocator(), memo_count); - self.next_doc_order = 1; self.rule_layers = .empty; self.last_rule_sheet = null; - self.id_rules = .empty; - try self.id_rules.ensureTotalCapacity(self.arena.allocator(), id_rules_count); - - self.class_rules = .empty; - try self.class_rules.ensureTotalCapacity(self.arena.allocator(), class_rules_count); - - self.tag_rules = .empty; - try self.tag_rules.ensureTotalCapacity(self.arena.allocator(), tag_rules_count); - - self.other_rules = .{}; - try self.other_rules.ensureTotalCapacity(self.arena.allocator(), other_rules_count); + inline for (group_fields, caps) |field, c| { + try @field(self, field).reset(self.arena.allocator(), c); + } self.custom_rules = .empty; try self.custom_rules.ensureTotalCapacity(self.arena.allocator(), custom_rules_count); @@ -652,41 +610,11 @@ fn rebuildIfDirty(self: *StyleManager) !void { try self.finalizeLayerRanks(build_arena.allocator()); } -/// Own-element cascade result, resolved for every property at once so one -/// entry serves any probe. -const Props = packed struct(u10) { - // Author value (inline or sheet). Without `author_display` it's the UA - // fallback: .none when matchesUaDisplayNoneRule, else .other. - display: Display = .other, - author_display: bool = false, - visibility_hidden: bool = false, - opacity_zero: bool = false, - pointer_events_none: bool = false, - overflow_x_scrolls: bool = false, - overflow_y_scrolls: bool = false, - overscroll_x_contains: bool = false, - overscroll_y_contains: bool = false, - - fn probe(self: Props, comptime what: Probe, options: CheckVisibilityOptions) bool { - return switch (what) { - .hidden => self.display == .none or - (options.check_visibility and self.visibility_hidden) or - (options.check_opacity and self.opacity_zero), - .visibility => self.visibility_hidden, - .pointer_events => self.pointer_events_none, - }; - } -}; - -const Probe = enum { hidden, visibility, pointer_events }; - -const Memo = std.AutoHashMapUnmanaged(*Element, Props); - pub fn isHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool { self.assertOwns(el); self.rebuildIfDirty() catch return false; if (!options.ancestors) { - return self.ownProps(el).probe(.hidden, options); + return self.visibilityProps(el).probe(.hidden, options); } return self.anyInChain(el, .hidden, options); } @@ -701,7 +629,7 @@ pub fn hasDisplayNone(self: *StyleManager, el: *Element) bool { pub fn display(self: *StyleManager, el: *Element) Display { self.assertOwns(el); self.rebuildIfDirty() catch return .other; - return self.ownProps(el).display; + return self.visibilityProps(el).display; } /// Computed display:none coming only from inline style or an author stylesheet @@ -711,7 +639,7 @@ pub fn display(self: *StyleManager, el: *Element) Display { pub fn hasAuthorDisplayNone(self: *StyleManager, el: *Element) bool { self.assertOwns(el); self.rebuildIfDirty() catch return false; - const p = self.ownProps(el); + const p = self.visibilityProps(el); return p.author_display and p.display == .none; } @@ -735,7 +663,7 @@ pub fn hasPointerEventsNone(self: *StyleManager, el: *Element) bool { /// on that axis is auto, scroll or overlay. No ancestor walk. pub fn overflowAxes(self: *StyleManager, el: *Element) Element.ScrollAxes { self.rebuildIfDirty() catch return .{}; - const p = self.ownProps(el); + const p = self.geometryProps(el); return .{ .x = p.overflow_x_scrolls, .y = p.overflow_y_scrolls }; } @@ -744,123 +672,213 @@ pub fn overflowAxes(self: *StyleManager, el: *Element) Element.ScrollAxes { /// walk. pub fn overscrollContainAxes(self: *StyleManager, el: *Element) Element.ScrollAxes { self.rebuildIfDirty() catch return .{}; - const p = self.ownProps(el); + const p = self.geometryProps(el); return .{ .x = p.overscroll_x_contains, .y = p.overscroll_y_contains }; } -fn anyInChain(self: *StyleManager, el: *Element, comptime what: Probe, options: CheckVisibilityOptions) bool { +fn anyInChain(self: *StyleManager, el: *Element, comptime what: Visibility.Probe, options: CheckVisibilityOptions) bool { var current: ?*Element = el; while (current) |elem| : (current = elem.parentElement()) { - if (self.ownProps(elem).probe(what, options)) { + if (self.visibilityProps(elem).probe(what, options)) { return true; } } return false; } -/// The memoized own-element result. Callers must have run rebuildIfDirty, -/// which resets the memo. -fn ownProps(self: *StyleManager, el: *Element) Props { - const version = self.frame.page.style_version; - if (self.memo_version != version) { - self.memo.clearRetainingCapacity(); - self.memo_version = version; - } - - const gop = self.memo.getOrPut(self.arena.allocator(), el) catch |err| { - log.warn(.browser, "StyleManager memo", .{ .err = err }); - return self.compute(el); - }; - if (gop.found_existing) { - return gop.value_ptr.*; - } - gop.value_ptr.* = self.compute(el); - return gop.value_ptr.*; +/// Callers must have run rebuildIfDirty, which resets the memo. +fn visibilityProps(self: *StyleManager, el: *Element) Visibility.Computed { + return self.visibility.ownProps(self.arena.allocator(), el, self.frame); } -const property_fields = std.meta.fieldNames(TrackedProperties); +/// Callers must have run rebuildIfDirty, which resets the memo. +fn geometryProps(self: *StyleManager, el: *Element) Geometry.Computed { + return self.geometry.ownProps(self.arena.allocator(), el, self.frame); +} -const Priorities = struct { - display: u64 = 0, - visibility_hidden: u64 = 0, - opacity_zero: u64 = 0, - pointer_events_none: u64 = 0, - overflow_x_scrolls: u64 = 0, - overflow_y_scrolls: u64 = 0, - overscroll_x_contains: u64 = 0, - overscroll_y_contains: u64 = 0, +/// Taken before the arena resets, to presize the new containers. +const Capacities = struct { + id_rules: u32, + class_rules: u32, + tag_rules: u32, + other_rules: usize, + memo: u32, }; -fn compute(self: *StyleManager, el: *Element) Props { - const frame = self.frame; - var p: Props = .{}; - var priorities: Priorities = .{}; +/// One property group's rules and memo. A rule joins only the groups it +/// declares something in. +fn Group(comptime Spec: type) type { + return struct { + const Self = @This(); + const Declared = Spec.Declared; + const Computed = Spec.Computed; + const Field = std.meta.FieldEnum(Declared); + const fields = std.meta.fieldNames(Declared); - const inline_props = inlineProps(el, frame); - inline for (property_fields) |field| { - if (@field(inline_props, field)) |value| { - @field(p, field) = value; - @field(priorities, field) = INLINE_PRIORITY; - } - } - - if (el.getId()) |id| { - if (self.id_rules.get(id)) |rules| { - checkRules(&rules, &p, &priorities, el, frame); - } - } - - if (el.getClassName()) |class_attr| { - var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); - while (it.next()) |class| { - if (self.class_rules.get(class)) |rules| { - checkRules(&rules, &p, &priorities, el, frame); + comptime { + // compute copies each declared value into its Computed namesake + for (fields) |field| { + std.debug.assert(@FieldType(Computed, field) == @typeInfo(@FieldType(Declared, field)).optional.child); } } - } - if (self.tag_rules.get(el.getTag())) |rules| { - checkRules(&rules, &p, &priorities, el, frame); - } + const Rule = struct { + selector: Selector.Selector, // Single selector, not a list + props: Declared, - checkRules(&self.other_rules, &p, &priorities, el, frame); + // Packed priority: layer_rank:12 | specificity:30 | doc_order:22. A + // layered rule's rank bits stay 0 until finalizeLayerRanks knows + // every layer and stamps them from rule_layers[doc_order - 1]. + priority: u64, + }; - // UA stylesheet display:none fallback (HTML Rendering §15.3.1 "Hidden - // elements"). Applied only when no author rule for `display` matched the - // element — per CSS Cascade §6.1 any normal-origin author rule beats UA - // origin regardless of specificity, so `.x { display: flex }` on a - // `