mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1897 from lightpanda-io/css-improvements-perf
Introduce StyleManager
This commit is contained in:
@@ -100,6 +100,11 @@ pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
|||||||
_ = arena.reset(.{ .retain_with_limit = retain });
|
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resetRetain(_: *const ArenaPool, allocator: Allocator) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
_ = arena.reset(.retain_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
test "arena pool - basic acquire and use" {
|
test "arena pool - basic acquire and use" {
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
|
|||||||
log.err(.app, "listener map failed", .{ .err = err });
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
var css_cache: Element.CssCache = .empty;
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &css_cache) catch |err| {
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache) catch |err| {
|
||||||
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -59,8 +60,9 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
|
|||||||
log.err(.app, "listener map failed", .{ .err = err });
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
var css_cache: Element.CssCache = .empty;
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &css_cache) catch |err| {
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache) catch |err| {
|
||||||
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -84,7 +86,7 @@ const NodeData = struct {
|
|||||||
node_name: []const u8,
|
node_name: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, css_cache: ?*Element.CssCache) !void {
|
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, visibility_cache: ?*Element.VisibilityCache, pointer_events_cache: ?*Element.PointerEventsCache) !void {
|
||||||
// 1. Skip non-content nodes
|
// 1. Skip non-content nodes
|
||||||
if (node.is(Element)) |el| {
|
if (node.is(Element)) |el| {
|
||||||
const tag = el.getTag();
|
const tag = el.getTag();
|
||||||
@@ -94,7 +96,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
||||||
|
|
||||||
// Check visibility using the engine's checkVisibility which handles CSS display: none
|
// Check visibility using the engine's checkVisibility which handles CSS display: none
|
||||||
if (!el.checkVisibilityCached(css_cache, self.page)) {
|
if (!el.checkVisibilityCached(visibility_cache, self.page)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +137,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (el.is(Element.Html)) |html_el| {
|
if (el.is(Element.Html)) |html_el| {
|
||||||
if (interactive.classifyInteractivity(self.page, el, html_el, listener_targets, css_cache) != null) {
|
if (interactive.classifyInteractivity(self.page, el, html_el, listener_targets, pointer_events_cache) != null) {
|
||||||
is_interactive = true;
|
is_interactive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +217,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
}
|
}
|
||||||
gop.value_ptr.* += 1;
|
gop.value_ptr.* += 1;
|
||||||
|
|
||||||
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, css_cache);
|
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, visibility_cache, pointer_events_cache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const Factory = @import("Factory.zig");
|
|||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
const EventManager = @import("EventManager.zig");
|
const EventManager = @import("EventManager.zig");
|
||||||
const ScriptManager = @import("ScriptManager.zig");
|
const ScriptManager = @import("ScriptManager.zig");
|
||||||
|
const StyleManager = @import("StyleManager.zig");
|
||||||
|
|
||||||
const Parser = @import("parser/Parser.zig");
|
const Parser = @import("parser/Parser.zig");
|
||||||
|
|
||||||
@@ -143,6 +144,7 @@ _blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
|
|||||||
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
||||||
_to_load: std.ArrayList(*Element.Html) = .{},
|
_to_load: std.ArrayList(*Element.Html) = .{},
|
||||||
|
|
||||||
|
_style_manager: StyleManager,
|
||||||
_script_manager: ScriptManager,
|
_script_manager: ScriptManager,
|
||||||
|
|
||||||
// List of active live ranges (for mutation updates per DOM spec)
|
// List of active live ranges (for mutation updates per DOM spec)
|
||||||
@@ -268,6 +270,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
._factory = factory,
|
._factory = factory,
|
||||||
._pending_loads = 1, // always 1 for the ScriptManager
|
._pending_loads = 1, // always 1 for the ScriptManager
|
||||||
._type = if (parent == null) .root else .frame,
|
._type = if (parent == null) .root else .frame,
|
||||||
|
._style_manager = undefined,
|
||||||
._script_manager = undefined,
|
._script_manager = undefined,
|
||||||
._event_manager = EventManager.init(session.page_arena, self),
|
._event_manager = EventManager.init(session.page_arena, self),
|
||||||
};
|
};
|
||||||
@@ -297,6 +300,9 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
._visual_viewport = visual_viewport,
|
._visual_viewport = visual_viewport,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self._style_manager = try StyleManager.init(self);
|
||||||
|
errdefer self._style_manager.deinit();
|
||||||
|
|
||||||
const browser = session.browser;
|
const browser = session.browser;
|
||||||
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
||||||
errdefer self._script_manager.deinit();
|
errdefer self._script_manager.deinit();
|
||||||
@@ -353,6 +359,7 @@ pub fn deinit(self: *Page, abort_http: bool) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._script_manager.deinit();
|
self._script_manager.deinit();
|
||||||
|
self._style_manager.deinit();
|
||||||
|
|
||||||
session.releaseArena(self.call_arena);
|
session.releaseArena(self.call_arena);
|
||||||
}
|
}
|
||||||
@@ -2538,6 +2545,17 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
|
|||||||
}
|
}
|
||||||
|
|
||||||
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
|
||||||
|
|
||||||
|
// If a <style> element is being removed, remove its sheet from the list
|
||||||
|
if (el.is(Element.Html.Style)) |style| {
|
||||||
|
if (style._sheet) |sheet| {
|
||||||
|
if (self.document._style_sheets) |sheets| {
|
||||||
|
sheets.remove(sheet);
|
||||||
|
}
|
||||||
|
style._sheet = null;
|
||||||
|
}
|
||||||
|
self._style_manager.sheetModified();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
569
src/browser/StyleManager.zig
Normal file
569
src/browser/StyleManager.zig
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
const CssParser = @import("css/Parser.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
const SelectorParser = @import("webapi/selector/Parser.zig");
|
||||||
|
const SelectorList = @import("webapi/selector/List.zig");
|
||||||
|
|
||||||
|
const CSSStyleRule = @import("webapi/css/CSSStyleRule.zig");
|
||||||
|
const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
|
||||||
|
const CSSStyleProperties = @import("webapi/css/CSSStyleProperties.zig");
|
||||||
|
const CSSStyleProperty = @import("webapi/css/CSSStyleDeclaration.zig").Property;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const VisibilityCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||||
|
pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||||
|
|
||||||
|
// Tracks visibility-relevant CSS rules from <style> elements.
|
||||||
|
const StyleManager = @This();
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// Sorted in document order. Only ryles that we care about (display, visibility, ...)
|
||||||
|
rules: std.ArrayList(VisibilityRule) = .empty,
|
||||||
|
|
||||||
|
// When true, rules need to be rebuilt
|
||||||
|
dirty: bool = false,
|
||||||
|
|
||||||
|
pub fn init(page: *Page) !StyleManager {
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.arena = try page.getArena(.{ .debug = "StyleManager" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *StyleManager) void {
|
||||||
|
self.page.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetAdded(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
||||||
|
const css_rules = sheet._css_rules orelse return;
|
||||||
|
|
||||||
|
for (css_rules._rules.items) |rule| {
|
||||||
|
const style_rule = rule.is(CSSStyleRule) orelse continue;
|
||||||
|
try self.addRule(style_rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetRemoved(self: *StyleManager) void {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetModified(self: *StyleManager) void {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the rule list from all document stylesheets.
|
||||||
|
/// Called lazily when dirty flag is set and rules are needed.
|
||||||
|
fn rebuildIfDirty(self: *StyleManager) !void {
|
||||||
|
if (!self.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dirty = false;
|
||||||
|
const item_count = self.rules.items.len;
|
||||||
|
self.page._session.arena_pool.resetRetain(self.arena);
|
||||||
|
self.rules = try .initCapacity(self.arena, item_count);
|
||||||
|
const sheets = self.page.document._style_sheets orelse return;
|
||||||
|
for (sheets._sheets.items) |sheet| {
|
||||||
|
self.sheetAdded(sheet) catch |err| {
|
||||||
|
log.err(.browser, "StyleManager sheetAdded", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an element is hidden based on options.
|
||||||
|
// By default only checks display:none.
|
||||||
|
// Walks up the tree to check ancestors.
|
||||||
|
pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, options: CheckVisibilityOptions) bool {
|
||||||
|
var current: ?*Element = el;
|
||||||
|
|
||||||
|
while (current) |elem| {
|
||||||
|
// Check cache first (only when checking all properties for caching consistency)
|
||||||
|
if (cache) |c| {
|
||||||
|
if (c.get(elem)) |hidden| {
|
||||||
|
if (hidden) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidden = self.isElementHidden(elem, options);
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
if (cache) |c| {
|
||||||
|
c.put(self.page.call_arena, elem, hidden) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a single element (not ancestors) is hidden.
|
||||||
|
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
|
||||||
|
// Track best match per property (value + specificity)
|
||||||
|
// Initialize spec to INLINE_SPECIFICITY for properties we don't care about - this makes
|
||||||
|
// the loop naturally skip them since no stylesheet rule can have specificity >= INLINE_SPECIFICITY
|
||||||
|
var display_none: ?bool = null;
|
||||||
|
var display_spec: u32 = 0;
|
||||||
|
|
||||||
|
var visibility_hidden: ?bool = null;
|
||||||
|
var visibility_spec: u32 = 0;
|
||||||
|
|
||||||
|
var opacity_zero: ?bool = null;
|
||||||
|
var opacity_spec: u32 = 0;
|
||||||
|
|
||||||
|
// Check inline styles FIRST - they use INLINE_SPECIFICITY so no stylesheet can beat them
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
|
return true; // Early exit for hiding value
|
||||||
|
}
|
||||||
|
display_none = false;
|
||||||
|
display_spec = INLINE_SPECIFICITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.check_visibility) {
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
visibility_hidden = false;
|
||||||
|
visibility_spec = INLINE_SPECIFICITY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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 can just compare the specificity.
|
||||||
|
visibility_spec = INLINE_SPECIFICITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.check_opacity) {
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("0"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
opacity_zero = false;
|
||||||
|
opacity_spec = INLINE_SPECIFICITY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opacity_spec = INLINE_SPECIFICITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_spec == INLINE_SPECIFICITY and visibility_spec == INLINE_SPECIFICITY and opacity_spec == INLINE_SPECIFICITY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
|
for (self.rules.items) |rule| {
|
||||||
|
// Skip rules that can't possibly update any property we care about
|
||||||
|
const dominated = (rule.props.display_none == null or rule.specificity < display_spec) and
|
||||||
|
(rule.props.visibility_hidden == null or rule.specificity < visibility_spec) and
|
||||||
|
(rule.props.opacity_zero == null or rule.specificity < opacity_spec);
|
||||||
|
if (dominated) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(el, rule.selector, self.page)) {
|
||||||
|
if (rule.props.display_none != null and rule.specificity >= display_spec) {
|
||||||
|
display_none = rule.props.display_none;
|
||||||
|
display_spec = rule.specificity;
|
||||||
|
}
|
||||||
|
if (rule.props.visibility_hidden != null and rule.specificity >= visibility_spec) {
|
||||||
|
visibility_hidden = rule.props.visibility_hidden;
|
||||||
|
visibility_spec = rule.specificity;
|
||||||
|
}
|
||||||
|
if (rule.props.opacity_zero != null and rule.specificity >= opacity_spec) {
|
||||||
|
opacity_zero = rule.props.opacity_zero;
|
||||||
|
opacity_spec = rule.specificity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an element has pointer-events:none.
|
||||||
|
/// Checks inline style first - if set, skips stylesheet lookup.
|
||||||
|
/// Walks up the tree to check ancestors.
|
||||||
|
pub fn hasPointerEventsNone(self: *StyleManager, el: *Element, cache: ?*PointerEventsCache) bool {
|
||||||
|
var current: ?*Element = el;
|
||||||
|
|
||||||
|
while (current) |elem| {
|
||||||
|
// Check cache first
|
||||||
|
if (cache) |c| {
|
||||||
|
if (c.get(elem)) |pe_none| {
|
||||||
|
if (pe_none) return true;
|
||||||
|
current = elem.parentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pe_none = self.elementHasPointerEventsNone(elem);
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
if (cache) |c| {
|
||||||
|
c.put(self.page.call_arena, elem, pe_none) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pe_none) return true;
|
||||||
|
current = elem.parentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a single element (not ancestors) has pointer-events:none.
|
||||||
|
fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
var result: ?bool = null;
|
||||||
|
var best_spec: u32 = 0;
|
||||||
|
|
||||||
|
// Check inline style first
|
||||||
|
if (getInlineStyleProperty(el, .wrap("pointer-events"), page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Inline set to non-none - no stylesheet can override
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check stylesheet rules
|
||||||
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
|
for (self.rules.items) |rule| {
|
||||||
|
if (rule.props.pointer_events_none == null or rule.specificity < best_spec) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchesSelector(el, rule.selector, page)) {
|
||||||
|
result = rule.props.pointer_events_none;
|
||||||
|
best_spec = rule.specificity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result orelse false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts visibility-relevant rules from a CSS rule.
|
||||||
|
// Creates one VisibilityRule per selector (not per selector list) so each has correct specificity.
|
||||||
|
fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
||||||
|
const selector_text = style_rule._selector_text;
|
||||||
|
if (selector_text.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the rule has visibility-relevant properties
|
||||||
|
const style = style_rule._style orelse return;
|
||||||
|
const props = extractVisibilityProperties(style);
|
||||||
|
if (!props.isRelevant()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the selector list
|
||||||
|
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||||
|
if (selectors.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one rule per selector - each has its own specificity
|
||||||
|
// e.g., "#id, .class { display: none }" becomes two rules with different specificities
|
||||||
|
for (selectors) |selector| {
|
||||||
|
const rule = VisibilityRule{
|
||||||
|
.props = props,
|
||||||
|
.selector = selector,
|
||||||
|
.specificity = computeSpecificity(selector),
|
||||||
|
};
|
||||||
|
try self.rules.append(self.arena, rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts visibility-relevant properties from a style declaration.
|
||||||
|
fn extractVisibilityProperties(style: *CSSStyleProperties) VisibilityProperties {
|
||||||
|
var props = VisibilityProperties{};
|
||||||
|
const decl = style.asCSSStyleDeclaration();
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("display"))) |property| {
|
||||||
|
props.display_none = property._value.eql(comptime .wrap("none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("visibility"))) |property| {
|
||||||
|
props.visibility_hidden = property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("opacity"))) |property| {
|
||||||
|
props.opacity_zero = property._value.eql(comptime .wrap("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(.wrap("pointer-events"))) |property| {
|
||||||
|
props.pointer_events_none = property._value.eql(comptime .wrap("none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes CSS specificity for a selector.
|
||||||
|
// Returns packed value: (id_count << 20) | (class_count << 10) | element_count
|
||||||
|
pub fn computeSpecificity(selector: Selector.Selector) u32 {
|
||||||
|
var ids: u32 = 0;
|
||||||
|
var classes: u32 = 0; // includes classes, attributes, pseudo-classes
|
||||||
|
var elements: u32 = 0; // includes elements, pseudo-elements
|
||||||
|
|
||||||
|
// Count specificity for first compound
|
||||||
|
countCompoundSpecificity(selector.first, &ids, &classes, &elements);
|
||||||
|
|
||||||
|
// Count specificity for subsequent segments
|
||||||
|
for (selector.segments) |segment| {
|
||||||
|
countCompoundSpecificity(segment.compound, &ids, &classes, &elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack into single u32: (ids << 20) | (classes << 10) | elements
|
||||||
|
// This gives us 10 bits each, supporting up to 1023 of each type
|
||||||
|
return (@as(u32, @min(ids, 1023)) << 20) | (@as(u32, @min(classes, 1023)) << 10) | @min(elements, 1023);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn countCompoundSpecificity(compound: Selector.Compound, ids: *u32, classes: *u32, elements: *u32) void {
|
||||||
|
for (compound.parts) |part| {
|
||||||
|
switch (part) {
|
||||||
|
.id => ids.* += 1,
|
||||||
|
.class => classes.* += 1,
|
||||||
|
.tag, .tag_name => elements.* += 1,
|
||||||
|
.universal => {}, // zero specificity
|
||||||
|
.attribute => classes.* += 1,
|
||||||
|
.pseudo_class => |pc| {
|
||||||
|
switch (pc) {
|
||||||
|
// :where() has zero specificity
|
||||||
|
.where => {},
|
||||||
|
// :not(), :is(), :has() take specificity of their most specific argument
|
||||||
|
.not, .is, .has => |nested| {
|
||||||
|
var max_nested: u32 = 0;
|
||||||
|
for (nested) |nested_sel| {
|
||||||
|
const spec = computeSpecificity(nested_sel);
|
||||||
|
if (spec > max_nested) max_nested = spec;
|
||||||
|
}
|
||||||
|
// Unpack and add to our counts
|
||||||
|
ids.* += (max_nested >> 20) & 0x3FF;
|
||||||
|
classes.* += (max_nested >> 10) & 0x3FF;
|
||||||
|
elements.* += max_nested & 0x3FF;
|
||||||
|
},
|
||||||
|
// All other pseudo-classes count as class-level specificity
|
||||||
|
else => classes.* += 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matchesSelector(el: *Element, selector: Selector.Selector, page: *Page) bool {
|
||||||
|
const node = el.asNode();
|
||||||
|
return SelectorList.matches(node, selector, node, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VisibilityProperties = struct {
|
||||||
|
display_none: ?bool = null,
|
||||||
|
visibility_hidden: ?bool = null,
|
||||||
|
opacity_zero: ?bool = null,
|
||||||
|
pointer_events_none: ?bool = null,
|
||||||
|
|
||||||
|
// returne true if any field in VisibilityProperties is not null
|
||||||
|
fn isRelevant(self: VisibilityProperties) bool {
|
||||||
|
return self.display_none != null or
|
||||||
|
self.visibility_hidden != null or
|
||||||
|
self.opacity_zero != null or
|
||||||
|
self.pointer_events_none != null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisibilityRule = struct {
|
||||||
|
selector: Selector.Selector, // Single selector, not a list
|
||||||
|
props: VisibilityProperties,
|
||||||
|
|
||||||
|
// Packed specificity: (id_count << 20) | (class_count << 10) | element_count
|
||||||
|
// Document order is implicit in array position - equal specificity with later position wins
|
||||||
|
specificity: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckVisibilityOptions = struct {
|
||||||
|
check_opacity: bool = false,
|
||||||
|
check_visibility: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inline styles always win over stylesheets - use max u32 as sentinel
|
||||||
|
const INLINE_SPECIFICITY: u32 = std.math.maxInt(u32);
|
||||||
|
|
||||||
|
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
|
||||||
|
const style = el.getOrCreateStyle(page) catch |err| {
|
||||||
|
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return style.asCSSStyleDeclaration().findProperty(property_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "StyleManager: computeSpecificity: element selector" {
|
||||||
|
// div -> (0, 0, 1)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: class selector" {
|
||||||
|
// .foo -> (0, 1, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: id selector" {
|
||||||
|
// #bar -> (1, 0, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: combined selector" {
|
||||||
|
// div.foo#bar -> (1, 1, 1)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .tag = .div },
|
||||||
|
.{ .class = "foo" },
|
||||||
|
.{ .id = "bar" },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual((1 << 20) | (1 << 10) | 1, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: universal selector" {
|
||||||
|
// * -> (0, 0, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.universal} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(0, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: multiple classes" {
|
||||||
|
// .a.b.c -> (0, 3, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .class = "a" },
|
||||||
|
.{ .class = "b" },
|
||||||
|
.{ .class = "c" },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(3 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: descendant combinator" {
|
||||||
|
// div span -> (0, 0, 2)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||||
|
.segments = &.{
|
||||||
|
.{ .combinator = .descendant, .compound = .{ .parts = &.{.{ .tag = .span }} } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(2, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :where() has zero specificity" {
|
||||||
|
// :where(.foo) -> (0, 0, 0) regardless of what's inside
|
||||||
|
const inner_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .where = &.{inner_selector} } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(0, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :not() takes inner specificity" {
|
||||||
|
// :not(.foo) -> (0, 1, 0) - takes specificity of .foo
|
||||||
|
const inner_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .not = &.{inner_selector} } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :is() takes most specific inner" {
|
||||||
|
// :is(.foo, #bar) -> (1, 0, 0) - takes the most specific (#bar)
|
||||||
|
const class_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const id_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .is = &.{ class_selector, id_selector } } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: pseudo-class (general)" {
|
||||||
|
// :hover -> (0, 1, 0) - pseudo-classes count as class-level
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .hover },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
@@ -133,7 +133,7 @@ pub fn collectInteractiveElements(
|
|||||||
// so classify and getListenerTypes are both O(1) per element.
|
// so classify and getListenerTypes are both O(1) per element.
|
||||||
const listener_targets = try buildListenerTargetMap(page, arena);
|
const listener_targets = try buildListenerTargetMap(page, arena);
|
||||||
|
|
||||||
var css_cache: Element.CssCache = .empty;
|
var css_cache: Element.PointerEventsCache = .empty;
|
||||||
|
|
||||||
var results: std.ArrayList(InteractiveElement) = .empty;
|
var results: std.ArrayList(InteractiveElement) = .empty;
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ pub fn classifyInteractivity(
|
|||||||
el: *Element,
|
el: *Element,
|
||||||
html_el: *Element.Html,
|
html_el: *Element.Html,
|
||||||
listener_targets: ListenerTargetMap,
|
listener_targets: ListenerTargetMap,
|
||||||
cache: ?*Element.CssCache,
|
cache: ?*Element.PointerEventsCache,
|
||||||
) ?InteractivityType {
|
) ?InteractivityType {
|
||||||
if (el.hasPointerEventsNone(cache, page)) return null;
|
if (el.hasPointerEventsNone(cache, page)) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -77,33 +77,3 @@
|
|||||||
testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc'));
|
testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="checkVisibility">
|
|
||||||
{
|
|
||||||
const el = document.createElement("div");
|
|
||||||
document.documentElement.appendChild(el);
|
|
||||||
testing.expectEqual(true, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.display = "none";
|
|
||||||
testing.expectEqual(false, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.display = "block";
|
|
||||||
testing.expectEqual(true, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.visibility = "hidden";
|
|
||||||
testing.expectEqual(false, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.visibility = "collapse";
|
|
||||||
testing.expectEqual(false, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.visibility = "visible";
|
|
||||||
testing.expectEqual(true, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.opacity = "0";
|
|
||||||
testing.expectEqual(false, el.checkVisibility());
|
|
||||||
|
|
||||||
el.style.opacity = "1";
|
|
||||||
testing.expectEqual(true, el.checkVisibility());
|
|
||||||
document.documentElement.removeChild(el);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
226
src/browser/tests/element/check_visibility.html
Normal file
226
src/browser/tests/element/check_visibility.html
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<body></body>
|
||||||
|
|
||||||
|
<!-
|
||||||
|
<script id="inline_display_none">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.style.display = "none";
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
el.style.display = "block";
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="inline_visibility_hidden">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
el.style.visibility = "hidden";
|
||||||
|
// Without visibilityProperty option, visibility:hidden is not checked
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
// With visibilityProperty: true, visibility:hidden is detected
|
||||||
|
testing.expectEqual(false, el.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
el.style.visibility = "collapse";
|
||||||
|
testing.expectEqual(false, el.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
el.style.visibility = "visible";
|
||||||
|
testing.expectEqual(true, el.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="inline_opacity_zero">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
el.style.opacity = "0";
|
||||||
|
// Without checkOpacity option, opacity:0 is not checked
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
// With checkOpacity: true, opacity:0 is detected
|
||||||
|
testing.expectEqual(false, el.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
el.style.opacity = "0.5";
|
||||||
|
testing.expectEqual(true, el.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
el.style.opacity = "1";
|
||||||
|
testing.expectEqual(true, el.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="parent_hidden_hides_child">
|
||||||
|
{
|
||||||
|
const parent = document.createElement("div");
|
||||||
|
const child = document.createElement("span");
|
||||||
|
parent.appendChild(child);
|
||||||
|
document.body.appendChild(parent);
|
||||||
|
|
||||||
|
testing.expectEqual(true, child.checkVisibility());
|
||||||
|
|
||||||
|
// display:none on parent hides children (no option needed)
|
||||||
|
parent.style.display = "none";
|
||||||
|
testing.expectEqual(false, child.checkVisibility());
|
||||||
|
|
||||||
|
// visibility:hidden on parent - needs visibilityProperty option
|
||||||
|
parent.style.display = "block";
|
||||||
|
parent.style.visibility = "hidden";
|
||||||
|
testing.expectEqual(true, child.checkVisibility()); // without option
|
||||||
|
testing.expectEqual(false, child.checkVisibility({ visibilityProperty: true }));
|
||||||
|
|
||||||
|
// opacity:0 on parent - needs checkOpacity option
|
||||||
|
parent.style.visibility = "visible";
|
||||||
|
parent.style.opacity = "0";
|
||||||
|
testing.expectEqual(true, child.checkVisibility()); // without option
|
||||||
|
testing.expectEqual(false, child.checkVisibility({ checkOpacity: true }));
|
||||||
|
|
||||||
|
parent.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-basic">
|
||||||
|
.hidden-by-class { display: none; }
|
||||||
|
.visible-by-class { display: block; }
|
||||||
|
</style>
|
||||||
|
<script id="style_tag_basic">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.className = "hidden-by-class";
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
el.className = "visible-by-class";
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.className = "";
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-specificity">
|
||||||
|
.spec-hidden { display: none; }
|
||||||
|
#spec-visible { display: block; }
|
||||||
|
</style>
|
||||||
|
<script id="specificity_id_beats_class">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.id = "spec-visible";
|
||||||
|
el.className = "spec-hidden";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
// ID selector (#spec-visible: display:block) should beat class selector (.spec-hidden: display:none)
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-order-1">
|
||||||
|
.order-test { display: none; }
|
||||||
|
</style>
|
||||||
|
<style id="style-order-2">
|
||||||
|
.order-test { display: block; }
|
||||||
|
</style>
|
||||||
|
<script id="rule_order_later_wins">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "order-test";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
// Second style block should win (display: block)
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style id="style-override">
|
||||||
|
.should-be-hidden { display: none; }
|
||||||
|
</style>
|
||||||
|
<script id="inline_overrides_stylesheet">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "should-be-hidden";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
// Inline style should override
|
||||||
|
el.style.display = "block";
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="dynamic_style_element">
|
||||||
|
{
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "dynamic-style-test";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
// Add a style element
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = ".dynamic-style-test { display: none; }";
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
testing.expectEqual(false, el.checkVisibility());
|
||||||
|
|
||||||
|
// Remove the style element
|
||||||
|
style.remove();
|
||||||
|
testing.expectEqual(true, el.checkVisibility());
|
||||||
|
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="deep_nesting">
|
||||||
|
{
|
||||||
|
const levels = 5;
|
||||||
|
let current = document.body;
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < levels; i++) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
current.appendChild(el);
|
||||||
|
elements.push(el);
|
||||||
|
current = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All should be visible
|
||||||
|
for (let i = 0; i < levels; i++) {
|
||||||
|
testing.expectEqual(true, elements[i].checkVisibility());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide middle element
|
||||||
|
elements[2].style.display = "none";
|
||||||
|
|
||||||
|
// Elements 0, 1 should still be visible
|
||||||
|
testing.expectEqual(true, elements[0].checkVisibility());
|
||||||
|
testing.expectEqual(true, elements[1].checkVisibility());
|
||||||
|
|
||||||
|
// Elements 2, 3, 4 should be hidden
|
||||||
|
testing.expectEqual(false, elements[2].checkVisibility());
|
||||||
|
testing.expectEqual(false, elements[3].checkVisibility());
|
||||||
|
testing.expectEqual(false, elements[4].checkVisibility());
|
||||||
|
|
||||||
|
elements[0].remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -586,7 +586,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
|
|||||||
while (stack.items.len > 0) {
|
while (stack.items.len > 0) {
|
||||||
const node = stack.pop() orelse break;
|
const node = stack.pop() orelse break;
|
||||||
if (node.is(Element)) |element| {
|
if (node.is(Element)) |element| {
|
||||||
if (element.checkVisibility(page)) {
|
if (element.checkVisibilityCached(null, page)) {
|
||||||
const rect = element.getBoundingClientRectForVisible(page);
|
const rect = element.getBoundingClientRectForVisible(page);
|
||||||
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
|
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
|
||||||
topmost = element;
|
topmost = element;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const String = @import("../../string.zig").String;
|
|||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const StyleManager = @import("../StyleManager.zig");
|
||||||
const reflect = @import("../reflect.zig");
|
const reflect = @import("../reflect.zig");
|
||||||
|
|
||||||
const Node = @import("Node.zig");
|
const Node = @import("Node.zig");
|
||||||
@@ -1041,135 +1042,32 @@ pub fn parentElement(self: *Element) ?*Element {
|
|||||||
return self._proto.parentElement();
|
return self._proto.parentElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
const CSSStyleRule = @import("css/CSSStyleRule.zig");
|
/// Cache for visibility checks - re-exported from StyleManager for convenience.
|
||||||
const StyleSheetList = @import("css/StyleSheetList.zig");
|
pub const VisibilityCache = StyleManager.VisibilityCache;
|
||||||
|
|
||||||
pub const CssProperties = struct {
|
/// Cache for pointer-events checks - re-exported from StyleManager for convenience.
|
||||||
display_none: bool = false,
|
pub const PointerEventsCache = StyleManager.PointerEventsCache;
|
||||||
visibility_hidden: bool = false,
|
|
||||||
opacity_zero: bool = false,
|
pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, page: *Page) bool {
|
||||||
pointer_events_none: bool = false,
|
return page._style_manager.hasPointerEventsNone(self, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, page: *Page) bool {
|
||||||
|
return !page._style_manager.isHidden(self, cache, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckVisibilityOpts = struct {
|
||||||
|
checkOpacity: bool = false,
|
||||||
|
opacityProperty: bool = false,
|
||||||
|
checkVisibilityCSS: bool = false,
|
||||||
|
visibilityProperty: bool = false,
|
||||||
};
|
};
|
||||||
|
pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, page: *Page) bool {
|
||||||
pub const CssCache = std.AutoHashMapUnmanaged(*Element, CssProperties);
|
const opts = opts_ orelse CheckVisibilityOpts{};
|
||||||
|
return !page._style_manager.isHidden(self, null, .{
|
||||||
fn getCssProperties(el: *Element, doc_sheets: ?*StyleSheetList, cache: ?*CssCache, page: *Page) CssProperties {
|
.check_opacity = opts.checkOpacity or opts.opacityProperty,
|
||||||
if (cache) |c| {
|
.check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
|
||||||
if (c.get(el)) |props| return props;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
var props = CssProperties{};
|
|
||||||
|
|
||||||
// Check stylesheets first
|
|
||||||
if (doc_sheets) |sheets| {
|
|
||||||
for (0..sheets.length()) |i| {
|
|
||||||
const sheet = sheets.item(i) orelse continue;
|
|
||||||
const rules = sheet.getCssRules(page) catch continue;
|
|
||||||
for (0..rules.length()) |j| {
|
|
||||||
const rule = rules.item(j) orelse continue;
|
|
||||||
if (rule.is(CSSStyleRule)) |style_rule| {
|
|
||||||
const selector = style_rule.getSelectorText();
|
|
||||||
if (el.matches(selector, page) catch false) {
|
|
||||||
const style = (style_rule.getStyle(page) catch continue).asCSSStyleDeclaration();
|
|
||||||
|
|
||||||
const display = style.getPropertyValue("display", page);
|
|
||||||
if (std.mem.eql(u8, display, "none")) {
|
|
||||||
props.display_none = true;
|
|
||||||
} else if (display.len > 0) {
|
|
||||||
props.display_none = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibility = style.getPropertyValue("visibility", page);
|
|
||||||
if (std.mem.eql(u8, visibility, "hidden") or std.mem.eql(u8, visibility, "collapse")) {
|
|
||||||
props.visibility_hidden = true;
|
|
||||||
} else if (visibility.len > 0) {
|
|
||||||
props.visibility_hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const opacity = style.getPropertyValue("opacity", page);
|
|
||||||
if (std.mem.eql(u8, opacity, "0")) {
|
|
||||||
props.opacity_zero = true;
|
|
||||||
} else if (opacity.len > 0) {
|
|
||||||
props.opacity_zero = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pointer_events = style.getPropertyValue("pointer-events", page);
|
|
||||||
if (std.mem.eql(u8, pointer_events, "none")) {
|
|
||||||
props.pointer_events_none = true;
|
|
||||||
} else if (pointer_events.len > 0) {
|
|
||||||
props.pointer_events_none = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check inline styles overrides
|
|
||||||
if (el.getOrCreateStyle(page) catch null) |style| {
|
|
||||||
const decl = style.asCSSStyleDeclaration();
|
|
||||||
const display = decl.getPropertyValue("display", page);
|
|
||||||
if (std.mem.eql(u8, display, "none")) {
|
|
||||||
props.display_none = true;
|
|
||||||
} else if (display.len > 0) {
|
|
||||||
props.display_none = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibility = decl.getPropertyValue("visibility", page);
|
|
||||||
if (std.mem.eql(u8, visibility, "hidden") or std.mem.eql(u8, visibility, "collapse")) {
|
|
||||||
props.visibility_hidden = true;
|
|
||||||
} else if (visibility.len > 0) {
|
|
||||||
props.visibility_hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const opacity = decl.getPropertyValue("opacity", page);
|
|
||||||
if (std.mem.eql(u8, opacity, "0")) {
|
|
||||||
props.opacity_zero = true;
|
|
||||||
} else if (opacity.len > 0) {
|
|
||||||
props.opacity_zero = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pointer_events = decl.getPropertyValue("pointer-events", page);
|
|
||||||
if (std.mem.eql(u8, pointer_events, "none")) {
|
|
||||||
props.pointer_events_none = true;
|
|
||||||
} else if (pointer_events.len > 0) {
|
|
||||||
props.pointer_events_none = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache) |c| {
|
|
||||||
c.put(page.call_arena, el, props) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hasPointerEventsNone(self: *Element, cache: ?*CssCache, page: *Page) bool {
|
|
||||||
const doc_sheets = page.document.getStyleSheets(page) catch null;
|
|
||||||
var current: ?*Element = self;
|
|
||||||
while (current) |el| {
|
|
||||||
const props = el.getCssProperties(doc_sheets, cache, page);
|
|
||||||
if (props.pointer_events_none) return true;
|
|
||||||
current = el.parentElement();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checkVisibilityCached(self: *Element, cache: ?*CssCache, page: *Page) bool {
|
|
||||||
const doc_sheets = page.document.getStyleSheets(page) catch null;
|
|
||||||
var current: ?*Element = self;
|
|
||||||
|
|
||||||
while (current) |el| {
|
|
||||||
const props = getCssProperties(el, doc_sheets, cache, page);
|
|
||||||
if (props.display_none or props.visibility_hidden or props.opacity_zero) return false;
|
|
||||||
current = el.parentElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checkVisibility(self: *Element, page: *Page) bool {
|
|
||||||
return self.checkVisibilityCached(null, page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
|
fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
|
||||||
@@ -1206,7 +1104,7 @@ fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1214,7 +1112,7 @@ pub fn getClientWidth(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1222,7 +1120,7 @@ pub fn getClientHeight(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
|
pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return .{
|
return .{
|
||||||
._x = 0.0,
|
._x = 0.0,
|
||||||
._y = 0.0,
|
._y = 0.0,
|
||||||
@@ -1252,7 +1150,7 @@ pub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return &.{};
|
return &.{};
|
||||||
}
|
}
|
||||||
const rects = try page.call_arena.alloc(DOMRect, 1);
|
const rects = try page.call_arena.alloc(DOMRect, 1);
|
||||||
@@ -1297,7 +1195,7 @@ pub fn getScrollWidth(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1305,7 +1203,7 @@ pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const dims = self.getElementDimensions(page);
|
const dims = self.getElementDimensions(page);
|
||||||
@@ -1313,14 +1211,14 @@ pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetTop(self: *Element, page: *Page) f64 {
|
pub fn getOffsetTop(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
return calculateDocumentPosition(self.asNode());
|
return calculateDocumentPosition(self.asNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
|
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
|
||||||
if (!self.checkVisibility(page)) {
|
if (!self.checkVisibilityCached(null, page)) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
return calculateSiblingPosition(self.asNode());
|
return calculateSiblingPosition(self.asNode());
|
||||||
|
|||||||
@@ -77,10 +77,11 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 {
|
|||||||
|
|
||||||
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
|
pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
|
||||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||||
const prop = self.findProperty(normalized) orelse {
|
const wrapped = String.wrap(normalized);
|
||||||
|
const prop = self.findProperty(wrapped) orelse {
|
||||||
// Only return default values for computed styles
|
// Only return default values for computed styles
|
||||||
if (self._is_computed) {
|
if (self._is_computed) {
|
||||||
return getDefaultPropertyValue(self, normalized);
|
return getDefaultPropertyValue(self, wrapped);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
@@ -89,7 +90,7 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
|
|||||||
|
|
||||||
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
|
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, page: *Page) []const u8 {
|
||||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||||
const prop = self.findProperty(normalized) orelse return "";
|
const prop = self.findProperty(.wrap(normalized)) orelse return "";
|
||||||
return if (prop._important) "important" else "";
|
return if (prop._important) "important" else "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
|
|||||||
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
|
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
|
||||||
|
|
||||||
// Find existing property
|
// Find existing property
|
||||||
if (self.findProperty(normalized)) |existing| {
|
if (self.findProperty(.wrap(normalized))) |existing| {
|
||||||
existing._value = try String.init(page.arena, normalized_value, .{});
|
existing._value = try String.init(page.arena, normalized_value, .{});
|
||||||
existing._important = important;
|
existing._important = important;
|
||||||
return;
|
return;
|
||||||
@@ -144,7 +145,7 @@ pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, pag
|
|||||||
|
|
||||||
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
|
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, page: *Page) ![]const u8 {
|
||||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||||
const prop = self.findProperty(normalized) orelse return "";
|
const prop = self.findProperty(.wrap(normalized)) orelse return "";
|
||||||
|
|
||||||
// the value might not be on the heap (it could be inlined in the small string
|
// the value might not be on the heap (it could be inlined in the small string
|
||||||
// optimization), so we need to dupe it.
|
// optimization), so we need to dupe it.
|
||||||
@@ -208,11 +209,11 @@ pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findProperty(self: *const CSSStyleDeclaration, name: []const u8) ?*Property {
|
pub fn findProperty(self: *const CSSStyleDeclaration, name: String) ?*Property {
|
||||||
var node = self._properties.first;
|
var node = self._properties.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
const prop = Property.fromNodeLink(n);
|
const prop = Property.fromNodeLink(n);
|
||||||
if (prop._name.eqlSlice(name)) {
|
if (prop._name.eql(name)) {
|
||||||
return prop;
|
return prop;
|
||||||
}
|
}
|
||||||
node = n.next;
|
node = n.next;
|
||||||
@@ -617,26 +618,36 @@ fn isLengthProperty(name: []const u8) bool {
|
|||||||
return length_properties.has(name);
|
return length_properties.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
|
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, name: String) []const u8 {
|
||||||
if (std.mem.eql(u8, normalized_name, "visibility")) {
|
switch (name.len) {
|
||||||
return "visible";
|
5 => {
|
||||||
|
if (name.eql(comptime .wrap("color"))) {
|
||||||
|
const element = self._element orelse return "";
|
||||||
|
return getDefaultColor(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
7 => {
|
||||||
|
if (name.eql(comptime .wrap("opacity"))) {
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
if (name.eql(comptime .wrap("display"))) {
|
||||||
|
const element = self._element orelse return "";
|
||||||
|
return getDefaultDisplay(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
10 => {
|
||||||
|
if (name.eql(comptime .wrap("visibility"))) {
|
||||||
|
return "visible";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
16 => {
|
||||||
|
if (name.eqlSlice("background-color")) {
|
||||||
|
// transparent
|
||||||
|
return "rgba(0, 0, 0, 0)";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
if (std.mem.eql(u8, normalized_name, "opacity")) {
|
|
||||||
return "1";
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, normalized_name, "display")) {
|
|
||||||
const element = self._element orelse return "";
|
|
||||||
return getDefaultDisplay(element);
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, normalized_name, "color")) {
|
|
||||||
const element = self._element orelse return "";
|
|
||||||
return getDefaultColor(element);
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, normalized_name, "background-color")) {
|
|
||||||
// transparent
|
|
||||||
return "rgba(0, 0, 0, 0)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,19 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, pag
|
|||||||
|
|
||||||
const rules = try self.getCssRules(page);
|
const rules = try self.getCssRules(page);
|
||||||
try rules.insert(index, style_rule._proto, page);
|
try rules.insert(index, style_rule._proto, page);
|
||||||
|
|
||||||
|
// Notify StyleManager that rules have changed
|
||||||
|
page._style_manager.sheetModified();
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
|
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
|
||||||
const rules = try self.getCssRules(page);
|
const rules = try self.getCssRules(page);
|
||||||
try rules.remove(index);
|
try rules.remove(index);
|
||||||
|
|
||||||
|
// Notify StyleManager that rules have changed
|
||||||
|
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) !js.Promise {
|
||||||
@@ -99,6 +106,9 @@ pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void {
|
|||||||
try rules.insert(index, style_rule._proto, page);
|
try rules.insert(index, style_rule._proto, page);
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify StyleManager that rules have changed
|
||||||
|
page._style_manager.sheetModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ pub fn add(self: *StyleSheetList, sheet: *CSSStyleSheet, page: *Page) !void {
|
|||||||
try self._sheets.append(page.arena, sheet);
|
try self._sheets.append(page.arena, sheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: *StyleSheetList, sheet: *CSSStyleSheet) void {
|
||||||
|
for (self._sheets.items, 0..) |s, i| {
|
||||||
|
if (s == sheet) {
|
||||||
|
_ = self._sheets.orderedRemove(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(StyleSheetList);
|
pub const bridge = js.Bridge(StyleSheetList);
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,10 @@ 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
|
||||||
_ = self.getSheet(page) catch null;
|
if (self.getSheet(page) catch null) |sheet| {
|
||||||
|
// Notify StyleManager about the new stylesheet
|
||||||
|
page._style_manager.sheetAdded(sheet) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
if (page.isGoingAway()) {
|
if (page.isGoingAway()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user