diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 894a73913..d3f529d9c 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -654,7 +654,7 @@ fn rebuildIfDirty(self: *StyleManager) !void { /// Own-element cascade result, resolved for every property at once so one /// entry serves any probe. -const Props = packed struct(u8) { +const Props = packed struct(u10) { // Author value (inline or sheet). Without `author_display` it's the UA // fallback: .none when matchesUaDisplayNoneRule, else .other. display: Display = .other, @@ -664,6 +664,8 @@ const Props = packed struct(u8) { pointer_events_none: bool = false, overflow_x_scrolls: bool = false, overflow_y_scrolls: bool = false, + overscroll_x_contains: bool = false, + overscroll_y_contains: bool = false, fn probe(self: Props, comptime what: Probe, options: CheckVisibilityOptions) bool { return switch (what) { @@ -737,6 +739,15 @@ pub fn overflowAxes(self: *StyleManager, el: *Element) Element.ScrollAxes { return .{ .x = p.overflow_x_scrolls, .y = p.overflow_y_scrolls }; } +/// The axes along which `el` keeps a scroll from chaining out of it: its own +/// computed overscroll-behavior on that axis is contain or none. No ancestor +/// walk. +pub fn overscrollContainAxes(self: *StyleManager, el: *Element) Element.ScrollAxes { + self.rebuildIfDirty() catch return .{}; + const p = self.ownProps(el); + return .{ .x = p.overscroll_x_contains, .y = p.overscroll_y_contains }; +} + fn anyInChain(self: *StyleManager, el: *Element, comptime what: Probe, options: CheckVisibilityOptions) bool { var current: ?*Element = el; while (current) |elem| : (current = elem.parentElement()) { @@ -776,6 +787,8 @@ const Priorities = struct { pointer_events_none: u64 = 0, overflow_x_scrolls: u64 = 0, overflow_y_scrolls: u64 = 0, + overscroll_x_contains: u64 = 0, + overscroll_y_contains: u64 = 0, }; fn compute(self: *StyleManager, el: *Element) Props { @@ -1020,7 +1033,7 @@ fn getBucketKey(compound: Selector.Compound) ?BucketKey { } // The declaration names behind TrackedProperties, in field order. -const property_names = [_][]const u8{ "display", "visibility", "opacity", "pointer-events", "overflow-x", "overflow-y" }; +const property_names = [_][]const u8{ "display", "visibility", "opacity", "pointer-events", "overflow-x", "overflow-y", "overscroll-behavior-x", "overscroll-behavior-y" }; /// Extracts the tracked properties from a style declaration. The object holds /// one entry per name in first-declared order, so folding it in order gives a @@ -1113,6 +1126,8 @@ const TrackedProperties = struct { pointer_events_none: ?bool = null, overflow_x_scrolls: ?bool = null, overflow_y_scrolls: ?bool = null, + overscroll_x_contains: ?bool = null, + overscroll_y_contains: ?bool = null, fn apply(self: *TrackedProperties, name: []const u8, value: []const u8) void { if (std.ascii.eqlIgnoreCase(name, "display")) { @@ -1127,6 +1142,10 @@ const TrackedProperties = struct { self.overflow_x_scrolls = overflowScrolls(value); } else if (std.ascii.eqlIgnoreCase(name, "overflow-y")) { self.overflow_y_scrolls = overflowScrolls(value); + } else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-x")) { + self.overscroll_x_contains = overscrollContains(value); + } else if (std.ascii.eqlIgnoreCase(name, "overscroll-behavior-y")) { + self.overscroll_y_contains = overscrollContains(value); } } @@ -1137,6 +1156,13 @@ const TrackedProperties = struct { std.ascii.eqlIgnoreCase(value, "overlay"); } + // `contain` keeps the scroll in the box, `none` also kills the bounce we + // don't render anyway; only `auto` lets a scroll chain outward. + fn overscrollContains(value: []const u8) bool { + return std.ascii.eqlIgnoreCase(value, "contain") or + std.ascii.eqlIgnoreCase(value, "none"); + } + fn isRelevant(self: TrackedProperties) bool { inline for (property_fields) |field| { if (@field(self, field) != null) { @@ -1277,6 +1303,12 @@ fn foldDeclarations(block: []const u8, customs: ?*CustomSink) !TrackedProperties return slots.props(); } +// The ` []` shorthands the cascade expands into the tracked longhands. +const axis_shorthands = [_]struct { name: []const u8, x: []const u8, y: []const u8 }{ + .{ .name = "overflow", .x = "overflow-x", .y = "overflow-y" }, + .{ .name = "overscroll-behavior", .x = "overscroll-behavior-x", .y = "overscroll-behavior-y" }, +}; + /// One block's winning value per tracked property, folded in declaration /// order. const Slots = struct { @@ -1300,11 +1332,13 @@ const Slots = struct { slots: [property_names.len]Slot = @splat(.{}), fn apply(self: *Slots, name: []const u8, value: []const u8, important: bool) void { - if (std.ascii.eqlIgnoreCase(name, "overflow")) { - const values = CssParser.splitOverflow(value) orelse return; - self.apply("overflow-x", values.x, important); - self.apply("overflow-y", values.y, important); - return; + for (axis_shorthands) |shorthand| { + if (std.ascii.eqlIgnoreCase(name, shorthand.name)) { + const values = CssParser.splitAxisPair(value) orelse return; + self.apply(shorthand.x, values.x, important); + self.apply(shorthand.y, values.y, important); + return; + } } for (property_names, &self.slots) |tracked, *slot| { if (std.ascii.eqlIgnoreCase(name, tracked)) { @@ -1752,6 +1786,15 @@ test "StyleManager: memo: reuse and invalidation" { try (try b.getOrCreateStyle(frame)).asCSSStyleDeclaration().setProperty("overflow", "hidden", null, frame); try testing.expectEqual(Element.ScrollAxes{}, sm.overflowAxes(b)); + // overscroll-behavior expands the same way; only `auto` chains outward. + try b.setStyle("overscroll-behavior: contain auto", frame); + try testing.expectEqual(Element.ScrollAxes{ .x = true, .y = false }, sm.overscrollContainAxes(b)); + try testing.expectEqual(Element.ScrollAxes{}, sm.overscrollContainAxes(p)); + try b.setStyle("overscroll-behavior-y: none", frame); + try testing.expectEqual(Element.ScrollAxes{ .x = false, .y = true }, sm.overscrollContainAxes(b)); + try b.setStyle("overscroll-behavior: contain; overscroll-behavior-x: auto", frame); + try testing.expectEqual(Element.ScrollAxes{ .x = false, .y = true }, sm.overscrollContainAxes(b)); + // A stylesheet change resets the memo sm.sheetModified(); try testing.expectEqual(false, sm.isHidden(p, .{})); diff --git a/src/browser/css/Parser.zig b/src/browser/css/Parser.zig index dc8806dae..8cda3dcc2 100644 --- a/src/browser/css/Parser.zig +++ b/src/browser/css/Parser.zig @@ -25,11 +25,12 @@ pub const Declaration = struct { important: bool, }; -pub const OverflowValues = struct { x: []const u8, y: []const u8 }; +pub const AxisPair = 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 { +/// An ` []` axis shorthand such as `overflow` or `overscroll-behavior`; +/// a single value applies to both axes. More than two values is invalid and +/// null, as is an empty declaration. +pub fn splitAxisPair(value: []const u8) ?AxisPair { var it = std.mem.tokenizeAny(u8, value, &std.ascii.whitespace); const x = it.next() orelse return null; const y = it.next() orelse x; diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 1eb09385a..7b0abf2b7 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -241,7 +241,7 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value: fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void { const normalized = normalizePropertyName(declaration.name, &frame.buf); if (overflow_shorthand.eqlSlice(normalized)) { - const values = CssParser.splitOverflow(declaration.value) orelse return; + const values = CssParser.splitAxisPair(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; @@ -267,7 +267,7 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value: const normalized = normalizePropertyName(property_name, &frame.buf); if (overflow_shorthand.eqlSlice(normalized)) { - const values = CssParser.splitOverflow(value) orelse return false; + const values = CssParser.splitAxisPair(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;