diff --git a/src/ArenaPool.zig b/src/ArenaPool.zig
index 2f8de17f..608445f6 100644
--- a/src/ArenaPool.zig
+++ b/src/ArenaPool.zig
@@ -100,6 +100,11 @@ pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
_ = 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;
test "arena pool - basic acquire and use" {
diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig
index b64f222d..95561c9f 100644
--- a/src/SemanticTree.zig
+++ b/src/SemanticTree.zig
@@ -45,8 +45,9 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
log.err(.app, "listener map failed", .{ .err = err });
return error.WriteFailed;
};
- var css_cache: Element.CssCache = .empty;
- self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &css_cache) catch |err| {
+ var visibility_cache: Element.VisibilityCache = .empty;
+ 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 });
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 });
return error.WriteFailed;
};
- var css_cache: Element.CssCache = .empty;
- self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &css_cache) catch |err| {
+ var visibility_cache: Element.VisibilityCache = .empty;
+ 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 });
return error.WriteFailed;
};
@@ -84,7 +86,7 @@ const NodeData = struct {
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
if (node.is(Element)) |el| {
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;
// 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;
}
@@ -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 (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;
}
}
@@ -215,7 +217,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
}
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);
}
}
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index cb62cb31..e8df5ab0 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -35,6 +35,7 @@ const Factory = @import("Factory.zig");
const Session = @import("Session.zig");
const EventManager = @import("EventManager.zig");
const ScriptManager = @import("ScriptManager.zig");
+const StyleManager = @import("StyleManager.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.
_to_load: std.ArrayList(*Element.Html) = .{},
+_style_manager: StyleManager,
_script_manager: ScriptManager,
// 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,
._pending_loads = 1, // always 1 for the ScriptManager
._type = if (parent == null) .root else .frame,
+ ._style_manager = undefined,
._script_manager = undefined,
._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,
});
+ self._style_manager = try StyleManager.init(self);
+ errdefer self._style_manager.deinit();
+
const browser = session.browser;
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
errdefer self._script_manager.deinit();
@@ -353,6 +359,7 @@ pub fn deinit(self: *Page, abort_http: bool) void {
}
self._script_manager.deinit();
+ self._style_manager.deinit();
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);
+
+ // If a
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index 53a0b07a..33e29952 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -586,7 +586,7 @@ pub fn elementFromPoint(self: *Document, x: f64, y: f64, page: *Page) !?*Element
while (stack.items.len > 0) {
const node = stack.pop() orelse break;
if (node.is(Element)) |element| {
- if (element.checkVisibility(page)) {
+ if (element.checkVisibilityCached(null, page)) {
const rect = element.getBoundingClientRectForVisible(page);
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
topmost = element;
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index d2d61451..58cc168c 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -24,6 +24,7 @@ const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
+const StyleManager = @import("../StyleManager.zig");
const reflect = @import("../reflect.zig");
const Node = @import("Node.zig");
@@ -1041,135 +1042,32 @@ pub fn parentElement(self: *Element) ?*Element {
return self._proto.parentElement();
}
-const CSSStyleRule = @import("css/CSSStyleRule.zig");
-const StyleSheetList = @import("css/StyleSheetList.zig");
+/// Cache for visibility checks - re-exported from StyleManager for convenience.
+pub const VisibilityCache = StyleManager.VisibilityCache;
-pub const CssProperties = struct {
- display_none: bool = false,
- visibility_hidden: bool = false,
- opacity_zero: bool = false,
- pointer_events_none: bool = false,
+/// Cache for pointer-events checks - re-exported from StyleManager for convenience.
+pub const PointerEventsCache = StyleManager.PointerEventsCache;
+
+pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, page: *Page) bool {
+ 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 const CssCache = std.AutoHashMapUnmanaged(*Element, CssProperties);
-
-fn getCssProperties(el: *Element, doc_sheets: ?*StyleSheetList, cache: ?*CssCache, page: *Page) CssProperties {
- if (cache) |c| {
- 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);
+pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, page: *Page) bool {
+ const opts = opts_ orelse CheckVisibilityOpts{};
+ return !page._style_manager.isHidden(self, null, .{
+ .check_opacity = opts.checkOpacity or opts.opacityProperty,
+ .check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
+ });
}
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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return 0.0;
}
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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return 0.0;
}
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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return .{
._x = 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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return &.{};
}
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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return 0.0;
}
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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return 0.0;
}
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 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return 0.0;
}
return calculateDocumentPosition(self.asNode());
}
pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
- if (!self.checkVisibility(page)) {
+ if (!self.checkVisibilityCached(null, page)) {
return 0.0;
}
return calculateSiblingPosition(self.asNode());
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index 13499033..d207c11a 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -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 {
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
if (self._is_computed) {
- return getDefaultPropertyValue(self, normalized);
+ return getDefaultPropertyValue(self, wrapped);
}
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 {
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 "";
}
@@ -120,7 +121,7 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
// Find existing property
- if (self.findProperty(normalized)) |existing| {
+ if (self.findProperty(.wrap(normalized))) |existing| {
existing._value = try String.init(page.arena, normalized_value, .{});
existing._important = important;
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 {
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
// 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;
while (node) |n| {
const prop = Property.fromNodeLink(n);
- if (prop._name.eqlSlice(name)) {
+ if (prop._name.eql(name)) {
return prop;
}
node = n.next;
@@ -617,26 +618,36 @@ fn isLengthProperty(name: []const u8) bool {
return length_properties.has(name);
}
-fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
- if (std.mem.eql(u8, normalized_name, "visibility")) {
- return "visible";
+fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, name: String) []const u8 {
+ switch (name.len) {
+ 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 "";
}
diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig
index aa8655f1..2b41fffa 100644
--- a/src/browser/webapi/css/CSSStyleSheet.zig
+++ b/src/browser/webapi/css/CSSStyleSheet.zig
@@ -69,12 +69,19 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, pag
const rules = try self.getCssRules(page);
try rules.insert(index, style_rule._proto, page);
+
+ // Notify StyleManager that rules have changed
+ page._style_manager.sheetModified();
+
return index;
}
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
const rules = try self.getCssRules(page);
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 {
@@ -99,6 +106,9 @@ pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void {
try rules.insert(index, style_rule._proto, page);
index += 1;
}
+
+ // Notify StyleManager that rules have changed
+ page._style_manager.sheetModified();
}
pub const JsApi = struct {
diff --git a/src/browser/webapi/css/StyleSheetList.zig b/src/browser/webapi/css/StyleSheetList.zig
index 11efb45c..c0732c73 100644
--- a/src/browser/webapi/css/StyleSheetList.zig
+++ b/src/browser/webapi/css/StyleSheetList.zig
@@ -24,6 +24,15 @@ pub fn add(self: *StyleSheetList, sheet: *CSSStyleSheet, page: *Page) !void {
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 bridge = js.Bridge(StyleSheetList);
diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig
index b6389d61..fc52cf38 100644
--- a/src/browser/webapi/element/html/Style.zig
+++ b/src/browser/webapi/element/html/Style.zig
@@ -106,7 +106,10 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
pub fn styleAddedCallback(self: *Style, page: *Page) !void {
// 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 (page.isGoingAway()) {