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
- // `
` must report visible.
- p.author_display = priorities.display != 0;
- if (!p.author_display and matchesUaDisplayNoneRule(el)) {
- p.display = .none;
- }
+ const RuleList = std.MultiArrayList(Rule);
- return p;
-}
+ // 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
-fn checkRules(rules: *const RuleList, p: *Props, priorities: *Priorities, el: *Element, frame: *Frame) void {
- for (rules.items(.priority), rules.items(.props), rules.items(.selector)) |priority, rule, selector| {
- // Only rules that set a property nothing stronger has set yet are
- // worth matching.
- var relevant = false;
- inline for (property_fields) |field| {
- if (@field(rule, field) != null and priority > @field(priorities, field)) {
- relevant = true;
- }
- }
- if (!relevant or !matchesSelector(el, selector, frame)) {
- continue;
+ // Own-element results, valid while Page.style_version == memo_version.
+ memo: std.AutoHashMapUnmanaged(*Element, Computed) = .empty,
+ memo_version: usize = 0,
+
+ fn capacities(self: *const Self) Capacities {
+ return .{
+ .id_rules = self.id_rules.count(),
+ .class_rules = self.class_rules.count(),
+ .tag_rules = self.tag_rules.count(),
+ .other_rules = self.other_rules.len,
+ .memo = self.memo.count(),
+ };
}
- inline for (property_fields) |field| {
- if (@field(rule, field)) |value| {
- if (priority > @field(priorities, field)) {
- @field(p, field) = value;
- @field(priorities, field) = priority;
+ fn reset(self: *Self, allocator: Allocator, caps: Capacities) !void {
+ self.memo = .empty;
+ try self.memo.ensureTotalCapacity(allocator, caps.memo);
+
+ self.id_rules = .empty;
+ try self.id_rules.ensureTotalCapacity(allocator, caps.id_rules);
+
+ self.class_rules = .empty;
+ try self.class_rules.ensureTotalCapacity(allocator, caps.class_rules);
+
+ self.tag_rules = .empty;
+ try self.tag_rules.ensureTotalCapacity(allocator, caps.tag_rules);
+
+ self.other_rules = .{};
+ try self.other_rules.ensureTotalCapacity(allocator, caps.other_rules);
+ }
+
+ fn bucket(self: *Self, allocator: Allocator, key: BucketKey, rule: Rule) !void {
+ 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 stampRules(self: *Self, sm: *const StyleManager) void {
+ stampRuleList(sm, &self.other_rules);
+ inline for (.{ &self.id_rules, &self.class_rules, &self.tag_rules }) |map| {
+ var it = map.valueIterator();
+ while (it.next()) |rules| {
+ stampRuleList(sm, rules);
}
}
}
- }
+
+ fn stampRuleList(sm: *const StyleManager, rules: *RuleList) void {
+ for (rules.items(.priority)) |*priority| {
+ priority.* |= sm.layerRank(priority.*) << RANK_SHIFT;
+ }
+ }
+
+ fn ownProps(self: *Self, allocator: Allocator, el: *Element, frame: *Frame) Computed {
+ const version = frame.page.style_version;
+ if (self.memo_version != version) {
+ self.memo.clearRetainingCapacity();
+ self.memo_version = version;
+ }
+
+ const gop = self.memo.getOrPut(allocator, el) catch |err| {
+ log.warn(.browser, "StyleManager memo", .{ .err = err });
+ return self.compute(el, frame);
+ };
+ if (gop.found_existing) {
+ return gop.value_ptr.*;
+ }
+ gop.value_ptr.* = self.compute(el, frame);
+ return gop.value_ptr.*;
+ }
+
+ fn compute(self: *const Self, el: *Element, frame: *Frame) Computed {
+ var p: Computed = .{};
+ var priorities: Priorities(Declared) = .initFill(0);
+
+ const inline_declared = inlineDeclared(Declared, el, frame);
+ inline for (fields) |field| {
+ if (@field(inline_declared, field)) |value| {
+ @field(p, field) = value;
+ priorities.set(@field(Field, 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);
+ }
+ }
+ }
+
+ if (self.tag_rules.get(el.getTag())) |rules| {
+ checkRules(&rules, &p, &priorities, el, frame);
+ }
+
+ checkRules(&self.other_rules, &p, &priorities, el, frame);
+
+ if (@hasDecl(Spec, "finish")) {
+ Spec.finish(&p, el, &priorities);
+ }
+ return p;
+ }
+
+ fn checkRules(rules: *const RuleList, p: *Computed, priorities: *Priorities(Declared), el: *Element, frame: *Frame) void {
+ for (rules.items(.priority), rules.items(.props), rules.items(.selector)) |priority, rule, selector| {
+ // Only rules that set a property nothing stronger has set yet are
+ // worth matching.
+ var relevant = false;
+ inline for (fields) |field| {
+ if (@field(rule, field) != null and priority > priorities.get(@field(Field, field))) {
+ relevant = true;
+ }
+ }
+ if (!relevant or !matchesSelector(el, selector, frame)) {
+ continue;
+ }
+
+ inline for (fields) |field| {
+ if (@field(rule, field)) |value| {
+ if (priority > priorities.get(@field(Field, field))) {
+ @field(p, field) = value;
+ priorities.set(@field(Field, field), priority);
+ }
+ }
+ }
+ }
+ }
+ };
}
/// Centralizes UA-stylesheet display:none truth so `getComputedStyle().display`
@@ -905,7 +923,7 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !bool {
return false;
}
const style = style_rule._style orelse return false;
- const props = extractTrackedProperties(style);
+ const props = extractDeclared(Declarations, style);
const customs = try self.extractCustomDeclarations(style);
// A custom rule holds selector_text until the property is looked up, but it
@@ -1032,14 +1050,11 @@ fn getBucketKey(compound: Selector.Compound) ?BucketKey {
return best_key;
}
-// The declaration names behind TrackedProperties, in field order.
-const property_names = [_][]const u8{ "display", "visibility", "opacity", "pointer-events", "overflow-x", "overflow-y", "overscroll-behavior-x", "overscroll-behavior-y" };
-
/// Extracts the tracked properties from a style declaration. The object holds
/// one entry per name in first-declared order, so folding it in order gives a
/// shorthand and its longhands the same precedence as the source text.
-fn extractTrackedProperties(style: *CSSStyleProperties) TrackedProperties {
- var slots: Slots = .{};
+fn extractDeclared(comptime Declared: type, style: *CSSStyleProperties) Declared {
+ var slots: Slots(Declared) = .{};
var it = style.asCSSStyleDeclaration().iterator();
while (it.next()) |property| {
slots.apply(property._name.str(), property._value.str(), property._important);
@@ -1119,53 +1134,126 @@ pub const Display = enum(u2) {
}
};
-const TrackedProperties = struct {
- display: ?Display = null,
- visibility_hidden: ?bool = null,
- opacity_zero: ?bool = null,
- pointer_events_none: ?bool = null,
- overflow_x_scrolls: ?bool = null,
- overflow_y_scrolls: ?bool = null,
- overscroll_x_contains: ?bool = null,
- overscroll_y_contains: ?bool = null,
+/// Whether an element is rendered and takes pointer input.
+const Visibility = struct {
+ const Declared = struct {
+ const names = [_][]const u8{ "display", "visibility", "opacity", "pointer-events" };
- fn apply(self: *TrackedProperties, name: []const u8, value: []const u8) void {
- if (std.ascii.eqlIgnoreCase(name, "display")) {
- self.display = Display.parse(value);
- } else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
- self.visibility_hidden = std.ascii.eqlIgnoreCase(value, "hidden") or std.ascii.eqlIgnoreCase(value, "collapse");
- } else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
- self.opacity_zero = std.ascii.eqlIgnoreCase(value, "0");
- } else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) {
- self.pointer_events_none = std.ascii.eqlIgnoreCase(value, "none");
- } else if (std.ascii.eqlIgnoreCase(name, "overflow-x")) {
- self.overflow_x_scrolls = overflowScrolls(value);
- } else if (std.ascii.eqlIgnoreCase(name, "overflow-y")) {
- self.overflow_y_scrolls = overflowScrolls(value);
- } else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-x")) {
- self.overscroll_x_contains = overscrollContains(value);
- } else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-y")) {
- self.overscroll_y_contains = overscrollContains(value);
+ display: ?Display = null,
+ visibility_hidden: ?bool = null,
+ opacity_zero: ?bool = null,
+ pointer_events_none: ?bool = null,
+
+ fn apply(self: *Declared, name: []const u8, value: []const u8) void {
+ if (std.ascii.eqlIgnoreCase(name, "display")) {
+ self.display = Display.parse(value);
+ } else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
+ self.visibility_hidden = std.ascii.eqlIgnoreCase(value, "hidden") or std.ascii.eqlIgnoreCase(value, "collapse");
+ } else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
+ self.opacity_zero = std.ascii.eqlIgnoreCase(value, "0");
+ } else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) {
+ self.pointer_events_none = std.ascii.eqlIgnoreCase(value, "none");
+ }
+ }
+ };
+
+ const Computed = packed struct(u6) {
+ // 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,
+
+ fn probe(self: Computed, 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 };
+
+ // 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
+ // `
` must report visible.
+ fn finish(p: *Computed, el: *Element, priorities: *const Priorities(Declared)) void {
+ p.author_display = priorities.get(.display) != 0;
+ if (!p.author_display and matchesUaDisplayNoneRule(el)) {
+ p.display = .none;
+ }
+ }
+};
+
+/// How an element's box scrolls.
+const Geometry = struct {
+ const Declared = struct {
+ const names = [_][]const u8{ "overflow-x", "overflow-y", "overscroll-behavior-x", "overscroll-behavior-y" };
+
+ overflow_x_scrolls: ?bool = null,
+ overflow_y_scrolls: ?bool = null,
+ overscroll_x_contains: ?bool = null,
+ overscroll_y_contains: ?bool = null,
+
+ fn apply(self: *Declared, name: []const u8, value: []const u8) void {
+ if (std.ascii.eqlIgnoreCase(name, "overflow-x")) {
+ self.overflow_x_scrolls = overflowScrolls(value);
+ } else if (std.ascii.eqlIgnoreCase(name, "overflow-y")) {
+ self.overflow_y_scrolls = overflowScrolls(value);
+ } else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-x")) {
+ self.overscroll_x_contains = overscrollContains(value);
+ } else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-y")) {
+ self.overscroll_y_contains = overscrollContains(value);
+ }
+ }
+
+ // `overlay` is Chrome's legacy alias of auto.
+ fn overflowScrolls(value: []const u8) bool {
+ return std.ascii.eqlIgnoreCase(value, "auto") or
+ std.ascii.eqlIgnoreCase(value, "scroll") or
+ std.ascii.eqlIgnoreCase(value, "overlay");
+ }
+
+ // `contain` keeps the scroll in the box, `none` also kills the bounce we
+ // don't render anyway; only `auto` lets a scroll chain outward.
+ fn overscrollContains(value: []const u8) bool {
+ return std.ascii.eqlIgnoreCase(value, "contain") or
+ std.ascii.eqlIgnoreCase(value, "none");
+ }
+ };
+
+ const Computed = packed struct(u4) {
+ overflow_x_scrolls: bool = false,
+ overflow_y_scrolls: bool = false,
+ overscroll_x_contains: bool = false,
+ overscroll_y_contains: bool = false,
+ };
+};
+
+/// Every group's share of one declaration block, so a sheet's block is folded
+/// once. Field names match the StyleManager's group fields.
+const Declarations = struct {
+ const names = Visibility.Declared.names ++ Geometry.Declared.names;
+
+ visibility: Visibility.Declared = .{},
+ geometry: Geometry.Declared = .{},
+
+ fn apply(self: *Declarations, name: []const u8, value: []const u8) void {
+ inline for (group_fields) |field| {
+ @field(self, field).apply(name, value);
}
}
- // `overlay` is Chrome's legacy alias of auto.
- fn overflowScrolls(value: []const u8) bool {
- return std.ascii.eqlIgnoreCase(value, "auto") or
- std.ascii.eqlIgnoreCase(value, "scroll") or
- std.ascii.eqlIgnoreCase(value, "overlay");
- }
-
- // `contain` keeps the scroll in the box, `none` also kills the bounce we
- // don't render anyway; only `auto` lets a scroll chain outward.
- fn overscrollContains(value: []const u8) bool {
- return std.ascii.eqlIgnoreCase(value, "contain") or
- std.ascii.eqlIgnoreCase(value, "none");
- }
-
- fn isRelevant(self: TrackedProperties) bool {
- inline for (property_fields) |field| {
- if (@field(self, field) != null) {
+ fn isRelevant(self: Declarations) bool {
+ inline for (group_fields) |field| {
+ if (declaresAny(@field(self, field))) {
return true;
}
}
@@ -1173,15 +1261,22 @@ const TrackedProperties = struct {
}
};
-const TrackedRule = struct {
- selector: Selector.Selector, // Single selector, not a list
- props: TrackedProperties,
+const group_fields = std.meta.fieldNames(Declarations);
- // 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,
-};
+fn declaresAny(declared: anytype) bool {
+ inline for (comptime std.meta.fieldNames(@TypeOf(declared))) |field| {
+ if (@field(declared, field) != null) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Per property, the priority of the declaration winning so far; 0 while
+/// none has.
+fn Priorities(comptime Declared: type) type {
+ return std.enums.EnumArray(std.meta.FieldEnum(Declared), u64);
+}
// custom_rules map is property_name -> CustomProperty, loosely:
// --color => [(:root, "#ff0"), (.card, "#000")]
@@ -1203,7 +1298,7 @@ const CustomRule = struct {
const ParsedCustomRule = struct {
selector: Selector.Selector,
value: []const u8,
- priority: u64, // packed the same way as TrackedRule.priority
+ priority: u64, // packed the same way as a group Rule.priority
};
/// A `--*` declaration on its way from a rule block into custom_rules.
@@ -1238,7 +1333,7 @@ const UNLAYERED_RANK: u32 = std.math.maxInt(u12);
// its 12 bits of VisibleRule.priority
const MAX_LAYERS: usize = 1024;
-// TrackedRule.priority field offsets (layer_rank:12 | spec:30 | doc:22).
+// Group Rule.priority field offsets (layer_rank:12 | spec:30 | doc:22).
const SPEC_SHIFT: u6 = 22;
const RANK_SHIFT: u6 = 52;
@@ -1260,17 +1355,17 @@ const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
// `frame` must be el's owner frame (el.ownerFrame): that is the map where a
// parsed inline style lives. Without one the attribute text is folded in
// place; layout materializes the object itself when it needs it.
-fn inlineProps(el: *Element, frame: *Frame) TrackedProperties {
+fn inlineDeclared(comptime Declared: type, el: *Element, frame: *Frame) Declared {
if (!el._flags.has_inline_style) {
// Neither a style object nor a style attribute; skip both lookups.
return .{};
}
if (el.existingStyle(frame)) |style| {
- return extractTrackedProperties(style);
+ return extractDeclared(Declared, style);
}
const attr = el.getAttributeInterned("style") orelse return .{};
// Without a sink nothing allocates
- return foldDeclarations(attr, null) catch unreachable;
+ return foldDeclarations(Declared, attr, null) catch unreachable;
}
/// `--*` declarations of one block, keyed by name so a block declaring the same
@@ -1285,8 +1380,8 @@ const CustomSink = struct {
// earlier !important one, and an empty value removes the property. The tracked
// properties are matched case-insensitively; a custom property's name is
// case-sensitive and goes to `customs` when there is one.
-fn foldDeclarations(block: []const u8, customs: ?*CustomSink) !TrackedProperties {
- var slots: Slots = .{};
+fn foldDeclarations(comptime Declared: type, block: []const u8, customs: ?*CustomSink) !Declared {
+ var slots: Slots(Declared) = .{};
var it = CssParser.parseDeclarationsList(block);
while (it.next()) |declaration| {
if (isCustomProperty(declaration.name)) {
@@ -1303,51 +1398,55 @@ fn foldDeclarations(block: []const u8, customs: ?*CustomSink) !TrackedProperties
return slots.props();
}
-/// One block's winning value per tracked property, folded in declaration
-/// order.
-const Slots = struct {
- const Slot = struct {
- value: ?[]const u8 = null,
- important: bool = false,
+/// One block's winning value per property in `Declared.names`, folded in
+/// declaration order.
+fn Slots(comptime Declared: type) type {
+ return struct {
+ const Self = @This();
- fn apply(self: *Slot, value: []const u8, important: bool) void {
- if (self.important and !important) {
+ slots: [Declared.names.len]Slot = @splat(.{}),
+
+ fn apply(self: *Self, name: []const u8, value: []const u8, important: bool) void {
+ if (CssParser.axisShorthand(name)) |shorthand| {
+ const values = CssParser.splitAxisPair(value) orelse return;
+ self.apply(shorthand.x, values.x, important);
+ self.apply(shorthand.y, values.y, important);
return;
}
- if (value.len == 0) {
- self.* = .{};
- return;
+ for (Declared.names, &self.slots) |tracked, *slot| {
+ if (std.ascii.eqlIgnoreCase(name, tracked)) {
+ slot.apply(value, important);
+ return;
+ }
}
- self.value = value;
- self.important = important;
+ }
+
+ fn props(self: Self) Declared {
+ var p: Declared = .{};
+ for (Declared.names, self.slots) |name, s| {
+ if (s.value) |value| {
+ p.apply(name, value);
+ }
+ }
+ return p;
}
};
+}
- slots: [property_names.len]Slot = @splat(.{}),
+const Slot = struct {
+ value: ?[]const u8 = null,
+ important: bool = false,
- fn apply(self: *Slots, name: []const u8, value: []const u8, important: bool) void {
- if (CssParser.axisShorthand(name)) |shorthand| {
- const values = CssParser.splitAxisPair(value) orelse return;
- self.apply(shorthand.x, values.x, important);
- self.apply(shorthand.y, values.y, important);
+ fn apply(self: *Slot, value: []const u8, important: bool) void {
+ if (self.important and !important) {
return;
}
- for (property_names, &self.slots) |tracked, *slot| {
- if (std.ascii.eqlIgnoreCase(name, tracked)) {
- slot.apply(value, important);
- return;
- }
+ if (value.len == 0) {
+ self.* = .{};
+ return;
}
- }
-
- fn props(self: Slots) TrackedProperties {
- var p: TrackedProperties = .{};
- for (property_names, self.slots) |name, s| {
- if (s.value) |value| {
- p.apply(name, value);
- }
- }
- return p;
+ self.value = value;
+ self.important = important;
}
};
@@ -1664,7 +1763,7 @@ test "StyleManager: packed priority bounds" {
try testing.expect(MAX_LAYERS < UNLAYERED_RANK);
}
-test "StyleManager: inlineProps: scan matches the parsed style object" {
+test "StyleManager: inlineDeclared: scan matches the parsed style object" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
@@ -1688,23 +1787,23 @@ test "StyleManager: inlineProps: scan matches the parsed style object" {
\\
\\
);
- const expected = [_]TrackedProperties{
- .{ .display = .none },
- .{ .display = .none },
- .{ .display = .none },
- .{ .display = .other },
- .{ .display = .none, .visibility_hidden = true },
- .{ .opacity_zero = true, .pointer_events_none = true },
- .{ .opacity_zero = false, .pointer_events_none = true },
- .{ .visibility_hidden = true },
+ const expected = [_]Declarations{
+ .{ .visibility = .{ .display = .none } },
+ .{ .visibility = .{ .display = .none } },
+ .{ .visibility = .{ .display = .none } },
+ .{ .visibility = .{ .display = .other } },
+ .{ .visibility = .{ .display = .none, .visibility_hidden = true } },
+ .{ .visibility = .{ .opacity_zero = true, .pointer_events_none = true } },
+ .{ .visibility = .{ .opacity_zero = false, .pointer_events_none = true } },
+ .{ .visibility = .{ .visibility_hidden = true } },
.{},
.{},
- .{ .overflow_x_scrolls = true, .overflow_y_scrolls = true },
- .{ .overflow_x_scrolls = false, .overflow_y_scrolls = true },
- .{ .overflow_x_scrolls = false, .overflow_y_scrolls = true },
- .{ .overflow_x_scrolls = false, .overflow_y_scrolls = false },
- .{ .overflow_x_scrolls = false, .overflow_y_scrolls = true },
- .{ .overflow_x_scrolls = true },
+ .{ .geometry = .{ .overflow_x_scrolls = true, .overflow_y_scrolls = true } },
+ .{ .geometry = .{ .overflow_x_scrolls = false, .overflow_y_scrolls = true } },
+ .{ .geometry = .{ .overflow_x_scrolls = false, .overflow_y_scrolls = true } },
+ .{ .geometry = .{ .overflow_x_scrolls = false, .overflow_y_scrolls = false } },
+ .{ .geometry = .{ .overflow_x_scrolls = false, .overflow_y_scrolls = true } },
+ .{ .geometry = .{ .overflow_x_scrolls = true } },
.{},
};
@@ -1712,14 +1811,14 @@ test "StyleManager: inlineProps: scan matches the parsed style object" {
var child = div.asNode().firstChild();
while (child) |node| : (child = node.nextSibling()) {
const el = node.is(Element) orelse continue;
- const scanned = inlineProps(el, frame);
+ const scanned = inlineDeclared(Declarations, el, frame);
+ try testing.expectEqual(expected[i], scanned);
+ try testing.expectEqual(expected[i].visibility, inlineDeclared(Visibility.Declared, el, frame));
+ try testing.expectEqual(expected[i].geometry, inlineDeclared(Geometry.Declared, el, frame));
// scanning never creates the style object
try testing.expectEqual(null, el.existingStyle(frame));
- const materialized = extractTrackedProperties(try el.getOrCreateStyle(frame));
- inline for (property_fields) |field| {
- try testing.expectEqual(@field(expected[i], field), @field(scanned, field));
- try testing.expectEqual(@field(expected[i], field), @field(materialized, field));
- }
+ const materialized = extractDeclared(Declarations, try el.getOrCreateStyle(frame));
+ try testing.expectEqual(expected[i], materialized);
i += 1;
}
try testing.expectEqual(expected.len, i);
@@ -1739,9 +1838,10 @@ test "StyleManager: memo: reuse and invalidation" {
// The walk memoizes the element and every ancestor
try testing.expectEqual(false, sm.isHidden(b, .{}));
- try testing.expectEqual(3, sm.memo.count());
+ try testing.expectEqual(3, sm.visibility.memo.count());
+ try testing.expectEqual(0, sm.geometry.memo.count());
try testing.expectEqual(false, sm.isHidden(b, .{}));
- try testing.expectEqual(3, sm.memo.count());
+ try testing.expectEqual(3, sm.visibility.memo.count());
// Probes never create the style object
try testing.expectEqual(null, b.existingStyle(frame));
@@ -1790,7 +1890,7 @@ test "StyleManager: memo: reuse and invalidation" {
// A stylesheet change resets the memo
sm.sheetModified();
try testing.expectEqual(false, sm.isHidden(p, .{}));
- try testing.expectEqual(2, sm.memo.count());
+ try testing.expectEqual(2, sm.visibility.memo.count());
}
test "StyleManager: ruleInserted: append joins the buckets without a rebuild" {