diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index fe1d4ec1..4fb65b70 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -488,6 +488,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMImplementation.zig"), @import("../webapi/DOMTreeWalker.zig"), @import("../webapi/DOMNodeIterator.zig"), + @import("../webapi/DOMRect.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), @import("../webapi/element/DOMStringMap.zig"), diff --git a/src/browser/webapi/DOMRect.zig b/src/browser/webapi/DOMRect.zig new file mode 100644 index 00000000..6309a20e --- /dev/null +++ b/src/browser/webapi/DOMRect.zig @@ -0,0 +1,64 @@ +const DOMRect = @This(); + +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +_x: f64, +_y: f64, +_width: f64, +_height: f64, +_top: f64, +_right: f64, +_bottom: f64, +_left: f64, + +pub fn getX(self: *DOMRect) f64 { + return self._x; +} + +pub fn getY(self: *DOMRect) f64 { + return self._y; +} + +pub fn getWidth(self: *DOMRect) f64 { + return self._width; +} + +pub fn getHeight(self: *DOMRect) f64 { + return self._height; +} + +pub fn getTop(self: *DOMRect) f64 { + return self._top; +} + +pub fn getRight(self: *DOMRect) f64 { + return self._right; +} + +pub fn getBottom(self: *DOMRect) f64 { + return self._bottom; +} + +pub fn getLeft(self: *DOMRect) f64 { + return self._left; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DOMRect); + + pub const Meta = struct { + pub const name = "DOMRect"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const x = bridge.accessor(DOMRect.getX, null, .{}); + pub const y = bridge.accessor(DOMRect.getY, null, .{}); + pub const width = bridge.accessor(DOMRect.getWidth, null, .{}); + pub const height = bridge.accessor(DOMRect.getHeight, null, .{}); + pub const top = bridge.accessor(DOMRect.getTop, null, .{}); + pub const right = bridge.accessor(DOMRect.getRight, null, .{}); + pub const bottom = bridge.accessor(DOMRect.getBottom, null, .{}); + pub const left = bridge.accessor(DOMRect.getLeft, null, .{}); +}; diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 0ca65757..7849e3e3 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -13,6 +13,8 @@ const Selector = @import("selector/Selector.zig"); pub const Attribute = @import("element/Attribute.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); pub const DOMStringMap = @import("element/DOMStringMap.zig"); +const DOMRect = @import("DOMRect.zig"); +const css = @import("css.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); @@ -467,6 +469,126 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select return Selector.querySelectorAll(self.asNode(), input, page); } +pub fn parentElement(self: *Element) ?*Element { + return self._proto.parentElement(); +} + +pub fn checkVisibility(self: *Element, page: *Page) !bool { + var current: ?*Element = self; + + while (current) |el| { + const style = try el.getStyle(page); + const display = style.asCSSStyleDeclaration().getPropertyValue("display", page); + if (std.mem.eql(u8, display, "none")) { + return false; + } + current = el.parentElement(); + } + + return true; +} + +pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect { + const is_visible = try self.checkVisibility(page); + if (!is_visible) { + return page._factory.create(DOMRect{ + ._x = 0.0, + ._y = 0.0, + ._width = 0.0, + ._height = 0.0, + ._top = 0.0, + ._right = 0.0, + ._bottom = 0.0, + ._left = 0.0, + }); + } + + const y = calculateDocumentPosition(self.asNode()); + + var width: f64 = 1.0; + var height: f64 = 1.0; + + const style = try self.getStyle(page); + const decl = style.asCSSStyleDeclaration(); + width = css.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0; + height = css.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0; + + if (width == 1.0 or height == 1.0) { + const tag = self.getTag(); + if (tag == .img or tag == .iframe) { + if (self.getAttributeSafe("width")) |w| { + width = std.fmt.parseFloat(f64, w) catch width; + } + if (self.getAttributeSafe("height")) |h| { + height = std.fmt.parseFloat(f64, h) catch height; + } + } + } + + const x: f64 = 0.0; + const top = y; + const left = x; + const right = x + width; + const bottom = y + height; + + return page._factory.create(DOMRect{ + ._x = x, + ._y = y, + ._width = width, + ._height = height, + ._top = top, + ._right = right, + ._bottom = bottom, + ._left = left, + }); +} + +// Calculates a pseudo-position in the document using an efficient heuristic. +// +// Instead of walking the entire DOM tree (which would be O(total_nodes)), this +// function walks UP the tree counting previous siblings at each level. Each level +// uses exponential weighting (1000x per depth level) to preserve document order. +// +// This gives O(depth * avg_siblings) complexity while maintaining relative positioning +// that's useful for scraping and understanding element flow in the document. +// +// Example: +// → position 0 +//
→ position 0 (0 siblings at level 1) +// → position 0 (0 siblings at level 2) +// → position 1 (1 sibling at level 2) +//
+//
→ position 1000 (1 sibling at level 1, weighted by 1000) +//

→ position 1000 (0 siblings at level 2, parent has 1000) +//
+// +// +// Trade-offs: +// - Much faster than full tree-walking for deep/large DOMs +// - Positions reflect document order and parent-child relationships +// - Not pixel-accurate, but sufficient for 1x1 layout heuristics +fn calculateDocumentPosition(node: *Node) f64 { + var position: f64 = 0.0; + var multiplier: f64 = 1.0; + var current = node; + + while (current.parentNode()) |parent| { + var count: f64 = 0.0; + var sibling = parent.firstChild(); + while (sibling) |s| { + if (s == current) break; + count += 1.0; + sibling = s.nextSibling(); + } + + position += count * multiplier; + multiplier *= 1000.0; + current = parent; + } + + return position; +} + const GetElementsByTagNameResult = union(enum) { tag: collections.NodeLive(.tag), tag_name: collections.NodeLive(.tag_name), @@ -702,6 +824,8 @@ pub const JsApi = struct { pub const matches = bridge.function(Element.matches, .{ .dom_exception = true }); pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); + pub const checkVisibility = bridge.function(Element.checkVisibility, .{}); + pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{}); pub const children = bridge.accessor(Element.getChildren, null, .{}); diff --git a/src/browser/webapi/css.zig b/src/browser/webapi/css.zig new file mode 100644 index 00000000..ea4b1e90 --- /dev/null +++ b/src/browser/webapi/css.zig @@ -0,0 +1,14 @@ +const std = @import("std"); + +pub fn parseDimension(value: []const u8) ?f64 { + if (value.len == 0) { + return null; + } + + var num_str = value; + if (std.mem.endsWith(u8, value, "px")) { + num_str = value[0 .. value.len - 2]; + } + + return std.fmt.parseFloat(f64, num_str) catch null; +} diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 1b8c8424..36569866 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -57,13 +57,13 @@ pub fn item(self: *const CSSStyleDeclaration, index: u32) []const u8 { return ""; } -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 prop = self.findProperty(normalized) orelse return ""; return prop._value.str(); } -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 prop = self.findProperty(normalized) orelse return ""; return if (prop._important) "important" else ""; diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index 2eeefff6..1de71ea4 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -154,7 +154,7 @@ pub const JsApi = struct { } } - const value = try self._proto.getPropertyValue(dash_case, page); + const value = self._proto.getPropertyValue(dash_case, page); // Property accessors have special handling for empty values: // - Known CSS properties return '' when not set