feat: support middle-click paste in text fields (#60)
Selecting an nsec in another Linux application and middle-clicking Coop's identity import field currently does nothing. The shared input widget handles left and right clicks but never reads the primary selection for a middle click. Add middle-click paste from PRIMARY on Linux and FreeBSD, inserting at the nearest visible caret to the click. Read the selection before changing focus, preserve disabled/read-only inputs, strip line breaks in single-line fields, and use the input state's atomic insertion path so Undo removes the paste. Also register Shift+Insert for standard clipboard paste on non-macOS platforms. The existing right-click editing menu and Ctrl+V/Cmd+V remain available. Document the gestures in the README. Enable GPUI test support for the UI crate; the lockfile additions are its test dependencies. Validation: - `cargo check -p ui --locked` - `cargo test -p ui --lib --locked` — all five tests pass, including empty-field paste, Unicode caret placement, primary/clipboard separation, Undo, and disabled/read-only protection - `git -c core.whitespace=cr-at-eol diff --check` (preserves the existing CRLF Cargo manifest) Validated with GPUI's simulated input and clipboard platform; no live X11/Wayland compositor test was performed. Release Notes: - Fixed middle-click paste in Linux text fields, including identity import, and added Shift+Insert clipboard paste. Reviewed-on: reya/coop#60 Co-authored-by: Vincent Bernardoff <vb@luminar.eu.org>
This commit is contained in:
Generated
+108
-9
@@ -674,15 +674,30 @@ dependencies = [
|
|||||||
"unicode-normalization",
|
"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]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
|
checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-vec",
|
"bit-vec 0.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-vec"
|
name = "bit-vec"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -1362,6 +1377,15 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -1798,7 +1822,7 @@ version = "2.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case 0.10.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
@@ -2758,6 +2782,7 @@ dependencies = [
|
|||||||
"pollster 0.4.0",
|
"pollster 0.4.0",
|
||||||
"postage",
|
"postage",
|
||||||
"profiling",
|
"profiling",
|
||||||
|
"proptest",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -3775,7 +3800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
"quick-error",
|
"quick-error 2.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4463,7 +4488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df"
|
checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bit-set",
|
"bit-set 0.9.1",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
@@ -5814,6 +5839,38 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"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]]
|
[[package]]
|
||||||
name = "pulp"
|
name = "pulp"
|
||||||
version = "0.22.3"
|
version = "0.22.3"
|
||||||
@@ -5852,6 +5909,12 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -6029,6 +6092,15 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"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]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -6085,7 +6157,7 @@ dependencies = [
|
|||||||
"avif-serialize",
|
"avif-serialize",
|
||||||
"imgref",
|
"imgref",
|
||||||
"loop9",
|
"loop9",
|
||||||
"quick-error",
|
"quick-error 2.0.1",
|
||||||
"rav1e",
|
"rav1e",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rgb",
|
"rgb",
|
||||||
@@ -6512,6 +6584,18 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
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]]
|
[[package]]
|
||||||
name = "rustybuzz"
|
name = "rustybuzz"
|
||||||
version = "0.20.1"
|
version = "0.20.1"
|
||||||
@@ -7503,7 +7587,7 @@ dependencies = [
|
|||||||
"fax",
|
"fax",
|
||||||
"flate2",
|
"flate2",
|
||||||
"half",
|
"half",
|
||||||
"quick-error",
|
"quick-error 2.0.1",
|
||||||
"weezl",
|
"weezl",
|
||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
@@ -7964,6 +8048,12 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unarray"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@@ -8260,6 +8350,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wait-timeout"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "waker-fn"
|
name = "waker-fn"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -8586,8 +8685,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7"
|
checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bit-set",
|
"bit-set 0.9.1",
|
||||||
"bit-vec",
|
"bit-vec 0.9.1",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
@@ -8658,7 +8757,7 @@ dependencies = [
|
|||||||
"android_system_properties",
|
"android_system_properties",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"ash",
|
"ash",
|
||||||
"bit-set",
|
"bit-set 0.9.1",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms.
|
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
|
### License
|
||||||
|
|
||||||
Copyright (C) 2025 Ren Amamiya & other Coop contributors
|
Copyright (C) 2025 Ren Amamiya & other Coop contributors
|
||||||
|
|||||||
@@ -14,3 +14,6 @@ serde.workspace = true
|
|||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
|
|
||||||
uuid = "1.10"
|
uuid = "1.10"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -13,6 +13,35 @@ use crate::indicator::Indicator;
|
|||||||
use crate::input::clear_button;
|
use crate::input::clear_button;
|
||||||
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
fn primary_paste_offset(
|
||||||
|
value: &str,
|
||||||
|
position: gpui::Point<Pixels>,
|
||||||
|
mut caret_bounds: impl FnMut(usize) -> Option<gpui::Bounds<Pixels>>,
|
||||||
|
) -> Option<usize> {
|
||||||
|
value
|
||||||
|
.char_indices()
|
||||||
|
.map(|(offset, _)| offset)
|
||||||
|
.chain(std::iter::once(value.len()))
|
||||||
|
.filter_map(|offset| {
|
||||||
|
let bounds = caret_bounds(offset)?;
|
||||||
|
let vertical_distance = if position.y < bounds.top() {
|
||||||
|
bounds.top() - position.y
|
||||||
|
} else if position.y > bounds.bottom() {
|
||||||
|
position.y - bounds.bottom()
|
||||||
|
} else {
|
||||||
|
px(0.)
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
offset,
|
||||||
|
vertical_distance,
|
||||||
|
(position.x - bounds.left()).abs(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.min_by(|left, right| left.1.cmp(&right.1).then_with(|| left.2.cmp(&right.2)))
|
||||||
|
.map(|(offset, _, _)| offset)
|
||||||
|
}
|
||||||
|
|
||||||
/// The background of an input frame, which reads muted while the input is disabled.
|
/// The background of an input frame, which reads muted while the input is disabled.
|
||||||
fn input_background(disabled: bool, cx: &App) -> Hsla {
|
fn input_background(disabled: bool, cx: &App) -> Hsla {
|
||||||
if disabled {
|
if disabled {
|
||||||
@@ -225,6 +254,45 @@ impl<M: InputModeKind> RenderOnce for Input<M> {
|
|||||||
let state_entity = state_entity.clone();
|
let state_entity = state_entity.clone();
|
||||||
move |_, window, cx| state_entity.update(cx, |state, cx| state.focus(window, cx))
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
.items_center()
|
.items_center()
|
||||||
.when(multi_line, |this| {
|
.when(multi_line, |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
@@ -273,3 +341,167 @@ impl<M: InputModeKind> RenderOnce for Input<M> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<InputState>,
|
||||||
|
disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TestInput {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ mod window_ext;
|
|||||||
/// You can initialize the UI module at your application's entry point.
|
/// You can initialize the UI module at your application's entry point.
|
||||||
pub fn init(cx: &mut gpui::App) {
|
pub fn init(cx: &mut gpui::App) {
|
||||||
gpui_base::init(cx);
|
gpui_base::init(cx);
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
cx.bind_keys([gpui::KeyBinding::new(
|
||||||
|
"shift-insert",
|
||||||
|
gpui_base::input::Paste,
|
||||||
|
Some("Input"),
|
||||||
|
)]);
|
||||||
theme::sync_base(cx);
|
theme::sync_base(cx);
|
||||||
menu::init(cx);
|
menu::init(cx);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user