diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index ba91aebf8..187462d3d 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -431,7 +431,7 @@ fn anyNonPassive(self: *EventManager, path: []const *EventTarget, event: *Event) return true; } if (self.listenersFor(target, event)) |listeners| { - if (EventManagerBase.hasNonPassiveListener(listeners.list)) { + if (EventManagerBase.hasListener(listeners.list, .non_passive)) { return true; } } @@ -449,7 +449,7 @@ const TargetListeners = struct { /// `mousewheel` listener fires only where no `wheel` listener is registered. fn listenersFor(self: *EventManager, target: *EventTarget, event: *const Event) ?TargetListeners { if (self.base.getListeners(target, event._type_string)) |list| { - if (EventManagerBase.hasLiveListener(list)) { + if (EventManagerBase.hasListener(list, .any)) { return .{ .list = list, .typ = event._type_string }; } } diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig index ea33c71a9..e5609b01b 100644 --- a/src/browser/EventManagerBase.zig +++ b/src/browser/EventManagerBase.zig @@ -205,27 +205,18 @@ pub fn getListeners(self: *EventManagerBase, target: *EventTarget, event_type: S }); } -/// Whether the list still holds a listener. One removed during a dispatch -/// stays linked until the dispatch unwinds, but isn't "in" the list anymore, -/// same as findListener. -pub fn hasLiveListener(list: *const std.DoublyLinkedList) bool { +/// Whether the list still holds a listener, or one that can call +/// preventDefault. A listener removed during a dispatch stays linked until +/// the dispatch unwinds, but isn't "in" the list anymore, same as +/// findListener. +pub fn hasListener(list: *const std.DoublyLinkedList, comptime which: enum { any, non_passive }) bool { var node = list.first; while (node) |n| : (node = n.next) { const listener: *align(8) Listener = @fieldParentPtr("node", n); - if (!listener.removed) { - return true; + if (listener.removed) { + continue; } - } - return false; -} - -/// Whether a listener in the list can call preventDefault. A removed -/// listener isn't "in" the list anymore, same as findListener. -pub fn hasNonPassiveListener(list: *const std.DoublyLinkedList) bool { - var node = list.first; - while (node) |n| : (node = n.next) { - const listener: *align(8) Listener = @fieldParentPtr("node", n); - if (!listener.removed and !listener.passive) { + if (which == .any or !listener.passive) { return true; } } diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 1262d38a1..2fb24b388 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -1286,14 +1286,12 @@ const Slots = struct { fn apply(self: *Slots, name: []const u8, value: []const u8, important: bool) void { if (std.ascii.eqlIgnoreCase(name, "overflow")) { - // `overflow: []`; a single value applies to both axes. - var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace); - const x = it.next() orelse ""; - self.apply("overflow-x", x, important); - self.apply("overflow-y", it.next() orelse x, important); + const values = CssParser.splitOverflow(value) orelse return; + self.apply("overflow-x", values.x, important); + self.apply("overflow-y", values.y, important); return; } - inline for (property_names, &self.slots) |tracked, *slot| { + for (property_names, &self.slots) |tracked, *slot| { if (std.ascii.eqlIgnoreCase(name, tracked)) { slot.apply(value, important); return; @@ -1303,7 +1301,7 @@ const Slots = struct { fn props(self: Slots) TrackedProperties { var p: TrackedProperties = .{}; - inline for (property_names, self.slots) |name, s| { + for (property_names, self.slots) |name, s| { if (s.value) |value| { p.apply(name, value); } diff --git a/src/browser/css/Parser.zig b/src/browser/css/Parser.zig index 7e9ec41a8..dc8806dae 100644 --- a/src/browser/css/Parser.zig +++ b/src/browser/css/Parser.zig @@ -25,6 +25,20 @@ pub const Declaration = struct { important: bool, }; +pub const OverflowValues = struct { x: []const u8, y: []const u8 }; + +/// `overflow: []`; a single value applies to both axes. More than two +/// values is invalid and null, as is an empty declaration. +pub fn splitOverflow(value: []const u8) ?OverflowValues { + var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace); + const x = it.next() orelse return null; + const y = it.next() orelse x; + if (it.next() != null) { + return null; + } + return .{ .x = x, .y = y }; +} + const TokenSpan = struct { token: Tokenizer.Token, start: usize, diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig index 93cc8f417..5ac120f65 100644 --- a/src/browser/frame/user_input.zig +++ b/src/browser/frame/user_input.zig @@ -320,10 +320,9 @@ pub fn wheel(frame: *Frame, target: *Element, x: f64, y: f64, delta_x: f64, delt /// else the viewport. Relative deltas may land on different scrollers per /// axis, unlike an absolute position. fn wheelScroll(target: *Element, delta_x: i32, delta_y: i32, frame: *Frame) !void { - const targets = target.scrollContainers(.{ .x = delta_x != 0, .y = delta_y != 0 }, frame); // A zero delta resolves to .viewport and scrolls it by nothing. - try targets.x.scrollBy(delta_x, 0, frame); - try targets.y.scrollBy(0, delta_y, frame); + try target.scrollContainer(.{ .x = delta_x != 0 }, frame).scrollBy(delta_x, 0, frame); + try target.scrollContainer(.{ .y = delta_y != 0 }, frame).scrollBy(0, delta_y, frame); } fn deltaToScroll(d: f64) i32 { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 62f8d3e94..ddb480267 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -22,7 +22,6 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const dump = @import("../dump.zig"); const Frame = @import("../Frame.zig"); -const StyleManager = @import("../StyleManager.zig"); const Factory = @import("../Factory.zig"); const CSS = @import("CSS.zig"); @@ -1624,15 +1623,12 @@ const ScrollTarget = union(enum) { } }; -const ScrollTargets = struct { - x: ScrollTarget = .viewport, - y: ScrollTarget = .viewport, -}; - -/// Nearest ancestor-or-self scroll container along any of `axes`, for callers -/// positioning one scroller with an absolute offset. +/// Nearest ancestor-or-self scroll container along any of `axes`. The walk +/// is cheap to repeat per axis: the cascade memoizes each element's props. pub fn scrollContainer(self: *Element, axes: ScrollAxes, frame: *Frame) ScrollTarget { - const style_manager = self.scrollStyleManager(axes, frame) orelse return .viewport; + if (!axes.x and !axes.y) return .viewport; + const owner = self.ownerFrame(frame) orelse return .viewport; + const style_manager = &owner._style_manager; var current: ?*Element = self; while (current) |el| : (current = el.parentElement()) { if (el.scrollsViewport()) break; @@ -1644,37 +1640,6 @@ pub fn scrollContainer(self: *Element, axes: ScrollAxes, frame: *Frame) ScrollTa return .viewport; } -/// Nearest ancestor-or-self scroll container per requested axis, in one walk. -/// Relative deltas may land on a different scroller per axis. An axis not in -/// `axes` stays `.viewport`. -pub fn scrollContainers(self: *Element, axes: ScrollAxes, frame: *Frame) ScrollTargets { - var targets: ScrollTargets = .{}; - var pending = axes; - const style_manager = self.scrollStyleManager(axes, frame) orelse return targets; - var current: ?*Element = self; - while (current) |el| : (current = el.parentElement()) { - if (el.scrollsViewport()) break; - const scrolls = style_manager.overflowAxes(el); - if (pending.x and scrolls.x) { - targets.x = .{ .container = el }; - pending.x = false; - } - if (pending.y and scrolls.y) { - targets.y = .{ .container = el }; - pending.y = false; - } - if (!pending.x and !pending.y) break; - } - return targets; -} - -/// null when the walk cannot find anything: no axis asked for, or detached. -fn scrollStyleManager(self: *Element, axes: ScrollAxes, frame: *Frame) ?*StyleManager { - if (!axes.x and !axes.y) return null; - const owner = self.ownerFrame(frame) orelse return null; - return &owner._style_manager; -} - fn scrollsViewport(self: *const Element) bool { return switch (self.getTag()) { .html, .body => true, diff --git a/src/browser/webapi/WebDriver.zig b/src/browser/webapi/WebDriver.zig index b8db4f5be..da36d393e 100644 --- a/src/browser/webapi/WebDriver.zig +++ b/src/browser/webapi/WebDriver.zig @@ -603,8 +603,6 @@ fn dispatch(target: *EventTarget, event: *Event, frame: *Frame, typ: []const u8) } fn dispatchTouch(el: *Element, comptime typ: []const u8, frame: *Frame) void { - // Listeners live in the event manager of the element's own frame, which - // is not the caller's when the element belongs to an iframe's document. const owner = el.ownerFrame(frame) orelse return; const event = TouchEvent.initTrusted(typ, .{ .bubbles = true, diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 0445bdf7e..1eb09385a 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -142,11 +142,10 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const pub fn declaredValue(self: *const CSSStyleDeclaration, name: String, frame: *Frame) ?[]const u8 { if (name.eql(overflow_shorthand)) { const x = self.findProperty(comptime .wrap("overflow-x")) orelse return null; - const y = self.overflowPartner(x) orelse return null; - if (x._value.eql(y._value)) { - return x._value.str(); - } - return std.fmt.allocPrint(frame.local_arena, "{f} {f}", .{ x._value, y._value }) catch return null; + const pair = self.overflowPair(x) orelse return null; + var buf = std.Io.Writer.Allocating.init(frame.local_arena); + pair.formatValue(&buf.writer) catch return null; + return buf.written(); } const prop = self.findProperty(name) orelse return null; return prop._value.str(); @@ -158,34 +157,40 @@ pub fn declaredValue(self: *const CSSStyleDeclaration, name: String, frame: *Fra // sets both longhands, reading or serializing it recombines them. const overflow_shorthand: String = .wrap("overflow"); -const OverflowValues = struct { x: []const u8, y: []const u8 }; +/// Both overflow longhands, declared with the same priority: the pair reads +/// and serializes as the shorthand. +const OverflowPair = struct { + x: *const Property, + y: *const Property, -// `overflow: []`; a single value applies to both axes. More than two -// values is invalid and ignored. -fn splitOverflow(value: []const u8) ?OverflowValues { - var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace); - const x = it.next() orelse return null; - const y = it.next() orelse x; - if (it.next() != null) { + fn formatValue(self: OverflowPair, writer: *std.Io.Writer) !void { + try self.x._value.format(writer); + if (!self.x._value.eql(self.y._value)) { + try writer.writeByte(' '); + try self.y._value.format(writer); + } + } + + fn format(self: OverflowPair, writer: *std.Io.Writer) !void { + try writer.writeAll("overflow: "); + try self.formatValue(writer); + try formatDeclarationEnd(self.x._important, writer); + } +}; + +/// The pair `prop` belongs to, when it is an overflow longhand and the other +/// is declared with the same priority. +fn overflowPair(self: *const CSSStyleDeclaration, prop: *const Property) ?OverflowPair { + const is_x = prop._name.eql(comptime .wrap("overflow-x")); + if (!is_x and !prop._name.eql(comptime .wrap("overflow-y"))) { return null; } - return .{ .x = x, .y = y }; -} - -/// The other overflow longhand when `prop` is one and both are declared with -/// the same priority: the pair reads and serializes as the shorthand. -fn overflowPartner(self: *const CSSStyleDeclaration, prop: *const Property) ?*Property { - const other: String = if (prop._name.eql(comptime .wrap("overflow-x"))) - comptime .wrap("overflow-y") - else if (prop._name.eql(comptime .wrap("overflow-y"))) - comptime .wrap("overflow-x") - else - return null; + const other: String = if (is_x) comptime .wrap("overflow-y") else comptime .wrap("overflow-x"); const partner = self.findProperty(other) orelse return null; if (partner._important != prop._important) { return null; } - return partner; + return if (is_x) .{ .x = prop, .y = partner } else .{ .x = partner, .y = prop }; } fn resolvedDimension(element: *Element, dimension: enum { width, height }, frame: *Frame) []const u8 { @@ -203,10 +208,9 @@ pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []co const normalized = normalizePropertyName(property_name, &frame.buf); const wrapped = String.wrap(normalized); if (wrapped.eql(overflow_shorthand)) { - // A shorthand is important when all its longhands are. const x = self.findProperty(comptime .wrap("overflow-x")) orelse return ""; - const y = self.findProperty(comptime .wrap("overflow-y")) orelse return ""; - return if (x._important and y._important) "important" else ""; + const pair = self.overflowPair(x) orelse return ""; + return if (pair.x._important) "important" else ""; } const prop = self.findProperty(wrapped) orelse return ""; return if (prop._important) "important" else ""; @@ -236,8 +240,8 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: /// not override an earlier !important one (CSS cascade precedence). fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void { const normalized = normalizePropertyName(declaration.name, &frame.buf); - if (std.mem.eql(u8, normalized, "overflow")) { - const values = splitOverflow(declaration.value) orelse return; + if (overflow_shorthand.eqlSlice(normalized)) { + const values = CssParser.splitOverflow(declaration.value) orelse return; try self.applyParsedDeclaration(.{ .name = "overflow-x", .value = values.x, .important = declaration.important }, frame); try self.applyParsedDeclaration(.{ .name = "overflow-y", .value = values.y, .important = declaration.important }, frame); return; @@ -262,8 +266,8 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: } const normalized = normalizePropertyName(property_name, &frame.buf); - if (std.mem.eql(u8, normalized, "overflow")) { - const values = splitOverflow(value) orelse return false; + if (overflow_shorthand.eqlSlice(normalized)) { + const values = CssParser.splitOverflow(value) orelse return false; const x = try self.setPropertyImpl("overflow-x", values.x, important, frame); const y = try self.setPropertyImpl("overflow-y", values.y, important, frame); return x or y; @@ -305,8 +309,8 @@ pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, fra fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, frame: *Frame) !?[]const u8 { const normalized = normalizePropertyName(property_name, &frame.buf); - if (std.mem.eql(u8, normalized, "overflow")) { - const old_value = try frame.call_arena.dupe(u8, self.declaredValue(overflow_shorthand, frame) orelse ""); + if (overflow_shorthand.eqlSlice(normalized)) { + const old_value = self.declaredValue(overflow_shorthand, frame) orelse ""; const x = try self.removePropertyImpl("overflow-x", frame); const y = try self.removePropertyImpl("overflow-y", frame); if (x == null and y == null) { @@ -398,30 +402,20 @@ pub fn replaceCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Fram pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void { var first = true; - var node = self._properties.first; - while (node) |n| : (node = n.next) { - const prop = Property.fromNodeLink(n); - // A pair of overflow longhands serializes once, as the shorthand, - // where overflow-x sits. - const partner = self.overflowPartner(prop); - if (partner != null and prop._name.eql(comptime .wrap("overflow-y"))) { + // An overflow pair serializes once, where its first longhand sits. + var skip: ?*const Property = null; + var it = self.iterator(); + while (it.next()) |prop| { + if (prop == skip) { continue; } if (!first) { try writer.writeByte(' '); } first = false; - if (partner) |y| { - try writer.writeAll("overflow: "); - try prop._value.format(writer); - if (!prop._value.eql(y._value)) { - try writer.writeByte(' '); - try y._value.format(writer); - } - if (prop._important) { - try writer.writeAll(" !important"); - } - try writer.writeByte(';'); + if (self.overflowPair(prop)) |pair| { + try pair.format(writer); + skip = if (pair.x == prop) pair.y else pair.x; } else { try prop.format(writer); } @@ -1049,14 +1043,17 @@ pub const Property = struct { try self._name.format(writer); try writer.writeAll(": "); try self._value.format(writer); - - if (self._important) { - try writer.writeAll(" !important"); - } - try writer.writeByte(';'); + try formatDeclarationEnd(self._important, writer); } }; +fn formatDeclarationEnd(important: bool, writer: *std.Io.Writer) !void { + if (important) { + try writer.writeAll(" !important"); + } + try writer.writeByte(';'); +} + pub const JsApi = struct { pub const bridge = js.Bridge(CSSStyleDeclaration);