Skip to content

Commit

Permalink
LibWeb: Implement user-select
Browse files Browse the repository at this point in the history
This implements all values of user-select.
  • Loading branch information
Psychpsyo committed Jan 8, 2025
1 parent 89296b8 commit 124ae9e
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 29 deletions.
6 changes: 6 additions & 0 deletions Libraries/LibWeb/CSS/ComputedProperties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,12 @@ Optional<CSS::WritingMode> ComputedProperties::writing_mode() const
return keyword_to_writing_mode(value.to_keyword());
}

Optional<CSS::UserSelect> ComputedProperties::user_select() const
{
auto const& value = property(CSS::PropertyID::UserSelect);
return keyword_to_user_select(value.to_keyword());
}

Optional<CSS::MaskType> ComputedProperties::mask_type() const
{
auto const& value = property(CSS::PropertyID::MaskType);
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/CSS/ComputedProperties.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ class ComputedProperties final : public JS::Cell {
Optional<CSS::Direction> direction() const;
Optional<CSS::UnicodeBidi> unicode_bidi() const;
Optional<CSS::WritingMode> writing_mode() const;
Optional<CSS::UserSelect> user_select() const;

static Vector<CSS::Transformation> transformations_for_style_value(CSSStyleValue const& value);
Vector<CSS::Transformation> transformations() const;
Expand Down
5 changes: 5 additions & 0 deletions Libraries/LibWeb/CSS/ComputedValues.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class InitialValues {
static CSS::Direction direction() { return CSS::Direction::Ltr; }
static CSS::UnicodeBidi unicode_bidi() { return CSS::UnicodeBidi::Normal; }
static CSS::WritingMode writing_mode() { return CSS::WritingMode::HorizontalTb; }
static CSS::UserSelect user_select() { return CSS::UserSelect::Auto; }

// https://www.w3.org/TR/SVG/geometry.html
static LengthPercentage cx() { return CSS::Length::make_px(0); }
Expand Down Expand Up @@ -421,6 +422,7 @@ class ComputedValues {
CSS::Direction direction() const { return m_inherited.direction; }
CSS::UnicodeBidi unicode_bidi() const { return m_noninherited.unicode_bidi; }
CSS::WritingMode writing_mode() const { return m_inherited.writing_mode; }
CSS::UserSelect user_select() const { return m_noninherited.user_select; }

CSS::LengthBox const& inset() const { return m_noninherited.inset; }
const CSS::LengthBox& margin() const { return m_noninherited.margin; }
Expand Down Expand Up @@ -671,6 +673,8 @@ class ComputedValues {
CSS::ObjectFit object_fit { InitialValues::object_fit() };
CSS::ObjectPosition object_position { InitialValues::object_position() };
CSS::UnicodeBidi unicode_bidi { InitialValues::unicode_bidi() };
CSS::UserSelect user_select { InitialValues::user_select() };

Optional<CSS::Transformation> rotate;
Optional<CSS::Transformation> translate;
Optional<CSS::Transformation> scale;
Expand Down Expand Up @@ -841,6 +845,7 @@ class MutableComputedValues final : public ComputedValues {
void set_direction(CSS::Direction value) { m_inherited.direction = value; }
void set_unicode_bidi(CSS::UnicodeBidi value) { m_noninherited.unicode_bidi = value; }
void set_writing_mode(CSS::WritingMode value) { m_inherited.writing_mode = value; }
void set_user_select(CSS::UserSelect value) { m_noninherited.user_select = value; }

void set_fill(SVGPaint value) { m_inherited.fill = move(value); }
void set_stroke(SVGPaint value) { m_inherited.stroke = move(value); }
Expand Down
7 changes: 7 additions & 0 deletions Libraries/LibWeb/CSS/Enums.json
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,13 @@
"normal",
"plaintext"
],
"user-select": [
"all",
"auto",
"contain",
"none",
"text"
],
"vertical-align": [
"baseline",
"bottom",
Expand Down
8 changes: 2 additions & 6 deletions Libraries/LibWeb/CSS/Properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -2819,12 +2819,8 @@
"animation-type": "discrete",
"inherited": false,
"initial": "auto",
"valid-identifiers": [
"all",
"auto",
"contain",
"none",
"text"
"valid-types": [
"user-select"
]
},
"vertical-align": {
Expand Down
53 changes: 53 additions & 0 deletions Libraries/LibWeb/Layout/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <LibWeb/CSS/StyleValues/URLStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/FormattingContext.h>
Expand Down Expand Up @@ -996,6 +997,9 @@ void NodeWithStyle::apply_style(const CSS::ComputedProperties& computed_style)
if (auto writing_mode = computed_style.writing_mode(); writing_mode.has_value())
computed_values.set_writing_mode(writing_mode.value());

if (auto user_select = computed_style.user_select(); user_select.has_value())
computed_values.set_user_select(user_select.value());

propagate_style_to_anonymous_wrappers();
}

Expand Down Expand Up @@ -1206,4 +1210,53 @@ DOM::Document const& Node::document() const
return m_dom_node->document();
}

// https://drafts.csswg.org/css-ui/#propdef-user-select
CSS::UserSelect Node::user_select_used_value() const
{
// The used value is the same as the computed value, except:
auto computed_value = computed_values().user_select();

// 1. on editable elements where the used value is always 'contain' regardless of the computed value

// 2. when the computed value is 'auto', in which case the used value is one of the other values as defined below

// For the purpose of this specification, an editable element is either an editing host or a mutable form control with
// textual content, such as textarea.
auto* form_control = dynamic_cast<HTML::FormAssociatedTextControlElement const*>(dom_node());
// FIXME: Check if this needs to exclude input elements with types such as color or range, and if so, which ones exactly.
if ((dom_node() && dom_node()->is_editing_host()) || (form_control && form_control->is_mutable())) {
return CSS::UserSelect::Contain;
} else if (computed_value == CSS::UserSelect::Auto) {
// The used value of 'auto' is determined as follows:
// - On the '::before' and '::after' pseudo-elements, the used value is 'none'
if (is_generated_for_before_pseudo_element() || is_generated_for_after_pseudo_element()) {
return CSS::UserSelect::None;
}

// - If the element is an editable element, the used value is 'contain'
// NOTE: We already handled this above.

auto parent_element = parent();
if (parent_element) {
auto parent_used_value = parent_element->user_select_used_value();

// - Otherwise, if the used value of user-select on the parent of this element is 'all', the used value is 'all'
if (parent_used_value == CSS::UserSelect::All) {
return CSS::UserSelect::All;
}

// - Otherwise, if the used value of user-select on the parent of this element is 'none', the used value is
// 'none'
if (parent_used_value == CSS::UserSelect::None) {
return CSS::UserSelect::None;
}
}

// - Otherwise, the used value is 'text'
return CSS::UserSelect::Text;
}

return computed_value;
}

}
3 changes: 3 additions & 0 deletions Libraries/LibWeb/Layout/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ class Node
return false;
}

// https://drafts.csswg.org/css-ui/#propdef-user-select
CSS::UserSelect user_select_used_value() const;

protected:
Node(DOM::Document&, DOM::Node*);

Expand Down
175 changes: 156 additions & 19 deletions Libraries/LibWeb/Page/EventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,137 @@ static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Painting
return offset;
}

// https://drafts.csswg.org/css-ui/#propdef-user-select
static void set_user_selection(GC::Ptr<DOM::Node> anchor_node, unsigned anchor_offset, GC::Ptr<DOM::Node> focus_node, unsigned focus_offset, Selection::Selection* selection, CSS::UserSelect user_select)
{
// https://drafts.csswg.org/css-ui/#valdef-user-select-contain
// NOTE: This is clamping the focus node to any node with user-select: contain that stands between it and the anchor node.
if (focus_node != anchor_node) {
// UAs must not allow a selection which is started in this element to be extended outside of this element.
auto potential_contain_node = anchor_node;

// NOTE: The way we do this is searching up the tree from the anchor, to find 'this element', i.e. its nearest contain ancestor.
// We stop the search early when we reach an element that contains both the anchor and the focus node, as this means they
// are inside the same contain element, or not in a contain element at all.
// This takes care of the "selection trying to escape from a contain" case.
while (
(!potential_contain_node->is_element() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && !potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
potential_contain_node = potential_contain_node->parent();
}

if (
potential_contain_node->layout_node()->user_select_used_value() == CSS::UserSelect::Contain && !potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
if (focus_node->is_before(*potential_contain_node)) {
focus_offset = 0;
} else {
focus_offset = potential_contain_node->length();
}
focus_node = potential_contain_node;
// NOTE: Prevents this from being handled again further down
user_select = CSS::UserSelect::Contain;
} else {
// A selection started outside of this element must not end in this element. If the user attempts to create such a
// selection, the UA must instead end the selection range at the element boundary.

// NOTE: This branch takes care of the "selection trying to intrude into a contain" case.
// This is done by searching up the tree from the focus node, to see if there is a
// contain element between it and the common ancestor that also includes the anchor.
// We stop once reaching target_node, which is the common ancestor identified in step 1.
// If target_node wasn't a common ancestor, we would not be here.
auto target_node = potential_contain_node;
potential_contain_node = focus_node;
while (
(!potential_contain_node->is_element() || potential_contain_node->layout_node()->user_select_used_value() != CSS::UserSelect::Contain) && potential_contain_node->parent() && potential_contain_node != target_node) {
potential_contain_node = potential_contain_node->parent();
}
if (
potential_contain_node->layout_node()->user_select_used_value() == CSS::UserSelect::Contain && !potential_contain_node->is_inclusive_ancestor_of(*anchor_node)) {
if (potential_contain_node->is_before(*anchor_node)) {
focus_node = potential_contain_node->next_in_pre_order();
while (potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
focus_node = focus_node->next_in_pre_order();
}
focus_offset = 0;
} else {
focus_node = potential_contain_node->previous_in_pre_order();
while (potential_contain_node->is_inclusive_ancestor_of(*focus_node)) {
focus_node = focus_node->previous_in_pre_order();
}
focus_offset = focus_node->length();
}
// NOTE: Prevents this from being handled again further down
user_select = CSS::UserSelect::Contain;
}
}
}

switch (user_select) {
case CSS::UserSelect::None:
// https://drafts.csswg.org/css-ui/#valdef-user-select-none

// The UA must not allow selections to be started in this element.
if (anchor_node == focus_node) {
return;
}

// A selection started outside of this element must not end in this element. If the user attempts to create such a
// selection, the UA must instead end the selection range at the element boundary.
if (focus_node->is_before(*anchor_node)) {
do {
focus_node = focus_node->next_in_pre_order();
} while (focus_node->layout_node()->user_select_used_value() == CSS::UserSelect::None);
focus_offset = 0;
} else {
do {
focus_node = focus_node->previous_in_pre_order();
} while (focus_node->layout_node()->user_select_used_value() == CSS::UserSelect::None);
focus_offset = focus_node->length();
}
break;
case CSS::UserSelect::All:
// https://drafts.csswg.org/css-ui/#valdef-user-select-all

// The content of the element must be selected atomically: If a selection would contain part of the element,
// then the selection must contain the entire element including all its descendants. If the element is selected
// and the used value of 'user-select' on its parent is 'all', then the parent must be included in the selection,
// recursively.
while (focus_node->parent() && focus_node->parent()->layout_node()->user_select_used_value() == CSS::UserSelect::All) {
if (anchor_node == focus_node) {
anchor_node = focus_node->parent();
}
focus_node = focus_node->parent();
}

if (focus_node == anchor_node) {
if (anchor_offset > focus_offset) {
anchor_offset = focus_node->length();
focus_offset = 0;
} else {
anchor_offset = 0;
focus_offset = focus_node->length();
}
} else if (focus_node->is_before(*anchor_node)) {
focus_offset = 0;
} else {
focus_offset = focus_node->length();
}
break;
case CSS::UserSelect::Contain:
// NOTE: This is handled at the start of this function
break;
case CSS::UserSelect::Text:
// https://drafts.csswg.org/css-ui/#valdef-user-select-text

// The element imposes no constraint on the selection.
break;
case CSS::UserSelect::Auto:
VERIFY_NOT_REACHED();
break;
}

(void)selection->set_base_and_extent(*anchor_node, anchor_offset, *focus_node, focus_offset);
}

EventHandler::EventHandler(Badge<HTML::Navigable>, HTML::Navigable& navigable)
: m_navigable(navigable)
, m_drag_and_drop_event_handler(make<DragAndDropEventHandler>())
Expand Down Expand Up @@ -502,23 +633,29 @@ EventResult EventHandler::handle_mousedown(CSSPixelPoint viewport_position, CSSP
else if (auto* focused_element = document->focused_element())
HTML::run_unfocusing_steps(focused_element);

auto target = document->active_input_events_target();
if (target) {
m_in_mouse_selection = true;
m_mouse_selection_target = target;
if (modifiers & UIEvents::KeyModifier::Mod_Shift) {
target->set_selection_focus(*dom_node, result->index_in_node);
} else {
target->set_selection_anchor(*dom_node, result->index_in_node);
}
} else if (!focus_candidate) {
m_in_mouse_selection = true;
if (auto selection = document->get_selection()) {
auto anchor_node = selection->anchor_node();
if (anchor_node && modifiers & UIEvents::KeyModifier::Mod_Shift) {
(void)selection->set_base_and_extent(*anchor_node, selection->anchor_offset(), *dom_node, result->index_in_node);
// https://drafts.csswg.org/css-ui/#valdef-user-select-none
// Attempting to start a selection in an element where user-select is none, such as by clicking in it or starting
// a drag in it, must not cause a pre-existing selection to become unselected or to be affected in any way.
auto user_select = paintable->layout_node().user_select_used_value();
if (user_select != CSS::UserSelect::None) {
auto target = document->active_input_events_target();
if (target) {
m_in_mouse_selection = true;
m_mouse_selection_target = target;
if (modifiers & UIEvents::KeyModifier::Mod_Shift) {
target->set_selection_focus(*dom_node, result->index_in_node);
} else {
(void)selection->set_base_and_extent(*dom_node, result->index_in_node, *dom_node, result->index_in_node);
target->set_selection_anchor(*dom_node, result->index_in_node);
}
} else if (!focus_candidate) {
m_in_mouse_selection = true;
if (auto selection = document->get_selection()) {
auto anchor_node = selection->anchor_node();
if (anchor_node && modifiers & UIEvents::KeyModifier::Mod_Shift) {
set_user_selection(*anchor_node, selection->anchor_offset(), *dom_node, result->index_in_node, selection, user_select);
} else {
set_user_selection(*dom_node, result->index_in_node, *dom_node, result->index_in_node, selection, user_select);
}
}
}
}
Expand Down Expand Up @@ -635,9 +772,9 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
auto anchor_node = selection->anchor_node();
if (anchor_node) {
if (&anchor_node->root() == &hit->dom_node()->root())
(void)selection->set_base_and_extent(*anchor_node, selection->anchor_offset(), *hit->paintable->dom_node(), hit->index_in_node);
set_user_selection(*anchor_node, selection->anchor_offset(), *hit->paintable->dom_node(), hit->index_in_node, selection, hit->paintable->layout_node().user_select_used_value());
} else {
(void)selection->set_base_and_extent(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node);
set_user_selection(*hit->paintable->dom_node(), hit->index_in_node, *hit->paintable->dom_node(), hit->index_in_node, selection, hit->paintable->layout_node().user_select_used_value());
}
}

Expand Down Expand Up @@ -751,7 +888,7 @@ EventResult EventHandler::handle_doubleclick(CSSPixelPoint viewport_position, CS
target->set_selection_anchor(hit_dom_node, previous_boundary);
target->set_selection_focus(hit_dom_node, next_boundary);
} else if (auto selection = node->document().get_selection()) {
(void)selection->set_base_and_extent(hit_dom_node, previous_boundary, hit_dom_node, next_boundary);
set_user_selection(hit_dom_node, previous_boundary, hit_dom_node, next_boundary, selection, hit_paintable.layout_node().user_select_used_value());
}
}
}
Expand Down
Loading

0 comments on commit 124ae9e

Please sign in to comment.