From 75c7dbc5d2dd045733a4c319aeab6644b5b7b36d Mon Sep 17 00:00:00 2001 From: Annya Date: Mon, 16 Sep 2024 19:38:14 +0800 Subject: [PATCH] LibWeb: Implement Range's extension method This patch implements `Range::getClientRects` and `Range::getBoundingClientRect`. Since the rects returned by invoking getClientRects can be accessed without adding them to the Selection, `ViewportPaintable::recompute_selection_states` has been updated to accept a Range as a parameter, rather than acquiring it through the Document's Selection. With this change, the following tests now pass: - wpt[css/cssom-view/range-bounding-client-rect-with-nested-text.html] - wpt[css/cssom-view/DOMRectList.html] Note: The test "css/cssom-view/range-bounding-client-rect-with-display-contents.html" still fails due to an issue with Element::getClientRects, which will be addressed in a future commit. --- ...-bounding-client-rect-with-nested-text.txt | 2 + .../expected/DOM/range-get-client-rects.txt | 2 + ...bounding-client-rect-with-nested-text.html | 47 ++++++++ .../input/DOM/range-get-client-rects.html | 19 ++++ Userland/Libraries/LibWeb/DOM/Document.cpp | 2 +- Userland/Libraries/LibWeb/DOM/Range.cpp | 103 ++++++++++++++++-- Userland/Libraries/LibWeb/DOM/Range.h | 4 +- .../Libraries/LibWeb/Painting/Paintable.h | 3 + .../LibWeb/Painting/PaintableFragment.cpp | 43 +++++--- .../LibWeb/Painting/PaintableFragment.h | 1 + .../LibWeb/Painting/ViewportPaintable.cpp | 34 ++++-- .../LibWeb/Painting/ViewportPaintable.h | 3 +- 12 files changed, 225 insertions(+), 38 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/DOM/range-bounding-client-rect-with-nested-text.txt create mode 100644 Tests/LibWeb/Text/expected/DOM/range-get-client-rects.txt create mode 100644 Tests/LibWeb/Text/input/DOM/range-bounding-client-rect-with-nested-text.html create mode 100644 Tests/LibWeb/Text/input/DOM/range-get-client-rects.html diff --git a/Tests/LibWeb/Text/expected/DOM/range-bounding-client-rect-with-nested-text.txt b/Tests/LibWeb/Text/expected/DOM/range-bounding-client-rect-with-nested-text.txt new file mode 100644 index 00000000000..eeaea842de5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/DOM/range-bounding-client-rect-with-nested-text.txt @@ -0,0 +1,2 @@ + Hello Ladybird Ladybird again World 6 +4 diff --git a/Tests/LibWeb/Text/expected/DOM/range-get-client-rects.txt b/Tests/LibWeb/Text/expected/DOM/range-get-client-rects.txt new file mode 100644 index 00000000000..7c6c2d09a74 --- /dev/null +++ b/Tests/LibWeb/Text/expected/DOM/range-get-client-rects.txt @@ -0,0 +1,2 @@ + x [object DOMRectList] +[object DOMRect] \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/DOM/range-bounding-client-rect-with-nested-text.html b/Tests/LibWeb/Text/input/DOM/range-bounding-client-rect-with-nested-text.html new file mode 100644 index 00000000000..ec3cae1eb88 --- /dev/null +++ b/Tests/LibWeb/Text/input/DOM/range-bounding-client-rect-with-nested-text.html @@ -0,0 +1,47 @@ + + + +All the rectangles for the text nodes must included in getClientRects + + +
+
+
Hello
+
+
+
Ladybird
+
+
+
Ladybird again
+
+
+
World
+
+
+ diff --git a/Tests/LibWeb/Text/input/DOM/range-get-client-rects.html b/Tests/LibWeb/Text/input/DOM/range-get-client-rects.html new file mode 100644 index 00000000000..dcfd402af0b --- /dev/null +++ b/Tests/LibWeb/Text/input/DOM/range-get-client-rects.html @@ -0,0 +1,19 @@ + + +
x
+ + + + diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index ff1d94488b3..ac32dbb467a 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -1194,7 +1194,7 @@ void Document::update_layout() page().client().page_did_layout(); } - paintable()->recompute_selection_states(); + paintable()->update_selection(); m_needs_layout = false; diff --git a/Userland/Libraries/LibWeb/DOM/Range.cpp b/Userland/Libraries/LibWeb/DOM/Range.cpp index 0a4ab926f1d..19c9e741800 100644 --- a/Userland/Libraries/LibWeb/DOM/Range.cpp +++ b/Userland/Libraries/LibWeb/DOM/Range.cpp @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include namespace Web::DOM { @@ -97,7 +97,8 @@ void Range::set_associated_selection(Badge, JS::GCPtrdocument().paintable()) { - viewport->recompute_selection_states(); + viewport->recompute_selection_states(*this); + viewport->update_selection(); viewport->set_needs_display(); } @@ -1165,17 +1166,103 @@ WebIDL::ExceptionOr Range::delete_contents() } // https://drafts.csswg.org/cssom-view/#dom-element-getclientrects -JS::NonnullGCPtr Range::get_client_rects() const +// https://drafts.csswg.org/cssom-view/#extensions-to-the-range-interface +JS::NonnullGCPtr Range::get_client_rects() { - dbgln("(STUBBED) Range::get_client_rects()"); - return Geometry::DOMRectList::create(realm(), {}); + // 1. return an empty DOMRectList object if the range is not in the document + if (!start_container()->document().navigable()) + return Geometry::DOMRectList::create(realm(), {}); + + start_container()->document().update_layout(); + update_associated_selection(); + Vector> rects; + // FIXME: take Range collapsed into consideration + // 2. Iterate the node included in Range + auto start_node = start_container(); + auto end_node = end_container(); + if (!is(start_node)) { + start_node = start_node->child_at_index(m_start_offset); + } + if (!is(end_node)) { + // end offset shouldn't be 0 + if (m_end_offset == 0) + return Geometry::DOMRectList::create(realm(), {}); + end_node = end_node->child_at_index(m_end_offset - 1); + } + for (Node const* node = start_node; node && node != end_node->next_in_pre_order(); node = node->next_in_pre_order()) { + auto node_type = static_cast(node->node_type()); + if (node_type == NodeType::ELEMENT_NODE) { + // 1. For each element selected by the range, whose parent is not selected by the range, include the border + // areas returned by invoking getClientRects() on the element. + if (contains_node(*node) && !contains_node(*node->parent())) { + auto const& element = static_cast(*node); + JS::NonnullGCPtr const element_rects = element.get_client_rects(); + for (u32 i = 0; i < element_rects->length(); i++) { + auto rect = element_rects->item(i); + rects.append(Geometry::DOMRect::create(realm(), + Gfx::FloatRect(rect->x(), rect->y(), rect->width(), rect->height()))); + } + } + } else if (node_type == NodeType::TEXT_NODE) { + // 2. For each Text node selected or partially selected by the range (including when the boundary-points + // are identical), include scaled DOMRect object (for the part that is selected, not the whole line box). + auto const& text = static_cast(*node); + auto const* paintable = text.paintable(); + if (paintable) { + auto const* containing_block = paintable->containing_block(); + if (is(*containing_block)) { + auto const& paintable_lines = static_cast(*containing_block); + auto fragments = paintable_lines.fragments(); + auto const& font = paintable->layout_node().first_available_font(); + for (auto frag = fragments.begin(); frag != fragments.end(); frag++) { + auto rect = frag->range_rect(font, *this); + if (rect.is_empty()) + continue; + rects.append(Geometry::DOMRect::create(realm(), + Gfx::FloatRect(rect))); + } + } else { + dbgln("FIXME: Failed to get client rects for node {}", node->debug_description()); + } + } + } + } + return Geometry::DOMRectList::create(realm(), move(rects)); } // https://w3c.github.io/csswg-drafts/cssom-view/#dom-range-getboundingclientrect -JS::NonnullGCPtr Range::get_bounding_client_rect() const +JS::NonnullGCPtr Range::get_bounding_client_rect() { - dbgln("(STUBBED) Range::get_bounding_client_rect()"); - return Geometry::DOMRect::construct_impl(realm(), 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors(); + // 1. Let list be the result of invoking getClientRects() on element. + auto list = get_client_rects(); + + // 2. If the list is empty return a DOMRect object whose x, y, width and height members are zero. + if (list->length() == 0) + return Geometry::DOMRect::construct_impl(realm(), 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors(); + + // 3. If all rectangles in list have zero width or height, return the first rectangle in list. + auto all_rectangle_has_zero_width_or_height = true; + for (auto i = 0u; i < list->length(); ++i) { + auto const& rect = list->item(i); + if (rect->width() != 0 && rect->height() != 0) { + all_rectangle_has_zero_width_or_height = false; + break; + } + } + if (all_rectangle_has_zero_width_or_height) + return JS::NonnullGCPtr { *const_cast(list->item(0)) }; + + // 4. Otherwise, return a DOMRect object describing the smallest rectangle that includes all of the rectangles in + // list of which the height or width is not zero. + auto const* first_rect = list->item(0); + auto bounding_rect = Gfx::Rect { first_rect->x(), first_rect->y(), first_rect->width(), first_rect->height() }; + for (auto i = 1u; i < list->length(); ++i) { + auto const& rect = list->item(i); + if (rect->width() == 0 || rect->height() == 0) + continue; + bounding_rect = bounding_rect.united({ rect->x(), rect->y(), rect->width(), rect->height() }); + } + return Geometry::DOMRect::create(realm(), bounding_rect.to_type()); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-range-createcontextualfragment diff --git a/Userland/Libraries/LibWeb/DOM/Range.h b/Userland/Libraries/LibWeb/DOM/Range.h index 748318ac543..8c6031b08d8 100644 --- a/Userland/Libraries/LibWeb/DOM/Range.h +++ b/Userland/Libraries/LibWeb/DOM/Range.h @@ -88,8 +88,8 @@ public: static HashTable& live_ranges(); - JS::NonnullGCPtr get_client_rects() const; - JS::NonnullGCPtr get_bounding_client_rect() const; + JS::NonnullGCPtr get_client_rects(); + JS::NonnullGCPtr get_bounding_client_rect(); bool contains_node(Node const&) const; diff --git a/Userland/Libraries/LibWeb/Painting/Paintable.h b/Userland/Libraries/LibWeb/Painting/Paintable.h index b7bd15df8e8..134a1231fe2 100644 --- a/Userland/Libraries/LibWeb/Painting/Paintable.h +++ b/Userland/Libraries/LibWeb/Painting/Paintable.h @@ -61,6 +61,7 @@ public: [[nodiscard]] bool is_absolutely_positioned() const { return m_absolutely_positioned; } [[nodiscard]] bool is_floating() const { return m_floating; } [[nodiscard]] bool is_inline() const { return m_inline; } + [[nodiscard]] bool is_selected() const { return m_selected; } [[nodiscard]] CSS::Display display() const { return layout_node().display(); } template @@ -240,6 +241,7 @@ public: SelectionState selection_state() const { return m_selection_state; } void set_selection_state(SelectionState state) { m_selection_state = state; } + void set_selected(bool selected) { m_selected = selected; } Gfx::AffineTransform compute_combined_css_transform() const; @@ -266,6 +268,7 @@ private: bool m_absolutely_positioned : 1 { false }; bool m_floating : 1 { false }; bool m_inline : 1 { false }; + bool m_selected : 1 { false }; }; inline DOM::Node* HitTestResult::dom_node() diff --git a/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp b/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp index 27587bd2968..1c65066376a 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp +++ b/Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp @@ -56,8 +56,7 @@ int PaintableFragment::text_index_at(CSSPixels x) const return m_start + m_length; } - -CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const +CSSPixelRect PaintableFragment::range_rect(Gfx::Font const& font, DOM::Range const& range) const { if (paintable().selection_state() == Paintable::SelectionState::None) return {}; @@ -65,13 +64,6 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const if (paintable().selection_state() == Paintable::SelectionState::Full) return absolute_rect(); - auto selection = paintable().document().get_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(m_start); auto const end_index = static_cast(m_start) + static_cast(m_length); @@ -80,16 +72,16 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const if (paintable().selection_state() == Paintable::SelectionState::StartAndEnd) { // we are in the start/end node (both the same) - if (start_index > range->end_offset()) + if (start_index > range.end_offset()) return {}; - if (end_index < range->start_offset()) + if (end_index < range.start_offset()) return {}; - if (range->start_offset() == range->end_offset()) + 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 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; @@ -101,10 +93,10 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const } if (paintable().selection_state() == Paintable::SelectionState::Start) { // we are in the start node - if (end_index < range->start_offset()) + if (end_index < range.start_offset()) return {}; - auto selection_start_in_this_fragment = max(0, range->start_offset() - m_start); + 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; @@ -117,11 +109,11 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const } if (paintable().selection_state() == Paintable::SelectionState::End) { // we are in the end node - if (start_index > range->end_offset()) + 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 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; @@ -134,6 +126,21 @@ CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const return {}; } +CSSPixelRect PaintableFragment::selection_rect(Gfx::Font const& font) const +{ + if (!paintable().is_selected()) + return {}; + + auto selection = paintable().document().get_selection(); + if (!selection) + return {}; + auto range = selection->range(); + if (!range) + return {}; + + return range_rect(font, *range); +} + StringView PaintableFragment::string_view() const { if (!is(paintable())) diff --git a/Userland/Libraries/LibWeb/Painting/PaintableFragment.h b/Userland/Libraries/LibWeb/Painting/PaintableFragment.h index 22504b158e5..d340fefcd44 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableFragment.h +++ b/Userland/Libraries/LibWeb/Painting/PaintableFragment.h @@ -46,6 +46,7 @@ public: RefPtr glyph_run() const { return m_glyph_run; } CSSPixelRect selection_rect(Gfx::Font const&) const; + CSSPixelRect range_rect(Gfx::Font const&, DOM::Range const&) const; CSSPixels width() const { return m_size.width(); } CSSPixels height() const { return m_size.height(); } diff --git a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp index c4149b00439..64c525d7539 100644 --- a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp @@ -284,11 +284,11 @@ JS::GCPtr ViewportPaintable::selection() const return const_cast(document()).get_selection(); } -void ViewportPaintable::recompute_selection_states() +void ViewportPaintable::update_selection() { - // 1. Start by resetting the selection state of all layout nodes to None. + // 1. Start by setting all layout nodes to unselected. for_each_in_inclusive_subtree([&](auto& layout_node) { - layout_node.set_selection_state(SelectionState::None); + layout_node.set_selected(false); return TraversalDecision::Continue; }); @@ -303,10 +303,28 @@ void ViewportPaintable::recompute_selection_states() auto* start_container = range->start_container(); auto* end_container = range->end_container(); - // 3. If the selection starts and ends in the same node: + // 3. Mark the nodes included in range selected. + for (auto* node = start_container; node && node != end_container->next_in_pre_order(); node = node->next_in_pre_order()) { + if (auto* paintable = node->paintable()) + paintable->set_selected(true); + } +} + +void ViewportPaintable::recompute_selection_states(DOM::Range& range) +{ + // 1. Start by resetting the selection state of all layout nodes to None. + for_each_in_inclusive_subtree([&](auto& layout_node) { + layout_node.set_selection_state(SelectionState::None); + return TraversalDecision::Continue; + }); + + auto* start_container = range.start_container(); + auto* end_container = range.end_container(); + + // 2. If the selection starts and ends in the same node: if (start_container == end_container) { // 1. If the selection starts and ends at the same offset, return. - if (range->start_offset() == range->end_offset()) { + if (range.start_offset() == range.end_offset()) { // NOTE: A zero-length selection should not be visible. return; } @@ -319,7 +337,7 @@ void ViewportPaintable::recompute_selection_states() } } - // 4. Mark the selection start node as Start (if text) or Full (if anything else). + // 3. Mark the selection start node as Start (if text) or Full (if anything else). if (auto* paintable = start_container->paintable()) { if (is(*start_container)) paintable->set_selection_state(SelectionState::Start); @@ -327,7 +345,7 @@ void ViewportPaintable::recompute_selection_states() paintable->set_selection_state(SelectionState::Full); } - // 5. Mark the selection end node as End (if text) or Full (if anything else). + // 4. Mark the selection end node as End (if text) or Full (if anything else). if (auto* paintable = end_container->paintable()) { if (is(*end_container)) paintable->set_selection_state(SelectionState::End); @@ -335,7 +353,7 @@ void ViewportPaintable::recompute_selection_states() paintable->set_selection_state(SelectionState::Full); } - // 6. Mark the nodes between start node and end node (in tree order) as Full. + // 5. Mark the nodes between start node and end node (in tree order) as Full. for (auto* node = start_container->next_in_pre_order(); node && node != end_container; node = node->next_in_pre_order()) { if (auto* paintable = node->paintable()) paintable->set_selection_state(SelectionState::Full); diff --git a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h index 70444b3bd78..6354ebbe469 100644 --- a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.h @@ -32,7 +32,8 @@ public: void resolve_paint_only_properties(); JS::GCPtr selection() const; - void recompute_selection_states(); + void recompute_selection_states(DOM::Range&); + void update_selection(); bool handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y) override;