feat: add text context menu (overlay and wayland popup)

This commit is contained in:
Hojjat 2026-05-19 19:36:37 -06:00 • committed by Michael Murphy
parent 2cd43a8b89
commit c6cf3db29d
6 changed files with 731 additions and 7 deletions

View file

@ -237,7 +237,7 @@ pub(super) struct MenuSlice {
#[derive(Debug, Clone)]
/// Menu bounds in overlay space
pub struct MenuBounds {
child_positions: Vec<f32>,
pub(crate) child_positions: Vec<f32>,
child_sizes: Vec<Size>,
children_bounds: Rectangle,
pub parent_bounds: Rectangle,

View file

@ -194,7 +194,7 @@ pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
) -> Button<'a, Message>
where
Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
Message: std::clone::Clone + 'a,
Message: std::clone::Clone + 'static,
{
widget::button::custom(widget::text(label))
.padding([4, 12])

View file

@ -89,8 +89,9 @@ pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider};
#[doc(inline)]
pub use iced::widget::{Svg, svg};
pub mod text_editor;
#[doc(inline)]
pub use iced::widget::{TextEditor, text_editor};
pub use text_editor::TextEditor;
#[doc(inline)]
pub use iced_core::widget::{Id, Operation, Widget};
@ -295,6 +296,10 @@ pub mod text;
#[doc(inline)]
pub use text::{Text, text};
pub mod text_context_menu;
#[doc(inline)]
pub use text_context_menu::HasSelectableText;
pub mod text_input;
#[doc(inline)]
pub use text_input::{

View file

@ -14,12 +14,12 @@ use taffy::AlignContent;
/// A settings item aligned in a row
#[must_use]
#[allow(clippy::module_name_repetitions)]
pub fn item<'a, Message: 'static>(
pub fn item<'a, Message: Clone + 'static>(
title: impl Into<Cow<'a, str>> + 'a,
widget: impl Into<Element<'a, Message>> + 'a,
) -> Row<'a, Message, Theme> {
#[inline(never)]
fn inner<'a, Message: 'static>(
fn inner<'a, Message: Clone + 'static>(
title: Cow<'a, str>,
widget: Element<'a, Message>,
) -> Row<'a, Message, Theme> {
@ -45,12 +45,12 @@ pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message, Theme>
/// A settings item aligned in a flex row
#[allow(clippy::module_name_repetitions)]
pub fn flex_item<'a, Message: 'static>(
pub fn flex_item<'a, Message: Clone + 'static>(
title: impl Into<Cow<'a, str>> + 'a,
widget: impl Into<Element<'a, Message>> + 'a,
) -> FlexRow<'a, Message> {
#[inline(never)]
fn inner<'a, Message: 'static>(
fn inner<'a, Message: Clone + 'static>(
title: Cow<'a, str>,
widget: Element<'a, Message>,
) -> FlexRow<'a, Message> {

View file

@ -0,0 +1,713 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Right-click context menu for widgets with selectable text.
//!
//! Use [`context_menu_overlay`] from your widget's `overlay()` method
//! with any widget that implements
//! [`HasSelectableText`](iced_core::widget::text::HasSelectableText)
//! to get a context menu with Copy, Select All, and optionally Cut/Paste.
//!
//! Internally uses libcosmic's [`Menu`](crate::widget::menu) system for
//! proper rendering, hover effects, and positioning.
//!
//! On Wayland, [`create_text_context_popup`] can be used instead to show
//! the context menu as a native popup surface.
pub use iced_core::widget::text::HasSelectableText;
use crate::widget::RcElementWrapper;
use crate::widget::menu::{
self, CloseCondition, ItemHeight, ItemWidth, Menu, MenuBarState, PathHighlight, menu_roots_diff,
};
use crate::{theme, widget};
use iced_core::layout::Limits;
use iced_core::widget::Tree;
use iced_core::{
Clipboard, Layout, Point, Rectangle, Shell, Size, Vector, clipboard, mouse, overlay, renderer,
};
use iced_widget::core::event;
use std::borrow::Cow;
use std::mem;
use std::sync::{Arc, Mutex};
/// Shared state for communicating deferred context menu actions
/// from a Wayland popup back to the owning text widget.
pub(crate) type PendingAction = Arc<Mutex<Option<TextCtxAction>>>;
/// Creates a new [`PendingAction`] for use with popup-based context menus.
pub(crate) fn pending_action() -> PendingAction {
Arc::new(Mutex::new(None))
}
/// Takes a pending action if one was set by a popup menu, and returns it.
pub(crate) fn take_pending_action(pending: &PendingAction) -> Option<TextCtxAction> {
pending.lock().ok().and_then(|mut guard| guard.take())
}
use std::cell::Cell;
use std::collections::HashMap;
use std::sync::LazyLock;
thread_local! {
static CURRENT_WINDOW_ID: Cell<iced_core::window::Id> = const { Cell::new(iced_core::window::Id::NONE) };
}
/// Data needed to construct a popup view. Stored in a global registry
/// so `Cosmic::view()` can build the `Element` with the correct message type.
#[cfg(feature = "wayland")]
struct PopupViewData {
menu: Menu<'static, TextCtxAction>,
selected_text: Option<String>,
pending_action: PendingAction,
}
#[cfg(feature = "wayland")]
static POPUP_VIEW_REGISTRY: LazyLock<Mutex<HashMap<iced_core::window::Id, PopupViewData>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn set_current_window_id(id: iced_core::window::Id) {
CURRENT_WINDOW_ID.set(id);
}
pub(crate) fn current_window_id() -> iced_core::window::Id {
CURRENT_WINDOW_ID.get()
}
/// Returns the popup view `Element` for a given popup ID, if one exists.
/// Called by `Cosmic::view()` where `Message` is known.
#[cfg(feature = "wayland")]
pub(crate) fn popup_view_for_id<Message: Clone + 'static>(
id: iced_core::window::Id,
) -> Option<crate::Element<'static, crate::Action<Message>>> {
let registry = POPUP_VIEW_REGISTRY.lock().ok()?;
let data = registry.get(&id)?;
let popup_widget: TextContextMenuPopup<Message> = TextContextMenuPopup {
menu: data.menu.clone(),
selected_text: data.selected_text.clone(),
pending_action: data.pending_action.clone(),
_phantom: std::marker::PhantomData,
};
drop(registry);
Some(
crate::Element::from(
crate::widget::container(popup_widget).center(iced_core::Length::Fill),
)
.map(crate::action::app),
)
}
/// Sends a wayland popup action directly to the SCTK event loop,
/// bypassing the iced Task system.
#[cfg(feature = "wayland")]
fn send_popup_direct(settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings) {
iced_winit::send_wayland_action_direct(
iced_runtime::platform_specific::wayland::Action::Popup(
iced_runtime::platform_specific::wayland::popup::Action::Popup { popup: settings },
),
);
}
/// Sends a destroy popup action directly to the SCTK event loop.
#[cfg(feature = "wayland")]
fn send_destroy_popup_direct(id: iced_core::window::Id) {
iced_winit::send_wayland_action_direct(
iced_runtime::platform_specific::wayland::Action::Popup(
iced_runtime::platform_specific::wayland::popup::Action::Destroy { id },
),
);
}
/// Creates a context menu overlay for any widget implementing
/// [`HasSelectableText`].
///
/// Call this from your widget's `overlay()` method. Pass `on_input` for
/// editable widgets so Cut and Paste can publish text-change messages.
///
/// The `menu_bar_state` parameter must be a persistent [`MenuBarState`]
/// stored in the widget's tree state.
pub(crate) fn context_menu_overlay<'a, W, Message>(
widget: &'a W,
tree: &'a mut Tree,
on_input: Option<&'a dyn Fn(String) -> Message>,
translation: Vector,
menu_bar_state: MenuBarState,
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
where
W: HasSelectableText + 'a,
Message: Clone + 'static,
{
let click_position = widget.context_menu_position(tree)?;
let selected_text = widget.selected_text(tree);
let is_editable = widget.is_editable();
let mut menu_roots = build_menu_roots(is_editable, selected_text.is_some());
menu_roots.iter_mut().for_each(menu::Tree::set_index);
let item_count = menu_roots[0].children.len();
menu_bar_state.inner.with_data_mut(|state| {
let stale = state
.menu_states
.first()
.is_some_and(|ms| ms.menu_bounds.child_positions.len() != item_count);
if !state.open || stale {
state.menu_states.clear();
state.active_root.clear();
state.open = true;
}
menu_roots_diff(&mut menu_roots, &mut state.tree);
});
let offscreen = Rectangle::new(Point::new(-10000.0, -10000.0), Size::ZERO);
let menu = Menu {
tree: menu_bar_state.clone(),
menu_roots: Cow::Owned(menu_roots),
bounds_expand: 16,
menu_overlays_parent: true,
close_condition: CloseCondition {
leave: false,
click_outside: true,
click_inside: true,
},
item_width: ItemWidth::Uniform(240),
item_height: ItemHeight::Dynamic(40),
bar_bounds: offscreen,
main_offset: -240,
cross_offset: 0,
root_bounds_list: vec![offscreen],
path_highlight: Some(PathHighlight::MenuActive),
style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default),
position: Point::new(translation.x, translation.y),
is_overlay: true,
window_id: iced::window::Id::NONE,
depth: 0,
on_surface_action: None,
};
Some(overlay::Element::new(Box::new(TextMenuOverlay {
menu,
widget,
tree,
on_input,
click_position,
})))
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextCtxAction {
Copy,
Cut,
Paste,
SelectAll,
}
fn build_menu_roots(is_editable: bool, has_selection: bool) -> Vec<menu::Tree<TextCtxAction>> {
let mut items = Vec::with_capacity(4);
if is_editable && has_selection {
items.push(menu::Tree::from(crate::Element::from(
menu::menu_button(vec![widget::text("Cut").into()]).on_press(TextCtxAction::Cut),
)));
}
if has_selection {
items.push(menu::Tree::from(crate::Element::from(
menu::menu_button(vec![widget::text("Copy").into()]).on_press(TextCtxAction::Copy),
)));
}
if is_editable {
items.push(menu::Tree::from(crate::Element::from(
menu::menu_button(vec![widget::text("Paste").into()]).on_press(TextCtxAction::Paste),
)));
}
items.push(menu::Tree::from(crate::Element::from(
menu::menu_button(vec![widget::text("Select All").into()])
.on_press(TextCtxAction::SelectAll),
)));
vec![menu::Tree::with_children(
RcElementWrapper::new(crate::Element::from(widget::Row::new())),
items,
)]
}
struct TextMenuOverlay<'a, W, Message: Clone + 'static> {
menu: Menu<'a, TextCtxAction>,
widget: &'a W,
tree: &'a mut Tree,
on_input: Option<&'a dyn Fn(String) -> Message>,
click_position: Point,
}
impl<W, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
for TextMenuOverlay<'_, W, Message>
where
W: HasSelectableText,
Message: Clone + 'static,
{
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node {
self.menu.layout(
renderer,
Limits::NONE
.min_width(bounds.width)
.max_width(bounds.width)
.min_height(bounds.height)
.max_height(bounds.height),
)
}
fn draw(
&self,
renderer: &mut crate::Renderer,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
self.menu.draw(renderer, theme, style, layout, cursor);
}
fn update(
&mut self,
event: &event::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
// Pre-initialize the menu if it hasn't been yet.
let needs_init = self
.menu
.tree
.inner
.with_data(|state| state.open && state.menu_states.is_empty());
if needs_init {
let viewport = layout.bounds();
let viewport_size = viewport.size();
let overlay_offset = Point::ORIGIN - viewport.position();
let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset;
let init_bounds = Rectangle {
x: self.click_position.x,
y: self.click_position.y,
width: 240.0,
height: 240.0,
};
// Temporarily set real bounds so init_root_menu can find
// root_bounds_list entries that contain the cursor.
let main_offset = self.menu.main_offset as f32;
let saved_bounds = mem::replace(&mut self.menu.bar_bounds, init_bounds);
let saved_roots = mem::replace(&mut self.menu.root_bounds_list, vec![init_bounds]);
let mut init_messages: Vec<TextCtxAction> = Vec::new();
let mut init_shell = Shell::new(&mut init_messages);
menu::init_root_menu(
&mut self.menu,
renderer,
&mut init_shell,
overlay_cursor,
viewport_size,
overlay_offset,
init_bounds,
main_offset,
);
// Restore offscreen bounds
self.menu.bar_bounds = saved_bounds;
self.menu.root_bounds_list = saved_roots;
if init_shell.is_layout_invalid() {
shell.invalidate_layout();
}
}
// Right-button releases are meaningless for menu interaction
if matches!(
event,
event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))
) {
return;
}
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
self.menu
.update(event, layout, cursor, renderer, clipboard, &mut local_shell);
if local_shell.is_event_captured() {
shell.capture_event();
}
shell.request_redraw_at(local_shell.redraw_request());
if local_shell.is_layout_invalid() {
shell.invalidate_layout();
}
for action in local_messages {
match action {
TextCtxAction::Copy => {
self.widget.copy_to_clipboard(self.tree, clipboard);
}
TextCtxAction::Cut => {
self.widget.copy_to_clipboard(self.tree, clipboard);
if let Some(contents) = self.widget.delete_selection(self.tree) {
if let Some(on_input) = self.on_input {
shell.publish((on_input)(contents));
}
}
}
TextCtxAction::Paste => {
let content: String = clipboard
.read(clipboard::Kind::Standard)
.unwrap_or_default();
if let Some(contents) = self.widget.paste_text(self.tree, &content) {
if let Some(on_input) = self.on_input {
shell.publish((on_input)(contents));
}
}
}
TextCtxAction::SelectAll => {
self.widget.select_all(self.tree);
}
}
self.widget.set_context_menu_position(self.tree, None);
}
let is_open = self.menu.tree.inner.with_data(|state| state.open);
if !is_open {
self.widget.set_context_menu_position(self.tree, None);
}
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &crate::Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
mouse::Interaction::Idle
} else {
mouse::Interaction::None
}
}
}
/// Creates a Wayland popup surface containing the text context menu.
///
/// Sends the popup action directly to the SCTK event loop and registers
/// the view data so `Cosmic::view()` can render it. No app-level wiring
/// is needed
#[cfg(feature = "wayland")]
pub(crate) fn create_text_context_popup(
click_position: Point,
selected_text: Option<String>,
is_editable: bool,
has_selection: bool,
menu_bar_state: &MenuBarState,
pending_action: &PendingAction,
renderer: &crate::Renderer,
viewport: &Rectangle,
cursor: mouse::Cursor,
) {
use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
let window_id = current_window_id();
if window_id == iced_core::window::Id::NONE {
return;
}
let mut menu_roots = build_menu_roots(is_editable, has_selection);
menu_roots.iter_mut().for_each(menu::Tree::set_index);
let id = menu_bar_state.inner.with_data_mut(|state| {
state.menu_states.clear();
state.active_root.clear();
menu_roots_diff(&mut menu_roots, &mut state.tree);
if let Some(id) = state.popup_id.get(&window_id).copied() {
send_destroy_popup_direct(id);
state.view_cursor = cursor;
id
} else {
state.open = true;
state.view_cursor = cursor;
iced::window::Id::unique()
}
});
let bounds = Rectangle {
x: click_position.x,
y: click_position.y,
width: 240.0,
height: 240.0,
};
let mut popup_menu: Menu<'static, TextCtxAction> = Menu {
tree: menu_bar_state.clone(),
menu_roots: Cow::Owned(menu_roots),
bounds_expand: 16,
menu_overlays_parent: true,
close_condition: CloseCondition {
leave: false,
click_outside: true,
click_inside: true,
},
item_width: ItemWidth::Uniform(240),
item_height: ItemHeight::Dynamic(40),
bar_bounds: bounds,
main_offset: -(bounds.height as i32),
cross_offset: 0,
root_bounds_list: vec![bounds],
path_highlight: Some(PathHighlight::MenuActive),
style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default),
position: Point::new(0., 0.),
is_overlay: false,
window_id: id,
depth: 0,
on_surface_action: None,
};
{
let mut init_messages: Vec<TextCtxAction> = Vec::new();
let mut init_shell = Shell::new(&mut init_messages);
menu::init_root_menu(
&mut popup_menu,
renderer,
&mut init_shell,
cursor.position().unwrap_or_default(),
viewport.size(),
Vector::new(0., 0.),
bounds,
-(bounds.height),
);
}
let anchor_rect = menu_bar_state.inner.with_data_mut(|state| {
state.popup_id.insert(window_id, id);
let pos = cursor.position().unwrap_or_default();
iced::Rectangle {
x: pos.x as i32,
y: pos.y as i32,
width: 1,
height: 1,
}
});
let menu_node = popup_menu.layout(renderer, iced::Limits::NONE.min_width(1.).min_height(1.));
let popup_size = menu_node.size();
let positioner = SctkPositioner {
size: Some((
popup_size.width.ceil() as u32 + 2,
popup_size.height.ceil() as u32 + 2,
)),
anchor_rect,
anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::None,
gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
reactive: true,
..Default::default()
};
// Register view data so Cosmic::view() can render this popup.
if let Ok(mut registry) = POPUP_VIEW_REGISTRY.lock() {
registry.insert(
id,
PopupViewData {
menu: popup_menu,
selected_text,
pending_action: pending_action.clone(),
},
);
}
// Send the popup creation directly to the SCTK event loop.
send_popup_direct(SctkPopupSettings {
parent: window_id,
id,
positioner,
parent_size: None,
grab: true,
close_with_children: false,
input_zone: None,
});
}
/// Checks whether a popup menu has closed and destroys it if needed.
/// Call this from the text widget's `update()` on each frame.
#[cfg(feature = "wayland")]
pub(crate) fn cleanup_text_popup(menu_bar_state: &MenuBarState) {
let window_id = current_window_id();
let should_destroy = menu_bar_state
.inner
.with_data(|state| !state.open && state.popup_id.contains_key(&window_id));
if should_destroy {
menu_bar_state.inner.with_data_mut(|state| {
if let Some(popup_id) = state.popup_id.remove(&window_id) {
send_destroy_popup_direct(popup_id);
}
state.reset();
});
}
}
/// Widget that wraps [`Menu`] inside a Wayland popup and intercepts
/// [`TextCtxAction`] messages for clipboard operations.
#[derive(Clone)]
struct TextContextMenuPopup<Message: Clone + 'static> {
menu: Menu<'static, TextCtxAction>,
selected_text: Option<String>,
pending_action: PendingAction,
_phantom: std::marker::PhantomData<Message>,
}
impl<Message: Clone + 'static> iced_core::widget::Widget<Message, crate::Theme, crate::Renderer>
for TextContextMenuPopup<Message>
{
fn size(&self) -> Size<iced_core::Length> {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::size(&self.menu)
}
fn tag(&self) -> iced_core::widget::tree::Tag {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::tag(&self.menu)
}
fn state(&self) -> iced_core::widget::tree::State {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::state(&self.menu)
}
fn children(&self) -> Vec<iced_core::widget::Tree> {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::children(&self.menu)
}
fn diff(&mut self, tree: &mut iced_core::widget::Tree) {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::diff(&mut self.menu, tree);
}
fn layout(
&mut self,
tree: &mut iced_core::widget::Tree,
renderer: &crate::Renderer,
limits: &iced_core::layout::Limits,
) -> iced_core::layout::Node {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::layout(
&mut self.menu,
tree,
renderer,
limits,
)
}
fn draw(
&self,
tree: &iced_core::widget::Tree,
renderer: &mut crate::Renderer,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::draw(
&self.menu, tree, renderer, theme, style, layout, cursor, viewport,
);
}
fn update(
&mut self,
tree: &mut iced_core::widget::Tree,
event: &event::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let mut local_messages: Vec<TextCtxAction> = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
{
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::update(
&mut self.menu,
tree,
event,
layout,
cursor,
renderer,
clipboard,
&mut local_shell,
viewport,
);
}
if local_shell.is_event_captured() {
shell.capture_event();
}
shell.request_redraw_at(local_shell.redraw_request());
if local_shell.is_layout_invalid() {
shell.invalidate_layout();
}
for action in local_messages {
match action {
TextCtxAction::Copy => {
if let Some(ref text) = self.selected_text {
clipboard.write(clipboard::Kind::Standard, text.clone());
}
}
TextCtxAction::Cut => {
if let Some(ref text) = self.selected_text {
clipboard.write(clipboard::Kind::Standard, text.clone());
}
if let Ok(mut guard) = self.pending_action.lock() {
*guard = Some(TextCtxAction::Cut);
}
}
TextCtxAction::Paste => {
if let Ok(mut guard) = self.pending_action.lock() {
*guard = Some(TextCtxAction::Paste);
}
}
TextCtxAction::SelectAll => {
if let Ok(mut guard) = self.pending_action.lock() {
*guard = Some(TextCtxAction::SelectAll);
}
}
}
}
}
fn mouse_interaction(
&self,
tree: &iced_core::widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &crate::Renderer,
) -> mouse::Interaction {
use iced_core::widget::Widget;
Widget::<TextCtxAction, crate::Theme, crate::Renderer>::mouse_interaction(
&self.menu, tree, layout, cursor, viewport, renderer,
)
}
}
impl<Message: Clone + 'static> From<TextContextMenuPopup<Message>>
for crate::Element<'static, Message>
{
fn from(popup: TextContextMenuPopup<Message>) -> Self {
Self::new(popup)
}
}

View file

@ -76,6 +76,12 @@ impl Cursor {
self.state = State::Index(position);
}
pub(crate) fn clear_selection(&mut self) {
if let State::Selection { end, .. } = self.state {
self.state = State::Index(end);
}
}
#[inline]
pub(crate) fn move_right(&mut self, value: &Value) {
self.move_right_by_amount(value, 1);