LibWeb: Implement painting for svg text

The implementation of painting for SVG text follows the same pattern
as the implementation of painting for SVG geometries. However, instead
of reusing the existing PaintableWithLines to draw text, a new class
called SVGTextPaintable is introduced. because everything that is
painted inside an SVG is expected to inherit from SVGGraphicsPaintable.
Therefore reusing the text painting from regular text nodes would
require significant refactoring.
This commit is contained in:
Aliaksandr Kalenik 2023-06-08 21:32:33 +03:00 committed by Andreas Kling
parent 81a6976e90
commit 0d8d7ae94e
Notes: sideshowbarker 2024-07-17 09:49:33 +09:00
9 changed files with 216 additions and 0 deletions

View file

@ -422,6 +422,7 @@ set(SOURCES
Layout/SVGGeometryBox.cpp
Layout/SVGGraphicsBox.cpp
Layout/SVGSVGBox.cpp
Layout/SVGTextBox.cpp
Layout/TableFormattingContext.cpp
Layout/TableWrapper.cpp
Layout/TextNode.cpp
@ -465,6 +466,7 @@ set(SOURCES
Painting/SVGGraphicsPaintable.cpp
Painting/SVGPaintable.cpp
Painting/SVGSVGPaintable.cpp
Painting/SVGTextPaintable.cpp
Painting/ShadowPainting.cpp
Painting/StackingContext.cpp
Painting/TextPaintable.cpp

View file

@ -12,6 +12,7 @@
#include <LibWeb/Layout/SVGFormattingContext.h>
#include <LibWeb/Layout/SVGGeometryBox.h>
#include <LibWeb/Layout/SVGSVGBox.h>
#include <LibWeb/Layout/SVGTextBox.h>
#include <LibWeb/SVG/SVGForeignObjectElement.h>
#include <LibWeb/SVG/SVGSVGElement.h>
@ -187,6 +188,10 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
} else if (is<SVGSVGBox>(descendant)) {
SVGFormattingContext nested_context(m_state, descendant, this);
nested_context.run(descendant, layout_mode, available_space);
} else if (is<SVGTextBox>(descendant)) {
auto const& svg_text_box = static_cast<SVGTextBox const&>(descendant);
// NOTE: This hack creates a layout state to ensure the existence of a paintable box node in LayoutState::commit(), even when none of the values from UsedValues impact the SVG text.
m_state.get_mutable(svg_text_box);
}
return IterationDecision::Continue;

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Layout/SVGTextBox.h>
#include <LibWeb/Painting/SVGTextPaintable.h>
#include <LibWeb/SVG/SVGSVGElement.h>
namespace Web::Layout {
SVGTextBox::SVGTextBox(DOM::Document& document, SVG::SVGTextContentElement& element, NonnullRefPtr<CSS::StyleProperties> properties)
: SVGGraphicsBox(document, element, properties)
{
}
CSSPixelPoint SVGTextBox::viewbox_origin() const
{
auto* svg_box = dom_node().first_ancestor_of_type<SVG::SVGSVGElement>();
if (!svg_box || !svg_box->view_box().has_value())
return { 0, 0 };
return { svg_box->view_box().value().min_x, svg_box->view_box().value().min_y };
}
Optional<Gfx::AffineTransform> SVGTextBox::layout_transform() const
{
auto& geometry_element = dom_node();
auto transform = geometry_element.get_transform();
auto* svg_box = geometry_element.first_ancestor_of_type<SVG::SVGSVGElement>();
auto origin = viewbox_origin().to_type<double>().to_type<float>();
Gfx::FloatPoint paint_offset = {};
if (svg_box && svg_box->view_box().has_value())
paint_offset = svg_box->paintable_box()->absolute_rect().location().to_type<double>().to_type<float>();
return Gfx::AffineTransform {}.translate(paint_offset).translate(-origin).multiply(transform);
}
JS::GCPtr<Painting::Paintable> SVGTextBox::create_paintable() const
{
return Painting::SVGTextPaintable::create(*this);
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <LibWeb/Layout/SVGGraphicsBox.h>
#include <LibWeb/SVG/SVGTextContentElement.h>
namespace Web::Layout {
class SVGTextBox final : public SVGGraphicsBox {
JS_CELL(SVGTextBox, SVGGraphicsBox);
public:
SVGTextBox(DOM::Document&, SVG::SVGTextContentElement&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~SVGTextBox() override = default;
SVG::SVGTextContentElement& dom_node() { return static_cast<SVG::SVGTextContentElement&>(SVGGraphicsBox::dom_node()); }
SVG::SVGTextContentElement const& dom_node() const { return static_cast<SVG::SVGTextContentElement const&>(SVGGraphicsBox::dom_node()); }
Optional<Gfx::AffineTransform> layout_transform() const;
virtual JS::GCPtr<Painting::Paintable> create_paintable() const override;
private:
CSSPixelPoint viewbox_origin() const;
};
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2023, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Painting/SVGTextPaintable.h>
#include <LibWeb/SVG/SVGSVGElement.h>
namespace Web::Painting {
JS::NonnullGCPtr<SVGTextPaintable> SVGTextPaintable::create(Layout::SVGTextBox const& layout_box)
{
return layout_box.heap().allocate_without_realm<SVGTextPaintable>(layout_box);
}
SVGTextPaintable::SVGTextPaintable(Layout::SVGTextBox const& layout_box)
: SVGGraphicsPaintable(layout_box)
{
}
Optional<HitTestResult> SVGTextPaintable::hit_test(CSSPixelPoint position, HitTestType type) const
{
(void)position;
(void)type;
return {};
}
void SVGTextPaintable::paint(PaintContext& context, PaintPhase phase) const
{
if (!is_visible())
return;
SVGGraphicsPaintable::paint(context, phase);
if (phase != PaintPhase::Foreground)
return;
auto& painter = context.painter();
Gfx::PainterStateSaver save_painter { painter };
auto& svg_context = context.svg_context();
auto svg_context_offset = context.floored_device_point(svg_context.svg_element_position()).to_type<int>();
painter.translate(svg_context_offset);
auto const& dom_node = layout_box().dom_node();
auto child_text_content = dom_node.child_text_content();
auto transform = layout_box().layout_transform();
auto& scaled_font = layout_node().scaled_font(context);
auto text_offset = context.floored_device_point(dom_node.get_offset().transformed(*transform).to_type<CSSPixels>());
painter.draw_text_run(text_offset.to_type<int>(), Utf8View { child_text_content }, scaled_font, layout_node().computed_values().fill()->as_color());
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Layout/SVGTextBox.h>
#include <LibWeb/Painting/SVGGraphicsPaintable.h>
namespace Web::Painting {
class SVGTextPaintable final : public SVGGraphicsPaintable {
JS_CELL(SVGTextPaintable, SVGGraphicsPaintable);
public:
static JS::NonnullGCPtr<SVGTextPaintable> create(Layout::SVGTextBox const&);
virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
virtual void paint(PaintContext&, PaintPhase) const override;
Layout::SVGTextBox const& layout_box() const
{
return static_cast<Layout::SVGTextBox const&>(layout_node());
}
protected:
SVGTextPaintable(Layout::SVGTextBox const&);
};
}

View file

@ -22,6 +22,8 @@ namespace Web::SVG::AttributeNames {
E(contentStyleType) \
E(cx) \
E(cy) \
E(dx) \
E(dy) \
E(diffuseConstant) \
E(edgeMode) \
E(filterUnits) \

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2023, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -8,6 +9,10 @@
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Utf16String.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Layout/SVGTextBox.h>
#include <LibWeb/SVG/AttributeNames.h>
#include <LibWeb/SVG/AttributeParser.h>
#include <LibWeb/SVG/SVGGeometryElement.h>
#include <LibWeb/SVG/SVGTextContentElement.h>
namespace Web::SVG {
@ -25,6 +30,26 @@ JS::ThrowCompletionOr<void> SVGTextContentElement::initialize(JS::Realm& realm)
return {};
}
void SVGTextContentElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value)
{
SVGGraphicsElement::parse_attribute(name, value);
if (name == SVG::AttributeNames::x) {
m_x = AttributeParser::parse_coordinate(value).value();
} else if (name == SVG::AttributeNames::y) {
m_y = AttributeParser::parse_coordinate(value).value();
} else if (name == SVG::AttributeNames::dx) {
m_dx = AttributeParser::parse_coordinate(value).value();
} else if (name == SVG::AttributeNames::dy) {
m_dy = AttributeParser::parse_coordinate(value).value();
}
}
JS::GCPtr<Layout::Node> SVGTextContentElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
{
return heap().allocate_without_realm<Layout::SVGTextBox>(document(), *this, move(style));
}
// https://svgwg.org/svg2-draft/text.html#__svg__SVGTextContentElement__getNumberOfChars
WebIDL::ExceptionOr<int> SVGTextContentElement::get_number_of_chars() const
{
@ -32,4 +57,9 @@ WebIDL::ExceptionOr<int> SVGTextContentElement::get_number_of_chars() const
return static_cast<int>(chars.size());
}
Gfx::FloatPoint SVGTextContentElement::get_offset() const
{
return { m_x + m_dx, m_y + m_dy };
}
}

View file

@ -16,12 +16,24 @@ class SVGTextContentElement : public SVGGraphicsElement {
WEB_PLATFORM_OBJECT(SVGTextContentElement, SVGGraphicsElement);
public:
virtual JS::GCPtr<Layout::Node> create_layout_node(NonnullRefPtr<CSS::StyleProperties>) override;
virtual void parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) override;
WebIDL::ExceptionOr<int> get_number_of_chars() const;
Gfx::FloatPoint get_offset() const;
protected:
SVGTextContentElement(DOM::Document&, DOM::QualifiedName);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
private:
float m_x { 0 };
float m_y { 0 };
float m_dx { 0 };
float m_dy { 0 };
};
}