diff --git a/src/widget/context_input.rs b/src/widget/context_input.rs index 914138d..ea5ca30 100644 --- a/src/widget/context_input.rs +++ b/src/widget/context_input.rs @@ -70,6 +70,13 @@ pub fn paste(value: &str, start: usize, end: usize, clip: &str) -> Edit { } } +/// Strip control characters (e.g. a trailing newline on an X11 PRIMARY +/// selection) from clipboard text before it is pasted. Shared by the +/// right-click menu Paste and the middle-click PRIMARY paste. +pub fn sanitize_clip(raw: &str) -> String { + raw.chars().filter(|c| !c.is_control()).collect() +} + pub fn select_all_range(value: &str) -> (usize, usize) { let value = text_input::Value::new(value); @@ -362,6 +369,48 @@ where return; } + // Middle-click pastes the X11 PRIMARY selection at the cursor. iced's + // base text_input only wires Ctrl+V to the Standard (CLIPBOARD) + // selection, so without this the common "select text, middle-click to + // paste" workflow does nothing on X11. + let middle_click_on_input = matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) + ) && cursor.is_over(layout.bounds()); + + if middle_click_on_input && !self.locked { + let clip = sanitize_clip(&clipboard.read(clipboard::Kind::Primary).unwrap_or_default()); + + if !clip.is_empty() { + let value = text_input::Value::new(&self.value); + let input_state = tree.children[0] + .state + .downcast_mut::>(); + let (start, end) = match input_state.cursor().state(&value) { + text_input::cursor::State::Index(index) => { + let index = index.min(value.len()); + (index, index) + } + text_input::cursor::State::Selection { start, end } => { + normalized_range(&value, start, end) + } + }; + + let edit = paste(&self.value, start, end, &clip); + input_state.move_cursor_to(edit.cursor); + + if let Some(on_paste) = &self.on_paste { + shell.publish(on_paste.as_ref()(edit.value)); + } else if let Some(on_input) = &self.on_input { + shell.publish(on_input.as_ref()(edit.value)); + } + } + + shell.capture_event(); + shell.request_redraw(); + return; + } + Widget::update( &mut self.input, &mut tree.children[0], @@ -717,12 +766,7 @@ where } } MenuAction::Paste => { - let clip = clipboard - .read(clipboard::Kind::Standard) - .unwrap_or_default() - .chars() - .filter(|c| !c.is_control()) - .collect::(); + let clip = sanitize_clip(&clipboard.read(clipboard::Kind::Standard).unwrap_or_default()); let edit = paste(self.value, start, end, &clip); self.publish_paste(edit, shell); @@ -842,6 +886,16 @@ mod tests { assert_eq!(clip, None); } + #[test] + fn sanitize_clip_strips_control_chars_keeps_text() { + // An X11 PRIMARY selection commonly carries a trailing newline. + assert_eq!(sanitize_clip("pixelpassF1:abc\n"), "pixelpassF1:abc"); + assert_eq!(sanitize_clip("a\tb\r\nc"), "abc"); + // Non-control unicode is preserved. + assert_eq!(sanitize_clip("héllo🦀"), "héllo🦀"); + assert_eq!(sanitize_clip(""), ""); + } + #[test] fn paste_replaces_selection_or_inserts_at_cursor() { assert_eq!(