Files
browser/src/browser/webapi/svg/Length.zig
Karl Seguin 93d2305041 Angle/Lenght value setter unit
Length.setValue should preserve the existing unit. Angle.setValue should use
degrees (copying what Chrome/Firefox do).
2026-07-29 15:37:11 +08:00

390 lines
13 KiB
Zig

// 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/>.
const std = @import("std");
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;
const Length = @This();
// A reflected length is created with one ref held by the frame's animated
// lookup cache, so a JS wrapper being collected can never free it. Only a
// detached length (own arena, zero initial refs) dies with its last wrapper.
_rc: lp.RC = .init(1),
_arena: ?std.mem.Allocator = null,
_value: f64 = 0,
_unit: Unit = .number,
_element: ?*Element = null,
_attr_name: String = .empty,
_direction: Direction = .unspecified,
_read_only: bool = false,
_default_value: f64 = 0,
_default_unit: Unit = .number,
pub const Direction = enum {
horizontal,
vertical,
unspecified,
};
pub const Unit = enum(u16) {
unknown = 0,
number = 1,
percentage = 2,
em = 3,
ex = 4,
px = 5,
cm = 6,
mm = 7,
in = 8,
pt = 9,
pc = 10,
};
const MAX_ANCESTOR_DEPTH = 32;
pub fn detached(frame: *Frame) !*Length {
const arena = try frame._page.getArena(.tiny, "SVGLength");
errdefer frame._page.releaseArena(arena);
const self = try arena.create(Length);
self.* = .{ ._rc = .{}, ._arena = arena };
return self;
}
pub fn deinit(self: *Length, page: *Page) void {
page.releaseArena(self._arena.?);
}
pub fn acquireRef(self: *Length) void {
self._rc.acquire();
}
pub fn releaseRef(self: *Length, page: *Page) void {
self._rc.release(self, page);
}
pub fn reflectedConfigured(
element: *Element,
attr_name: String,
direction: Direction,
default_value: f64,
default_unit: Unit,
read_only: bool,
frame: *Frame,
) !*Length {
return frame._factory.create(Length{
._element = element,
._attr_name = attr_name,
._direction = direction,
._read_only = read_only,
._default_value = default_value,
._default_unit = default_unit,
});
}
pub fn getUnitType(self: *Length) u16 {
self.syncFromAttribute();
return @intFromEnum(self._unit);
}
// An attribute SVG could not parse reports the unknown unit type. Callers that
// need geometry substitute the property's default instead.
pub fn hasKnownUnit(self: *Length) bool {
self.syncFromAttribute();
return self._unit != .unknown;
}
pub fn getValue(self: *Length, frame: *Frame) f64 {
self.syncFromAttribute();
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.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);
}
pub fn getValueInSpecifiedUnits(self: *Length) f64 {
self.syncFromAttribute();
return self._value;
}
pub fn setValueInSpecifiedUnits(self: *Length, value: f64, frame: *Frame) !void {
try self.ensureWritable();
try ensureFinite(value);
self.syncFromAttribute();
if (self._unit == .unknown) {
self._unit = .number;
}
self._value = value;
try self.writeBack(frame);
}
pub fn getValueAsString(self: *Length, frame: *Frame) ![]const u8 {
self.syncFromAttribute();
return self.serialize(frame);
}
pub fn setValueAsString(self: *Length, value: String, frame: *Frame) !void {
try self.ensureWritable();
const parsed = parse(value.str()) catch return error.SyntaxError;
self._value = parsed.value;
self._unit = parsed.unit;
try self.writeBack(frame);
}
pub fn newValueSpecifiedUnits(self: *Length, unit_type: u16, value: f64, frame: *Frame) !void {
try self.ensureWritable();
const unit = try checkedUnit(unit_type);
try ensureFinite(value);
self._unit = unit;
self._value = value;
try self.writeBack(frame);
}
pub fn convertToSpecifiedUnits(self: *Length, unit_type: u16, frame: *Frame) !void {
try self.ensureWritable();
const target = try checkedUnit(unit_type);
const absolute = self.getValue(frame);
const factor = self.unitToUserUnits(target, frame);
self._unit = target;
self._value = if (factor == 0) 0 else absolute / factor;
try self.writeBack(frame);
}
fn ensureWritable(self: *const Length) !void {
if (self._read_only) {
return error.NoModificationAllowed;
}
}
fn ensureFinite(value: f64) !void {
if (!std.math.isFinite(value)) {
return error.TypeError;
}
}
fn syncFromAttribute(self: *Length) void {
const element = self._element orelse return;
const raw = element.getAttributeSafe(self._attr_name) orelse {
self._value = self._default_value;
self._unit = self._default_unit;
return;
};
const parsed = parse(raw) catch {
self._value = self._default_value;
self._unit = self._default_unit;
return;
};
self._value = parsed.value;
self._unit = parsed.unit;
}
fn writeBack(self: *Length, frame: *Frame) !void {
const element = self._element orelse return;
const value = try self.serialize(frame);
try element.setAttributeSafe(self._attr_name, .wrap(value), frame);
}
fn serialize(self: *const Length, frame: *Frame) ![]const u8 {
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 {
return switch (unit) {
.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 => units.absoluteLengthFactor(toShared(unit)).?,
};
}
fn percentageBasis(self: *const Length, frame: *Frame) f64 {
const element = self._element orelse return 100;
return switch (self._direction) {
.horizontal => ancestorViewportDimension(element, .horizontal, frame),
.vertical => ancestorViewportDimension(element, .vertical, frame),
.unspecified => blk: {
const width = ancestorViewportDimension(element, .horizontal, frame);
const height = ancestorViewportDimension(element, .vertical, frame);
break :blk @sqrt((width * width + height * height) / 2.0);
},
};
}
fn ancestorViewportDimension(element: *Element, direction: Direction, frame: *Frame) f64 {
return ancestorViewportDimensionAt(element, direction, frame, 0);
}
fn ancestorViewportDimensionAt(element: *Element, direction: Direction, frame: *Frame, depth: u8) f64 {
if (depth >= MAX_ANCESTOR_DEPTH) {
return pageViewportDimension(direction, frame);
}
const viewport_element = nearestSvgViewport(element) orelse return pageViewportDimension(direction, frame);
const attr_name: String = switch (direction) {
.horizontal => comptime .wrap("width"),
.vertical => comptime .wrap("height"),
.unspecified => unreachable,
};
const raw = viewport_element.getAttributeSafe(attr_name) orelse {
// SVG2 treats an omitted nested width/height as auto, whose used value
// is 100% of the containing SVG viewport.
return ancestorViewportDimensionAt(viewport_element, direction, frame, depth + 1);
};
const parsed = parse(raw) catch return ancestorViewportDimensionAt(viewport_element, direction, frame, depth + 1);
return resolveParsedLength(parsed, viewport_element, direction, frame, depth + 1);
}
fn nearestSvgViewport(element: *Element) ?*Element {
var current = element.parentElement();
while (current) |parent| : (current = parent.parentElement()) {
if (parent._namespace != .svg) return null;
if (std.mem.eql(u8, parent.getTagNameLower(), "svg")) return parent;
}
return null;
}
fn pageViewportDimension(direction: Direction, frame: *Frame) f64 {
const viewport = frame._page.getViewport();
return switch (direction) {
.horizontal => @floatFromInt(viewport.width),
.vertical => @floatFromInt(viewport.height),
.unspecified => unreachable,
};
}
fn resolveParsedLength(parsed: Parsed, element: *Element, direction: Direction, frame: *Frame, depth: u8) f64 {
const factor = switch (parsed.unit) {
.percentage => ancestorViewportDimensionAt(element, direction, frame, depth) / 100.0,
.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 frame._style_manager.computedFontSize(self._element);
}
const Parsed = struct {
value: f64,
unit: Unit,
};
fn parse(input: []const u8) !Parsed {
const parsed = try units.parse(input);
const unit = fromShared(parsed.unit) orelse return error.SyntaxError;
return .{ .value = parsed.value, .unit = unit };
}
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 {
return switch (value) {
1 => .number,
2 => .percentage,
3 => .em,
4 => .ex,
5 => .px,
6 => .cm,
7 => .mm,
8 => .in,
9 => .pt,
10 => .pc,
else => error.NotSupported,
};
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Length);
pub const Meta = struct {
pub const name = "SVGLength";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const SVG_LENGTHTYPE_UNKNOWN = bridge.property(0, .{ .template = true });
pub const SVG_LENGTHTYPE_NUMBER = bridge.property(1, .{ .template = true });
pub const SVG_LENGTHTYPE_PERCENTAGE = bridge.property(2, .{ .template = true });
pub const SVG_LENGTHTYPE_EMS = bridge.property(3, .{ .template = true });
pub const SVG_LENGTHTYPE_EXS = bridge.property(4, .{ .template = true });
pub const SVG_LENGTHTYPE_PX = bridge.property(5, .{ .template = true });
pub const SVG_LENGTHTYPE_CM = bridge.property(6, .{ .template = true });
pub const SVG_LENGTHTYPE_MM = bridge.property(7, .{ .template = true });
pub const SVG_LENGTHTYPE_IN = bridge.property(8, .{ .template = true });
pub const SVG_LENGTHTYPE_PT = bridge.property(9, .{ .template = true });
pub const SVG_LENGTHTYPE_PC = bridge.property(10, .{ .template = true });
pub const unitType = bridge.accessor(Length.getUnitType, null, .{});
pub const value = bridge.accessor(Length.getValue, Length.setValue, .{});
pub const valueInSpecifiedUnits = bridge.accessor(Length.getValueInSpecifiedUnits, Length.setValueInSpecifiedUnits, .{});
pub const valueAsString = bridge.accessor(Length.getValueAsString, Length.setValueAsString, .{});
pub const newValueSpecifiedUnits = bridge.function(Length.newValueSpecifiedUnits, .{});
pub const convertToSpecifiedUnits = bridge.function(Length.convertToSpecifiedUnits, .{});
};