Skip to content

Commit

Permalink
LibWeb: Implement Range's extension method
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
An-n-ya authored and kalenikaliaksandr committed Sep 20, 2024
1 parent a3472ae commit 75c7dbc
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hello Ladybird Ladybird again World 6
4
2 changes: 2 additions & 0 deletions Tests/LibWeb/Text/expected/DOM/range-get-client-rects.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
x [object DOMRectList]
[object DOMRect]
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--refer to https://wpt.live/css/cssom-view/range-bounding-client-rect-with-nested-text.html -->
<!DOCTYPE html>
<meta charset="utf-8">
<title>All the rectangles for the text nodes must included in getClientRects</title>
<script src="../include.js"></script>
<style>
.nullDims {
width: 0;
height: 0;
}

.nullDims > div {
position: absolute;
left: 10px;
}
</style>
<div id="container">
<div class="nullDims">
<div id="first" style="top: 10px">Hello</div>
</div>
<div class="nullDims">
<div id="second" style="top: 40px">Ladybird</div>
</div>
<div class="nullDims">
<div id="third" style="top: 70px">Ladybird again</div>
</div>
<div class="nullDims">
<div id="last" style="top: 100px">World</div>
</div>
</div>
<script>
test(function () {
const first = document.getElementById("first");
const last = document.getElementById("last");
const range = document.createRange();
range.setStart(first.firstChild, 0);
range.setEnd(last.firstChild, 3);

const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
let rects = Array.from(range.getClientRects());
println(rects.length);
rects = rects.filter(({ width, height }) => width > 0 && height > 0);
println(rects.length);
})
</script>
19 changes: 19 additions & 0 deletions Tests/LibWeb/Text/input/DOM/range-get-client-rects.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!--refer to https://wpt.live/css/cssom-view/DOMRectList.html -->
<body>
<div id="x">x</div>
<script src="../include.js"></script>
<script>
function print_class_string(object) {
println({}.toString.call(object));
}
test(() => {
const x = document.getElementById("x");
const range = document.createRange();
range.selectNodeContents(x);
const domRectList = range.getClientRects();
print_class_string(domRectList);
print_class_string(domRectList.item(0));
});
</script>

</body>
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ void Document::update_layout()
page().client().page_did_layout();
}

paintable()->recompute_selection_states();
paintable()->update_selection();

m_needs_layout = false;

Expand Down
103 changes: 95 additions & 8 deletions Userland/Libraries/LibWeb/DOM/Range.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/InlinePaintable.h>
#include <LibWeb/Painting/ViewportPaintable.h>

namespace Web::DOM {
Expand Down Expand Up @@ -97,7 +97,8 @@ void Range::set_associated_selection(Badge<Selection::Selection>, JS::GCPtr<Sele
void Range::update_associated_selection()
{
if (auto* viewport = m_start_container->document().paintable()) {
viewport->recompute_selection_states();
viewport->recompute_selection_states(*this);
viewport->update_selection();
viewport->set_needs_display();
}

Expand Down Expand Up @@ -1165,17 +1166,103 @@ WebIDL::ExceptionOr<void> Range::delete_contents()
}

// https://drafts.csswg.org/cssom-view/#dom-element-getclientrects
JS::NonnullGCPtr<Geometry::DOMRectList> Range::get_client_rects() const
// https://drafts.csswg.org/cssom-view/#extensions-to-the-range-interface
JS::NonnullGCPtr<Geometry::DOMRectList> 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<JS::Handle<Geometry::DOMRect>> 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<DOM::Text>(start_node)) {
start_node = start_node->child_at_index(m_start_offset);
}
if (!is<DOM::Text>(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<NodeType>(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<DOM::Element const&>(*node);
JS::NonnullGCPtr<Geometry::DOMRectList> 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<DOM::Text const&>(*node);
auto const* paintable = text.paintable();
if (paintable) {
auto const* containing_block = paintable->containing_block();
if (is<Painting::PaintableWithLines>(*containing_block)) {
auto const& paintable_lines = static_cast<Painting::PaintableWithLines const&>(*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<Geometry::DOMRect> Range::get_bounding_client_rect() const
JS::NonnullGCPtr<Geometry::DOMRect> 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<Geometry::DOMRect*>(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<float>());
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-range-createcontextualfragment
Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/DOM/Range.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ class Range final : public AbstractRange {

static HashTable<Range*>& live_ranges();

JS::NonnullGCPtr<Geometry::DOMRectList> get_client_rects() const;
JS::NonnullGCPtr<Geometry::DOMRect> get_bounding_client_rect() const;
JS::NonnullGCPtr<Geometry::DOMRectList> get_client_rects();
JS::NonnullGCPtr<Geometry::DOMRect> get_bounding_client_rect();

bool contains_node(Node const&) const;

Expand Down
3 changes: 3 additions & 0 deletions Userland/Libraries/LibWeb/Painting/Paintable.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Paintable
[[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<typename U, typename Callback>
Expand Down Expand Up @@ -240,6 +241,7 @@ class Paintable

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;

Expand All @@ -266,6 +268,7 @@ class Paintable
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()
Expand Down
43 changes: 25 additions & 18 deletions Userland/Libraries/LibWeb/Painting/PaintableFragment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,14 @@ 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 {};

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<unsigned>(m_start);
auto const end_index = static_cast<unsigned>(m_start) + static_cast<unsigned>(m_length);
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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<TextPaintable>(paintable()))
Expand Down
1 change: 1 addition & 0 deletions Userland/Libraries/LibWeb/Painting/PaintableFragment.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class PaintableFragment {
RefPtr<Gfx::GlyphRun> 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(); }
Expand Down
Loading

0 comments on commit 75c7dbc

Please sign in to comment.