//! Keep Iced's single-line input from silently filtering pasted control bytes. //! Only the alias field uses this adapter. The child retains editing and focus. use iced::advanced::{ Clipboard, Layout, Shell, Widget, clipboard, layout, overlay, renderer, text, widget, }; use iced::{Element, Event, Length, Rectangle, Size, Vector, keyboard, mouse}; pub(super) fn guard<'a, Message: Clone + 'a, Theme: 'a, R: text::Renderer + 'a>( input: Element<'a, Message, Theme, R>, rejected: Option, ) -> Element<'a, Message, Theme, R> { Element::new(AliasInput { input, rejected }) } struct AliasInput<'a, Message, Theme, R> { input: Element<'a, Message, Theme, R>, rejected: Option, } #[derive(Default)] struct State { modifiers: keyboard::Modifiers, } // Each fresh child clipboard read uses the checked snapshot. Iced may reuse its // previously admitted paste while V is held, until KeyReleased clears that cache. struct CapturedClipboard<'a> { content: Option, original: &'a mut dyn Clipboard, } impl Clipboard for CapturedClipboard<'_> { fn read(&self, kind: clipboard::Kind) -> Option { if kind == clipboard::Kind::Standard { self.content.clone() } else { self.original.read(kind) } } fn write(&mut self, kind: clipboard::Kind, contents: String) { self.original.write(kind, contents); } } impl Widget for AliasInput<'_, Message, Theme, R> { fn size(&self) -> Size { self.input.as_widget().size() } fn size_hint(&self) -> Size { self.input.as_widget().size_hint() } fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::() } fn state(&self) -> widget::tree::State { widget::tree::State::new(State::default()) } fn children(&self) -> Vec { vec![widget::Tree::new(self.input.as_widget())] } fn diff(&self, tree: &mut widget::Tree) { tree.diff_children(std::slice::from_ref(&self.input)); } fn layout( &mut self, tree: &mut widget::Tree, renderer: &R, limits: &layout::Limits, ) -> layout::Node { self.input .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn draw( &self, tree: &widget::Tree, renderer: &mut R, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.input.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); } fn operate( &mut self, tree: &mut widget::Tree, layout: Layout<'_>, renderer: &R, operation: &mut dyn widget::Operation, ) { self.input .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &R, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { let focused = tree.children[0] .state .downcast_ref::>() .is_focused(); let state = tree.state.downcast_mut::(); // Iced 0.14.2 uses stored ModifiersChanged state, not KeyPressed.modifiers. // Mirror its Escape reset and its Latin/physical-key paste shortcut. if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event { state.modifiers = *modifiers; } let paste = matches!(event, Event::Keyboard(keyboard::Event::KeyPressed { key, physical_key, .. }) if key.to_latin(*physical_key) == Some('v') && state.modifiers.command() && !state.modifiers.alt()); if focused && paste && let Some(rejected) = &self.rejected { let content = clipboard.read(clipboard::Kind::Standard); if content .as_ref() .is_some_and(|value| value.chars().any(char::is_control)) { // Do not invoke the child: even an empty replacement would erase // its selection. Approval invalidation is an explicit App event. shell.publish(rejected.clone()); shell.capture_event(); return; } self.input.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, &mut CapturedClipboard { content, original: clipboard, }, shell, viewport, ); return; } self.input.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); // In Iced 0.14.2 the only KeyPressed branch that removes focus is its // Escape branch, which also resets stored modifiers. Observe that branch // after its text/shortcut early returns and effective-key conversion. if focused && matches!(event, Event::Keyboard(keyboard::Event::KeyPressed { .. })) && !tree.children[0] .state .downcast_ref::>() .is_focused() { tree.state.downcast_mut::().modifiers = keyboard::Modifiers::default(); } } fn mouse_interaction( &self, tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &R, ) -> mouse::Interaction { self.input.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn overlay<'a>( &'a mut self, tree: &'a mut widget::Tree, layout: Layout<'a>, renderer: &R, viewport: &Rectangle, translation: Vector, ) -> Option> { self.input.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, viewport, translation, ) } } #[cfg(all(test, debug_assertions))] mod tests;