use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; use iced::advanced::layout; use iced::advanced::overlay; use iced::advanced::renderer; use iced::advanced::widget::{Operation, Tree, tree}; use iced::advanced::{Clipboard, Layout, Shell, Widget}; use iced::keyboard::{self, Key, Location, Modifiers, key}; use iced::mouse; use iced::{Element, Event, Length, Rectangle, Size, Vector}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum EditCommand { Undo, Redo, Cut, Copy, Paste, SelectAll, } pub(crate) type EditCommandQueue = Arc>>; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct EditAvailability { pub(crate) undo: bool, pub(crate) redo: bool, pub(crate) cut: bool, pub(crate) copy: bool, pub(crate) paste: bool, pub(crate) select_all: bool, } pub(crate) type EditAvailabilityState = Arc>; pub(crate) fn command_queue() -> EditCommandQueue { Arc::new(Mutex::new(VecDeque::new())) } pub(crate) fn availability_state() -> EditAvailabilityState { Arc::new(Mutex::new(EditAvailability::default())) } pub(crate) fn queue_command(queue: &EditCommandQueue, command: EditCommand) { queue .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .push_back(command); } fn pop_command(queue: &EditCommandQueue) -> Option { queue .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .pop_front() } /// Replays native Edit menu actions through the focused Iced widget. pub(crate) struct NativeEdit<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { content: Element<'a, Message, Theme, Renderer>, commands: EditCommandQueue, availability: EditAvailabilityState, } impl<'a, Message, Theme, Renderer> NativeEdit<'a, Message, Theme, Renderer> { fn new( content: impl Into>, commands: EditCommandQueue, availability: EditAvailabilityState, ) -> Self { Self { content: content.into(), commands, availability, } } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] enum ControlId { Named(iced::widget::Id), Position(usize), } #[derive(Debug)] struct History { values: Vec, current: usize, } #[derive(Default)] struct State { histories: HashMap, active: Option, window_focused: bool, } #[derive(Default)] struct FocusedControl { current: usize, pending_text: Option, focused: Option<(ControlId, Option)>, } impl Operation for FocusedControl { fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { operate(self); } fn text_input( &mut self, _id: Option<&iced::widget::Id>, _bounds: Rectangle, state: &mut dyn iced::advanced::widget::operation::TextInput, ) { self.pending_text = Some(state.text().to_owned()); } fn focusable( &mut self, id: Option<&iced::widget::Id>, _bounds: Rectangle, state: &mut dyn iced::advanced::widget::operation::Focusable, ) { if state.is_focused() { self.focused = Some(( id.cloned() .map_or(ControlId::Position(self.current), ControlId::Named), self.pending_text.take(), )); } else { self.pending_text = None; } self.current += 1; } } impl State { fn observe(&mut self, focused: Option<&(ControlId, Option)>) { self.active = focused.map(|(control, _)| control.clone()); let Some((control, Some(value))) = focused else { return; }; let history = self .histories .entry(control.clone()) .or_insert_with(|| History { values: vec![value.clone()], current: 0, }); if history.values[history.current] != *value { history.values.truncate(history.current + 1); history.values.push(value.clone()); history.current += 1; } } fn replacement(&mut self, command: EditCommand) -> Option { let history = self.histories.get_mut(self.active.as_ref()?)?; match command { EditCommand::Undo if history.current > 0 => history.current -= 1, EditCommand::Redo if history.current + 1 < history.values.len() => { history.current += 1; } _ => return None, } Some(history.values[history.current].clone()) } fn availability(&self, focused: Option<&(ControlId, Option)>) -> EditAvailability { let Some((control, text)) = focused else { return EditAvailability::default(); }; let editable = text.is_some(); let history = self.histories.get(control); EditAvailability { undo: editable && history.is_some_and(|history| history.current > 0), redo: editable && history.is_some_and(|history| history.current + 1 < history.values.len()), cut: editable, copy: true, paste: editable, select_all: true, } } } fn modifier_sync_event(event: &Event) -> Option { let Event::Keyboard(keyboard::Event::KeyPressed { modifiers, .. }) = event else { return None; }; Some(Event::Keyboard(keyboard::Event::ModifiersChanged( *modifiers, ))) } fn command_events(command: EditCommand) -> [Event; 4] { let (character, modified_character, physical_key, modifiers) = match command { EditCommand::Undo => ("z", "z", key::Code::KeyZ, Modifiers::COMMAND), EditCommand::Redo => ( "z", "Z", key::Code::KeyZ, Modifiers::COMMAND | Modifiers::SHIFT, ), EditCommand::Cut => ("x", "x", key::Code::KeyX, Modifiers::COMMAND), EditCommand::Copy => ("c", "c", key::Code::KeyC, Modifiers::COMMAND), EditCommand::Paste => ("v", "v", key::Code::KeyV, Modifiers::COMMAND), EditCommand::SelectAll => ("a", "a", key::Code::KeyA, Modifiers::COMMAND), }; let key = Key::Character(character.into()); [ Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)), Event::Keyboard(keyboard::Event::KeyPressed { key: key.clone(), modified_key: Key::Character(modified_character.into()), physical_key: key::Physical::Code(physical_key), location: Location::Standard, modifiers, text: None, repeat: false, }), Event::Keyboard(keyboard::Event::KeyReleased { key, modified_key: Key::Character(modified_character.into()), physical_key: key::Physical::Code(physical_key), location: Location::Standard, modifiers, }), Event::Keyboard(keyboard::Event::ModifiersChanged(Modifiers::default())), ] } impl<'a, Message, Theme, Renderer> Widget for NativeEdit<'a, Message, Theme, Renderer> where Renderer: renderer::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::default()) } fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { self.content .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); match event { Event::Window(iced::window::Event::Focused) => state.window_focused = true, Event::Window(iced::window::Event::Unfocused) => state.window_focused = false, _ => {} } if state.window_focused && matches!( event, Event::Window(iced::window::Event::RedrawRequested(_)) ) { while let Some(command) = pop_command(&self.commands) { if let Some(replacement) = state.replacement(command) { let previous = clipboard.read(iced::advanced::clipboard::Kind::Standard); clipboard.write(iced::advanced::clipboard::Kind::Standard, replacement); for command in [EditCommand::SelectAll, EditCommand::Paste] { for command_event in command_events(command) { self.content.as_widget_mut().update( &mut tree.children[0], &command_event, layout, cursor, renderer, clipboard, shell, viewport, ); } } if let Some(previous) = previous { clipboard.write(iced::advanced::clipboard::Kind::Standard, previous); } continue; } if matches!(command, EditCommand::Undo | EditCommand::Redo) { continue; } for command_event in command_events(command) { self.content.as_widget_mut().update( &mut tree.children[0], &command_event, layout, cursor, renderer, clipboard, shell, viewport, ); } } } if let Some(sync_event) = modifier_sync_event(event) { self.content.as_widget_mut().update( &mut tree.children[0], &sync_event, layout, cursor, renderer, clipboard, shell, viewport, ); } self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); let mut focused = FocusedControl::default(); self.content .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, &mut focused); if state.window_focused { state.observe(focused.focused.as_ref()); *self .availability .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) = state.availability(focused.focused.as_ref()); } } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'b>, renderer: &Renderer, viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, viewport, translation, ) } } impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, Theme: 'a, Renderer: 'a + renderer::Renderer, { fn from(bridge: NativeEdit<'a, Message, Theme, Renderer>) -> Self { Element::new(bridge) } } pub(crate) fn native_edit<'a, Message, Theme, Renderer>( content: impl Into>, commands: EditCommandQueue, availability: EditAvailabilityState, ) -> NativeEdit<'a, Message, Theme, Renderer> { NativeEdit::new(content, commands, availability) } #[cfg(test)] mod tests { use super::*; #[test] fn paste_replays_a_complete_command_shortcut() { let events = command_events(EditCommand::Paste); assert!(matches!( events[0], Event::Keyboard(keyboard::Event::ModifiersChanged(Modifiers::COMMAND)) )); assert!(matches!( events[1], Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Character(ref key), modifiers: Modifiers::COMMAND, .. }) if key == "v" )); assert!(matches!( events[3], Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) if modifiers.is_empty() )); } #[test] fn edit_history_supports_undo_and_redo() { let control = ControlId::Position(0); let mut state = State::default(); state.observe(Some(&(control.clone(), Some("a".into())))); state.observe(Some(&(control.clone(), Some("ab".into())))); assert_eq!(state.replacement(EditCommand::Undo).as_deref(), Some("a")); assert_eq!(state.replacement(EditCommand::Redo).as_deref(), Some("ab")); } }