mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
Merge pull request #3079 from lightpanda-io/dedupe-svg-css
chore: dedupe SVG / CSS code
This commit is contained in:
@@ -22,6 +22,7 @@ const lp = @import("lightpanda");
|
||||
|
||||
const Frame = @import("Frame.zig");
|
||||
|
||||
const units = @import("css/units.zig");
|
||||
const CssParser = @import("css/Parser.zig");
|
||||
const MediaQuery = @import("css/MediaQuery.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
@@ -1214,6 +1215,56 @@ pub fn inlineStyleValue(self: *StyleManager, el: *Element, property_name: String
|
||||
return property._value.str();
|
||||
}
|
||||
|
||||
/// Bounds computedFontSize's ancestor recursion (the parent walk and
|
||||
/// `inherit`/relative-unit chains share it).
|
||||
const MAX_FONT_ANCESTOR_DEPTH = 32;
|
||||
|
||||
/// Computed font-size in CSS pixels. We do what we can, namely inline styles
|
||||
/// font-related attributes, up the parent chain.
|
||||
pub fn computedFontSize(self: *StyleManager, element: ?*Element) f64 {
|
||||
return self.computedFontSizeAt(element, 0);
|
||||
}
|
||||
|
||||
fn computedFontSizeAt(self: *StyleManager, element: ?*Element, depth: u8) f64 {
|
||||
if (depth >= MAX_FONT_ANCESTOR_DEPTH) {
|
||||
return 16;
|
||||
}
|
||||
const current = element orelse return 16;
|
||||
const parent = current.parentElement();
|
||||
|
||||
if (self.inlineStyleValue(current, comptime .wrap("font-size"))) |raw| {
|
||||
if (self.parseFontSize(raw, parent, depth + 1)) |size| {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
if (current.getAttributeSafe(comptime .wrap("font-size"))) |raw| {
|
||||
if (self.parseFontSize(raw, parent, depth + 1)) |size| {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
return self.computedFontSizeAt(parent, depth + 1);
|
||||
}
|
||||
|
||||
fn parseFontSize(self: *StyleManager, raw: []const u8, parent: ?*Element, depth: u8) ?f64 {
|
||||
const value = std.mem.trim(u8, raw, " \t\r\n\x0c");
|
||||
if (std.ascii.eqlIgnoreCase(value, "inherit") or std.ascii.eqlIgnoreCase(value, "unset")) {
|
||||
return self.computedFontSizeAt(parent, depth);
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(value, "initial") or std.ascii.eqlIgnoreCase(value, "medium")) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
const parsed = units.parse(value) catch return null;
|
||||
const factor = units.absoluteLengthFactor(parsed.unit) orelse switch (parsed.unit) {
|
||||
.percentage => self.computedFontSizeAt(parent, depth) / 100.0,
|
||||
.em => self.computedFontSizeAt(parent, depth),
|
||||
.ex => self.computedFontSizeAt(parent, depth) / 2.0,
|
||||
else => return null,
|
||||
};
|
||||
const size = parsed.value * factor;
|
||||
return if (size >= 0 and std.math.isFinite(size)) size else null;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "StyleManager: computeSpecificity: element selector" {
|
||||
// div -> (0, 0, 1)
|
||||
|
||||
168
src/browser/css/units.zig
Normal file
168
src/browser/css/units.zig
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shared lexing and conversion for CSS/SVG dimension values: one suffix
|
||||
//! table and one set of absolute-length factors. Consumers (svg/Length,
|
||||
//! svg/Angle, webapi/CSS, StyleManager's font-size resolution) map the
|
||||
//! result onto their own domain, rejecting units that don't apply.
|
||||
//!
|
||||
//! css/MediaQuery keeps its own parser on purpose: MQ4 is fail-closed and
|
||||
//! deliberately more restrictive than this lenient attribute-value lexer.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const Unit = enum {
|
||||
none, // bare number, no suffix
|
||||
percentage,
|
||||
em,
|
||||
ex,
|
||||
px,
|
||||
cm,
|
||||
mm,
|
||||
in,
|
||||
pt,
|
||||
pc,
|
||||
vh,
|
||||
vw,
|
||||
deg,
|
||||
rad,
|
||||
grad,
|
||||
turn,
|
||||
};
|
||||
|
||||
pub const Parsed = struct {
|
||||
value: f64,
|
||||
unit: Unit,
|
||||
};
|
||||
|
||||
const WHITESPACE = " \t\r\n\x0c";
|
||||
|
||||
// Longer suffixes first: "5grad" must not match the "rad" entry.
|
||||
const suffixes = [_]struct { []const u8, Unit }{
|
||||
.{ "turn", .turn },
|
||||
.{ "grad", .grad },
|
||||
.{ "deg", .deg },
|
||||
.{ "rad", .rad },
|
||||
.{ "%", .percentage },
|
||||
.{ "em", .em },
|
||||
.{ "ex", .ex },
|
||||
.{ "px", .px },
|
||||
.{ "cm", .cm },
|
||||
.{ "mm", .mm },
|
||||
.{ "in", .in },
|
||||
.{ "pt", .pt },
|
||||
.{ "pc", .pc },
|
||||
.{ "vh", .vh },
|
||||
.{ "vw", .vw },
|
||||
};
|
||||
|
||||
pub fn parse(input: []const u8) error{SyntaxError}!Parsed {
|
||||
const value = std.mem.trim(u8, input, WHITESPACE);
|
||||
if (value.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
for (suffixes) |entry| {
|
||||
const sfx, const unit = entry;
|
||||
if (value.len <= sfx.len) {
|
||||
continue;
|
||||
}
|
||||
if (!std.ascii.eqlIgnoreCase(value[value.len - sfx.len ..], sfx)) {
|
||||
continue;
|
||||
}
|
||||
const number = std.mem.trim(u8, value[0 .. value.len - sfx.len], WHITESPACE);
|
||||
return .{ .value = try parseNumber(number), .unit = unit };
|
||||
}
|
||||
return .{ .value = try parseNumber(value), .unit = .none };
|
||||
}
|
||||
|
||||
fn parseNumber(value: []const u8) !f64 {
|
||||
const number = std.fmt.parseFloat(f64, value) catch return error.SyntaxError;
|
||||
if (!std.math.isFinite(number)) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
/// CSS pixels per unit for the absolute length units (CSS Values §6.2,
|
||||
/// 96px per inch). Null for relative and angle units, which need context.
|
||||
pub fn absoluteLengthFactor(unit: Unit) ?f64 {
|
||||
return switch (unit) {
|
||||
.none, .px => 1,
|
||||
.cm => 96.0 / 2.54,
|
||||
.mm => 96.0 / 25.4,
|
||||
.in => 96,
|
||||
.pt => 96.0 / 72.0,
|
||||
.pc => 16,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn suffix(unit: Unit) []const u8 {
|
||||
return switch (unit) {
|
||||
.none => "",
|
||||
.percentage => "%",
|
||||
else => @tagName(unit),
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "css units: parse" {
|
||||
try testing.expectEqual(Parsed{ .value = 12, .unit = .none }, try parse("12"));
|
||||
try testing.expectEqual(Parsed{ .value = -3.5, .unit = .px }, try parse("-3.5px"));
|
||||
try testing.expectEqual(Parsed{ .value = 50, .unit = .percentage }, try parse("50%"));
|
||||
try testing.expectEqual(Parsed{ .value = 2.54, .unit = .cm }, try parse("2.54cm"));
|
||||
try testing.expectEqual(Parsed{ .value = 1.5, .unit = .em }, try parse("1.5EM"));
|
||||
try testing.expectEqual(Parsed{ .value = 90, .unit = .deg }, try parse("90deg"));
|
||||
try testing.expectEqual(Parsed{ .value = 100, .unit = .grad }, try parse("100grad"));
|
||||
try testing.expectEqual(Parsed{ .value = 0.5, .unit = .turn }, try parse("0.5turn"));
|
||||
try testing.expectEqual(Parsed{ .value = 50, .unit = .vh }, try parse("50vh"));
|
||||
|
||||
// surrounding whitespace, and the historical leniency between number
|
||||
// and suffix, are both accepted
|
||||
try testing.expectEqual(Parsed{ .value = 10, .unit = .px }, try parse(" 10px "));
|
||||
try testing.expectEqual(Parsed{ .value = 10, .unit = .px }, try parse("10 px"));
|
||||
|
||||
try testing.expectError(error.SyntaxError, parse(""));
|
||||
try testing.expectError(error.SyntaxError, parse(" "));
|
||||
try testing.expectError(error.SyntaxError, parse("px"));
|
||||
try testing.expectError(error.SyntaxError, parse("abc"));
|
||||
try testing.expectError(error.SyntaxError, parse("10furlong"));
|
||||
try testing.expectError(error.SyntaxError, parse("inf"));
|
||||
try testing.expectError(error.SyntaxError, parse("nan"));
|
||||
try testing.expectError(error.SyntaxError, parse("infpx"));
|
||||
}
|
||||
|
||||
test "css units: absoluteLengthFactor" {
|
||||
try testing.expectEqual(1, absoluteLengthFactor(.none).?);
|
||||
try testing.expectEqual(1, absoluteLengthFactor(.px).?);
|
||||
try testing.expectEqual(96, absoluteLengthFactor(.in).?);
|
||||
try testing.expectEqual(16, absoluteLengthFactor(.pc).?);
|
||||
try testing.expectEqual(null, absoluteLengthFactor(.percentage));
|
||||
try testing.expectEqual(null, absoluteLengthFactor(.em));
|
||||
try testing.expectEqual(null, absoluteLengthFactor(.vh));
|
||||
try testing.expectEqual(null, absoluteLengthFactor(.deg));
|
||||
}
|
||||
|
||||
test "css units: suffix" {
|
||||
try testing.expectEqual("", suffix(.none));
|
||||
try testing.expectEqual("%", suffix(.percentage));
|
||||
try testing.expectEqual("px", suffix(.px));
|
||||
try testing.expectEqual("turn", suffix(.turn));
|
||||
}
|
||||
@@ -44,10 +44,13 @@
|
||||
testing.expectEqual(96, Math.round(length.value));
|
||||
testing.expectEqual('2.54cm', length.valueAsString);
|
||||
|
||||
// `value` is an absolute user-unit setter and resets the unit to NUMBER.
|
||||
// `value` is an absolute user-unit setter; the unit is preserved and
|
||||
// valueInSpecifiedUnits converts (12 user units = 0.3175cm).
|
||||
length.value = 12;
|
||||
testing.expectEqual(SVGLength.SVG_LENGTHTYPE_NUMBER, length.unitType);
|
||||
testing.expectEqual('12', length.valueAsString);
|
||||
testing.expectEqual(SVGLength.SVG_LENGTHTYPE_CM, length.unitType);
|
||||
testing.expectEqual(0.3175, length.valueInSpecifiedUnits);
|
||||
testing.expectEqual(12, length.value);
|
||||
testing.expectEqual('0.3175cm', length.valueAsString);
|
||||
length.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX, 4.5);
|
||||
testing.expectEqual('4.5px', length.valueAsString);
|
||||
testing.expectError('NotSupportedError', () => length.newValueSpecifiedUnits(0, 1));
|
||||
@@ -66,8 +69,9 @@
|
||||
testing.expectEqual(180, Math.round(angle.valueInSpecifiedUnits));
|
||||
testing.expectEqual('180deg', angle.valueAsString);
|
||||
angle.value = 45;
|
||||
testing.expectEqual(SVGAngle.SVG_ANGLETYPE_UNSPECIFIED, angle.unitType);
|
||||
testing.expectEqual('45', angle.valueAsString);
|
||||
testing.expectEqual(SVGAngle.SVG_ANGLETYPE_DEG, angle.unitType);
|
||||
testing.expectEqual(45, angle.valueInSpecifiedUnits);
|
||||
testing.expectEqual('45deg', angle.valueAsString);
|
||||
testing.expectError('NotSupportedError', () => angle.convertToSpecifiedUnits(0));
|
||||
testing.expectError('TypeError', () => { angle.value = NaN; });
|
||||
|
||||
@@ -155,8 +159,8 @@
|
||||
svg.x.baseVal.valueInSpecifiedUnits = 4.5;
|
||||
testing.expectEqual('4.5px', svg.getAttribute('x'));
|
||||
svg.x.baseVal.value = 7;
|
||||
testing.expectEqual('7', svg.getAttribute('x'));
|
||||
testing.expectEqual(SVGLength.SVG_LENGTHTYPE_NUMBER, svg.x.animVal.unitType);
|
||||
testing.expectEqual('7px', svg.getAttribute('x'));
|
||||
testing.expectEqual(SVGLength.SVG_LENGTHTYPE_PX, svg.x.animVal.unitType);
|
||||
testing.expectError('NoModificationAllowedError', () => { svg.x.animVal.value = 3; });
|
||||
testing.expectError('NoModificationAllowedError', () => { svg.x.animVal.valueAsString = '3px'; });
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const units = @import("../css/units.zig");
|
||||
|
||||
const CSS = @This();
|
||||
_pad: bool = false,
|
||||
@@ -26,39 +27,23 @@ _pad: bool = false,
|
||||
pub const init: CSS = .{};
|
||||
|
||||
pub fn parseDimension(value: []const u8) ?f64 {
|
||||
if (value.len == 0) {
|
||||
return null;
|
||||
}
|
||||
return parseNonEmptyDimension(value);
|
||||
const parsed = units.parse(value) catch return null;
|
||||
return switch (parsed.unit) {
|
||||
.none, .px => parsed.value,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
// parseDimension plus viewport-relative units, which the faux layout
|
||||
// resolves against the page viewport.
|
||||
pub fn parseDimensionViewport(value: []const u8, frame: *Frame) ?f64 {
|
||||
if (value.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, value, "vh")) {
|
||||
const n = std.fmt.parseFloat(f64, value[0 .. value.len - 2]) catch return null;
|
||||
return n * @as(f64, @floatFromInt(frame._page.getViewport().height)) / 100.0;
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, value, "vw")) {
|
||||
const n = std.fmt.parseFloat(f64, value[0 .. value.len - 2]) catch return null;
|
||||
return n * @as(f64, @floatFromInt(frame._page.getViewport().width)) / 100.0;
|
||||
}
|
||||
|
||||
return parseNonEmptyDimension(value);
|
||||
}
|
||||
|
||||
fn parseNonEmptyDimension(value: []const u8) ?f64 {
|
||||
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;
|
||||
const parsed = units.parse(value) catch return null;
|
||||
return switch (parsed.unit) {
|
||||
.none, .px => parsed.value,
|
||||
.vh => parsed.value * @as(f64, @floatFromInt(frame._page.getViewport().height)) / 100.0,
|
||||
.vw => parsed.value * @as(f64, @floatFromInt(frame._page.getViewport().width)) / 100.0,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Escapes a CSS identifier string
|
||||
|
||||
@@ -25,7 +25,6 @@ const Element = @import("../../Element.zig");
|
||||
|
||||
const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
const Length = @import("../../svg/Length.zig");
|
||||
|
||||
const Graphics = @import("Graphics.zig");
|
||||
|
||||
@@ -65,7 +64,7 @@ fn text(self: *TextContent, frame: *Frame) []const u8 {
|
||||
}
|
||||
|
||||
fn fontSize(self: *TextContent, frame: *Frame) f64 {
|
||||
return Length.fontSizeForElement(self.asElement(), frame);
|
||||
return frame._style_manager.computedFontSize(self.asElement());
|
||||
}
|
||||
|
||||
fn getTextLength(self: *TextContent, frame: *Frame) !*AnimatedLength {
|
||||
|
||||
@@ -22,6 +22,7 @@ const lp = @import("lightpanda");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const units = @import("../../css/units.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
const String = lp.String;
|
||||
@@ -77,11 +78,16 @@ pub fn getValue(self: *Angle) f64 {
|
||||
return toDegrees(self._value, self._unit);
|
||||
}
|
||||
|
||||
// Sets the value in degrees; the stored unit is preserved and
|
||||
// valueInSpecifiedUnits converts, matching Blink and Gecko.
|
||||
pub fn setValue(self: *Angle, value: f64, frame: *Frame) !void {
|
||||
try self.ensureWritable();
|
||||
try ensureFinite(value);
|
||||
self._value = value;
|
||||
self._unit = .unspecified;
|
||||
self.syncFromAttribute();
|
||||
if (self._unit == .unknown) {
|
||||
self._unit = .unspecified;
|
||||
}
|
||||
self._value = fromDegrees(value, self._unit);
|
||||
try self.writeBack(frame);
|
||||
}
|
||||
|
||||
@@ -176,37 +182,16 @@ const Parsed = struct {
|
||||
};
|
||||
|
||||
fn parse(input: []const u8) !Parsed {
|
||||
const value = std.mem.trim(u8, input, " \t\r\n\x0c");
|
||||
if (value.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
const suffixes = [_]struct { []const u8, Unit }{
|
||||
.{ "turn", .turn },
|
||||
.{ "grad", .grad },
|
||||
.{ "deg", .deg },
|
||||
.{ "rad", .rad },
|
||||
const parsed = try units.parse(input);
|
||||
const unit: Unit = switch (parsed.unit) {
|
||||
.none => .unspecified,
|
||||
.deg => .deg,
|
||||
.rad => .rad,
|
||||
.grad => .grad,
|
||||
.turn => .turn,
|
||||
else => return error.SyntaxError,
|
||||
};
|
||||
for (suffixes) |entry| {
|
||||
const suffix, const unit = entry;
|
||||
if (value.len <= suffix.len) {
|
||||
continue;
|
||||
}
|
||||
if (!std.ascii.eqlIgnoreCase(value[value.len - suffix.len ..], suffix)) {
|
||||
continue;
|
||||
}
|
||||
const number = std.mem.trim(u8, value[0 .. value.len - suffix.len], " \t\r\n\x0c");
|
||||
return .{ .value = try parseNumber(number), .unit = unit };
|
||||
}
|
||||
return .{ .value = try parseNumber(value), .unit = .unspecified };
|
||||
}
|
||||
|
||||
fn parseNumber(value: []const u8) !f64 {
|
||||
const number = std.fmt.parseFloat(f64, value) catch return error.SyntaxError;
|
||||
if (!std.math.isFinite(number)) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
return number;
|
||||
return .{ .value = parsed.value, .unit = unit };
|
||||
}
|
||||
|
||||
fn checkedUnit(value: u16) !Unit {
|
||||
|
||||
@@ -22,6 +22,7 @@ const lp = @import("lightpanda");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const units = @import("../../css/units.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
|
||||
const String = lp.String;
|
||||
@@ -119,11 +120,17 @@ pub fn getValue(self: *Length, frame: *Frame) f64 {
|
||||
return self._value * self.unitToUserUnits(self._unit, frame);
|
||||
}
|
||||
|
||||
// Sets the value in user units; the stored unit is preserved and
|
||||
// valueInSpecifiedUnits converts, matching Blink and Gecko.
|
||||
pub fn setValue(self: *Length, value: f64, frame: *Frame) !void {
|
||||
try self.ensureWritable();
|
||||
try ensureFinite(value);
|
||||
self._value = value;
|
||||
self._unit = .number;
|
||||
self.syncFromAttribute();
|
||||
if (self._unit == .unknown) {
|
||||
self._unit = .number;
|
||||
}
|
||||
const factor = self.unitToUserUnits(self._unit, frame);
|
||||
self._value = if (factor == 0) 0 else value / factor;
|
||||
try self.writeBack(frame);
|
||||
}
|
||||
|
||||
@@ -210,21 +217,17 @@ fn writeBack(self: *Length, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
fn serialize(self: *const Length, frame: *Frame) ![]const u8 {
|
||||
return std.fmt.allocPrint(frame.local_arena, "{d}{s}", .{ self._value, unitSuffix(self._unit) });
|
||||
return std.fmt.allocPrint(frame.local_arena, "{d}{s}", .{ self._value, units.suffix(toShared(self._unit)) });
|
||||
}
|
||||
|
||||
fn unitToUserUnits(self: *const Length, unit: Unit, frame: *Frame) f64 {
|
||||
if (absoluteUnitFactor(unit)) |factor| {
|
||||
return factor;
|
||||
}
|
||||
return switch (unit) {
|
||||
.unknown => 1,
|
||||
.percentage => self.percentageBasis(frame) / 100.0,
|
||||
.em => self.fontSize(frame),
|
||||
// Lightpanda does not load font metrics for DOM-only SVG values. CSS
|
||||
// defines 0.5em as the fallback when the x-height is unavailable.
|
||||
.ex => self.fontSize(frame) / 2.0,
|
||||
else => unreachable,
|
||||
else => units.absoluteLengthFactor(toShared(unit)).?,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -285,83 +288,17 @@ fn pageViewportDimension(direction: Direction, frame: *Frame) f64 {
|
||||
}
|
||||
|
||||
fn resolveParsedLength(parsed: Parsed, element: *Element, direction: Direction, frame: *Frame, depth: u8) f64 {
|
||||
const factor = absoluteUnitFactor(parsed.unit) orelse switch (parsed.unit) {
|
||||
.unknown => 1,
|
||||
const factor = switch (parsed.unit) {
|
||||
.percentage => ancestorViewportDimensionAt(element, direction, frame, depth) / 100.0,
|
||||
.em => resolvedFontSizeAt(element, frame, depth),
|
||||
.ex => resolvedFontSizeAt(element, frame, depth) / 2.0,
|
||||
else => unreachable,
|
||||
.em => frame._style_manager.computedFontSize(element),
|
||||
.ex => frame._style_manager.computedFontSize(element) / 2.0,
|
||||
else => units.absoluteLengthFactor(toShared(parsed.unit)).?,
|
||||
};
|
||||
return parsed.value * factor;
|
||||
}
|
||||
|
||||
fn fontSize(self: *const Length, frame: *Frame) f64 {
|
||||
return resolvedFontSize(self._element, frame);
|
||||
}
|
||||
|
||||
// The style engine currently exposes inline declarations but does not compute
|
||||
// stylesheet font inheritance. Resolve the sources it can represent exactly,
|
||||
// then fall back to CSS's initial medium size (16px).
|
||||
fn resolvedFontSize(element: ?*Element, frame: *Frame) f64 {
|
||||
return resolvedFontSizeAt(element, frame, 0);
|
||||
}
|
||||
|
||||
fn resolvedFontSizeAt(element: ?*Element, frame: *Frame, depth: u8) f64 {
|
||||
if (depth >= MAX_ANCESTOR_DEPTH) {
|
||||
return 16;
|
||||
}
|
||||
const current = element orelse return 16;
|
||||
const parent = current.parentElement();
|
||||
|
||||
if (frame._style_manager.inlineStyleValue(current, comptime .wrap("font-size"))) |raw| {
|
||||
if (parseFontSize(raw, parent, frame, depth + 1)) |size| {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
if (current.getAttributeSafe(comptime .wrap("font-size"))) |raw| {
|
||||
if (parseFontSize(raw, parent, frame, depth + 1)) |size| {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
return resolvedFontSizeAt(parent, frame, depth + 1);
|
||||
}
|
||||
|
||||
pub fn fontSizeForElement(element: *Element, frame: *Frame) f64 {
|
||||
return resolvedFontSize(element, frame);
|
||||
}
|
||||
|
||||
fn parseFontSize(raw: []const u8, parent: ?*Element, frame: *Frame, depth: u8) ?f64 {
|
||||
const value = std.mem.trim(u8, raw, " \t\r\n\x0c");
|
||||
if (std.ascii.eqlIgnoreCase(value, "inherit") or std.ascii.eqlIgnoreCase(value, "unset")) {
|
||||
return resolvedFontSizeAt(parent, frame, depth);
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(value, "initial") or std.ascii.eqlIgnoreCase(value, "medium")) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
const parsed = parse(value) catch return null;
|
||||
const parent_size = resolvedFontSizeAt(parent, frame, depth);
|
||||
const factor = absoluteUnitFactor(parsed.unit) orelse switch (parsed.unit) {
|
||||
.unknown => return null,
|
||||
.percentage => parent_size / 100.0,
|
||||
.em => parent_size,
|
||||
.ex => parent_size / 2.0,
|
||||
else => unreachable,
|
||||
};
|
||||
const size = parsed.value * factor;
|
||||
return if (size >= 0 and std.math.isFinite(size)) size else null;
|
||||
}
|
||||
|
||||
fn absoluteUnitFactor(unit: Unit) ?f64 {
|
||||
return switch (unit) {
|
||||
.number, .px => 1,
|
||||
.cm => 96.0 / 2.54,
|
||||
.mm => 96.0 / 25.4,
|
||||
.in => 96,
|
||||
.pt => 96.0 / 72.0,
|
||||
.pc => 16,
|
||||
.unknown, .percentage, .em, .ex => null,
|
||||
};
|
||||
return frame._style_manager.computedFontSize(self._element);
|
||||
}
|
||||
|
||||
const Parsed = struct {
|
||||
@@ -370,44 +307,40 @@ const Parsed = struct {
|
||||
};
|
||||
|
||||
fn parse(input: []const u8) !Parsed {
|
||||
const value = std.mem.trim(u8, input, " \t\r\n\x0c");
|
||||
if (value.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
const suffixes = [_]struct { []const u8, Unit }{
|
||||
.{ "%", .percentage },
|
||||
.{ "em", .em },
|
||||
.{ "ex", .ex },
|
||||
.{ "px", .px },
|
||||
.{ "cm", .cm },
|
||||
.{ "mm", .mm },
|
||||
.{ "in", .in },
|
||||
.{ "pt", .pt },
|
||||
.{ "pc", .pc },
|
||||
};
|
||||
|
||||
for (suffixes) |entry| {
|
||||
const suffix, const unit = entry;
|
||||
if (value.len <= suffix.len) {
|
||||
continue;
|
||||
}
|
||||
if (!std.ascii.eqlIgnoreCase(value[value.len - suffix.len ..], suffix)) {
|
||||
continue;
|
||||
}
|
||||
const number = std.mem.trim(u8, value[0 .. value.len - suffix.len], " \t\r\n\x0c");
|
||||
return .{ .value = try parseNumber(number), .unit = unit };
|
||||
}
|
||||
|
||||
return .{ .value = try parseNumber(value), .unit = .number };
|
||||
const parsed = try units.parse(input);
|
||||
const unit = fromShared(parsed.unit) orelse return error.SyntaxError;
|
||||
return .{ .value = parsed.value, .unit = unit };
|
||||
}
|
||||
|
||||
fn parseNumber(value: []const u8) !f64 {
|
||||
const number = std.fmt.parseFloat(f64, value) catch return error.SyntaxError;
|
||||
if (!std.math.isFinite(number)) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
return number;
|
||||
fn fromShared(unit: units.Unit) ?Unit {
|
||||
return switch (unit) {
|
||||
.none => .number,
|
||||
.percentage => .percentage,
|
||||
.em => .em,
|
||||
.ex => .ex,
|
||||
.px => .px,
|
||||
.cm => .cm,
|
||||
.mm => .mm,
|
||||
.in => .in,
|
||||
.pt => .pt,
|
||||
.pc => .pc,
|
||||
.vh, .vw, .deg, .rad, .grad, .turn => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn toShared(unit: Unit) units.Unit {
|
||||
return switch (unit) {
|
||||
.unknown, .number => .none,
|
||||
.percentage => .percentage,
|
||||
.em => .em,
|
||||
.ex => .ex,
|
||||
.px => .px,
|
||||
.cm => .cm,
|
||||
.mm => .mm,
|
||||
.in => .in,
|
||||
.pt => .pt,
|
||||
.pc => .pc,
|
||||
};
|
||||
}
|
||||
|
||||
fn checkedUnit(value: u16) !Unit {
|
||||
@@ -426,21 +359,6 @@ fn checkedUnit(value: u16) !Unit {
|
||||
};
|
||||
}
|
||||
|
||||
fn unitSuffix(unit: Unit) []const u8 {
|
||||
return switch (unit) {
|
||||
.unknown, .number => "",
|
||||
.percentage => "%",
|
||||
.em => "em",
|
||||
.ex => "ex",
|
||||
.px => "px",
|
||||
.cm => "cm",
|
||||
.mm => "mm",
|
||||
.in => "in",
|
||||
.pt => "pt",
|
||||
.pc => "pc",
|
||||
};
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Length);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
@@ -24,6 +25,7 @@ const Page = @import("../../Page.zig");
|
||||
const DOMPoint = @import("../DOMPoint.zig");
|
||||
const DOMPointReadOnly = @import("../DOMPointReadOnly.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
const reflected_list = @import("reflected_list.zig");
|
||||
|
||||
const PointList = @This();
|
||||
|
||||
@@ -35,6 +37,16 @@ _snapshot: std.ArrayList(u8) = .empty,
|
||||
_items: std.ArrayList(*DOMPoint) = .empty,
|
||||
_retired: std.ArrayList(*DOMPoint) = .empty,
|
||||
|
||||
const M = reflected_list.Mixin(PointList, DOMPoint, .{
|
||||
.attrName = attrName,
|
||||
.parse = parse,
|
||||
.writeItem = writeItem,
|
||||
.prepareItem = prepareItem,
|
||||
.attach = attach,
|
||||
.detachItem = detachItem,
|
||||
.releaseItem = releaseItem,
|
||||
});
|
||||
|
||||
pub const Kind = enum { base, animated };
|
||||
|
||||
pub const Key = struct {
|
||||
@@ -61,114 +73,19 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*PointList {
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *PointList, page: *Page) void {
|
||||
for (self._items.items) |point| {
|
||||
point._proto.detach(self);
|
||||
point._proto.releaseRef(page);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
self.releaseRetired(page);
|
||||
}
|
||||
pub const deinit = M.deinit;
|
||||
pub const getLength = M.getLength;
|
||||
pub const getNumberOfItems = M.getNumberOfItems;
|
||||
pub const clear = M.clear;
|
||||
pub const initialize = M.initialize;
|
||||
pub const getItem = M.getItem;
|
||||
pub const insertItemBefore = M.insertItemBefore;
|
||||
pub const replaceItem = M.replaceItem;
|
||||
pub const removeItem = M.removeItem;
|
||||
pub const appendItem = M.appendItem;
|
||||
|
||||
pub fn getLength(self: *PointList, frame: *Frame) !u32 {
|
||||
try self.sync(frame);
|
||||
return @intCast(self._items.items.len);
|
||||
}
|
||||
|
||||
pub fn getNumberOfItems(self: *PointList, frame: *Frame) !u32 {
|
||||
return self.getLength(frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *PointList, frame: *Frame) !void {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
try self.retireAll(frame);
|
||||
try self.setAttribute(&.{}, frame);
|
||||
}
|
||||
|
||||
pub fn initialize(self: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared._proto.releaseRef(frame._page);
|
||||
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try self.setAttribute(&.{prepared}, frame);
|
||||
self._items.appendAssumeCapacity(prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn getItem(self: *PointList, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
return self._items.items[index];
|
||||
}
|
||||
|
||||
pub fn insertItemBefore(self: *PointList, item: *DOMPoint, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared._proto.releaseRef(frame._page);
|
||||
const at = @min(@as(usize, index), self._items.items.len);
|
||||
const next = try frame.local_arena.alloc(*DOMPoint, self._items.items.len + 1);
|
||||
@memcpy(next[0..at], self._items.items[0..at]);
|
||||
next[at] = prepared;
|
||||
@memcpy(next[at + 1 ..], self._items.items[at..]);
|
||||
|
||||
try self._items.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
self._items.insertAssumeCapacity(at, prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn replaceItem(self: *PointList, item: *DOMPoint, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared._proto.releaseRef(frame._page);
|
||||
const next = try frame.local_arena.dupe(*DOMPoint, self._items.items);
|
||||
next[index] = prepared;
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const replaced = self._items.items[index];
|
||||
replaced._proto.detach(self);
|
||||
self._retired.appendAssumeCapacity(replaced);
|
||||
self._items.items[index] = prepared;
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *PointList, index: u32, frame: *Frame) !*DOMPoint {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
|
||||
const next = try frame.local_arena.alloc(*DOMPoint, self._items.items.len - 1);
|
||||
@memcpy(next[0..index], self._items.items[0..index]);
|
||||
@memcpy(next[index..], self._items.items[index + 1 ..]);
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const removed = self._items.orderedRemove(index);
|
||||
removed._proto.detach(self);
|
||||
self._retired.appendAssumeCapacity(removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
pub fn appendItem(self: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
|
||||
return self.insertItemBefore(item, std.math.maxInt(u32), frame);
|
||||
}
|
||||
|
||||
fn requireMutable(self: *const PointList) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
fn attrName(_: *const PointList) lp.String {
|
||||
return .wrap("points");
|
||||
}
|
||||
|
||||
fn prepareItem(_: *PointList, item: *DOMPoint, frame: *Frame) !*DOMPoint {
|
||||
@@ -189,6 +106,14 @@ fn attach(self: *PointList, point: *DOMPoint) void {
|
||||
}, self._read_only);
|
||||
}
|
||||
|
||||
fn detachItem(point: *DOMPoint, owner: *PointList) void {
|
||||
point._proto.detach(owner);
|
||||
}
|
||||
|
||||
fn releaseItem(point: *DOMPoint, page: *Page) void {
|
||||
point._proto.releaseRef(page);
|
||||
}
|
||||
|
||||
fn mutatePoint(
|
||||
context: *anyopaque,
|
||||
point: *DOMPointReadOnly,
|
||||
@@ -197,7 +122,7 @@ fn mutatePoint(
|
||||
) anyerror!void {
|
||||
const self: *PointList = @ptrCast(@alignCast(context));
|
||||
const frame = self._frame;
|
||||
try self.sync(frame);
|
||||
try M.sync(self, frame);
|
||||
|
||||
// An external attribute mutation detaches the old item during sync. The
|
||||
// caller still owns that DOMPoint identity, but it no longer mutates the list.
|
||||
@@ -220,43 +145,6 @@ fn mutatePoint(
|
||||
point.setCoordinateRaw(coordinate, value);
|
||||
}
|
||||
|
||||
fn sync(self: *PointList, frame: *Frame) !void {
|
||||
self.releaseRetired(frame._page);
|
||||
|
||||
const raw = self._element.getAttributeSafe(comptime .wrap("points")) orelse "";
|
||||
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._synced = false;
|
||||
var parsed = parse(raw, frame) catch |err| switch (err) {
|
||||
error.SyntaxError => std.ArrayList(*DOMPoint).empty,
|
||||
else => return err,
|
||||
};
|
||||
errdefer for (parsed.items) |point| point._proto.releaseRef(frame._page);
|
||||
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, raw);
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
|
||||
for (parsed.items) |point| {
|
||||
self._items.appendAssumeCapacity(point);
|
||||
self.attach(point);
|
||||
}
|
||||
parsed.clearRetainingCapacity();
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
// A retired item must outlive the operation that retired it: removeItem's
|
||||
// return value has no JS wrapper until the bridge wraps it after we return.
|
||||
// By the next operation, anything still reachable holds its own ref.
|
||||
fn releaseRetired(self: *PointList, page: *Page) void {
|
||||
for (self._retired.items) |point| {
|
||||
point._proto.releaseRef(page);
|
||||
}
|
||||
self._retired.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*DOMPoint) {
|
||||
var scanner = NumberScanner{ .input = raw };
|
||||
var parsed: std.ArrayList(*DOMPoint) = .empty;
|
||||
@@ -276,24 +164,8 @@ fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*DOMPoint) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
fn retireAll(self: *PointList, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
|
||||
for (self._items.items) |point| {
|
||||
point._proto.detach(self);
|
||||
self._retired.appendAssumeCapacity(point);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn setAttribute(self: *PointList, items: []const *DOMPoint, frame: *Frame) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (items, 0..) |point, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try writer.print("{d} {d}", .{ point._proto._x, point._proto._y });
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
fn writeItem(point: *DOMPoint, writer: *std.Io.Writer) !void {
|
||||
try writer.print("{d} {d}", .{ point._proto._x, point._proto._y });
|
||||
}
|
||||
|
||||
fn setAttributeWithOverride(
|
||||
@@ -311,15 +183,7 @@ fn setAttributeWithOverride(
|
||||
const y = if (i == index and coordinate == .y) value else point._proto._y;
|
||||
try writer.print("{d} {d}", .{ x, y });
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
}
|
||||
|
||||
fn commitAttribute(self: *PointList, serialized: []const u8, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._element.setAttributeSafe(comptime .wrap("points"), .wrap(serialized), frame);
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, serialized);
|
||||
self._synced = true;
|
||||
try M.commitAttribute(self, serialized.written(), frame);
|
||||
}
|
||||
|
||||
const NumberScanner = struct {
|
||||
|
||||
@@ -25,6 +25,7 @@ const Page = @import("../../Page.zig");
|
||||
const DOMMatrixReadOnly = @import("../DOMMatrixReadOnly.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
const Transform = @import("Transform.zig");
|
||||
const reflected_list = @import("reflected_list.zig");
|
||||
|
||||
const TransformList = @This();
|
||||
|
||||
@@ -37,6 +38,16 @@ _snapshot: std.ArrayList(u8) = .empty,
|
||||
_items: std.ArrayList(*Transform) = .empty,
|
||||
_retired: std.ArrayList(*Transform) = .empty,
|
||||
|
||||
const M = reflected_list.Mixin(TransformList, Transform, .{
|
||||
.attrName = attrName,
|
||||
.parse = parse,
|
||||
.writeItem = writeItem,
|
||||
.prepareItem = prepareItem,
|
||||
.attach = attach,
|
||||
.detachItem = detachItem,
|
||||
.releaseItem = releaseItem,
|
||||
});
|
||||
|
||||
pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bool, frame: *Frame) !*TransformList {
|
||||
return frame._factory.create(TransformList{
|
||||
._frame = frame,
|
||||
@@ -46,111 +57,20 @@ pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bo
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TransformList, page: *Page) void {
|
||||
for (self._items.items) |transform| {
|
||||
transform.detach(self);
|
||||
transform.releaseRef(page);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
self.releaseRetired(page);
|
||||
}
|
||||
|
||||
pub fn getLength(self: *TransformList, frame: *Frame) !u32 {
|
||||
try self.sync(frame);
|
||||
return @intCast(self._items.items.len);
|
||||
}
|
||||
|
||||
pub fn getNumberOfItems(self: *TransformList, frame: *Frame) !u32 {
|
||||
return self.getLength(frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *TransformList, frame: *Frame) !void {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
try self.retireAll(frame);
|
||||
try self.setAttribute(&.{}, frame);
|
||||
}
|
||||
|
||||
pub fn initialize(self: *TransformList, item: *Transform, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared.releaseRef(frame._page);
|
||||
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try self.setAttribute(&.{prepared}, frame);
|
||||
self._items.appendAssumeCapacity(prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn getItem(self: *TransformList, index: u32, frame: *Frame) !*Transform {
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
return self._items.items[index];
|
||||
}
|
||||
|
||||
pub fn insertItemBefore(self: *TransformList, item: *Transform, index: u32, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared.releaseRef(frame._page);
|
||||
const at = @min(@as(usize, index), self._items.items.len);
|
||||
const next = try frame.local_arena.alloc(*Transform, self._items.items.len + 1);
|
||||
@memcpy(next[0..at], self._items.items[0..at]);
|
||||
next[at] = prepared;
|
||||
@memcpy(next[at + 1 ..], self._items.items[at..]);
|
||||
|
||||
try self._items.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
self._items.insertAssumeCapacity(at, prepared);
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn replaceItem(self: *TransformList, item: *Transform, index: u32, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
const prepared = try self.prepareItem(item, frame);
|
||||
errdefer prepared.releaseRef(frame._page);
|
||||
const next = try frame.local_arena.dupe(*Transform, self._items.items);
|
||||
next[index] = prepared;
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const replaced = self._items.items[index];
|
||||
replaced.detach(self);
|
||||
self._retired.appendAssumeCapacity(replaced);
|
||||
self._items.items[index] = prepared;
|
||||
self.attach(prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *TransformList, index: u32, frame: *Frame) !*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
const next = try frame.local_arena.alloc(*Transform, self._items.items.len - 1);
|
||||
@memcpy(next[0..index], self._items.items[0..index]);
|
||||
@memcpy(next[index..], self._items.items[index + 1 ..]);
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try self.setAttribute(next, frame);
|
||||
const removed = self._items.orderedRemove(index);
|
||||
removed.detach(self);
|
||||
self._retired.appendAssumeCapacity(removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
pub fn appendItem(self: *TransformList, item: *Transform, frame: *Frame) !*Transform {
|
||||
return self.insertItemBefore(item, std.math.maxInt(u32), frame);
|
||||
}
|
||||
pub const deinit = M.deinit;
|
||||
pub const getLength = M.getLength;
|
||||
pub const getNumberOfItems = M.getNumberOfItems;
|
||||
pub const clear = M.clear;
|
||||
pub const initialize = M.initialize;
|
||||
pub const getItem = M.getItem;
|
||||
pub const insertItemBefore = M.insertItemBefore;
|
||||
pub const replaceItem = M.replaceItem;
|
||||
pub const removeItem = M.removeItem;
|
||||
pub const appendItem = M.appendItem;
|
||||
|
||||
pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
|
||||
try self.requireMutable();
|
||||
try self.sync(frame);
|
||||
try M.requireMutable(self);
|
||||
try M.sync(self, frame);
|
||||
if (self._items.items.len == 0) return null;
|
||||
|
||||
var matrix = DOMMatrixReadOnly.identity();
|
||||
@@ -173,16 +93,16 @@ pub fn consolidate(self: *TransformList, frame: *Frame) !?*Transform {
|
||||
consolidated.acquireRef();
|
||||
errdefer consolidated.releaseRef(frame._page);
|
||||
|
||||
try self.retireAll(frame);
|
||||
try M.retireAll(self, frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try self.setAttribute(&.{consolidated}, frame);
|
||||
try M.setAttribute(self, &.{consolidated}, frame);
|
||||
self._items.appendAssumeCapacity(consolidated);
|
||||
self.attach(consolidated);
|
||||
attach(self, consolidated);
|
||||
return consolidated;
|
||||
}
|
||||
|
||||
fn requireMutable(self: *const TransformList) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
fn attrName(self: *const TransformList) lp.String {
|
||||
return self._attr_name;
|
||||
}
|
||||
|
||||
fn prepareItem(_: *TransformList, item: *Transform, frame: *Frame) !*Transform {
|
||||
@@ -198,10 +118,18 @@ fn attach(self: *TransformList, transform: *Transform) void {
|
||||
}, self._read_only);
|
||||
}
|
||||
|
||||
fn detachItem(transform: *Transform, owner: *TransformList) void {
|
||||
transform.detach(owner);
|
||||
}
|
||||
|
||||
fn releaseItem(transform: *Transform, page: *Page) void {
|
||||
transform.releaseRef(page);
|
||||
}
|
||||
|
||||
fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.State) anyerror!void {
|
||||
const self: *TransformList = @ptrCast(@alignCast(context));
|
||||
const frame = self._frame;
|
||||
try self.sync(frame);
|
||||
try M.sync(self, frame);
|
||||
if (!transform.isAttachedTo(self)) {
|
||||
transform.applyStateRaw(state);
|
||||
return;
|
||||
@@ -213,41 +141,6 @@ fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.
|
||||
transform.applyStateRaw(state);
|
||||
}
|
||||
|
||||
fn sync(self: *TransformList, frame: *Frame) !void {
|
||||
self.releaseRetired(frame._page);
|
||||
|
||||
const raw = self._element.getAttributeSafe(self._attr_name) orelse "";
|
||||
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) return;
|
||||
|
||||
self._synced = false;
|
||||
var parsed = parse(raw, frame) catch |err| switch (err) {
|
||||
error.SyntaxError => std.ArrayList(*Transform).empty,
|
||||
else => return err,
|
||||
};
|
||||
errdefer for (parsed.items) |transform| transform.releaseRef(frame._page);
|
||||
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, raw);
|
||||
try self.retireAll(frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
|
||||
for (parsed.items) |transform| {
|
||||
self._items.appendAssumeCapacity(transform);
|
||||
self.attach(transform);
|
||||
}
|
||||
parsed.clearRetainingCapacity();
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
// A retired item must outlive the operation that retired it: removeItem's
|
||||
// return value has no JS wrapper until the bridge wraps it after we return.
|
||||
// By the next operation, anything still reachable holds its own ref.
|
||||
fn releaseRetired(self: *TransformList, page: *Page) void {
|
||||
for (self._retired.items) |transform| {
|
||||
transform.releaseRef(page);
|
||||
}
|
||||
self._retired.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*Transform) {
|
||||
var parsed: std.ArrayList(*Transform) = .empty;
|
||||
errdefer for (parsed.items) |transform| transform.releaseRef(frame._page);
|
||||
@@ -267,24 +160,8 @@ fn parse(raw: []const u8, frame: *Frame) !std.ArrayList(*Transform) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
fn retireAll(self: *TransformList, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
|
||||
for (self._items.items) |transform| {
|
||||
transform.detach(self);
|
||||
self._retired.appendAssumeCapacity(transform);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn setAttribute(self: *TransformList, items: []const *Transform, frame: *Frame) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (items, 0..) |transform, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try Transform.writeState(transform.getState(), writer);
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
fn writeItem(transform: *Transform, writer: *std.Io.Writer) !void {
|
||||
try Transform.writeState(transform.getState(), writer);
|
||||
}
|
||||
|
||||
fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform.State, frame: *Frame) !void {
|
||||
@@ -294,15 +171,7 @@ fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try Transform.writeState(if (i == index) state else transform.getState(), writer);
|
||||
}
|
||||
try self.commitAttribute(serialized.written(), frame);
|
||||
}
|
||||
|
||||
fn commitAttribute(self: *TransformList, serialized: []const u8, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._element.setAttributeSafe(self._attr_name, .wrap(serialized), frame);
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, serialized);
|
||||
self._synced = true;
|
||||
try M.commitAttribute(self, serialized.written(), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
226
src/browser/webapi/svg/reflected_list.zig
Normal file
226
src/browser/webapi/svg/reflected_list.zig
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shared machinery for the attribute-reflecting SVG list interfaces whose
|
||||
//! items are live, ref-counted objects (SVGPointList, SVGTransformList):
|
||||
//! attribute-snapshot sync, the retire/release item lifetime protocol, and
|
||||
//! the DOM list operations. The concrete list supplies the item specifics
|
||||
//! via `hooks` and re-exports the operations as decl aliases.
|
||||
//!
|
||||
//! StringList reflects an attribute too but is deliberately not built on
|
||||
//! this: its items are plain slices (no ref counting, no attach/retire) and
|
||||
//! its commit path is list-authoritative rather than reparse-based.
|
||||
//!
|
||||
//! `List` must carry the `_frame`, `_element`, `_read_only`, `_synced`,
|
||||
//! `_snapshot`, `_items` and `_retired` fields. `hooks` must provide:
|
||||
//! attrName(*const List) String
|
||||
//! parse([]const u8, *Frame) !std.ArrayList(*Item) (SyntaxError -> empty list)
|
||||
//! writeItem(*Item, *std.Io.Writer) !void
|
||||
//! prepareItem(*List, *Item, *Frame) !*Item
|
||||
//! attach(*List, *Item) void
|
||||
//! detachItem(*Item, *List) void
|
||||
//! releaseItem(*Item, *Page) void
|
||||
//!
|
||||
//! The operations keep a strict commit order: reserve list capacity, write
|
||||
//! the attribute, then apply the infallible list update — so a failed
|
||||
//! setAttribute never leaves the mirror out of step with the attribute.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
pub fn Mixin(comptime List: type, comptime Item: type, comptime hooks: anytype) type {
|
||||
return struct {
|
||||
pub fn deinit(self: *List, page: *Page) void {
|
||||
for (self._items.items) |item| {
|
||||
hooks.detachItem(item, self);
|
||||
hooks.releaseItem(item, page);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
releaseRetired(self, page);
|
||||
}
|
||||
|
||||
pub fn getLength(self: *List, frame: *Frame) !u32 {
|
||||
try sync(self, frame);
|
||||
return @intCast(self._items.items.len);
|
||||
}
|
||||
|
||||
pub fn getNumberOfItems(self: *List, frame: *Frame) !u32 {
|
||||
return getLength(self, frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *List, frame: *Frame) !void {
|
||||
try requireMutable(self);
|
||||
try sync(self, frame);
|
||||
try retireAll(self, frame);
|
||||
try setAttribute(self, &.{}, frame);
|
||||
}
|
||||
|
||||
pub fn initialize(self: *List, item: *Item, frame: *Frame) !*Item {
|
||||
try requireMutable(self);
|
||||
try sync(self, frame);
|
||||
|
||||
const prepared = try hooks.prepareItem(self, item, frame);
|
||||
errdefer hooks.releaseItem(prepared, frame._page);
|
||||
|
||||
try retireAll(self, frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, 1);
|
||||
try setAttribute(self, &.{prepared}, frame);
|
||||
self._items.appendAssumeCapacity(prepared);
|
||||
hooks.attach(self, prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn getItem(self: *List, index: u32, frame: *Frame) !*Item {
|
||||
try sync(self, frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
return self._items.items[index];
|
||||
}
|
||||
|
||||
pub fn insertItemBefore(self: *List, item: *Item, index: u32, frame: *Frame) !*Item {
|
||||
try requireMutable(self);
|
||||
try sync(self, frame);
|
||||
|
||||
const prepared = try hooks.prepareItem(self, item, frame);
|
||||
errdefer hooks.releaseItem(prepared, frame._page);
|
||||
const at = @min(@as(usize, index), self._items.items.len);
|
||||
const next = try frame.local_arena.alloc(*Item, self._items.items.len + 1);
|
||||
@memcpy(next[0..at], self._items.items[0..at]);
|
||||
next[at] = prepared;
|
||||
@memcpy(next[at + 1 ..], self._items.items[at..]);
|
||||
|
||||
try self._items.ensureUnusedCapacity(frame.arena, 1);
|
||||
try setAttribute(self, next, frame);
|
||||
self._items.insertAssumeCapacity(at, prepared);
|
||||
hooks.attach(self, prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn replaceItem(self: *List, item: *Item, index: u32, frame: *Frame) !*Item {
|
||||
try requireMutable(self);
|
||||
try sync(self, frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
|
||||
const prepared = try hooks.prepareItem(self, item, frame);
|
||||
errdefer hooks.releaseItem(prepared, frame._page);
|
||||
const next = try frame.local_arena.dupe(*Item, self._items.items);
|
||||
next[index] = prepared;
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try setAttribute(self, next, frame);
|
||||
const replaced = self._items.items[index];
|
||||
hooks.detachItem(replaced, self);
|
||||
self._retired.appendAssumeCapacity(replaced);
|
||||
self._items.items[index] = prepared;
|
||||
hooks.attach(self, prepared);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *List, index: u32, frame: *Frame) !*Item {
|
||||
try requireMutable(self);
|
||||
try sync(self, frame);
|
||||
if (index >= self._items.items.len) return error.IndexSizeError;
|
||||
|
||||
const next = try frame.local_arena.alloc(*Item, self._items.items.len - 1);
|
||||
@memcpy(next[0..index], self._items.items[0..index]);
|
||||
@memcpy(next[index..], self._items.items[index + 1 ..]);
|
||||
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, 1);
|
||||
try setAttribute(self, next, frame);
|
||||
const removed = self._items.orderedRemove(index);
|
||||
hooks.detachItem(removed, self);
|
||||
self._retired.appendAssumeCapacity(removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
pub fn appendItem(self: *List, item: *Item, frame: *Frame) !*Item {
|
||||
return insertItemBefore(self, item, std.math.maxInt(u32), frame);
|
||||
}
|
||||
|
||||
pub fn requireMutable(self: *const List) !void {
|
||||
if (self._read_only) return error.NoModificationAllowed;
|
||||
}
|
||||
|
||||
pub fn sync(self: *List, frame: *Frame) !void {
|
||||
releaseRetired(self, frame._page);
|
||||
|
||||
const raw = self._element.getAttributeSafe(hooks.attrName(self)) orelse "";
|
||||
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._synced = false;
|
||||
var parsed = hooks.parse(raw, frame) catch |err| switch (err) {
|
||||
error.SyntaxError => std.ArrayList(*Item).empty,
|
||||
else => return err,
|
||||
};
|
||||
errdefer for (parsed.items) |item| hooks.releaseItem(item, frame._page);
|
||||
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, raw);
|
||||
try retireAll(self, frame);
|
||||
try self._items.ensureTotalCapacity(frame.arena, parsed.items.len);
|
||||
for (parsed.items) |item| {
|
||||
self._items.appendAssumeCapacity(item);
|
||||
hooks.attach(self, item);
|
||||
}
|
||||
parsed.clearRetainingCapacity();
|
||||
self._synced = true;
|
||||
}
|
||||
|
||||
// A retired item must outlive the operation that retired it:
|
||||
// removeItem's return value has no JS wrapper until the bridge wraps
|
||||
// it after we return. By the next operation, anything still reachable
|
||||
// holds its own ref.
|
||||
pub fn releaseRetired(self: *List, page: *Page) void {
|
||||
for (self._retired.items) |item| {
|
||||
hooks.releaseItem(item, page);
|
||||
}
|
||||
self._retired.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn retireAll(self: *List, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._retired.ensureUnusedCapacity(frame.arena, self._items.items.len);
|
||||
for (self._items.items) |item| {
|
||||
hooks.detachItem(item, self);
|
||||
self._retired.appendAssumeCapacity(item);
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn setAttribute(self: *List, items: []const *Item, frame: *Frame) !void {
|
||||
var serialized: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
const writer = &serialized.writer;
|
||||
for (items, 0..) |item, i| {
|
||||
if (i != 0) try writer.writeByte(' ');
|
||||
try hooks.writeItem(item, writer);
|
||||
}
|
||||
try commitAttribute(self, serialized.written(), frame);
|
||||
}
|
||||
|
||||
pub fn commitAttribute(self: *List, serialized: []const u8, frame: *Frame) !void {
|
||||
self._synced = false;
|
||||
try self._element.setAttributeSafe(hooks.attrName(self), .wrap(serialized), frame);
|
||||
self._snapshot.clearRetainingCapacity();
|
||||
try self._snapshot.appendSlice(frame.arena, serialized);
|
||||
self._synced = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user