diff --git a/Cargo.lock b/Cargo.lock index 5782086e..e228ae33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -674,30 +674,15 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec 0.8.0", -] - [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ - "bit-vec 0.9.1", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bit-vec" version = "0.9.1" @@ -1377,15 +1362,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cookie" version = "0.18.2" @@ -1822,7 +1798,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -2782,7 +2758,6 @@ dependencies = [ "pollster 0.4.0", "postage", "profiling", - "proptest", "rand 0.9.5", "raw-window-handle", "regex", @@ -3800,7 +3775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", - "quick-error 2.0.1", + "quick-error", ] [[package]] @@ -4488,7 +4463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", - "bit-set 0.9.1", + "bit-set", "bitflags 2.13.2", "cfg-if", "cfg_aliases", @@ -5839,38 +5814,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", - "bitflags 2.13.2", - "num-traits", - "proptest-macro", - "rand 0.9.5", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-macro" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" -dependencies = [ - "convert_case 0.11.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "pulp" version = "0.22.3" @@ -5909,12 +5852,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -6092,15 +6029,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - [[package]] name = "range-alloc" version = "0.1.5" @@ -6157,7 +6085,7 @@ dependencies = [ "avif-serialize", "imgref", "loop9", - "quick-error 2.0.1", + "quick-error", "rav1e", "rayon", "rgb", @@ -6584,18 +6512,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - [[package]] name = "rustybuzz" version = "0.20.1" @@ -7587,7 +7503,7 @@ dependencies = [ "fax", "flate2", "half", - "quick-error 2.0.1", + "quick-error", "weezl", "zune-jpeg", ] @@ -8048,12 +7964,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicase" version = "2.10.0" @@ -8350,15 +8260,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "waker-fn" version = "1.2.0" @@ -8685,8 +8586,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" dependencies = [ "arrayvec", - "bit-set 0.9.1", - "bit-vec 0.9.1", + "bit-set", + "bit-vec", "bitflags 2.13.2", "bytemuck", "cfg_aliases", @@ -8757,7 +8658,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set 0.9.1", + "bit-set", "bitflags 2.13.2", "block2 0.6.2", "bytemuck", diff --git a/README.md b/README.md index 8a463391..329b7db8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,5 @@ Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms. -### Pasting into text fields - -Click a field and use Ctrl+V (Cmd+V on macOS), or right-click and choose Paste. -Shift+Insert also pastes from the clipboard on Linux and Windows. On Linux, -middle-click inserts the primary selection at the clicked position: select text -in another application first, then middle-click the field. This also works in -the identity import dialog. - ### License Copyright (C) 2025 Ren Amamiya & other Coop contributors diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index ba249c47..3fd75c4a 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -14,6 +14,3 @@ smallvec.workspace = true log.workspace = true uuid = "1.10" - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 61c8ceda..57911b1f 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -13,7 +13,7 @@ use crate::input::clear_button; use crate::{Sizable, Size, StyleSized, StyledExt, h_flex, v_flex}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn primary_paste_offset( +fn nearest_caret_offset( value: &str, position: gpui::Point, mut caret_bounds: impl FnMut(usize) -> Option>, @@ -41,6 +41,38 @@ fn primary_paste_offset( .map(|(offset, _, _)| offset) } +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn paste_from_primary( + state: &Entity>, + position: gpui::Point, + window: &mut Window, + cx: &mut App, +) { + if !state.read(cx).is_editable() { + return; + } + // Read before changing focus or selection: those can change PRIMARY. + let Some(text) = cx.read_from_primary().and_then(|item| item.text()) else { + return; + }; + state.update(cx, |state, cx| { + let Some(offset) = nearest_caret_offset(&state.value(), position, |offset| { + state.range_to_bounds(&(offset..offset)) + }) else { + return; + }; + let text = if state.is_multi_line() { + text + } else { + text.replace(['\r', '\n'], "") + }; + state.focus(window, cx); + state.set_selected_range(offset..offset, cx); + state.insert(text, window, cx); + }); + cx.stop_propagation(); +} + /// The background of an input frame, which reads muted while the input is disabled. fn input_background(disabled: bool, cx: &App) -> Hsla { if disabled { @@ -182,7 +214,7 @@ impl RenderOnce for Input { let state_entity = self.state.clone(); - InputBase::new(("input", self.state.entity_id())) + let input = InputBase::new(("input", self.state.entity_id())) .flex() .size_full() .line_height(LINE_HEIGHT) @@ -194,46 +226,15 @@ impl RenderOnce for Input { .on_mouse_down(MouseButton::Left, { let state_entity = state_entity.clone(); move |_, window, cx| state_entity.update(cx, |state, cx| state.focus(window, cx)) - }) - .when( - cfg!(any(target_os = "linux", target_os = "freebsd")), - |this| { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let this = this.on_mouse_down(MouseButton::Middle, { - let state_entity = state_entity.clone(); - move |event, window, cx| { - if disabled || !state_entity.read(cx).is_editable() { - return; - } - // Read before changing focus or selection: those can change PRIMARY. - let Some(text) = cx.read_from_primary().and_then(|item| item.text()) - else { - return; - }; - state_entity.update(cx, |state, cx| { - let value = state.value(); - let Some(offset) = - primary_paste_offset(&value, event.position, |offset| { - state.range_to_bounds(&(offset..offset)) - }) - else { - return; - }; - let text = if state.is_multi_line() { - text - } else { - text.replace(['\r', '\n'], "") - }; - state.focus(window, cx); - state.set_selected_range(offset..offset, cx); - state.insert(text, window, cx); - }); - cx.stop_propagation(); - } - }); - this - }, - ) + }); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + let input = input.on_mouse_down(MouseButton::Middle, { + let state_entity = state_entity.clone(); + move |event, window, cx| paste_from_primary(&state_entity, event.position, window, cx) + }); + + input .items_center() .when(multi_line, |this| this.h_auto()) .when(self.appearance, |this| { @@ -274,167 +275,3 @@ impl RenderOnce for Input { }) } } - -#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))] -mod tests { - use super::*; - use crate::input::InputState; - use gpui::{AppContext, ClipboardItem, Context, Render, TestAppContext, point}; - - struct TestInput { - input: Entity, - disabled: bool, - } - - impl Render for TestInput { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div() - .w(px(300.)) - .h(px(40.)) - .child(Input::new(&self.input).disabled(self.disabled)) - } - } - - #[gpui::test] - fn middle_click_pastes_primary_at_clicked_position(cx: &mut TestAppContext) { - cx.update(|cx| { - theme::init(cx); - crate::init(cx); - }); - let (view, cx) = cx.add_window_view(|window, cx| TestInput { - input: cx.new(|cx| InputState::new(window, cx).default_value("aéz")), - disabled: false, - }); - let input = view.read_with(cx, |view, _| view.input.clone()); - cx.update(|window, cx| { - window.draw(cx).clear(cx); - cx.write_to_clipboard(ClipboardItem::new_string("wrong clipboard".into())); - cx.write_to_primary(ClipboardItem::new_string("nsec1test\r\n".into())); - }); - let position = input.read_with(cx, |state, _| { - state.range_to_bounds(&(3..3)).map(|bounds| bounds.center()) - }); - assert!(position.is_some()); - if let Some(position) = position { - cx.simulate_mouse_down(position, MouseButton::Middle, Default::default()); - cx.simulate_mouse_up(position, MouseButton::Middle, Default::default()); - } - assert_eq!( - input.read_with(cx, |state, _| state.value()), - "aénsec1testz" - ); - - cx.update(|window, cx| { - window.dispatch_action(Box::new(gpui_base::input::Undo), cx); - }); - assert_eq!(input.read_with(cx, |state, _| state.value()), "aéz"); - - cx.update(|window, cx| { - input.update(cx, |state, cx| state.set_value("", window, cx)); - window.draw(cx).clear(cx); - }); - cx.simulate_mouse_down( - point(px(200.), px(20.)), - MouseButton::Middle, - Default::default(), - ); - cx.simulate_mouse_up( - point(px(200.), px(20.)), - MouseButton::Middle, - Default::default(), - ); - assert_eq!(input.read_with(cx, |state, _| state.value()), "nsec1test"); - } - - #[gpui::test] - fn clipboard_shortcuts_paste_without_using_primary(cx: &mut TestAppContext) { - cx.update(|cx| { - theme::init(cx); - crate::init(cx); - }); - let (view, cx) = cx.add_window_view(|window, cx| TestInput { - input: cx.new(|cx| InputState::new(window, cx)), - disabled: false, - }); - let input = view.read_with(cx, |view, _| view.input.clone()); - cx.update(|window, cx| { - input.update(cx, |state, cx| state.focus(window, cx)); - window.draw(cx).clear(cx); - cx.write_to_clipboard(ClipboardItem::new_string("clipboard".into())); - cx.write_to_primary(ClipboardItem::new_string("primary".into())); - }); - cx.simulate_keystrokes("shift-insert"); - assert_eq!(input.read_with(cx, |state, _| state.value()), "clipboard"); - cx.simulate_keystrokes("ctrl-a ctrl-v"); - assert_eq!(input.read_with(cx, |state, _| state.value()), "clipboard"); - } - - #[test] - fn primary_paste_uses_visible_unicode_carets_and_clicked_row() { - let offset = primary_paste_offset("é\nx", point(px(8.), px(35.)), |offset| { - let (x, y) = match offset { - 0 => return None, - 2 => (10., 0.), - 3 => (0., 20.), - 4 => (10., 20.), - _ => panic!("offset must be a UTF-8 character boundary"), - }; - Some(gpui::Bounds::new( - point(px(x), px(y)), - gpui::size(px(0.), px(20.)), - )) - }); - assert_eq!(offset, Some(4)); - assert_eq!( - primary_paste_offset("text", point(px(0.), px(0.)), |_| None), - None - ); - } - - #[gpui::test] - fn middle_click_preserves_disabled_and_readonly_fields(cx: &mut TestAppContext) { - cx.update(|cx| { - theme::init(cx); - crate::init(cx); - }); - let (view, cx) = cx.add_window_view(|window, cx| TestInput { - input: cx.new(|cx| InputState::new(window, cx).default_value("unchanged")), - disabled: true, - }); - let input = view.read_with(cx, |view, _| view.input.clone()); - cx.update(|window, cx| { - window.draw(cx).clear(cx); - cx.write_to_primary(ClipboardItem::new_string("must not paste".into())); - }); - cx.simulate_mouse_down( - point(px(20.), px(20.)), - MouseButton::Middle, - Default::default(), - ); - cx.simulate_mouse_up( - point(px(20.), px(20.)), - MouseButton::Middle, - Default::default(), - ); - assert_eq!(input.read_with(cx, |state, _| state.value()), "unchanged"); - cx.update(|window, cx| { - view.update(cx, |view, cx| { - view.disabled = false; - cx.notify(); - }); - input.update(cx, |state, cx| state.set_readonly(true, cx)); - window.draw(cx).clear(cx); - }); - cx.simulate_mouse_down( - point(px(20.), px(20.)), - MouseButton::Middle, - Default::default(), - ); - cx.simulate_mouse_up( - point(px(20.), px(20.)), - MouseButton::Middle, - Default::default(), - ); - assert_eq!(input.read_with(cx, |state, _| state.value()), "unchanged"); - } -}