Simplify the overflow and wheel changes

The overflow shorthand split lives once, in the CSS parser, for both the
style declaration and the StyleManager attribute fold; the two copies
disagreed on three-value input. The declaration states the longhand-pair
rule once and serializes the pair where its first longhand sits without
a second scan. The two listener scans merge into one with a filter. The
per-axis scroll walk goes: the cascade memoizes each element, so calling
the single-axis walk twice costs a few lookups.
This commit is contained in:
Adrià Arrufat committed 2026-09-16 15:27:35 +02:00
1 parent b0f6066be7
commit e7d151d982
8 files changed
+91 -129

No files matched your search

+2 -2
View File
@@ -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 };
}
}
+8 -17
View File
@@ -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;
}
}
+5 -7
View File
@@ -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: <x> [<y>]`; 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);
}
+14
View File
@@ -25,6 +25,20 @@ pub const Declaration = struct {
important: bool,
};
pub const OverflowValues = struct { x: []const u8, y: []const u8 };
/// `overflow: <x> [<y>]`; 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,
+2 -3
View File
@@ -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 {
+5 -40
View File
@@ -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,
-2
View File
@@ -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,
+55 -58
View File
@@ -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: <x> [<y>]`; 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);