ladybird/Libraries/LibWeb/Layout/TextOffsetMapping.h
Tim Ledbetter b67d73a661 LibWeb: Apply ::first-letter pseudo-element styles
We now apply first letter styles by splitting text with a first-letter
style applied into 2 `TextSliceNode` objects.  The
`DOM::Text` layout  node always points at the non first-letter slice
and the first-letter slice is  reachable via
`TextSliceNode::first_letter_slice()`.

First letter splitting works by `TreeBuilder` walking a block
container's inline descendants to find the first typographic letter
unit per the pattern given in  css-pseudo level 4, which is then
wrapped in an anonymous inline box styled with the `::first-letter`
computed properties.

Consumers that map between DOM offsets and layout geometry
are updated to visit all slices of a `DOM::Text` through
`TextOffsetMapping`.
2026-05-20 12:09:19 +01:00

75 lines
2.6 KiB
C++

/*
* Copyright (c) 2026, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Forward.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableFragment.h>
#include <LibWeb/Painting/PaintableWithLines.h>
namespace Web::Layout {
class TextOffsetMapping {
public:
explicit TextOffsetMapping(DOM::Text const&);
bool is_split() const { return m_first_letter_slice != nullptr; }
TextNode const* primary() const { return m_primary; }
TextSliceNode const* first_letter_slice() const { return m_first_letter_slice; }
template<typename Callback>
void for_each_fragment(Callback&& callback) const
{
if (m_first_letter_slice)
callback(static_cast<TextNode const&>(*m_first_letter_slice));
if (m_primary)
callback(*m_primary);
}
template<typename Callback>
void for_each_fragment(Callback&& callback)
{
if (m_first_letter_slice)
callback(const_cast<TextNode&>(static_cast<TextNode const&>(*m_first_letter_slice)));
if (m_primary)
callback(const_cast<TextNode&>(*m_primary));
}
TextNode const* fragment_containing(size_t dom_offset) const;
template<typename Callback>
void for_each_paintable_fragment_in_dom_range(size_t dom_start, size_t dom_end, Callback&& callback) const
{
for_each_fragment([&](TextNode const& fragment) {
auto fragment_paintable = fragment.first_paintable();
if (!fragment_paintable)
return;
auto paintable_with_lines = fragment_paintable->template first_ancestor_of_type<Painting::PaintableWithLines>();
if (!paintable_with_lines)
return;
for (auto const& paintable_fragment : paintable_with_lines->fragments()) {
if (&paintable_fragment.paintable() != fragment_paintable)
continue;
auto const fragment_dom_start = paintable_fragment.dom_start_offset_in_node();
auto const fragment_dom_end = paintable_fragment.dom_end_offset_in_node();
if (fragment_dom_end <= dom_start || fragment_dom_start >= dom_end)
continue;
callback(paintable_fragment);
}
});
}
private:
// TextOffsetMapping is a short-lived stack object, and the layout nodes are kept alive by the document's layout
// tree for the duration of its use, so there's no need to visit these.
GC::RawPtr<TextNode const> m_primary;
GC::RawPtr<TextSliceNode const> m_first_letter_slice;
};
}