mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-16 16:01:30 -04:00
Merge pull request #3032 from mh0pe/codex/svg-07-text
webapi: add dependency-free SVG text DOM
This commit is contained in:
15 files changed
+588
-6
No files matched your search
@@ -905,6 +905,7 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
|
||||
asUint("mask") => return createSvgElementT(frame, Element.Svg.Mask, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("rect") => return createSvgElementT(frame, Geometry.Rect, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("stop") => return createSvgElementT(frame, Element.Svg.Stop, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("text") => return createSvgElementT(frame, Graphics.TextContent.TextPositioning.Text, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("line") => return createSvgElementT(frame, Geometry.Line, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("path") => return createSvgElementT(frame, Geometry.Path, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("view") => return createSvgElementT(frame, Element.Svg.View, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
@@ -913,6 +914,7 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
|
||||
5 => switch (@as(u40, @bitCast(name[0..5].*))) {
|
||||
asUint("image") => return createSvgElementT(frame, Graphics.Image, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("title") => return createSvgElementT(frame, Element.Svg.Title, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("tspan") => return createSvgElementT(frame, Graphics.TextContent.TextPositioning.TSpan, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(name[0..6].*))) {
|
||||
@@ -932,6 +934,7 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
|
||||
asUint("clipPath") => return createSvgElementT(frame, Element.Svg.ClipPath, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("metadata") => return createSvgElementT(frame, Element.Svg.Metadata, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("polyline") => return createSvgElementT(frame, Geometry.Polyline, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
asUint("textPath") => return createSvgElementT(frame, Graphics.TextContent.TextPath, name, attribute_iterator, .{ ._proto = undefined }),
|
||||
else => {},
|
||||
},
|
||||
13 => switch (@as(u104, @bitCast(name[0..13].*))) {
|
||||
|
||||
@@ -812,6 +812,7 @@ fn PrototypeType(comptime T: type) ?type {
|
||||
}
|
||||
|
||||
fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {
|
||||
@setEvalBranchQuota(10_000);
|
||||
var index: usize = 0;
|
||||
var flat: [countFlattenedTypes(Types)]type = undefined;
|
||||
for (Types) |T| {
|
||||
@@ -829,6 +830,7 @@ fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {
|
||||
}
|
||||
|
||||
fn countFlattenedTypes(comptime Types: []const type) usize {
|
||||
@setEvalBranchQuota(10_000);
|
||||
var c: usize = 0;
|
||||
for (Types) |T| {
|
||||
c += if (@hasDecl(T, "registerTypes")) T.registerTypes().len else 1;
|
||||
@@ -1064,6 +1066,11 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/svg/Symbol.zig"),
|
||||
@import("../webapi/element/svg/Switch.zig"),
|
||||
@import("../webapi/element/svg/ForeignObject.zig"),
|
||||
@import("../webapi/element/svg/TextContent.zig"),
|
||||
@import("../webapi/element/svg/TextPositioning.zig"),
|
||||
@import("../webapi/element/svg/Text.zig"),
|
||||
@import("../webapi/element/svg/TSpan.zig"),
|
||||
@import("../webapi/element/svg/TextPath.zig"),
|
||||
@import("../webapi/element/svg/View.zig"),
|
||||
@import("../webapi/element/svg/Title.zig"),
|
||||
@import("../webapi/element/svg/Desc.zig"),
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
testing.expectEqual('function', typeof SVGMaskElement);
|
||||
testing.expectEqual('function', typeof SVGPatternElement);
|
||||
testing.expectEqual('function', typeof SVGStopElement);
|
||||
testing.expectEqual('function', typeof SVGTextContentElement);
|
||||
testing.expectEqual('function', typeof SVGTextPositioningElement);
|
||||
testing.expectEqual('function', typeof SVGTextElement);
|
||||
testing.expectEqual('function', typeof SVGTSpanElement);
|
||||
testing.expectEqual('function', typeof SVGTextPathElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset=utf-8>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<svg>
|
||||
<text id=text font-size="20">Hello<tspan id=tspan>🌍</tspan></text>
|
||||
<text id=invalidFont font-size="-10">A</text>
|
||||
<path id=guide d="M0 0 L100 0"></path>
|
||||
<text><textPath id=textPath href="#guide" startOffset="25%" method="stretch" spacing="auto">abc</textPath></text>
|
||||
</svg>
|
||||
|
||||
<script id=textIdentities>
|
||||
{
|
||||
const text = $('#text');
|
||||
testing.expectEqual(true, text instanceof SVGTextElement);
|
||||
testing.expectEqual(true, text instanceof SVGTextPositioningElement);
|
||||
testing.expectEqual(true, text instanceof SVGTextContentElement);
|
||||
testing.expectEqual(true, text instanceof SVGGraphicsElement);
|
||||
testing.expectEqual(text, document.querySelector('svg text'));
|
||||
testing.expectEqual(true, $('#tspan') instanceof SVGTSpanElement);
|
||||
testing.expectEqual(true, $('#tspan') instanceof SVGTextPositioningElement);
|
||||
testing.expectEqual(true, $('#textPath') instanceof SVGTextPathElement);
|
||||
testing.expectEqual(true, $('#textPath') instanceof SVGTextContentElement);
|
||||
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
testing.expectEqual(true, document.createElementNS(ns, 'textPath') instanceof SVGTextPathElement);
|
||||
testing.expectEqual(false, document.createElementNS(ns, 'textpath') instanceof SVGTextPathElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- This block asserts our deterministic fallback metrics; real browsers
|
||||
shape text and produce different numbers, so it is not expected to pass
|
||||
there. The other blocks are browser-comparable. -->
|
||||
<script id=fallbackMetrics>
|
||||
{
|
||||
const text = $('#text');
|
||||
// Characters are UTF-16 code units, like DOM strings: the emoji counts as
|
||||
// two, and addressing either of its halves measures the full glyph.
|
||||
testing.expectEqual(7, text.getNumberOfChars());
|
||||
testing.expectEqual(80, text.getComputedTextLength());
|
||||
testing.expectEqual(60, text.getSubStringLength(0, 5));
|
||||
testing.expectEqual(20, text.getSubStringLength(5, 1));
|
||||
testing.expectEqual(20, text.getSubStringLength(6, 1));
|
||||
testing.expectEqual(32, text.getSubStringLength(4, 99));
|
||||
testing.expectError('IndexSizeError', () => text.getSubStringLength(7, 1));
|
||||
testing.expectError('IndexSizeError', () => text.getSubStringLength(7, 0));
|
||||
testing.expectEqual(2, $('#tspan').getNumberOfChars());
|
||||
testing.expectEqual(20, $('#tspan').getComputedTextLength());
|
||||
testing.expectEqual(9.6, $('#invalidFont').getComputedTextLength());
|
||||
|
||||
// Chrome answers an absent textLength with the computed (shaped) text
|
||||
// length; without shaping we default to 0.
|
||||
testing.expectEqual(0, text.textLength.baseVal.value);
|
||||
|
||||
// Browsers return the shaped text box here; our text geometry is
|
||||
// unmeasured, so like the other unresolved elements getBBox answers an
|
||||
// empty box rather than an error.
|
||||
testing.expectEqual(0, text.getBBox().width);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=textReflection>
|
||||
{
|
||||
const text = $('#text');
|
||||
testing.expectEqual(true, text.textLength instanceof SVGAnimatedLength);
|
||||
testing.expectEqual(true, text.textLength === text.textLength);
|
||||
text.textLength.baseVal.value = 100;
|
||||
testing.expectEqual('100', text.getAttribute('textLength'));
|
||||
testing.expectEqual(100, text.textLength.animVal.value);
|
||||
testing.expectEqual(1, text.lengthAdjust.baseVal);
|
||||
testing.expectEqual(true, text.lengthAdjust === text.lengthAdjust);
|
||||
text.lengthAdjust.baseVal = 2;
|
||||
testing.expectEqual('spacingAndGlyphs', text.getAttribute('lengthAdjust'));
|
||||
|
||||
const textPath = $('#textPath');
|
||||
testing.expectEqual('#guide', textPath.href.baseVal);
|
||||
testing.expectEqual(true, textPath.startOffset === textPath.startOffset);
|
||||
testing.expectEqual(true, textPath.method === textPath.method);
|
||||
testing.expectEqual(true, textPath.spacing === textPath.spacing);
|
||||
testing.expectEqual('25%', textPath.startOffset.baseVal.valueAsString);
|
||||
testing.expectEqual(2, textPath.method.baseVal);
|
||||
testing.expectEqual(1, textPath.spacing.baseVal);
|
||||
textPath.spacing.baseVal = 2;
|
||||
testing.expectEqual('exact', textPath.getAttribute('spacing'));
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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");
|
||||
|
||||
// Deterministic DOM fallback only. This intentionally does not claim to shape
|
||||
// scripts, apply kerning, or select fonts; a real shaping backend can replace
|
||||
// this module without changing SVGTextContentElement.
|
||||
|
||||
// Character counts and indices are UTF-16 code units, to match DOM string
|
||||
// semantics: an astral codepoint is two addressable characters in browsers.
|
||||
pub fn utf16Length(text: []const u8) u32 {
|
||||
var iterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
|
||||
var result: u32 = 0;
|
||||
while (iterator.nextCodepoint()) |codepoint| {
|
||||
result += unitLength(codepoint);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn unitLength(codepoint: u21) u32 {
|
||||
return if (codepoint >= 0x10000) 2 else 1;
|
||||
}
|
||||
|
||||
pub fn width(text: []const u8, font_size: f64) f64 {
|
||||
var iterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
|
||||
var result: f64 = 0;
|
||||
while (iterator.nextCodepoint()) |codepoint| result += advance(codepoint, font_size);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn substringWidth(text: []const u8, charnum: u32, nchars: u32, font_size: f64) !f64 {
|
||||
if (charnum >= utf16Length(text)) return error.IndexSizeError;
|
||||
const end = charnum +| nchars;
|
||||
|
||||
// A glyph is measured whenever its unit range intersects the request, so
|
||||
// addressing either half of a surrogate pair yields the full glyph, like
|
||||
// Chrome does.
|
||||
var iterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
|
||||
var unit: u32 = 0;
|
||||
var result: f64 = 0;
|
||||
while (iterator.nextCodepoint()) |codepoint| {
|
||||
const units = unitLength(codepoint);
|
||||
if (unit >= end) break;
|
||||
if (unit + units > charnum) result += advance(codepoint, font_size);
|
||||
unit += units;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn advance(codepoint: u21, font_size: f64) f64 {
|
||||
if (isZeroWidth(codepoint)) return 0;
|
||||
if (codepoint == '\n' or codepoint == '\r') return 0;
|
||||
if (codepoint == '\t') return font_size * 1.32;
|
||||
if (std.ascii.isWhitespace(@intCast(@min(codepoint, 0x7f)))) return font_size * 0.33;
|
||||
if (isWide(codepoint)) return font_size;
|
||||
return font_size * 0.6;
|
||||
}
|
||||
|
||||
fn isZeroWidth(codepoint: u21) bool {
|
||||
return codepoint == 0x200c or codepoint == 0x200d or
|
||||
(codepoint >= 0x0300 and codepoint <= 0x036f) or
|
||||
(codepoint >= 0x1ab0 and codepoint <= 0x1aff) or
|
||||
(codepoint >= 0x1dc0 and codepoint <= 0x1dff) or
|
||||
(codepoint >= 0x20d0 and codepoint <= 0x20ff) or
|
||||
(codepoint >= 0xfe00 and codepoint <= 0xfe0f) or
|
||||
(codepoint >= 0xfe20 and codepoint <= 0xfe2f) or
|
||||
(codepoint >= 0xe0100 and codepoint <= 0xe01ef);
|
||||
}
|
||||
|
||||
fn isWide(codepoint: u21) bool {
|
||||
return (codepoint >= 0x1100 and codepoint <= 0x115f) or
|
||||
codepoint == 0x2329 or codepoint == 0x232a or
|
||||
(codepoint >= 0x2e80 and codepoint <= 0xa4cf) or
|
||||
(codepoint >= 0xac00 and codepoint <= 0xd7a3) or
|
||||
(codepoint >= 0xf900 and codepoint <= 0xfaff) or
|
||||
(codepoint >= 0xfe10 and codepoint <= 0xfe19) or
|
||||
(codepoint >= 0xfe30 and codepoint <= 0xfe6f) or
|
||||
(codepoint >= 0xff00 and codepoint <= 0xff60) or
|
||||
(codepoint >= 0xffe0 and codepoint <= 0xffe6) or
|
||||
(codepoint >= 0x1f000 and codepoint <= 0x1faff) or
|
||||
(codepoint >= 0x20000 and codepoint <= 0x3fffd);
|
||||
}
|
||||
|
||||
test "fallback metrics count utf-16 units and ignore combining marks" {
|
||||
try std.testing.expectEqual(@as(u32, 3), utf16Length("Aé界"));
|
||||
try std.testing.expectEqual(@as(u32, 2), utf16Length("🌍"));
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 16), width("A\u{0301}界", 10), 0.0001);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 10), try substringWidth("A界", 1, 1, 10), 0.0001);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 10), try substringWidth("🌍", 0, 1, 10), 0.0001);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 10), try substringWidth("🌍", 1, 1, 10), 0.0001);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 0), try substringWidth("A界", 1, 0, 10), 0.0001);
|
||||
try std.testing.expectError(error.IndexSizeError, substringWidth("A", 2, 1, 10));
|
||||
try std.testing.expectError(error.IndexSizeError, substringWidth("A", 1, 0, 10));
|
||||
try std.testing.expectError(error.IndexSizeError, substringWidth("", 0, 0, 10));
|
||||
}
|
||||
@@ -85,6 +85,13 @@ pub fn getTag(self: *const Svg) Element.Tag {
|
||||
// No dedicated Element.Tag values; tag-name matching falls back
|
||||
// to _tag_name, like it does for generic SVG elements.
|
||||
.a, .use, .image, .defs, .symbol, .switch_element, .foreign_object => .unknown,
|
||||
.text_content => |content| switch (content._type) {
|
||||
.positioning => |positioning| switch (positioning._type) {
|
||||
.text => .text,
|
||||
.tspan => .unknown,
|
||||
},
|
||||
.text_path => .unknown,
|
||||
},
|
||||
.geometry => |geo| switch (geo._type) {
|
||||
.rect => .rect,
|
||||
.circle => .circle,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub const Defs = @import("Defs.zig");
|
||||
pub const Symbol = @import("Symbol.zig");
|
||||
pub const Switch = @import("Switch.zig");
|
||||
pub const ForeignObject = @import("ForeignObject.zig");
|
||||
pub const TextContent = @import("TextContent.zig");
|
||||
pub const Geometry = @import("Geometry.zig");
|
||||
|
||||
const Graphics = @This();
|
||||
@@ -54,6 +55,7 @@ pub const Type = union(enum) {
|
||||
symbol: *Symbol,
|
||||
switch_element: *Switch,
|
||||
foreign_object: *ForeignObject,
|
||||
text_content: *TextContent,
|
||||
geometry: *Geometry,
|
||||
};
|
||||
|
||||
@@ -68,6 +70,9 @@ pub fn is(self: *Graphics, comptime T: type) ?*T {
|
||||
if (self._type == .geometry) {
|
||||
return self._type.geometry.is(T);
|
||||
}
|
||||
if (self._type == .text_content) {
|
||||
return self._type.text_content.is(T);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -106,7 +111,7 @@ pub fn getBBox(self: *Graphics, frame: *Frame) !*DOMRect {
|
||||
},
|
||||
.foreign_object => |foreign_object| bounds = try foreign_object.getBounds(frame),
|
||||
.g, .a, .svg => try accumulateChildren(self, .{}, &bounds, frame),
|
||||
.defs, .symbol, .switch_element, .use, .image => {},
|
||||
.defs, .symbol, .switch_element, .use, .image, .text_content => {},
|
||||
}
|
||||
if (bounds.isEmpty()) {
|
||||
return DOMRect.create(.{}, frame._factory);
|
||||
@@ -173,9 +178,10 @@ fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathD
|
||||
.matrix = child_matrix,
|
||||
}),
|
||||
// <defs> and <symbol> never render. Nested viewports, <switch>,
|
||||
// <use> and <image> have geometry we cannot resolve yet, so they
|
||||
// contribute nothing rather than making the whole box unavailable.
|
||||
.defs, .symbol, .svg, .switch_element, .use, .image => {},
|
||||
// <use>, <image> and text have geometry we cannot resolve yet, so
|
||||
// they contribute nothing rather than making the whole box
|
||||
// unavailable.
|
||||
.defs, .symbol, .svg, .switch_element, .use, .image, .text_content => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 js = @import("../../../js/js.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const TextPositioning = @import("TextPositioning.zig");
|
||||
|
||||
const TSpan = @This();
|
||||
_proto: *TextPositioning,
|
||||
|
||||
pub fn asElement(self: *TSpan) *Element {
|
||||
return self._proto.asElement();
|
||||
}
|
||||
pub fn asNode(self: *TSpan) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TSpan);
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGTSpanElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 js = @import("../../../js/js.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const TextPositioning = @import("TextPositioning.zig");
|
||||
|
||||
const Text = @This();
|
||||
_proto: *TextPositioning,
|
||||
|
||||
pub fn asElement(self: *Text) *Element {
|
||||
return self._proto.asElement();
|
||||
}
|
||||
pub fn asNode(self: *Text) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Text);
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGTextElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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 js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const text_measure = @import("../../../text_measure.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
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");
|
||||
|
||||
pub const TextPositioning = @import("TextPositioning.zig");
|
||||
pub const TextPath = @import("TextPath.zig");
|
||||
|
||||
const TextContent = @This();
|
||||
_proto: *Graphics,
|
||||
_type: Type,
|
||||
|
||||
pub const Type = union(enum) {
|
||||
positioning: *TextPositioning,
|
||||
text_path: *TextPath,
|
||||
};
|
||||
|
||||
pub fn is(self: *TextContent, comptime T: type) ?*T {
|
||||
inline for (@typeInfo(Type).@"union".fields) |field| {
|
||||
if (@field(Type, field.name) == self._type) {
|
||||
if (field.type == *T) return @field(self._type, field.name);
|
||||
}
|
||||
}
|
||||
if (self._type == .positioning) return self._type.positioning.is(T);
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn asElement(self: *TextContent) *Element {
|
||||
return self._proto.asElement();
|
||||
}
|
||||
pub fn asNode(self: *TextContent) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
fn text(self: *TextContent, frame: *Frame) []const u8 {
|
||||
return self.asNode().getTextContentAlloc(frame.local_arena) catch "";
|
||||
}
|
||||
|
||||
fn fontSize(self: *TextContent, frame: *Frame) f64 {
|
||||
return Length.fontSizeForElement(self.asElement(), frame);
|
||||
}
|
||||
|
||||
fn getTextLength(self: *TextContent, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .text_length, frame);
|
||||
}
|
||||
|
||||
fn getLengthAdjust(self: *TextContent, frame: *Frame) !*AnimatedEnumeration {
|
||||
return AnimatedEnumeration.getOrCreate(self.asElement(), .length_adjust, frame);
|
||||
}
|
||||
|
||||
pub fn getNumberOfChars(self: *TextContent, frame: *Frame) u32 {
|
||||
return text_measure.utf16Length(self.text(frame));
|
||||
}
|
||||
|
||||
pub fn getComputedTextLength(self: *TextContent, frame: *Frame) f64 {
|
||||
return text_measure.width(self.text(frame), self.fontSize(frame));
|
||||
}
|
||||
|
||||
pub fn getSubStringLength(self: *TextContent, charnum: u32, nchars: u32, frame: *Frame) !f64 {
|
||||
return text_measure.substringWidth(self.text(frame), charnum, nchars, self.fontSize(frame));
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TextContent);
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGTextContentElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const LENGTHADJUST_UNKNOWN = bridge.property(0, .{ .template = true });
|
||||
pub const LENGTHADJUST_SPACING = bridge.property(1, .{ .template = true });
|
||||
pub const LENGTHADJUST_SPACINGANDGLYPHS = bridge.property(2, .{ .template = true });
|
||||
|
||||
pub const textLength = bridge.accessor(TextContent.getTextLength, null, .{});
|
||||
pub const lengthAdjust = bridge.accessor(TextContent.getLengthAdjust, null, .{});
|
||||
pub const getNumberOfChars = bridge.function(TextContent.getNumberOfChars, .{});
|
||||
pub const getComputedTextLength = bridge.function(TextContent.getComputedTextLength, .{});
|
||||
pub const getSubStringLength = bridge.function(TextContent.getSubStringLength, .{});
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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 js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
|
||||
const AnimatedLength = @import("../../svg/AnimatedLength.zig");
|
||||
const AnimatedString = @import("../../svg/AnimatedString.zig");
|
||||
|
||||
const TextContent = @import("TextContent.zig");
|
||||
|
||||
const TextPath = @This();
|
||||
_proto: *TextContent,
|
||||
|
||||
pub fn asElement(self: *TextPath) *Element {
|
||||
return self._proto.asElement();
|
||||
}
|
||||
pub fn asNode(self: *TextPath) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
fn getStartOffset(self: *TextPath, frame: *Frame) !*AnimatedLength {
|
||||
return AnimatedLength.getOrCreate(self.asElement(), .text_path_start_offset, frame);
|
||||
}
|
||||
fn getHref(self: *TextPath, frame: *Frame) !*AnimatedString {
|
||||
return AnimatedString.getOrCreate(self.asElement(), .href, frame);
|
||||
}
|
||||
fn getMethod(self: *TextPath, frame: *Frame) !*AnimatedEnumeration {
|
||||
return AnimatedEnumeration.getOrCreate(self.asElement(), .text_path_method, frame);
|
||||
}
|
||||
fn getSpacing(self: *TextPath, frame: *Frame) !*AnimatedEnumeration {
|
||||
return AnimatedEnumeration.getOrCreate(self.asElement(), .text_path_spacing, frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TextPath);
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGTextPathElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
pub const TEXTPATH_METHODTYPE_UNKNOWN = bridge.property(0, .{ .template = true });
|
||||
pub const TEXTPATH_METHODTYPE_ALIGN = bridge.property(1, .{ .template = true });
|
||||
pub const TEXTPATH_METHODTYPE_STRETCH = bridge.property(2, .{ .template = true });
|
||||
pub const TEXTPATH_SPACINGTYPE_UNKNOWN = bridge.property(0, .{ .template = true });
|
||||
pub const TEXTPATH_SPACINGTYPE_AUTO = bridge.property(1, .{ .template = true });
|
||||
pub const TEXTPATH_SPACINGTYPE_EXACT = bridge.property(2, .{ .template = true });
|
||||
pub const startOffset = bridge.accessor(TextPath.getStartOffset, null, .{});
|
||||
pub const method = bridge.accessor(TextPath.getMethod, null, .{});
|
||||
pub const spacing = bridge.accessor(TextPath.getSpacing, null, .{});
|
||||
pub const href = bridge.accessor(TextPath.getHref, null, .{});
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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 js = @import("../../../js/js.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
|
||||
const TextContent = @import("TextContent.zig");
|
||||
|
||||
pub const Text = @import("Text.zig");
|
||||
pub const TSpan = @import("TSpan.zig");
|
||||
|
||||
const TextPositioning = @This();
|
||||
_proto: *TextContent,
|
||||
_type: Type,
|
||||
|
||||
pub const Type = union(enum) {
|
||||
text: *Text,
|
||||
tspan: *TSpan,
|
||||
};
|
||||
|
||||
pub fn is(self: *TextPositioning, comptime T: type) ?*T {
|
||||
inline for (@typeInfo(Type).@"union".fields) |field| {
|
||||
if (@field(Type, field.name) == self._type) {
|
||||
if (field.type == *T) return @field(self._type, field.name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn asElement(self: *TextPositioning) *Element {
|
||||
return self._proto.asElement();
|
||||
}
|
||||
pub fn asNode(self: *TextPositioning) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TextPositioning);
|
||||
pub const Meta = struct {
|
||||
pub const name = "SVGTextPositioningElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
};
|
||||
@@ -48,6 +48,18 @@ const spread_entries = [_]Entry{
|
||||
.{ .keyword = "reflect", .value = 2 },
|
||||
.{ .keyword = "repeat", .value = 3 },
|
||||
};
|
||||
const length_adjust_entries = [_]Entry{
|
||||
.{ .keyword = "spacing", .value = 1 },
|
||||
.{ .keyword = "spacingAndGlyphs", .value = 2 },
|
||||
};
|
||||
const text_path_method_entries = [_]Entry{
|
||||
.{ .keyword = "align", .value = 1 },
|
||||
.{ .keyword = "stretch", .value = 2 },
|
||||
};
|
||||
const text_path_spacing_entries = [_]Entry{
|
||||
.{ .keyword = "auto", .value = 1 },
|
||||
.{ .keyword = "exact", .value = 2 },
|
||||
};
|
||||
|
||||
pub const Kind = enum {
|
||||
clip_path_units,
|
||||
@@ -58,6 +70,9 @@ pub const Kind = enum {
|
||||
mask_content_units,
|
||||
pattern_units,
|
||||
pattern_content_units,
|
||||
length_adjust,
|
||||
text_path_method,
|
||||
text_path_spacing,
|
||||
|
||||
fn attributeName(self: Kind) lp.String {
|
||||
return switch (self) {
|
||||
@@ -69,6 +84,9 @@ pub const Kind = enum {
|
||||
.mask_content_units => .wrap("maskContentUnits"),
|
||||
.pattern_units => .wrap("patternUnits"),
|
||||
.pattern_content_units => .wrap("patternContentUnits"),
|
||||
.length_adjust => .wrap("lengthAdjust"),
|
||||
.text_path_method => .wrap("method"),
|
||||
.text_path_spacing => .wrap("spacing"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +94,9 @@ pub const Kind = enum {
|
||||
return switch (self) {
|
||||
.marker_units => &marker_unit_entries,
|
||||
.spread_method => &spread_entries,
|
||||
.length_adjust => &length_adjust_entries,
|
||||
.text_path_method => &text_path_method_entries,
|
||||
.text_path_spacing => &text_path_spacing_entries,
|
||||
else => &unit_entries,
|
||||
};
|
||||
}
|
||||
@@ -86,6 +107,8 @@ pub const Kind = enum {
|
||||
.mask_content_units,
|
||||
.pattern_content_units,
|
||||
.spread_method,
|
||||
.length_adjust,
|
||||
.text_path_method,
|
||||
=> 1,
|
||||
else => 2,
|
||||
};
|
||||
|
||||
@@ -67,6 +67,8 @@ pub const Kind = enum {
|
||||
pattern_y,
|
||||
pattern_width,
|
||||
pattern_height,
|
||||
text_length,
|
||||
text_path_start_offset,
|
||||
|
||||
fn attributeName(self: Kind) lp.String {
|
||||
return switch (self) {
|
||||
@@ -90,6 +92,8 @@ pub const Kind = enum {
|
||||
.marker_ref_y => comptime .wrap("refY"),
|
||||
.marker_width => comptime .wrap("markerWidth"),
|
||||
.marker_height => comptime .wrap("markerHeight"),
|
||||
.text_length => comptime .wrap("textLength"),
|
||||
.text_path_start_offset => comptime .wrap("startOffset"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,6 +116,7 @@ pub const Kind = enum {
|
||||
.mask_width,
|
||||
.pattern_x,
|
||||
.pattern_width,
|
||||
.text_length,
|
||||
=> .horizontal,
|
||||
.y,
|
||||
.height,
|
||||
@@ -131,7 +136,7 @@ pub const Kind = enum {
|
||||
.pattern_y,
|
||||
.pattern_height,
|
||||
=> .vertical,
|
||||
.r, .radial_gradient_r, .radial_gradient_fr => .unspecified,
|
||||
.r, .radial_gradient_r, .radial_gradient_fr, .text_path_start_offset => .unspecified,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,10 @@ fn resolvedFontSizeAt(element: ?*Element, frame: *Frame, depth: u8) f64 {
|
||||
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")) {
|
||||
@@ -331,7 +335,8 @@ fn parseFontSize(raw: []const u8, parent: ?*Element, frame: *Frame, depth: u8) ?
|
||||
.ex => parent_size / 2.0,
|
||||
else => unreachable,
|
||||
};
|
||||
return parsed.value * factor;
|
||||
const size = parsed.value * factor;
|
||||
return if (size >= 0 and std.math.isFinite(size)) size else null;
|
||||
}
|
||||
|
||||
fn absoluteUnitFactor(unit: Unit) ?f64 {
|
||||
|
||||
Reference in new issue
Block a user