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
+
+
+
+
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;