1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 19:17:44 +00:00

LibWeb: Use separate structure to represent fragments in paintable tree

This is a part of refactoring towards making the paintable tree
independent of the layout tree. Now, instead of transferring text
fragments from the layout tree to the paintable tree during the layout
commit phase, we allocate separate PaintableFragments that contain only
the information necessary for painting. Doing this also allows us to
get rid LineBoxes, as they are used only during layout.
This commit is contained in:
Aliaksandr Kalenik 2024-01-12 21:25:05 +01:00 committed by Andreas Kling
parent 785fa60cca
commit de32b77ceb
401 changed files with 2122 additions and 3614 deletions

View file

@ -500,6 +500,7 @@ set(SOURCES
Painting/PaintContext.cpp
Painting/Paintable.cpp
Painting/PaintableBox.cpp
Painting/PaintableFragment.cpp
Painting/PaintingCommandExecutorCPU.cpp
Painting/RadioButtonPaintable.cpp
Painting/RecordingPainter.cpp

View file

@ -343,38 +343,26 @@ void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool sho
if (is<Layout::BlockContainer>(layout_node) && static_cast<Layout::BlockContainer const&>(layout_node).children_are_inline()) {
auto& block = static_cast<Layout::BlockContainer const&>(layout_node);
for (size_t line_box_index = 0; block.paintable_with_lines() && line_box_index < block.paintable_with_lines()->line_boxes().size(); ++line_box_index) {
auto& line_box = block.paintable_with_lines()->line_boxes()[line_box_index];
for (size_t fragment_index = 0; block.paintable_with_lines() && fragment_index < block.paintable_with_lines()->fragments().size(); ++fragment_index) {
auto const& fragment = block.paintable_with_lines()->fragments()[fragment_index];
for (size_t i = 0; i < indent; ++i)
builder.append(" "sv);
builder.appendff(" {}line {}{} width: {}, height: {}, bottom: {}, baseline: {}\n",
line_box_color_on,
line_box_index,
builder.appendff(" {}frag {}{} from {} ",
fragment_color_on,
fragment_index,
color_off,
line_box.width(),
line_box.height(),
line_box.bottom(),
line_box.baseline());
for (size_t fragment_index = 0; fragment_index < line_box.fragments().size(); ++fragment_index) {
auto& fragment = line_box.fragments()[fragment_index];
fragment.layout_node().class_name());
builder.appendff("start: {}, length: {}, rect: {} baseline: {}\n",
fragment.start(),
fragment.length(),
fragment.absolute_rect(),
fragment.baseline());
if (is<Layout::TextNode>(fragment.layout_node())) {
for (size_t i = 0; i < indent; ++i)
builder.append(" "sv);
builder.appendff(" {}frag {}{} from {} ",
fragment_color_on,
fragment_index,
color_off,
fragment.layout_node().class_name());
builder.appendff("start: {}, length: {}, rect: {}\n",
fragment.start(),
fragment.length(),
fragment.absolute_rect());
if (is<Layout::TextNode>(fragment.layout_node())) {
for (size_t i = 0; i < indent; ++i)
builder.append(" "sv);
auto& layout_text = static_cast<Layout::TextNode const&>(fragment.layout_node());
auto fragment_text = MUST(layout_text.text_for_rendering().substring_from_byte_offset(fragment.start(), fragment.length()));
builder.appendff(" \"{}\"\n", fragment_text);
}
auto const& layout_text = static_cast<Layout::TextNode const&>(fragment.layout_node());
auto fragment_text = MUST(layout_text.text_for_rendering().substring_from_byte_offset(fragment.start(), fragment.length()));
builder.appendff(" \"{}\"\n", fragment_text);
}
}
}

View file

@ -8,9 +8,7 @@
#include <LibWeb/Layout/AvailableSpace.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/LayoutState.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Painting/BorderRadiiData.h>
#include <LibWeb/Painting/InlinePaintable.h>
#include <LibWeb/Painting/SVGPathPaintable.h>
@ -86,9 +84,8 @@ static CSSPixelRect measure_scrollable_overflow(Box const& box)
// - All line boxes directly contained by the scroll container.
if (is<Painting::PaintableWithLines>(box.paintable())) {
auto const& line_boxes = static_cast<Painting::PaintableWithLines const&>(*box.paintable()).line_boxes();
for (auto const& line_box : line_boxes) {
scrollable_overflow_rect = scrollable_overflow_rect.united(line_box.absolute_rect());
for (auto const& fragment : static_cast<Painting::PaintableWithLines const&>(*box.paintable()).fragments()) {
scrollable_overflow_rect = scrollable_overflow_rect.united(fragment.absolute_rect());
}
}
@ -156,8 +153,8 @@ void LayoutState::resolve_relative_positions(Vector<Painting::PaintableWithLines
// line box fragment in the parent block container that contains it.
auto const& containing_line_box_fragment = used_values.containing_line_box_fragment.value();
auto const& containing_block = *node.containing_block();
auto const& containing_block_paintable = verify_cast<Painting::PaintableWithLines>(*containing_block.paintable_box());
auto const& fragment = containing_block_paintable.line_boxes()[containing_line_box_fragment.line_box_index].fragments()[containing_line_box_fragment.fragment_index];
auto const& containing_block_used_values = get(containing_block);
auto const& fragment = containing_block_used_values.line_boxes[containing_line_box_fragment.line_box_index].fragments()[containing_line_box_fragment.fragment_index];
// The fragment has the final offset for the atomic inline, so we just need to copy it from there.
offset = fragment.offset();
@ -175,26 +172,24 @@ void LayoutState::resolve_relative_positions(Vector<Painting::PaintableWithLines
// Line box fragments:
for (auto const& paintable_with_lines : paintables_with_lines) {
for (auto const& line_box : paintable_with_lines.line_boxes()) {
for (auto& fragment : line_box.fragments()) {
auto const& fragment_node = fragment.layout_node();
if (!is<Layout::NodeWithStyleAndBoxModelMetrics>(*fragment_node.parent()))
continue;
// Collect effective relative position offset from inline-flow parent chain.
CSSPixelPoint offset;
for (auto* ancestor = fragment_node.parent(); ancestor; ancestor = ancestor->parent()) {
if (!is<Layout::NodeWithStyleAndBoxModelMetrics>(*ancestor))
break;
if (!ancestor->display().is_inline_outside() || !ancestor->display().is_flow_inside())
break;
if (ancestor->computed_values().position() == CSS::Positioning::Relative) {
auto const& ancestor_node = static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(*ancestor);
auto const& inset = ancestor_node.box_model().inset;
offset.translate_by(inset.left, inset.top);
}
for (auto& fragment : paintable_with_lines.fragments()) {
auto const& fragment_node = fragment.layout_node();
if (!is<Layout::NodeWithStyleAndBoxModelMetrics>(*fragment_node.parent()))
continue;
// Collect effective relative position offset from inline-flow parent chain.
CSSPixelPoint offset;
for (auto* ancestor = fragment_node.parent(); ancestor; ancestor = ancestor->parent()) {
if (!is<Layout::NodeWithStyleAndBoxModelMetrics>(*ancestor))
break;
if (!ancestor->display().is_inline_outside() || !ancestor->display().is_flow_inside())
break;
if (ancestor->computed_values().position() == CSS::Positioning::Relative) {
auto const& ancestor_node = static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(*ancestor);
auto const& inset = ancestor_node.box_model().inset;
offset.translate_by(inset.left, inset.top);
}
const_cast<LineBoxFragment&>(fragment).set_offset(fragment.offset().translated(offset));
}
const_cast<Painting::PaintableFragment&>(fragment).set_offset(fragment.offset().translated(offset));
}
}
}
@ -298,10 +293,10 @@ void LayoutState::resolve_border_radii()
}
for (auto& inline_paintable : inline_paintables) {
Vector<Layout::LineBoxFragment&> fragments;
Vector<Painting::PaintableFragment&> fragments;
verify_cast<Painting::PaintableWithLines>(*inline_paintable.containing_block()->paintable_box()).for_each_fragment([&](auto& fragment) {
if (inline_paintable.layout_node().is_inclusive_ancestor_of(fragment.layout_node()))
fragments.append(const_cast<Layout::LineBoxFragment&>(fragment));
fragments.append(const_cast<Painting::PaintableFragment&>(fragment));
return IterationDecision::Continue;
});
@ -410,7 +405,10 @@ void LayoutState::commit(Box& root)
if (is<Painting::PaintableWithLines>(paintable_box)) {
auto& paintable_with_lines = static_cast<Painting::PaintableWithLines&>(paintable_box);
paintable_with_lines.set_line_boxes(move(used_values.line_boxes));
for (auto& line_box : used_values.line_boxes) {
for (auto& fragment : line_box.fragments())
paintable_with_lines.add_fragment(fragment);
}
paintables_with_lines.append(paintable_with_lines);
}
@ -432,14 +430,9 @@ void LayoutState::commit(Box& root)
// - Measure absolute rect of each line box.
// - Collect all text nodes, so we can create paintables for them later.
for (auto& paintable_with_lines : paintables_with_lines) {
for (auto& line_box : paintable_with_lines.line_boxes()) {
CSSPixelRect line_box_absolute_rect;
for (auto const& fragment : line_box.fragments()) {
line_box_absolute_rect = line_box_absolute_rect.united(fragment.absolute_rect());
if (fragment.layout_node().is_text_node())
text_nodes.set(static_cast<Layout::TextNode*>(const_cast<Layout::Node*>(&fragment.layout_node())));
}
const_cast<LineBox&>(line_box).set_absolute_rect(line_box_absolute_rect);
for (auto& fragment : paintable_with_lines.fragments()) {
if (fragment.layout_node().is_text_node())
text_nodes.set(static_cast<Layout::TextNode*>(const_cast<Layout::Node*>(&fragment.layout_node())));
}
}

View file

@ -7,8 +7,6 @@
#include <AK/Utf8View.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/Layout/LayoutState.h>
#include <LibWeb/Layout/LineBoxFragment.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h>
#include <ctype.h>
@ -42,113 +40,6 @@ CSSPixelRect const LineBoxFragment::absolute_rect() const
return rect;
}
int LineBoxFragment::text_index_at(CSSPixels x) const
{
if (!is<TextNode>(layout_node()))
return 0;
auto& layout_text = verify_cast<TextNode>(layout_node());
auto& font = layout_text.first_available_font();
Utf8View view(text());
CSSPixels relative_x = x - absolute_x();
CSSPixels glyph_spacing = font.glyph_spacing();
if (relative_x < 0)
return 0;
CSSPixels width_so_far = 0;
for (auto it = view.begin(); it != view.end(); ++it) {
auto previous_it = it;
CSSPixels glyph_width = CSSPixels::nearest_value_for(font.glyph_or_emoji_width(it));
if ((width_so_far + glyph_width + glyph_spacing / 2) > relative_x)
return m_start + view.byte_offset_of(previous_it);
width_so_far += glyph_width + glyph_spacing;
}
return m_start + m_length;
}
CSSPixelRect LineBoxFragment::selection_rect(Gfx::Font const& font) const
{
if (layout_node().selection_state() == Node::SelectionState::None)
return {};
if (layout_node().selection_state() == Node::SelectionState::Full)
return absolute_rect();
if (!is<TextNode>(layout_node()))
return {};
auto selection = layout_node().root().selection();
if (!selection)
return {};
auto range = selection->range();
if (!range)
return {};
// FIXME: m_start and m_length should be unsigned and then we won't need these casts.
auto const start_index = static_cast<unsigned>(m_start);
auto const end_index = static_cast<unsigned>(m_start) + static_cast<unsigned>(m_length);
auto text = this->text();
if (layout_node().selection_state() == Node::SelectionState::StartAndEnd) {
// we are in the start/end node (both the same)
if (start_index > range->end_offset())
return {};
if (end_index < range->start_offset())
return {};
if (range->start_offset() == range->end_offset())
return {};
auto selection_start_in_this_fragment = max(0, range->start_offset() - m_start);
auto selection_end_in_this_fragment = min(m_length, range->end_offset() - m_start);
auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment)));
auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1;
auto rect = absolute_rect();
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
rect.set_width(pixel_width_of_selection);
return rect;
}
if (layout_node().selection_state() == Node::SelectionState::Start) {
// we are in the start node
if (end_index < range->start_offset())
return {};
auto selection_start_in_this_fragment = max(0, range->start_offset() - m_start);
auto selection_end_in_this_fragment = m_length;
auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment)));
auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1;
auto rect = absolute_rect();
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
rect.set_width(pixel_width_of_selection);
return rect;
}
if (layout_node().selection_state() == Node::SelectionState::End) {
// we are in the end node
if (start_index > range->end_offset())
return {};
auto selection_start_in_this_fragment = 0;
auto selection_end_in_this_fragment = min(range->end_offset() - m_start, m_length);
auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment)));
auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1;
auto rect = absolute_rect();
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
rect.set_width(pixel_width_of_selection);
return rect;
}
return {};
}
bool LineBoxFragment::is_atomic_inline() const
{
return layout_node().is_replaced_box() || (layout_node().display().is_inline_outside() && !layout_node().display().is_flow_inside());

View file

@ -36,10 +36,7 @@ public:
int length() const { return m_length; }
CSSPixelRect const absolute_rect() const;
CSSPixelPoint offset() const
{
return m_offset;
}
CSSPixelPoint offset() const { return m_offset; }
void set_offset(CSSPixelPoint offset) { m_offset = offset; }
// The baseline of a fragment is the number of pixels from the top to the text baseline.
@ -55,33 +52,16 @@ public:
CSSPixels width() const { return m_size.width(); }
CSSPixels height() const { return m_size.height(); }
CSSPixels border_box_height() const
{
return m_border_box_top + height() + m_border_box_bottom;
}
CSSPixels border_box_top() const { return m_border_box_top; }
CSSPixels border_box_bottom() const { return m_border_box_bottom; }
CSSPixels absolute_x() const { return absolute_rect().x(); }
bool ends_in_whitespace() const;
bool is_justifiable_whitespace() const;
StringView text() const;
int text_index_at(CSSPixels x) const;
CSSPixelRect selection_rect(Gfx::Font const&) const;
bool is_atomic_inline() const;
Vector<Gfx::DrawGlyphOrEmoji> const& glyph_run() const { return m_glyph_run; }
Painting::BorderRadiiData const& border_radii_data() const { return m_border_radii_data; }
void set_border_radii_data(Painting::BorderRadiiData const& border_radii_data) { m_border_radii_data = border_radii_data; }
bool contained_by_inline_node() const { return m_contained_by_inline_node; }
void set_contained_by_inline_node() { m_contained_by_inline_node = true; }
private:
JS::NonnullGCPtr<Node const> m_layout_node;
int m_start { 0 };
@ -92,8 +72,6 @@ private:
CSSPixels m_border_box_bottom { 0 };
CSSPixels m_baseline { 0 };
Vector<Gfx::DrawGlyphOrEmoji> m_glyph_run;
Painting::BorderRadiiData m_border_radii_data;
bool m_contained_by_inline_node { false };
};
}

View file

@ -7,10 +7,8 @@
#include <LibGfx/AntiAliasingPainter.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/ImageBox.h>
#include <LibWeb/Painting/BackgroundPainting.h>
#include <LibWeb/Painting/InlinePaintable.h>
#include <LibWeb/Painting/ShadowPainting.h>
namespace Web::Painting {
@ -168,7 +166,7 @@ template<typename Callback>
void InlinePaintable::for_each_fragment(Callback callback) const
{
// FIXME: This will be slow if the containing block has a lot of fragments!
Vector<Layout::LineBoxFragment const&> fragments;
Vector<PaintableFragment const&> fragments;
verify_cast<PaintableWithLines>(*containing_block()->paintable_box()).for_each_fragment([&](auto& fragment) {
if (layout_node().is_inclusive_ancestor_of(fragment.layout_node()))
fragments.append(fragment);
@ -184,7 +182,7 @@ void InlinePaintable::mark_contained_fragments()
{
verify_cast<PaintableWithLines>(*containing_block()->paintable_box()).for_each_fragment([&](auto& fragment) {
if (layout_node().is_inclusive_ancestor_of(fragment.layout_node()))
const_cast<Layout::LineBoxFragment&>(fragment).set_contained_by_inline_node();
const_cast<PaintableFragment&>(fragment).set_contained_by_inline_node();
return IterationDecision::Continue;
});
}

View file

@ -464,7 +464,7 @@ void PaintableBox::clear_clip_overflow_rect(PaintContext& context, PaintPhase ph
}
}
void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const& text_node, Layout::LineBoxFragment const& fragment)
void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const& text_node, PaintableFragment const& fragment)
{
auto const& browsing_context = text_node.browsing_context();
@ -486,8 +486,9 @@ void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const& text_
auto fragment_rect = fragment.absolute_rect();
auto text = text_node.text_for_rendering().bytes_as_string_view().substring_view(fragment.start(), fragment.length());
CSSPixelRect cursor_rect {
fragment_rect.x() + CSSPixels::nearest_value_for(text_node.first_available_font().width(fragment.text().substring_view(0, text_node.browsing_context().cursor_position()->offset() - fragment.start()))),
fragment_rect.x() + CSSPixels::nearest_value_for(text_node.first_available_font().width(text.substring_view(0, text_node.browsing_context().cursor_position()->offset() - fragment.start()))),
fragment_rect.top(),
1,
fragment_rect.height()
@ -498,7 +499,7 @@ void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const& text_
context.recording_painter().draw_rect(cursor_device_rect, text_node.computed_values().color());
}
void paint_text_decoration(PaintContext& context, Layout::Node const& text_node, Layout::LineBoxFragment const& fragment)
void paint_text_decoration(PaintContext& context, Layout::Node const& text_node, PaintableFragment const& fragment)
{
auto& painter = context.recording_painter();
auto& font = fragment.layout_node().first_available_font();
@ -580,7 +581,7 @@ void paint_text_decoration(PaintContext& context, Layout::Node const& text_node,
}
}
void paint_text_fragment(PaintContext& context, Layout::TextNode const& text_node, Layout::LineBoxFragment const& fragment, PaintPhase phase)
void paint_text_fragment(PaintContext& context, Layout::TextNode const& text_node, PaintableFragment const& fragment, PaintPhase phase)
{
auto& painter = context.recording_painter();
@ -625,7 +626,7 @@ void PaintableWithLines::paint(PaintContext& context, PaintPhase phase) const
PaintableBox::paint(context, phase);
if (m_line_boxes.is_empty())
if (fragments().is_empty())
return;
bool should_clip_overflow = computed_values().overflow_x() != CSS::Overflow::Visible && computed_values().overflow_y() != CSS::Overflow::Visible;
@ -658,47 +659,43 @@ void PaintableWithLines::paint(PaintContext& context, PaintPhase phase) const
// So, we paint the shadows before painting any text.
// FIXME: Find a smarter way to do this?
if (phase == PaintPhase::Foreground) {
for (auto& line_box : m_line_boxes) {
for (auto& fragment : line_box.fragments()) {
if (fragment.contained_by_inline_node())
continue;
if (is<Layout::TextNode>(fragment.layout_node())) {
auto& text_shadow = fragment.layout_node().computed_values().text_shadow();
if (!text_shadow.is_empty()) {
Vector<ShadowData> resolved_shadow_data;
resolved_shadow_data.ensure_capacity(text_shadow.size());
for (auto const& layer : text_shadow) {
resolved_shadow_data.empend(
layer.color,
layer.offset_x.to_px(layout_box()),
layer.offset_y.to_px(layout_box()),
layer.blur_radius.to_px(layout_box()),
layer.spread_distance.to_px(layout_box()),
ShadowPlacement::Outer);
}
context.recording_painter().set_font(fragment.layout_node().first_available_font());
paint_text_shadow(context, fragment, resolved_shadow_data);
for (auto& fragment : fragments()) {
if (fragment.contained_by_inline_node())
continue;
if (is<Layout::TextNode>(fragment.layout_node())) {
auto& text_shadow = fragment.layout_node().computed_values().text_shadow();
if (!text_shadow.is_empty()) {
Vector<ShadowData> resolved_shadow_data;
resolved_shadow_data.ensure_capacity(text_shadow.size());
for (auto const& layer : text_shadow) {
resolved_shadow_data.empend(
layer.color,
layer.offset_x.to_px(layout_box()),
layer.offset_y.to_px(layout_box()),
layer.blur_radius.to_px(layout_box()),
layer.spread_distance.to_px(layout_box()),
ShadowPlacement::Outer);
}
context.recording_painter().set_font(fragment.layout_node().first_available_font());
paint_text_shadow(context, fragment, resolved_shadow_data);
}
}
}
}
for (auto& line_box : m_line_boxes) {
for (auto& fragment : line_box.fragments()) {
if (fragment.contained_by_inline_node())
continue;
auto fragment_absolute_rect = fragment.absolute_rect();
auto fragment_absolute_device_rect = context.enclosing_device_rect(fragment_absolute_rect);
if (context.should_show_line_box_borders()) {
context.recording_painter().draw_rect(fragment_absolute_device_rect.to_type<int>(), Color::Green);
context.recording_painter().draw_line(
context.rounded_device_point(fragment_absolute_rect.top_left().translated(0, fragment.baseline())).to_type<int>(),
context.rounded_device_point(fragment_absolute_rect.top_right().translated(-1, fragment.baseline())).to_type<int>(), Color::Red);
}
if (is<Layout::TextNode>(fragment.layout_node()))
paint_text_fragment(context, static_cast<Layout::TextNode const&>(fragment.layout_node()), fragment, phase);
for (auto const& fragment : m_fragments) {
if (fragment.contained_by_inline_node())
continue;
auto fragment_absolute_rect = fragment.absolute_rect();
auto fragment_absolute_device_rect = context.enclosing_device_rect(fragment_absolute_rect);
if (context.should_show_line_box_borders()) {
context.recording_painter().draw_rect(fragment_absolute_device_rect.to_type<int>(), Color::Green);
context.recording_painter().draw_line(
context.rounded_device_point(fragment_absolute_rect.top_left().translated(0, fragment.baseline())).to_type<int>(),
context.rounded_device_point(fragment_absolute_rect.top_right().translated(-1, fragment.baseline())).to_type<int>(), Color::Red);
}
if (is<Layout::TextNode>(fragment.layout_node()))
paint_text_fragment(context, static_cast<Layout::TextNode const&>(fragment.layout_node()), fragment, phase);
}
if (should_clip_overflow) {
@ -758,40 +755,38 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
Optional<HitTestResult> PaintableWithLines::hit_test(CSSPixelPoint position, HitTestType type) const
{
if (!layout_box().children_are_inline() || m_line_boxes.is_empty())
if (!layout_box().children_are_inline() || m_fragments.is_empty())
return PaintableBox::hit_test(position, type);
Optional<HitTestResult> last_good_candidate;
for (auto& line_box : m_line_boxes) {
for (auto& fragment : line_box.fragments()) {
if (is<Layout::Box>(fragment.layout_node()) && static_cast<Layout::Box const&>(fragment.layout_node()).paintable_box()->stacking_context())
continue;
if (!fragment.layout_node().containing_block()) {
dbgln("FIXME: PaintableWithLines::hit_test(): Missing containing block on {}", fragment.layout_node().debug_description());
continue;
}
auto fragment_absolute_rect = fragment.absolute_rect();
if (fragment_absolute_rect.contains(position)) {
if (is<Layout::BlockContainer>(fragment.layout_node()) && fragment.layout_node().paintable())
return fragment.layout_node().paintable()->hit_test(position, type);
return HitTestResult { const_cast<Paintable&>(const_cast<Paintable&>(*fragment.layout_node().paintable())), fragment.text_index_at(position.x()) };
}
for (auto const& fragment : fragments()) {
if (is<Layout::Box>(fragment.layout_node()) && static_cast<Layout::Box const&>(fragment.layout_node()).paintable_box()->stacking_context())
continue;
if (!fragment.layout_node().containing_block()) {
dbgln("FIXME: PaintableWithLines::hit_test(): Missing containing block on {}", fragment.layout_node().debug_description());
continue;
}
auto fragment_absolute_rect = fragment.absolute_rect();
if (fragment_absolute_rect.contains(position)) {
if (is<Layout::BlockContainer>(fragment.layout_node()) && fragment.layout_node().paintable())
return fragment.layout_node().paintable()->hit_test(position, type);
return HitTestResult { const_cast<Paintable&>(const_cast<Paintable&>(*fragment.layout_node().paintable())), fragment.text_index_at(position.x()) };
}
// If we reached this point, the position is not within the fragment. However, the fragment start or end might be the place to place the cursor.
// This determines whether the fragment is a good candidate for the position. The last such good fragment is chosen.
// The best candidate is either the end of the line above, the beginning of the line below, or the beginning or end of the current line.
// We arbitrarily choose to consider the end of the line above and ignore the beginning of the line below.
// If we knew the direction of selection, we could make a better choice.
if (fragment_absolute_rect.bottom() - 1 <= position.y()) { // fully below the fragment
last_good_candidate = HitTestResult { const_cast<Paintable&>(*fragment.layout_node().paintable()), fragment.start() + fragment.length() };
} else if (fragment_absolute_rect.top() <= position.y()) { // vertically within the fragment
if (position.x() < fragment_absolute_rect.left()) { // left of the fragment
if (!last_good_candidate.has_value()) { // first fragment of the line
last_good_candidate = HitTestResult { const_cast<Paintable&>(*fragment.layout_node().paintable()), fragment.start() };
}
} else { // right of the fragment
last_good_candidate = HitTestResult { const_cast<Paintable&>(*fragment.layout_node().paintable()), fragment.start() + fragment.length() };
// If we reached this point, the position is not within the fragment. However, the fragment start or end might be the place to place the cursor.
// This determines whether the fragment is a good candidate for the position. The last such good fragment is chosen.
// The best candidate is either the end of the line above, the beginning of the line below, or the beginning or end of the current line.
// We arbitrarily choose to consider the end of the line above and ignore the beginning of the line below.
// If we knew the direction of selection, we could make a better choice.
if (fragment_absolute_rect.bottom() - 1 <= position.y()) { // fully below the fragment
last_good_candidate = HitTestResult { const_cast<Paintable&>(*fragment.layout_node().paintable()), fragment.start() + fragment.length() };
} else if (fragment_absolute_rect.top() <= position.y()) { // vertically within the fragment
if (position.x() < fragment_absolute_rect.left()) { // left of the fragment
if (!last_good_candidate.has_value()) { // first fragment of the line
last_good_candidate = HitTestResult { const_cast<Paintable&>(*fragment.layout_node().paintable()), fragment.start() };
}
} else { // right of the fragment
last_good_candidate = HitTestResult { const_cast<Paintable&>(*fragment.layout_node().paintable()), fragment.start() + fragment.length() };
}
}
}

View file

@ -9,6 +9,7 @@
#include <LibWeb/Painting/BorderPainting.h>
#include <LibWeb/Painting/BorderRadiusCornerClipper.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableFragment.h>
#include <LibWeb/Painting/ShadowPainting.h>
namespace Web::Painting {
@ -228,17 +229,19 @@ public:
Layout::BlockContainer const& layout_box() const;
Layout::BlockContainer& layout_box();
Vector<Layout::LineBox> const& line_boxes() const { return m_line_boxes; }
void set_line_boxes(Vector<Layout::LineBox>&& line_boxes) { m_line_boxes = move(line_boxes); }
Vector<PaintableFragment> const& fragments() const { return m_fragments; }
void add_fragment(Layout::LineBoxFragment const& fragment)
{
m_fragments.append(PaintableFragment { fragment });
}
template<typename Callback>
void for_each_fragment(Callback callback) const
{
for (auto& line_box : line_boxes()) {
for (auto& fragment : line_box.fragments()) {
if (callback(fragment) == IterationDecision::Break)
return;
}
for (auto& fragment : m_fragments) {
if (callback(fragment) == IterationDecision::Break)
return;
}
}
@ -247,17 +250,24 @@ public:
virtual Optional<HitTestResult> hit_test(CSSPixelPoint, HitTestType) const override;
virtual void visit_edges(Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
for (auto& fragment : m_fragments)
visitor.visit(JS::NonnullGCPtr { fragment.layout_node() });
}
protected:
PaintableWithLines(Layout::BlockContainer const&);
private:
[[nodiscard]] virtual bool is_paintable_with_lines() const final { return true; }
Vector<Layout::LineBox> m_line_boxes;
Vector<PaintableFragment> m_fragments;
};
void paint_text_decoration(PaintContext& context, Layout::Node const& text_node, Layout::LineBoxFragment const& fragment);
void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const& text_node, Layout::LineBoxFragment const& fragment);
void paint_text_fragment(PaintContext& context, Layout::TextNode const& text_node, Layout::LineBoxFragment const& fragment, PaintPhase phase);
void paint_text_decoration(PaintContext& context, Layout::Node const& text_node, PaintableFragment const& fragment);
void paint_cursor_if_needed(PaintContext& context, Layout::TextNode const& text_node, PaintableFragment const& fragment);
void paint_text_fragment(PaintContext& context, Layout::TextNode const& text_node, PaintableFragment const& fragment, PaintPhase phase);
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/Range.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Painting/PaintableBox.h>
namespace Web::Painting {
PaintableFragment::PaintableFragment(Layout::LineBoxFragment const& fragment)
: m_layout_node(fragment.layout_node())
, m_offset(fragment.offset())
, m_size(fragment.size())
, m_baseline(fragment.baseline())
, m_start(fragment.start())
, m_length(fragment.length())
, m_glyph_run(fragment.glyph_run())
{
}
CSSPixelRect const PaintableFragment::absolute_rect() const
{
CSSPixelRect rect { {}, size() };
rect.set_location(m_layout_node->containing_block()->paintable_box()->absolute_position());
rect.translate_by(offset());
return rect;
}
int PaintableFragment::text_index_at(CSSPixels x) const
{
if (!is<Layout::TextNode>(*m_layout_node))
return 0;
auto& layout_text = verify_cast<Layout::TextNode>(layout_node());
auto& font = layout_text.first_available_font();
Utf8View view(layout_text.text_for_rendering().bytes_as_string_view().substring_view(m_start, m_length));
CSSPixels relative_x = x - absolute_rect().x();
CSSPixels glyph_spacing = font.glyph_spacing();
if (relative_x < 0)
return 0;
CSSPixels width_so_far = 0;
for (auto it = view.begin(); it != view.end(); ++it) {
auto previous_it = it;
CSSPixels glyph_width = CSSPixels::nearest_value_for(font.glyph_or_emoji_width(it));
if ((width_so_far + glyph_width + glyph_spacing / 2) > relative_x)
return m_start + view.byte_offset_of(previous_it);
width_so_far += glyph_width + glyph_spacing;
}
return m_start + m_length;
}
CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const
{
if (layout_node().selection_state() == Layout::Node::SelectionState::None)
return {};
if (layout_node().selection_state() == Layout::Node::SelectionState::Full)
return absolute_rect();
if (!is<Layout::TextNode>(layout_node()))
return {};
auto selection = layout_node().root().selection();
if (!selection)
return {};
auto range = selection->range();
if (!range)
return {};
// FIXME: m_start and m_length should be unsigned and then we won't need these casts.
auto const start_index = static_cast<unsigned>(m_start);
auto const end_index = static_cast<unsigned>(m_start) + static_cast<unsigned>(m_length);
auto& layout_text = verify_cast<Layout::TextNode>(layout_node());
auto text = layout_text.text_for_rendering().bytes_as_string_view().substring_view(m_start, m_length);
if (layout_node().selection_state() == Layout::Node::SelectionState::StartAndEnd) {
// we are in the start/end node (both the same)
if (start_index > range->end_offset())
return {};
if (end_index < range->start_offset())
return {};
if (range->start_offset() == range->end_offset())
return {};
auto selection_start_in_this_fragment = max(0, range->start_offset() - m_start);
auto selection_end_in_this_fragment = min(m_length, range->end_offset() - m_start);
auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment)));
auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1;
auto rect = absolute_rect();
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
rect.set_width(pixel_width_of_selection);
return rect;
}
if (layout_node().selection_state() == Layout::Node::SelectionState::Start) {
// we are in the start node
if (end_index < range->start_offset())
return {};
auto selection_start_in_this_fragment = max(0, range->start_offset() - m_start);
auto selection_end_in_this_fragment = m_length;
auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment)));
auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1;
auto rect = absolute_rect();
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
rect.set_width(pixel_width_of_selection);
return rect;
}
if (layout_node().selection_state() == Layout::Node::SelectionState::End) {
// we are in the end node
if (start_index > range->end_offset())
return {};
auto selection_start_in_this_fragment = 0;
auto selection_end_in_this_fragment = min(range->end_offset() - m_start, m_length);
auto pixel_distance_to_first_selected_character = CSSPixels::nearest_value_for(font.width(text.substring_view(0, selection_start_in_this_fragment)));
auto pixel_width_of_selection = CSSPixels::nearest_value_for(font.width(text.substring_view(selection_start_in_this_fragment, selection_end_in_this_fragment - selection_start_in_this_fragment))) + 1;
auto rect = absolute_rect();
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
rect.set_width(pixel_width_of_selection);
return rect;
}
return {};
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Layout/LineBoxFragment.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/BorderRadiiData.h>
#include <LibWeb/PixelUnits.h>
namespace Web::Painting {
class PaintableFragment {
public:
explicit PaintableFragment(Layout::LineBoxFragment const&);
Layout::Node const& layout_node() const { return m_layout_node; }
int start() const { return m_start; }
int length() const { return m_length; }
CSSPixels baseline() const { return m_baseline; }
CSSPixelPoint offset() const { return m_offset; }
void set_offset(CSSPixelPoint offset) { m_offset = offset; }
CSSPixelSize size() const { return m_size; }
BorderRadiiData const& border_radii_data() const { return m_border_radii_data; }
void set_border_radii_data(BorderRadiiData const& border_radii_data) { m_border_radii_data = border_radii_data; }
CSSPixelRect const absolute_rect() const;
Vector<Gfx::DrawGlyphOrEmoji> const& glyph_run() const { return m_glyph_run; }
CSSPixelRect selection_rect(Gfx::Font const&) const;
bool contained_by_inline_node() const { return m_contained_by_inline_node; }
void set_contained_by_inline_node() { m_contained_by_inline_node = true; }
CSSPixels width() const { return m_size.width(); }
CSSPixels height() const { return m_size.height(); }
int text_index_at(CSSPixels) const;
private:
JS::NonnullGCPtr<Layout::Node const> m_layout_node;
CSSPixelPoint m_offset;
CSSPixelSize m_size;
CSSPixels m_baseline;
int m_start;
int m_length;
Painting::BorderRadiiData m_border_radii_data;
Vector<Gfx::DrawGlyphOrEmoji> m_glyph_run;
bool m_contained_by_inline_node { false };
};
}

View file

@ -17,6 +17,7 @@
#include <LibWeb/Painting/BorderRadiusCornerClipper.h>
#include <LibWeb/Painting/PaintContext.h>
#include <LibWeb/Painting/PaintOuterBoxShadowParams.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/ShadowPainting.h>
namespace Web::Painting {
@ -578,9 +579,9 @@ void paint_box_shadow(PaintContext& context,
}
}
void paint_text_shadow(PaintContext& context, Layout::LineBoxFragment const& fragment, Vector<ShadowData> const& shadow_layers)
void paint_text_shadow(PaintContext& context, PaintableFragment const& fragment, Vector<ShadowData> const& shadow_layers)
{
if (shadow_layers.is_empty() || fragment.text().is_empty())
if (shadow_layers.is_empty() || fragment.glyph_run().is_empty())
return;
auto fragment_width = context.enclosing_device_pixels(fragment.width()).value();

View file

@ -10,6 +10,7 @@
#include <LibWeb/Forward.h>
#include <LibWeb/Painting/PaintContext.h>
#include <LibWeb/Painting/PaintOuterBoxShadowParams.h>
#include <LibWeb/Painting/PaintableFragment.h>
#include <LibWeb/Painting/ShadowData.h>
namespace Web::Painting {
@ -26,6 +27,6 @@ void paint_box_shadow(
BordersData const& borders_data,
BorderRadiiData const&,
Vector<ShadowData> const&);
void paint_text_shadow(PaintContext&, Layout::LineBoxFragment const&, Vector<ShadowData> const&);
void paint_text_shadow(PaintContext&, PaintableFragment const&, Vector<ShadowData> const&);
}