diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 58721124..4ea71bc4 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -237,7 +237,7 @@ pub(super) struct MenuSlice { #[derive(Debug, Clone)] /// Menu bounds in overlay space pub struct MenuBounds { - child_positions: Vec, + pub(crate) child_positions: Vec, child_sizes: Vec, children_bounds: Rectangle, pub parent_bounds: Rectangle, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index bd4f1e6c..cf99335c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -194,7 +194,7 @@ pub fn menu_root<'a, Message, Renderer: renderer::Renderer>( ) -> Button<'a, Message> where Element<'a, Message, crate::Theme, Renderer>: From>, - Message: std::clone::Clone + 'a, + Message: std::clone::Clone + 'static, { widget::button::custom(widget::text(label)) .padding([4, 12]) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index ab1eef31..4c33f7e3 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -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::{ diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 4eb5bb17..af4b17ab 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -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> + 'a, widget: impl Into> + '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(children: Vec>) -> Row /// 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> + 'a, widget: impl Into> + '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> { diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs new file mode 100644 index 00000000..6bf530c9 --- /dev/null +++ b/src/widget/text_context_menu.rs @@ -0,0 +1,713 @@ +// Copyright 2025 System76 +// 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>>; + +/// 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 { + 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 = 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, + pending_action: PendingAction, +} + +#[cfg(feature = "wayland")] +static POPUP_VIEW_REGISTRY: LazyLock>> = + 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( + id: iced_core::window::Id, +) -> Option>> { + let registry = POPUP_VIEW_REGISTRY.lock().ok()?; + let data = registry.get(&id)?; + let popup_widget: TextContextMenuPopup = 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> +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> { + 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 overlay::Overlay + 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 = 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, + 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 = 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 { + menu: Menu<'static, TextCtxAction>, + selected_text: Option, + pending_action: PendingAction, + _phantom: std::marker::PhantomData, +} + +impl iced_core::widget::Widget + for TextContextMenuPopup +{ + fn size(&self) -> Size { + use iced_core::widget::Widget; + Widget::::size(&self.menu) + } + + fn tag(&self) -> iced_core::widget::tree::Tag { + use iced_core::widget::Widget; + Widget::::tag(&self.menu) + } + + fn state(&self) -> iced_core::widget::tree::State { + use iced_core::widget::Widget; + Widget::::state(&self.menu) + } + + fn children(&self) -> Vec { + use iced_core::widget::Widget; + Widget::::children(&self.menu) + } + + fn diff(&mut self, tree: &mut iced_core::widget::Tree) { + use iced_core::widget::Widget; + Widget::::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::::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::::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 = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + { + use iced_core::widget::Widget; + Widget::::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::::mouse_interaction( + &self.menu, tree, layout, cursor, viewport, renderer, + ) + } +} + +impl From> + for crate::Element<'static, Message> +{ + fn from(popup: TextContextMenuPopup) -> Self { + Self::new(popup) + } +} diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index 3ffb535c..d83d99b1 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -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);