mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
Merge pull request #1933 from lightpanda-io/css-improvements-perf3
Optimize CSS visibility engine with lazy parsing and cache-friendly evaluation
This commit is contained in:
@@ -45,7 +45,7 @@ pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
|
|||||||
const StyleManager = @This();
|
const StyleManager = @This();
|
||||||
|
|
||||||
const Tag = Element.Tag;
|
const Tag = Element.Tag;
|
||||||
const RuleList = std.ArrayList(VisibilityRule);
|
const RuleList = std.MultiArrayList(VisibilityRule);
|
||||||
|
|
||||||
page: *Page,
|
page: *Page,
|
||||||
|
|
||||||
@@ -74,13 +74,78 @@ pub fn deinit(self: *StyleManager) void {
|
|||||||
self.page.releaseArena(self.arena);
|
self.page.releaseArena(self.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sheetAdded(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
||||||
const css_rules = sheet._css_rules orelse return;
|
if (sheet._css_rules) |css_rules| {
|
||||||
|
|
||||||
for (css_rules._rules.items) |rule| {
|
for (css_rules._rules.items) |rule| {
|
||||||
const style_rule = rule.is(CSSStyleRule) orelse continue;
|
const style_rule = rule.is(CSSStyleRule) orelse continue;
|
||||||
try self.addRule(style_rule);
|
try self.addRule(style_rule);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner_node = sheet.getOwnerNode() orelse return;
|
||||||
|
if (owner_node.is(Element.Html.Style)) |style| {
|
||||||
|
const text = try style.asNode().getTextContentAlloc(self.arena);
|
||||||
|
var it = CssParser.parseStylesheet(text);
|
||||||
|
while (it.next()) |parsed_rule| {
|
||||||
|
try self.addRawRule(parsed_rule.selector, parsed_rule.block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []const u8) !void {
|
||||||
|
if (selector_text.len == 0) return;
|
||||||
|
|
||||||
|
var props = VisibilityProperties{};
|
||||||
|
var it = CssParser.parseDeclarationsList(block_text);
|
||||||
|
while (it.next()) |decl| {
|
||||||
|
const name = decl.name;
|
||||||
|
const val = decl.value;
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "display")) {
|
||||||
|
props.display_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
|
||||||
|
props.visibility_hidden = std.ascii.eqlIgnoreCase(val, "hidden") or std.ascii.eqlIgnoreCase(val, "collapse");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
|
||||||
|
props.opacity_zero = std.ascii.eqlIgnoreCase(val, "0");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) {
|
||||||
|
props.pointer_events_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.isRelevant()) return;
|
||||||
|
|
||||||
|
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||||
|
for (selectors) |selector| {
|
||||||
|
const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first;
|
||||||
|
const bucket_key = getBucketKey(rightmost) orelse continue;
|
||||||
|
const rule = VisibilityRule{
|
||||||
|
.props = props,
|
||||||
|
.selector = selector,
|
||||||
|
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||||
|
};
|
||||||
|
self.next_doc_order += 1;
|
||||||
|
|
||||||
|
switch (bucket_key) {
|
||||||
|
.id => |id| {
|
||||||
|
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.other => {
|
||||||
|
try self.other_rules.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sheetRemoved(self: *StyleManager) void {
|
pub fn sheetRemoved(self: *StyleManager) void {
|
||||||
@@ -103,27 +168,28 @@ fn rebuildIfDirty(self: *StyleManager) !void {
|
|||||||
const id_rules_count = self.id_rules.count();
|
const id_rules_count = self.id_rules.count();
|
||||||
const class_rules_count = self.class_rules.count();
|
const class_rules_count = self.class_rules.count();
|
||||||
const tag_rules_count = self.tag_rules.count();
|
const tag_rules_count = self.tag_rules.count();
|
||||||
const other_rules_count = self.other_rules.items.len;
|
const other_rules_count = self.other_rules.len;
|
||||||
|
|
||||||
self.page._session.arena_pool.resetRetain(self.arena);
|
self.page._session.arena_pool.resetRetain(self.arena);
|
||||||
|
|
||||||
self.next_doc_order = 0;
|
self.next_doc_order = 0;
|
||||||
|
|
||||||
self.id_rules = .empty;
|
self.id_rules = .empty;
|
||||||
try self.id_rules.ensureUnusedCapacity(self.arena, id_rules_count);
|
try self.id_rules.ensureTotalCapacity(self.arena, id_rules_count);
|
||||||
|
|
||||||
self.class_rules = .empty;
|
self.class_rules = .empty;
|
||||||
try self.class_rules.ensureUnusedCapacity(self.arena, class_rules_count);
|
try self.class_rules.ensureTotalCapacity(self.arena, class_rules_count);
|
||||||
|
|
||||||
self.tag_rules = .empty;
|
self.tag_rules = .empty;
|
||||||
try self.tag_rules.ensureUnusedCapacity(self.arena, tag_rules_count);
|
try self.tag_rules.ensureTotalCapacity(self.arena, tag_rules_count);
|
||||||
|
|
||||||
self.other_rules = try .initCapacity(self.arena, other_rules_count);
|
self.other_rules = .{};
|
||||||
|
try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count);
|
||||||
|
|
||||||
const sheets = self.page.document._style_sheets orelse return;
|
const sheets = self.page.document._style_sheets orelse return;
|
||||||
for (sheets._sheets.items) |sheet| {
|
for (sheets._sheets.items) |sheet| {
|
||||||
self.sheetAdded(sheet) catch |err| {
|
self.parseSheet(sheet) catch |err| {
|
||||||
log.err(.browser, "StyleManager sheetAdded", .{ .err = err });
|
log.err(.browser, "StyleManager parseSheet", .{ .err = err });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -165,25 +231,25 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt
|
|||||||
|
|
||||||
/// Check if a single element (not ancestors) is hidden.
|
/// Check if a single element (not ancestors) is hidden.
|
||||||
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
|
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
|
||||||
// Track best match per property (value + specificity)
|
// Track best match per property (value + priority)
|
||||||
// Initialize spec to INLINE_SPECIFICITY for properties we don't care about - this makes
|
// Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes
|
||||||
// the loop naturally skip them since no stylesheet rule can have specificity >= INLINE_SPECIFICITY
|
// the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY
|
||||||
var display_none: ?bool = null;
|
var display_none: ?bool = null;
|
||||||
var display_spec: u32 = 0;
|
var display_priority: u64 = 0;
|
||||||
|
|
||||||
var visibility_hidden: ?bool = null;
|
var visibility_hidden: ?bool = null;
|
||||||
var visibility_spec: u32 = 0;
|
var visibility_priority: u64 = 0;
|
||||||
|
|
||||||
var opacity_zero: ?bool = null;
|
var opacity_zero: ?bool = null;
|
||||||
var opacity_spec: u32 = 0;
|
var opacity_priority: u64 = 0;
|
||||||
|
|
||||||
// Check inline styles FIRST - they use INLINE_SPECIFICITY so no stylesheet can beat them
|
// Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them
|
||||||
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
|
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
|
||||||
if (property._value.eql(comptime .wrap("none"))) {
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
return true; // Early exit for hiding value
|
return true; // Early exit for hiding value
|
||||||
}
|
}
|
||||||
display_none = false;
|
display_none = false;
|
||||||
display_spec = INLINE_SPECIFICITY;
|
display_priority = INLINE_PRIORITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.check_visibility) {
|
if (options.check_visibility) {
|
||||||
@@ -192,13 +258,13 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
visibility_hidden = false;
|
visibility_hidden = false;
|
||||||
visibility_spec = INLINE_SPECIFICITY;
|
visibility_priority = INLINE_PRIORITY;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This can't be beat. Setting this means that, when checking rules
|
// This can't be beat. Setting this means that, when checking rules
|
||||||
// we no longer have to check if options.check_visibility is enabled.
|
// we no longer have to check if options.check_visibility is enabled.
|
||||||
// We can just compare the specificity.
|
// We can just compare the priority.
|
||||||
visibility_spec = INLINE_SPECIFICITY;
|
visibility_priority = INLINE_PRIORITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.check_opacity) {
|
if (options.check_opacity) {
|
||||||
@@ -207,87 +273,86 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
opacity_zero = false;
|
opacity_zero = false;
|
||||||
opacity_spec = INLINE_SPECIFICITY;
|
opacity_priority = INLINE_PRIORITY;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
opacity_spec = INLINE_SPECIFICITY;
|
opacity_priority = INLINE_PRIORITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display_spec == INLINE_SPECIFICITY and visibility_spec == INLINE_SPECIFICITY and opacity_spec == INLINE_SPECIFICITY) {
|
if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.rebuildIfDirty() catch return false;
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
// Track doc_order for tie-breaking (0 = inline style, which always wins)
|
|
||||||
var display_doc_order: u32 = 0;
|
|
||||||
var visibility_doc_order: u32 = 0;
|
|
||||||
var opacity_doc_order: u32 = 0;
|
|
||||||
|
|
||||||
// Helper to check a single rule
|
// Helper to check a single rule
|
||||||
const Ctx = struct {
|
const Ctx = struct {
|
||||||
display_none: *?bool,
|
display_none: *?bool,
|
||||||
display_spec: *u32,
|
display_priority: *u64,
|
||||||
display_doc_order: *u32,
|
|
||||||
visibility_hidden: *?bool,
|
visibility_hidden: *?bool,
|
||||||
visibility_spec: *u32,
|
visibility_priority: *u64,
|
||||||
visibility_doc_order: *u32,
|
|
||||||
opacity_zero: *?bool,
|
opacity_zero: *?bool,
|
||||||
opacity_spec: *u32,
|
opacity_priority: *u64,
|
||||||
opacity_doc_order: *u32,
|
|
||||||
el: *Element,
|
el: *Element,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
|
|
||||||
// Returns true if (spec, doc_order) beats (best_spec, best_doc_order)
|
fn checkRules(ctx: @This(), rules: *const RuleList) void {
|
||||||
fn beats(spec: u32, doc_order: u32, best_spec: u32, best_doc_order: u32) bool {
|
if (ctx.display_priority.* == INLINE_PRIORITY and
|
||||||
return spec > best_spec or (spec == best_spec and doc_order > best_doc_order);
|
ctx.visibility_priority.* == INLINE_PRIORITY and
|
||||||
|
ctx.opacity_priority.* == INLINE_PRIORITY)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checkRule(ctx: @This(), rule: VisibilityRule) void {
|
const priorities = rules.items(.priority);
|
||||||
// Skip rules that can't possibly beat current best for any property
|
const props_list = rules.items(.props);
|
||||||
const dominated = (rule.props.display_none == null or !beats(rule.specificity, rule.doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and
|
const selectors = rules.items(.selector);
|
||||||
(rule.props.visibility_hidden == null or !beats(rule.specificity, rule.doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and
|
|
||||||
(rule.props.opacity_zero == null or !beats(rule.specificity, rule.doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*));
|
|
||||||
if (dominated) return;
|
|
||||||
|
|
||||||
if (matchesSelector(ctx.el, rule.selector, ctx.page)) {
|
for (priorities, props_list, selectors) |p, props, selector| {
|
||||||
if (rule.props.display_none != null and beats(rule.specificity, rule.doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) {
|
// Fast skip using packed u64 priority
|
||||||
ctx.display_none.* = rule.props.display_none;
|
if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) {
|
||||||
ctx.display_spec.* = rule.specificity;
|
continue;
|
||||||
ctx.display_doc_order.* = rule.doc_order;
|
|
||||||
}
|
}
|
||||||
if (rule.props.visibility_hidden != null and beats(rule.specificity, rule.doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) {
|
|
||||||
ctx.visibility_hidden.* = rule.props.visibility_hidden;
|
// Logic for property dominance
|
||||||
ctx.visibility_spec.* = rule.specificity;
|
const dominated = (props.display_none == null or p <= ctx.display_priority.*) and
|
||||||
ctx.visibility_doc_order.* = rule.doc_order;
|
(props.visibility_hidden == null or p <= ctx.visibility_priority.*) and
|
||||||
|
(props.opacity_zero == null or p <= ctx.opacity_priority.*);
|
||||||
|
|
||||||
|
if (dominated) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(ctx.el, selector, ctx.page)) {
|
||||||
|
// Update best priorities
|
||||||
|
if (props.display_none != null and p > ctx.display_priority.*) {
|
||||||
|
ctx.display_none.* = props.display_none;
|
||||||
|
ctx.display_priority.* = p;
|
||||||
|
}
|
||||||
|
if (props.visibility_hidden != null and p > ctx.visibility_priority.*) {
|
||||||
|
ctx.visibility_hidden.* = props.visibility_hidden;
|
||||||
|
ctx.visibility_priority.* = p;
|
||||||
|
}
|
||||||
|
if (props.opacity_zero != null and p > ctx.opacity_priority.*) {
|
||||||
|
ctx.opacity_zero.* = props.opacity_zero;
|
||||||
|
ctx.opacity_priority.* = p;
|
||||||
}
|
}
|
||||||
if (rule.props.opacity_zero != null and beats(rule.specificity, rule.doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) {
|
|
||||||
ctx.opacity_zero.* = rule.props.opacity_zero;
|
|
||||||
ctx.opacity_spec.* = rule.specificity;
|
|
||||||
ctx.opacity_doc_order.* = rule.doc_order;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const ctx = Ctx{
|
const ctx = Ctx{
|
||||||
.display_none = &display_none,
|
.display_none = &display_none,
|
||||||
.display_spec = &display_spec,
|
.display_priority = &display_priority,
|
||||||
.display_doc_order = &display_doc_order,
|
|
||||||
.visibility_hidden = &visibility_hidden,
|
.visibility_hidden = &visibility_hidden,
|
||||||
.visibility_spec = &visibility_spec,
|
.visibility_priority = &visibility_priority,
|
||||||
.visibility_doc_order = &visibility_doc_order,
|
|
||||||
.opacity_zero = &opacity_zero,
|
.opacity_zero = &opacity_zero,
|
||||||
.opacity_spec = &opacity_spec,
|
.opacity_priority = &opacity_priority,
|
||||||
.opacity_doc_order = &opacity_doc_order,
|
|
||||||
.el = el,
|
.el = el,
|
||||||
.page = self.page,
|
.page = self.page,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||||
if (self.id_rules.get(id)) |rules| {
|
if (self.id_rules.get(id)) |rules| {
|
||||||
for (rules.items) |rule| {
|
ctx.checkRules(&rules);
|
||||||
ctx.checkRule(rule);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,22 +360,16 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
|
|||||||
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||||
while (it.next()) |class| {
|
while (it.next()) |class| {
|
||||||
if (self.class_rules.get(class)) |rules| {
|
if (self.class_rules.get(class)) |rules| {
|
||||||
for (rules.items) |rule| {
|
ctx.checkRules(&rules);
|
||||||
ctx.checkRule(rule);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.tag_rules.get(el.getTag())) |rules| {
|
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||||
for (rules.items) |rule| {
|
ctx.checkRules(&rules);
|
||||||
ctx.checkRule(rule);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (self.other_rules.items) |rule| {
|
ctx.checkRules(&self.other_rules);
|
||||||
ctx.checkRule(rule);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
|
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
|
||||||
}
|
}
|
||||||
@@ -362,32 +421,32 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
|
|||||||
self.rebuildIfDirty() catch return false;
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
var result: ?bool = null;
|
var result: ?bool = null;
|
||||||
var best_spec: u32 = 0;
|
var best_priority: u64 = 0;
|
||||||
var best_doc_order: u32 = 0;
|
|
||||||
|
|
||||||
// Helper to check a single rule
|
// Helper to check a single rule
|
||||||
const checkRule = struct {
|
const checkRules = struct {
|
||||||
fn beats(spec: u32, doc_order: u32, b_spec: u32, b_doc_order: u32) bool {
|
fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void {
|
||||||
return spec > b_spec or (spec == b_spec and doc_order > b_doc_order);
|
if (current_priority.* == INLINE_PRIORITY) return;
|
||||||
}
|
|
||||||
|
|
||||||
fn check(rule: VisibilityRule, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void {
|
const priorities = rules.items(.priority);
|
||||||
if (rule.props.pointer_events_none == null or !beats(rule.specificity, rule.doc_order, spec.*, doc_order.*)) {
|
const props_list = rules.items(.props);
|
||||||
return;
|
const selectors = rules.items(.selector);
|
||||||
|
|
||||||
|
for (priorities, props_list, selectors) |priority, props, selector| {
|
||||||
|
if (priority <= current_priority.*) continue;
|
||||||
|
if (props.pointer_events_none == null) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(elem, selector, p)) {
|
||||||
|
res.* = props.pointer_events_none;
|
||||||
|
current_priority.* = priority;
|
||||||
}
|
}
|
||||||
if (matchesSelector(elem, rule.selector, p)) {
|
|
||||||
res.* = rule.props.pointer_events_none;
|
|
||||||
spec.* = rule.specificity;
|
|
||||||
doc_order.* = rule.doc_order;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.check;
|
}.check;
|
||||||
|
|
||||||
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||||
if (self.id_rules.get(id)) |rules| {
|
if (self.id_rules.get(id)) |rules| {
|
||||||
for (rules.items) |rule| {
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,22 +454,16 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
|
|||||||
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||||
while (it.next()) |class| {
|
while (it.next()) |class| {
|
||||||
if (self.class_rules.get(class)) |rules| {
|
if (self.class_rules.get(class)) |rules| {
|
||||||
for (rules.items) |rule| {
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.tag_rules.get(el.getTag())) |rules| {
|
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||||
for (rules.items) |rule| {
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (self.other_rules.items) |rule| {
|
checkRules(&self.other_rules, &result, &best_priority, el, page);
|
||||||
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result orelse false;
|
return result orelse false;
|
||||||
}
|
}
|
||||||
@@ -452,8 +505,7 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
|||||||
const rule = VisibilityRule{
|
const rule = VisibilityRule{
|
||||||
.props = props,
|
.props = props,
|
||||||
.selector = selector,
|
.selector = selector,
|
||||||
.specificity = computeSpecificity(selector),
|
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||||
.doc_order = self.next_doc_order,
|
|
||||||
};
|
};
|
||||||
self.next_doc_order += 1;
|
self.next_doc_order += 1;
|
||||||
|
|
||||||
@@ -461,17 +513,17 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
|||||||
switch (bucket_key) {
|
switch (bucket_key) {
|
||||||
.id => |id| {
|
.id => |id| {
|
||||||
const gop = try self.id_rules.getOrPut(self.arena, id);
|
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||||
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
try gop.value_ptr.append(self.arena, rule);
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
},
|
},
|
||||||
.class => |class| {
|
.class => |class| {
|
||||||
const gop = try self.class_rules.getOrPut(self.arena, class);
|
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||||
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
try gop.value_ptr.append(self.arena, rule);
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
},
|
},
|
||||||
.tag => |tag| {
|
.tag => |tag| {
|
||||||
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||||
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
try gop.value_ptr.append(self.arena, rule);
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
},
|
},
|
||||||
.other => {
|
.other => {
|
||||||
@@ -629,11 +681,8 @@ const VisibilityRule = struct {
|
|||||||
selector: Selector.Selector, // Single selector, not a list
|
selector: Selector.Selector, // Single selector, not a list
|
||||||
props: VisibilityProperties,
|
props: VisibilityProperties,
|
||||||
|
|
||||||
// Packed specificity: (id_count << 20) | (class_count << 10) | element_count
|
// Packed priority: (specificity << 32) | doc_order
|
||||||
specificity: u32,
|
priority: u64,
|
||||||
|
|
||||||
// Document order for tie-breaking equal specificity (higher = later in document)
|
|
||||||
doc_order: u32,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckVisibilityOptions = struct {
|
const CheckVisibilityOptions = struct {
|
||||||
@@ -641,8 +690,8 @@ const CheckVisibilityOptions = struct {
|
|||||||
check_visibility: bool = false,
|
check_visibility: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inline styles always win over stylesheets - use max u32 as sentinel
|
// Inline styles always win over stylesheets - use max u64 as sentinel
|
||||||
const INLINE_SPECIFICITY: u32 = std.math.maxInt(u32);
|
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
|
||||||
|
|
||||||
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
|
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
|
||||||
const style = el.getOrCreateStyle(page) catch |err| {
|
const style = el.getOrCreateStyle(page) catch |err| {
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ const Parser = @import("../../css/Parser.zig");
|
|||||||
|
|
||||||
const CSSStyleSheet = @This();
|
const CSSStyleSheet = @This();
|
||||||
|
|
||||||
|
pub const CSSError = error{
|
||||||
|
OutOfMemory,
|
||||||
|
IndexSizeError,
|
||||||
|
WriteFailed,
|
||||||
|
StringTooLarge,
|
||||||
|
SyntaxError,
|
||||||
|
};
|
||||||
|
|
||||||
_href: ?[]const u8 = null,
|
_href: ?[]const u8 = null,
|
||||||
_title: []const u8 = "",
|
_title: []const u8 = "",
|
||||||
_disabled: bool = false,
|
_disabled: bool = false,
|
||||||
@@ -46,8 +54,17 @@ pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void {
|
|||||||
|
|
||||||
pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList {
|
pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList {
|
||||||
if (self._css_rules) |rules| return rules;
|
if (self._css_rules) |rules| return rules;
|
||||||
|
|
||||||
const rules = try CSSRuleList.init(page);
|
const rules = try CSSRuleList.init(page);
|
||||||
self._css_rules = rules;
|
self._css_rules = rules;
|
||||||
|
|
||||||
|
if (self.getOwnerNode()) |owner| {
|
||||||
|
if (owner.is(Element.Html.Style)) |style| {
|
||||||
|
const text = try style.asNode().getTextContentAlloc(page.call_arena);
|
||||||
|
try self.replaceSync(text, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,12 +101,12 @@ pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
|
|||||||
page._style_manager.sheetModified();
|
page._style_manager.sheetModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) CSSError!js.Promise {
|
||||||
try self.replaceSync(text, page);
|
try self.replaceSync(text, page);
|
||||||
return page.js.local.?.resolvePromise(self);
|
return page.js.local.?.resolvePromise(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void {
|
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) CSSError!void {
|
||||||
const rules = try self.getCssRules(page);
|
const rules = try self.getCssRules(page);
|
||||||
rules.clear();
|
rules.clear();
|
||||||
|
|
||||||
|
|||||||
@@ -95,9 +95,6 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
|
|||||||
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
|
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
|
||||||
self._sheet = sheet;
|
self._sheet = sheet;
|
||||||
|
|
||||||
const text = try self.asNode().getTextContentAlloc(page.call_arena);
|
|
||||||
try sheet.replaceSync(text, page);
|
|
||||||
|
|
||||||
const sheets = try page.document.getStyleSheets(page);
|
const sheets = try page.document.getStyleSheets(page);
|
||||||
try sheets.add(sheet, page);
|
try sheets.add(sheet, page);
|
||||||
|
|
||||||
@@ -106,9 +103,9 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
|
|||||||
|
|
||||||
pub fn styleAddedCallback(self: *Style, page: *Page) !void {
|
pub fn styleAddedCallback(self: *Style, page: *Page) !void {
|
||||||
// Force stylesheet initialization so rules are parsed immediately
|
// Force stylesheet initialization so rules are parsed immediately
|
||||||
if (self.getSheet(page) catch null) |sheet| {
|
if (self.getSheet(page) catch null) |_| {
|
||||||
// Notify StyleManager about the new stylesheet
|
// Notify StyleManager about the new stylesheet
|
||||||
page._style_manager.sheetAdded(sheet) catch {};
|
page._style_manager.sheetModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we're planning on navigating to another page, don't trigger load event.
|
// if we're planning on navigating to another page, don't trigger load event.
|
||||||
|
|||||||
Reference in New Issue
Block a user