mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-24 09:52:31 +00:00
When clicking on a glyph or starting a selection on it, we would use the glyph's offset/index as the position which represents the left side of the glyph, or the position between the glyph and the glyph before it. Instead of looking at which glyph is under the mouse pointer, look at which glyph boundary is closer. Now, if you click to the right of a glyph (but still on that glyph), it correctly selects the next glyph's offset as the position.
217 lines
8.2 KiB
C++
217 lines
8.2 KiB
C++
/*
|
|
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/DOM/Range.h>
|
|
#include <LibWeb/HTML/FormAssociatedElement.h>
|
|
#include <LibWeb/HTML/HTMLInputElement.h>
|
|
#include <LibWeb/HTML/HTMLTextAreaElement.h>
|
|
#include <LibWeb/Layout/Viewport.h>
|
|
#include <LibWeb/Painting/PaintableBox.h>
|
|
#include <LibWeb/Painting/TextPaintable.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())
|
|
, m_writing_mode(fragment.writing_mode())
|
|
{
|
|
}
|
|
|
|
CSSPixelRect const PaintableFragment::absolute_rect() const
|
|
{
|
|
CSSPixelRect rect { {}, size() };
|
|
auto const* containing_block = paintable().containing_block();
|
|
if (containing_block)
|
|
rect.set_location(containing_block->absolute_position());
|
|
rect.translate_by(offset());
|
|
return rect;
|
|
}
|
|
|
|
size_t PaintableFragment::text_index_at(CSSPixelPoint position) const
|
|
{
|
|
if (!is<TextPaintable>(paintable()))
|
|
return 0;
|
|
|
|
auto relative_inline_offset = [&] {
|
|
switch (orientation()) {
|
|
case Orientation::Horizontal:
|
|
return (position.x() - absolute_rect().x()).to_float();
|
|
case Orientation::Vertical:
|
|
return (position.y() - absolute_rect().y()).to_float();
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
}();
|
|
if (relative_inline_offset < 0)
|
|
return 0;
|
|
|
|
auto const& glyphs = m_glyph_run->glyphs();
|
|
auto smallest_distance = AK::NumericLimits<float>::max();
|
|
for (size_t i = 0; i < glyphs.size(); ++i) {
|
|
auto distance_to_position = AK::abs(glyphs[i].position.x() - relative_inline_offset);
|
|
|
|
// The last distance was smaller than this new distance, so we've found the closest glyph.
|
|
if (distance_to_position > smallest_distance)
|
|
return m_start + i - 1;
|
|
smallest_distance = distance_to_position;
|
|
}
|
|
|
|
return m_start + m_length - 1;
|
|
}
|
|
|
|
CSSPixelRect PaintableFragment::range_rect(size_t start_offset, size_t end_offset) const
|
|
{
|
|
if (paintable().selection_state() == Paintable::SelectionState::None)
|
|
return {};
|
|
|
|
if (paintable().selection_state() == Paintable::SelectionState::Full)
|
|
return absolute_rect();
|
|
|
|
auto const start_index = m_start;
|
|
auto const end_index = m_start + m_length;
|
|
|
|
auto const& font = glyph_run() ? glyph_run()->font() : layout_node().first_available_font();
|
|
auto text = string_view();
|
|
|
|
if (paintable().selection_state() == Paintable::SelectionState::StartAndEnd) {
|
|
// we are in the start/end node (both the same)
|
|
if (start_index > end_offset)
|
|
return {};
|
|
if (end_index < start_offset)
|
|
return {};
|
|
|
|
if (start_offset == end_offset)
|
|
return {};
|
|
|
|
auto selection_start_in_this_fragment = max(0, start_offset - m_start);
|
|
auto selection_end_in_this_fragment = min(m_length, 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();
|
|
switch (orientation()) {
|
|
case Gfx::Orientation::Horizontal:
|
|
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
|
|
rect.set_width(pixel_width_of_selection);
|
|
break;
|
|
case Gfx::Orientation::Vertical:
|
|
rect.set_y(rect.y() + pixel_distance_to_first_selected_character);
|
|
rect.set_height(pixel_width_of_selection);
|
|
break;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
if (paintable().selection_state() == Paintable::SelectionState::Start) {
|
|
// we are in the start node
|
|
if (end_index < start_offset)
|
|
return {};
|
|
|
|
auto selection_start_in_this_fragment = max(0, 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();
|
|
switch (orientation()) {
|
|
case Gfx::Orientation::Horizontal:
|
|
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
|
|
rect.set_width(pixel_width_of_selection);
|
|
break;
|
|
case Gfx::Orientation::Vertical:
|
|
rect.set_y(rect.y() + pixel_distance_to_first_selected_character);
|
|
rect.set_height(pixel_width_of_selection);
|
|
break;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
if (paintable().selection_state() == Paintable::SelectionState::End) {
|
|
// we are in the end node
|
|
if (start_index > end_offset)
|
|
return {};
|
|
|
|
auto selection_start_in_this_fragment = 0;
|
|
auto selection_end_in_this_fragment = min<int>(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();
|
|
switch (orientation()) {
|
|
case Gfx::Orientation::Horizontal:
|
|
rect.set_x(rect.x() + pixel_distance_to_first_selected_character);
|
|
rect.set_width(pixel_width_of_selection);
|
|
break;
|
|
case Gfx::Orientation::Vertical:
|
|
rect.set_y(rect.y() + pixel_distance_to_first_selected_character);
|
|
rect.set_height(pixel_width_of_selection);
|
|
break;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
return rect;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Gfx::Orientation PaintableFragment::orientation() const
|
|
{
|
|
switch (m_writing_mode) {
|
|
case CSS::WritingMode::HorizontalTb:
|
|
return Gfx::Orientation::Horizontal;
|
|
case CSS::WritingMode::VerticalRl:
|
|
case CSS::WritingMode::VerticalLr:
|
|
case CSS::WritingMode::SidewaysRl:
|
|
case CSS::WritingMode::SidewaysLr:
|
|
return Gfx::Orientation::Vertical;
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
CSSPixelRect PaintableFragment::selection_rect() const
|
|
{
|
|
if (auto const* focused_element = paintable().document().focused_element(); focused_element && is<HTML::FormAssociatedTextControlElement>(*focused_element)) {
|
|
HTML::FormAssociatedTextControlElement const* text_control_element = nullptr;
|
|
if (is<HTML::HTMLInputElement>(*focused_element)) {
|
|
text_control_element = static_cast<HTML::HTMLInputElement const*>(focused_element);
|
|
} else if (is<HTML::HTMLTextAreaElement>(*focused_element)) {
|
|
text_control_element = static_cast<HTML::HTMLTextAreaElement const*>(focused_element);
|
|
} else {
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
auto selection_start = text_control_element->selection_start();
|
|
auto selection_end = text_control_element->selection_end();
|
|
return range_rect(selection_start, selection_end);
|
|
}
|
|
auto selection = paintable().document().get_selection();
|
|
if (!selection)
|
|
return {};
|
|
auto range = selection->range();
|
|
if (!range)
|
|
return {};
|
|
|
|
return range_rect(range->start_offset(), range->end_offset());
|
|
}
|
|
|
|
StringView PaintableFragment::string_view() const
|
|
{
|
|
if (!is<TextPaintable>(paintable()))
|
|
return {};
|
|
return static_cast<TextPaintable const&>(paintable()).text_for_rendering().bytes_as_string_view().substring_view(m_start, m_length);
|
|
}
|
|
|
|
}
|