Files
servo/components/script/dom/textcontrol.rs
Martin Robinson 1e4feeaa0d script: Add context-based context menu options (#40501)
Add context menu options for images, links, and editable text areas. In
addition add the ability to show menu options that are disabled. This
also improves the visual style of the context menu in egui as part of
supporting disabled options.

Testing: This has been manually tested, but we could we should be able
to
easily add unit tests when enriching the API with information about the
active element under context menus.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
2025-11-11 11:03:41 +00:00

325 lines
9.8 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! This is an abstraction used by `HTMLInputElement` and `HTMLTextAreaElement` to implement the
//! text control selection DOM API.
//!
//! <https://html.spec.whatwg.org/multipage/#textFieldSelection>
use crate::clipboard_provider::EmbedderClipboardProvider;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode;
use crate::dom::bindings::conversions::DerivedFrom;
use crate::dom::bindings::error::{Error, ErrorResult};
use crate::dom::bindings::str::DOMString;
use crate::dom::event::{EventBubbles, EventCancelable};
use crate::dom::eventtarget::EventTarget;
use crate::dom::node::{Node, NodeDamage, NodeTraits};
use crate::textinput::{SelectionDirection, SelectionState, TextInput, UTF8Bytes};
pub(crate) trait TextControlElement: DerivedFrom<EventTarget> + DerivedFrom<Node> {
fn selection_api_applies(&self) -> bool;
fn has_selectable_text(&self) -> bool;
fn has_selection(&self) -> bool;
fn set_dirty_value_flag(&self, value: bool);
fn select_all(&self);
}
pub(crate) struct TextControlSelection<'a, E: TextControlElement> {
element: &'a E,
textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
}
impl<'a, E: TextControlElement> TextControlSelection<'a, E> {
pub(crate) fn new(
element: &'a E,
textinput: &'a DomRefCell<TextInput<EmbedderClipboardProvider>>,
) -> Self {
TextControlSelection { element, textinput }
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-select>
pub(crate) fn dom_select(&self) {
// Step 1: If this element is an input element, and either select() does not apply
// to this element or the corresponding control has no selectable text, return.
if !self.element.has_selectable_text() {
return;
}
// Step 2
self.set_range(Some(0), Some(u32::MAX), None, None);
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
pub(crate) fn dom_start(&self) -> Option<u32> {
// Step 1
if !self.element.selection_api_applies() {
return None;
}
// Steps 2-3
Some(self.start())
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
pub(crate) fn set_dom_start(&self, start: Option<u32>) -> ErrorResult {
// Step 1
if !self.element.selection_api_applies() {
return Err(Error::InvalidState(None));
}
// Step 2
let mut end = self.end();
// Step 3
if let Some(s) = start {
if end < s {
end = s;
}
}
// Step 4
self.set_range(start, Some(end), Some(self.direction()), None);
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
pub(crate) fn dom_end(&self) -> Option<u32> {
// Step 1
if !self.element.selection_api_applies() {
return None;
}
// Steps 2-3
Some(self.end())
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
pub(crate) fn set_dom_end(&self, end: Option<u32>) -> ErrorResult {
// Step 1
if !self.element.selection_api_applies() {
return Err(Error::InvalidState(None));
}
// Step 2
self.set_range(Some(self.start()), end, Some(self.direction()), None);
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
pub(crate) fn dom_direction(&self) -> Option<DOMString> {
// Step 1
if !self.element.selection_api_applies() {
return None;
}
Some(DOMString::from(self.direction()))
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
pub(crate) fn set_dom_direction(&self, direction: Option<DOMString>) -> ErrorResult {
// Step 1
if !self.element.selection_api_applies() {
return Err(Error::InvalidState(None));
}
// Step 2
self.set_range(
Some(self.start()),
Some(self.end()),
direction.map(SelectionDirection::from),
None,
);
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange
pub(crate) fn set_dom_range(
&self,
start: u32,
end: u32,
direction: Option<DOMString>,
) -> ErrorResult {
// Step 1
if !self.element.selection_api_applies() {
return Err(Error::InvalidState(None));
}
// Step 2
self.set_range(
Some(start),
Some(end),
direction.map(SelectionDirection::from),
None,
);
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext
pub(crate) fn set_dom_range_text(
&self,
replacement: DOMString,
start: Option<u32>,
end: Option<u32>,
selection_mode: SelectionMode,
) -> ErrorResult {
// Step 1
if !self.element.selection_api_applies() {
return Err(Error::InvalidState(None));
}
// Step 2
self.element.set_dirty_value_flag(true);
// Step 3
let mut start = start.unwrap_or_else(|| self.start());
let mut end = end.unwrap_or_else(|| self.end());
// Step 4
if start > end {
return Err(Error::IndexSize(None));
}
// Save the original selection state to later pass to set_selection_range, because we will
// change the selection state in order to replace the text in the range.
let original_selection_state = self.textinput.borrow().selection_state();
let UTF8Bytes(content_length) = self.textinput.borrow().len_utf8();
let content_length = content_length as u32;
// Step 5
if start > content_length {
start = content_length;
}
// Step 6
if end > content_length {
end = content_length;
}
// Step 7
let mut selection_start = self.start();
// Step 8
let mut selection_end = self.end();
// Step 11
// Must come before the textinput.replace_selection() call, as replacement gets moved in
// that call.
let new_length = replacement.len() as u32;
{
let mut textinput = self.textinput.borrow_mut();
// Steps 9-10
textinput.set_selection_range(start, end, SelectionDirection::None);
textinput.replace_selection(replacement);
}
// Step 12
let new_end = start + new_length;
// Step 13
match selection_mode {
SelectionMode::Select => {
selection_start = start;
selection_end = new_end;
},
SelectionMode::Start => {
selection_start = start;
selection_end = start;
},
SelectionMode::End => {
selection_start = new_end;
selection_end = new_end;
},
SelectionMode::Preserve => {
// Sub-step 1
let old_length = end - start;
// Sub-step 2
let delta = (new_length as isize) - (old_length as isize);
// Sub-step 3
if selection_start > end {
selection_start = ((selection_start as isize) + delta) as u32;
} else if selection_start > start {
selection_start = start;
}
// Sub-step 4
if selection_end > end {
selection_end = ((selection_end as isize) + delta) as u32;
} else if selection_end > start {
selection_end = new_end;
}
},
}
// Step 14
self.set_range(
Some(selection_start),
Some(selection_end),
None,
Some(original_selection_state),
);
Ok(())
}
fn start(&self) -> u32 {
let UTF8Bytes(offset) = self.textinput.borrow().selection_start_offset();
offset as u32
}
fn end(&self) -> u32 {
let UTF8Bytes(offset) = self.textinput.borrow().selection_end_offset();
offset as u32
}
fn direction(&self) -> SelectionDirection {
self.textinput.borrow().selection_direction()
}
/// <https://html.spec.whatwg.org/multipage/#set-the-selection-range>
fn set_range(
&self,
start: Option<u32>,
end: Option<u32>,
direction: Option<SelectionDirection>,
original_selection_state: Option<SelectionState>,
) {
let mut textinput = self.textinput.borrow_mut();
let original_selection_state =
original_selection_state.unwrap_or_else(|| textinput.selection_state());
// Step 1
let start = start.unwrap_or(0);
// Step 2
let end = end.unwrap_or(0);
// Steps 3-5
textinput.set_selection_range(start, end, direction.unwrap_or(SelectionDirection::None));
// Step 6
if textinput.selection_state() != original_selection_state {
self.element
.owner_global()
.task_manager()
.user_interaction_task_source()
.queue_event(
self.element.upcast::<EventTarget>(),
atom!("select"),
EventBubbles::Bubbles,
EventCancelable::NotCancelable,
);
}
self.element.upcast::<Node>().dirty(NodeDamage::Other);
}
}