ladybird/Libraries/LibWeb/SVG/SVGTextPositioningElement.h
Jelle Raaijmakers 2c5beeabe3 LibWeb: Improve support for SVG text positioning attributes
Previously, we only supported very basic numbers and a single level of
text positioning support in the `x`, `y`, `dx` and `dy` attributes in
`<text>` and `<tspan>` SVG elements.

This improves our support for them in the following ways:

  * Any `length-percentage` or `number` type value is accepted;
  * Nested `<text>` and `<tspan>` use the 'current text position'
    concept to determine where the next text run should go;
  * We expose the attributes' values through the API.

Though we still do not support:

  * Applying the `rotate` attribute;
  * Applying transformations on a per-character basis.
  * Proper horizontal and vertical glyph advancing (we just use the path
    bounding box for now).
2025-11-20 23:15:24 +01:00

81 lines
2.8 KiB
C++

/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/SVG/SVGTextContentElement.h>
namespace Web::SVG {
// https://svgwg.org/svg2-draft/text.html#TSpanNotes
// https://svgwg.org/svg2-draft/text.html#TSpanAttributes
struct TextPositioning {
using Position = Variant<CSS::LengthPercentage, CSS::Number>;
Vector<Position> x;
Vector<Position> y;
Vector<Position> dx;
Vector<Position> dy;
Vector<float> rotate;
void apply_to_text_position(Layout::Node const& node, CSSPixelSize viewport, Gfx::FloatPoint& current_text_position,
size_t character_index) const
{
auto value_for_character = [&](Vector<Position> const& values) -> float {
if (values.is_empty())
return 0.f;
auto position = character_index < values.size() ? values[character_index] : values.last();
return position.visit(
[](CSS::Number const& number) { return static_cast<float>(number.value()); },
[&](CSS::LengthPercentage const& length_percentage) {
auto reference = &values == &x || &values == &dx ? viewport.width() : viewport.height();
return length_percentage.to_px(node, reference).to_float();
});
};
if (!x.is_empty())
current_text_position.set_x(value_for_character(x));
if (!y.is_empty())
current_text_position.set_y(value_for_character(y));
current_text_position.translate_by(value_for_character(dx), value_for_character(dy));
}
};
// https://svgwg.org/svg2-draft/text.html#InterfaceSVGTextPositioningElement
class SVGTextPositioningElement : public SVGTextContentElement {
WEB_PLATFORM_OBJECT(SVGTextPositioningElement, SVGTextContentElement);
public:
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
TextPositioning text_positioning() const;
GC::Ref<SVGAnimatedLengthList> x();
GC::Ref<SVGAnimatedLengthList> y();
GC::Ref<SVGAnimatedLengthList> dx();
GC::Ref<SVGAnimatedLengthList> dy();
GC::Ref<SVGAnimatedNumberList> rotate();
protected:
SVGTextPositioningElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
private:
GC::Ref<SVGAnimatedLengthList> ensure_length_list(GC::Ptr<SVGAnimatedLengthList>&, FlyString const& attribute_name) const;
GC::Ptr<SVGAnimatedLengthList> m_x;
GC::Ptr<SVGAnimatedLengthList> m_y;
GC::Ptr<SVGAnimatedLengthList> m_dx;
GC::Ptr<SVGAnimatedLengthList> m_dy;
GC::Ptr<SVGAnimatedNumberList> m_rotate;
};
}