2020-01-18 09:38:21 +01:00
|
|
|
/*
|
2024-10-04 13:19:50 +02:00
|
|
|
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
2025-09-12 10:06:27 +02:00
|
|
|
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
|
2020-01-18 09:38:21 +01:00
|
|
|
*
|
2021-04-22 01:24:48 -07:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2020-01-18 09:38:21 +01:00
|
|
|
*/
|
|
|
|
|
2021-08-28 02:08:15 +00:00
|
|
|
#include <AK/CharacterTypes.h>
|
2020-02-15 00:27:50 +01:00
|
|
|
#include <AK/Utf8View.h>
|
LibWeb: Separate text control input events handling from contenteditable
This input event handling change is intended to address the following
design issues:
- Having `DOM::Position` is unnecessary complexity when `Selection`
exists because caret position could be described by the selection
object with a collapsed state. Before this change, we had to
synchronize those whenever one of them was modified, and there were
already bugs caused by that, i.e., caret position was not changed when
selection offset was modified from the JS side.
- Selection API exposes selection offset within `<textarea>` and
`<input>`, which is not supposed to happen. These objects should
manage their selection state by themselves and have selection offset
even when they are not displayed.
- `EventHandler` looks only at `DOM::Text` owned by `DOM::Position`
while doing text manipulations. It works fine for `<input>` and
`<textarea>`, but `contenteditable` needs to consider all text
descendant text nodes; i.e., if the cursor is moved outside of
`DOM::Text`, we need to look for an adjacent text node to move the
cursor there.
With this change, `EventHandler` no longer does direct manipulations on
caret position or text content, but instead delegates them to the active
`InputEventsTarget`, which could be either
`FormAssociatedTextControlElement` (for `<input>` and `<textarea>`) or
`EditingHostManager` (for `contenteditable`). The `Selection` object is
used to manage both selection and caret position for `contenteditable`,
and text control elements manage their own selection state that is not
exposed by Selection API.
This change improves text editing on Discord, as now we don't have to
refocus the `contenteditable` element after character input. The problem
was that selection manipulations from the JS side were not propagated
to `DOM::Position`.
I expect this change to make future correctness improvements for
`contenteditable` (and `designMode`) easier, as now it's decoupled from
`<input>` and `<textarea>` and separated from `EventHandler`, which is
quite a busy file.
2024-10-23 21:26:58 +02:00
|
|
|
#include <LibWeb/DOM/Position.h>
|
2020-11-22 15:53:01 +01:00
|
|
|
#include <LibWeb/Layout/Box.h>
|
2020-03-07 10:32:51 +01:00
|
|
|
#include <LibWeb/Layout/LineBox.h>
|
2020-11-22 15:53:01 +01:00
|
|
|
#include <LibWeb/Layout/Node.h>
|
|
|
|
#include <LibWeb/Layout/TextNode.h>
|
2019-10-03 15:20:13 +02:00
|
|
|
|
2020-11-22 15:53:01 +01:00
|
|
|
namespace Web::Layout {
|
2020-03-07 10:27:02 +01:00
|
|
|
|
2024-10-29 11:32:59 +00:00
|
|
|
CSSPixels LineBox::width() const
|
|
|
|
{
|
|
|
|
if (m_writing_mode != CSS::WritingMode::HorizontalTb)
|
|
|
|
return m_block_length;
|
|
|
|
return m_inline_length;
|
|
|
|
}
|
|
|
|
|
|
|
|
CSSPixels LineBox::height() const
|
|
|
|
{
|
|
|
|
if (m_writing_mode != CSS::WritingMode::HorizontalTb)
|
|
|
|
return m_inline_length;
|
|
|
|
return m_block_length;
|
|
|
|
}
|
|
|
|
|
|
|
|
CSSPixels LineBox::bottom() const
|
|
|
|
{
|
|
|
|
if (m_writing_mode != CSS::WritingMode::HorizontalTb)
|
|
|
|
return m_inline_length;
|
|
|
|
return m_bottom;
|
|
|
|
}
|
|
|
|
|
2025-09-12 10:06:27 +02:00
|
|
|
void LineBox::add_fragment(Node const& layout_node, size_t start, size_t length, CSSPixels leading_size,
|
|
|
|
CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width,
|
|
|
|
CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom, RefPtr<Gfx::GlyphRun> glyph_run)
|
2019-10-03 15:20:13 +02:00
|
|
|
{
|
2021-01-06 11:07:02 +01:00
|
|
|
bool text_align_is_justify = layout_node.computed_values().text_align() == CSS::TextAlign::Justify;
|
2025-09-12 10:06:27 +02:00
|
|
|
if (glyph_run && !text_align_is_justify && !m_fragments.is_empty()
|
|
|
|
&& &m_fragments.last().layout_node() == &layout_node
|
|
|
|
&& &m_fragments.last().m_glyph_run->font() == &glyph_run->font()
|
|
|
|
&& m_fragments.last().start() + m_fragments.last().length_in_code_units() == start) {
|
2020-11-22 15:53:01 +01:00
|
|
|
// The fragment we're adding is from the last Layout::Node on the line.
|
|
|
|
// Expand the last fragment instead of adding a new one with the same Layout::Node.
|
2025-09-12 10:06:27 +02:00
|
|
|
m_fragments.last().m_length_in_code_units += length;
|
2024-08-18 17:58:05 +01:00
|
|
|
m_fragments.last().append_glyph_run(glyph_run, content_width);
|
2019-10-03 15:20:13 +02:00
|
|
|
} else {
|
2024-10-29 11:32:59 +00:00
|
|
|
CSSPixels inline_offset = leading_margin + leading_size + m_inline_length;
|
|
|
|
CSSPixels block_offset = 0;
|
2025-09-12 10:06:27 +02:00
|
|
|
m_fragments.append(LineBoxFragment { layout_node, start, length, inline_offset, block_offset, content_width,
|
|
|
|
content_height, border_box_top, m_direction, m_writing_mode, move(glyph_run) });
|
2019-10-03 15:20:13 +02:00
|
|
|
}
|
2024-10-29 11:32:59 +00:00
|
|
|
m_inline_length += leading_margin + leading_size + content_width + trailing_size + trailing_margin;
|
|
|
|
m_block_length = max(m_block_length, content_height + border_box_top + border_box_bottom);
|
2019-10-03 15:20:13 +02:00
|
|
|
}
|
2019-10-20 17:18:28 +02:00
|
|
|
|
2025-03-26 16:11:50 +00:00
|
|
|
CSSPixels LineBox::calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace should_remove)
|
2019-10-20 17:18:28 +02:00
|
|
|
{
|
2023-07-16 14:10:47 +02:00
|
|
|
auto should_trim = [](LineBoxFragment* fragment) {
|
2025-05-22 00:31:24 +12:00
|
|
|
auto white_space_collapse = fragment->layout_node().computed_values().white_space_collapse();
|
|
|
|
|
|
|
|
return white_space_collapse == CSS::WhiteSpaceCollapse::Collapse || white_space_collapse == CSS::WhiteSpaceCollapse::PreserveBreaks;
|
2023-07-16 14:10:47 +02:00
|
|
|
};
|
2019-10-20 17:18:28 +02:00
|
|
|
|
2025-03-26 16:11:50 +00:00
|
|
|
CSSPixels whitespace_width = 0;
|
2023-07-16 14:10:47 +02:00
|
|
|
LineBoxFragment* last_fragment = nullptr;
|
2025-03-26 16:11:50 +00:00
|
|
|
size_t fragment_index = m_fragments.size();
|
2023-07-16 14:10:47 +02:00
|
|
|
for (;;) {
|
2025-03-26 16:11:50 +00:00
|
|
|
if (fragment_index == 0)
|
|
|
|
return whitespace_width;
|
|
|
|
|
|
|
|
last_fragment = &m_fragments[--fragment_index];
|
2025-09-12 10:06:27 +02:00
|
|
|
if (auto const* dom_node = last_fragment->layout_node().dom_node()) {
|
LibWeb: Separate text control input events handling from contenteditable
This input event handling change is intended to address the following
design issues:
- Having `DOM::Position` is unnecessary complexity when `Selection`
exists because caret position could be described by the selection
object with a collapsed state. Before this change, we had to
synchronize those whenever one of them was modified, and there were
already bugs caused by that, i.e., caret position was not changed when
selection offset was modified from the JS side.
- Selection API exposes selection offset within `<textarea>` and
`<input>`, which is not supposed to happen. These objects should
manage their selection state by themselves and have selection offset
even when they are not displayed.
- `EventHandler` looks only at `DOM::Text` owned by `DOM::Position`
while doing text manipulations. It works fine for `<input>` and
`<textarea>`, but `contenteditable` needs to consider all text
descendant text nodes; i.e., if the cursor is moved outside of
`DOM::Text`, we need to look for an adjacent text node to move the
cursor there.
With this change, `EventHandler` no longer does direct manipulations on
caret position or text content, but instead delegates them to the active
`InputEventsTarget`, which could be either
`FormAssociatedTextControlElement` (for `<input>` and `<textarea>`) or
`EditingHostManager` (for `contenteditable`). The `Selection` object is
used to manage both selection and caret position for `contenteditable`,
and text control elements manage their own selection state that is not
exposed by Selection API.
This change improves text editing on Discord, as now we don't have to
refocus the `contenteditable` element after character input. The problem
was that selection manipulations from the JS side were not propagated
to `DOM::Position`.
I expect this change to make future correctness improvements for
`contenteditable` (and `designMode`) easier, as now it's decoupled from
`<input>` and `<textarea>` and separated from `EventHandler`, which is
quite a busy file.
2024-10-23 21:26:58 +02:00
|
|
|
auto cursor_position = dom_node->document().cursor_position();
|
|
|
|
if (cursor_position && cursor_position->node() == dom_node)
|
2025-03-26 16:11:50 +00:00
|
|
|
return whitespace_width;
|
LibWeb: Separate text control input events handling from contenteditable
This input event handling change is intended to address the following
design issues:
- Having `DOM::Position` is unnecessary complexity when `Selection`
exists because caret position could be described by the selection
object with a collapsed state. Before this change, we had to
synchronize those whenever one of them was modified, and there were
already bugs caused by that, i.e., caret position was not changed when
selection offset was modified from the JS side.
- Selection API exposes selection offset within `<textarea>` and
`<input>`, which is not supposed to happen. These objects should
manage their selection state by themselves and have selection offset
even when they are not displayed.
- `EventHandler` looks only at `DOM::Text` owned by `DOM::Position`
while doing text manipulations. It works fine for `<input>` and
`<textarea>`, but `contenteditable` needs to consider all text
descendant text nodes; i.e., if the cursor is moved outside of
`DOM::Text`, we need to look for an adjacent text node to move the
cursor there.
With this change, `EventHandler` no longer does direct manipulations on
caret position or text content, but instead delegates them to the active
`InputEventsTarget`, which could be either
`FormAssociatedTextControlElement` (for `<input>` and `<textarea>`) or
`EditingHostManager` (for `contenteditable`). The `Selection` object is
used to manage both selection and caret position for `contenteditable`,
and text control elements manage their own selection state that is not
exposed by Selection API.
This change improves text editing on Discord, as now we don't have to
refocus the `contenteditable` element after character input. The problem
was that selection manipulations from the JS side were not propagated
to `DOM::Position`.
I expect this change to make future correctness improvements for
`contenteditable` (and `designMode`) easier, as now it's decoupled from
`<input>` and `<textarea>` and separated from `EventHandler`, which is
quite a busy file.
2024-10-23 21:26:58 +02:00
|
|
|
}
|
2023-07-16 14:10:47 +02:00
|
|
|
if (!should_trim(last_fragment))
|
2025-03-26 16:11:50 +00:00
|
|
|
return whitespace_width;
|
|
|
|
if (!last_fragment->is_justifiable_whitespace())
|
2023-07-16 14:10:47 +02:00
|
|
|
break;
|
2025-03-26 16:11:50 +00:00
|
|
|
|
|
|
|
whitespace_width += last_fragment->inline_length();
|
|
|
|
if (should_remove == RemoveTrailingWhitespace::Yes) {
|
|
|
|
m_inline_length -= last_fragment->inline_length();
|
|
|
|
m_fragments.remove(fragment_index);
|
2023-07-16 14:10:47 +02:00
|
|
|
}
|
|
|
|
}
|
2019-10-20 17:18:28 +02:00
|
|
|
|
2023-07-16 14:10:47 +02:00
|
|
|
auto last_text = last_fragment->text();
|
2019-10-20 17:18:28 +02:00
|
|
|
if (last_text.is_null())
|
2025-03-26 16:11:50 +00:00
|
|
|
return whitespace_width;
|
2019-10-20 17:18:28 +02:00
|
|
|
|
2025-09-12 10:06:27 +02:00
|
|
|
size_t last_text_length = last_text.length_in_code_units();
|
|
|
|
while (last_text_length) {
|
|
|
|
auto last_character = last_text.code_unit_at(--last_text_length);
|
2021-08-28 02:08:15 +00:00
|
|
|
if (!is_ascii_space(last_character))
|
|
|
|
break;
|
|
|
|
|
2024-12-05 17:57:30 +00:00
|
|
|
auto const& font = last_fragment->glyph_run() ? last_fragment->glyph_run()->font() : last_fragment->layout_node().first_available_font();
|
|
|
|
int last_character_width = font.glyph_width(last_character);
|
2025-03-26 16:11:50 +00:00
|
|
|
whitespace_width += last_character_width;
|
|
|
|
if (should_remove == RemoveTrailingWhitespace::Yes) {
|
2025-09-12 10:06:27 +02:00
|
|
|
--last_fragment->m_length_in_code_units;
|
2025-03-26 16:11:50 +00:00
|
|
|
last_fragment->set_inline_length(last_fragment->inline_length() - last_character_width);
|
|
|
|
m_inline_length -= last_character_width;
|
|
|
|
}
|
2019-10-20 17:18:28 +02:00
|
|
|
}
|
2025-03-26 16:11:50 +00:00
|
|
|
|
|
|
|
return whitespace_width;
|
|
|
|
}
|
|
|
|
|
|
|
|
CSSPixels LineBox::get_trailing_whitespace_width() const
|
|
|
|
{
|
|
|
|
return const_cast<LineBox&>(*this).calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace::No);
|
|
|
|
}
|
|
|
|
|
|
|
|
void LineBox::trim_trailing_whitespace()
|
|
|
|
{
|
|
|
|
calculate_or_trim_trailing_whitespace(RemoveTrailingWhitespace::Yes);
|
2019-10-20 17:18:28 +02:00
|
|
|
}
|
2020-03-07 10:27:02 +01:00
|
|
|
|
2020-12-17 20:17:29 +01:00
|
|
|
bool LineBox::is_empty_or_ends_in_whitespace() const
|
2020-06-13 14:59:17 +02:00
|
|
|
{
|
|
|
|
if (m_fragments.is_empty())
|
2020-12-17 20:17:29 +01:00
|
|
|
return true;
|
2021-10-26 16:15:53 +02:00
|
|
|
|
2020-06-13 14:59:17 +02:00
|
|
|
return m_fragments.last().ends_in_whitespace();
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:27:02 +01:00
|
|
|
}
|