mirror of
https://github.com/remorses/gpuix.git
synced 2026-10-28 18:59:22 +08:00
Make browser clipboard shortcuts reliable
Route painted-text Cmd/Ctrl+C through a renderer-owned browser key listener that writes the GPUIX selection, ignores shortcuts already handled by focused inputs, and unregisters cleanly with the renderer. Blur focused editors when a painted selection starts, keep the existing desktop copy listener, leave browser paste on GPUI Web DOM paste events, and retain native paste bindings on desktop. Validated with Playwriter against real macOS clipboard copy and paste round trips, the wasm release build, 109 native tests plus the desktop paste regression, 263 React tests, 11 chat tests, rustfmt, and lintcn. Session: ses_fcaff4583ffeEVO0HkYhV2PVuu
This commit is contained in:
1 parent
33f56aa7cc
commit
95545aaa75
4 files changed
+87
-14
No files matched your search
@@ -55,7 +55,14 @@ napi-derive = "3"
|
||||
gpui_platform = { path = "../../zed/crates/gpui_platform", default-features = false }
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "=0.2.127"
|
||||
web-sys = { version = "0.3", features = ["Navigator", "Window"] }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Clipboard",
|
||||
"Document",
|
||||
"EventTarget",
|
||||
"KeyboardEvent",
|
||||
"Navigator",
|
||||
"Window",
|
||||
] }
|
||||
|
||||
# macOS text/graphics deps - pinned to avoid core-graphics version conflicts
|
||||
# core-text 21.1.0 uses core-graphics 0.25 which conflicts with font-kit's core-graphics 0.24
|
||||
|
||||
@@ -1618,6 +1618,14 @@ mod tests {
|
||||
assert!(!has_binding(&bindings, "ctrl-v", &Paste));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_paste_uses_the_platform_clipboard_action() {
|
||||
let bindings = text_editor_bindings(INPUT_KEY_CONTEXT, false, true, true);
|
||||
|
||||
assert!(has_binding(&bindings, "cmd-v", &Paste));
|
||||
assert!(has_binding(&bindings, "ctrl-v", &Paste));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_platform_detection_recognizes_macos() {
|
||||
assert!(browser_platform_is_macos("MacIntel", ""));
|
||||
|
||||
@@ -1736,6 +1736,64 @@ fn notify_web() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
struct WebCopyListener {
|
||||
target: web_sys::EventTarget,
|
||||
closure: wasm_bindgen::closure::Closure<dyn FnMut(web_sys::KeyboardEvent)>,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
impl WebCopyListener {
|
||||
fn new(selection: SharedSelection) -> Option<Self> {
|
||||
let window = web_sys::window()?;
|
||||
let target: web_sys::EventTarget = window.document()?.into();
|
||||
let closure = wasm_bindgen::closure::Closure::new(move |event: web_sys::KeyboardEvent| {
|
||||
if event.default_prevented()
|
||||
|| !event.key().eq_ignore_ascii_case("c")
|
||||
|| !(event.meta_key() || event.ctrl_key())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(text) = selection.lock().selected_text() else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = web_sys::window() else {
|
||||
return;
|
||||
};
|
||||
let navigator = window.navigator();
|
||||
let clipboard_available = js_sys::Reflect::get(
|
||||
navigator.as_ref(),
|
||||
&wasm_bindgen::JsValue::from_str("clipboard"),
|
||||
)
|
||||
.is_ok_and(|clipboard| !clipboard.is_undefined() && !clipboard.is_null());
|
||||
if !clipboard_available {
|
||||
return;
|
||||
}
|
||||
drop(navigator.clipboard().write_text(&text));
|
||||
event.prevent_default();
|
||||
});
|
||||
if let Err(error) =
|
||||
target.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
|
||||
{
|
||||
log::error!("Failed to register the GPUIX browser copy listener: {error:?}");
|
||||
return None;
|
||||
}
|
||||
Some(Self { target, closure })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
impl Drop for WebCopyListener {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self
|
||||
.target
|
||||
.remove_event_listener_with_callback("keydown", self.closure.as_ref().unchecked_ref())
|
||||
{
|
||||
log::error!("Failed to remove the GPUIX browser copy listener: {error:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
fn web_event_callback(callback: js_sys::Function) -> EventCallback {
|
||||
Rc::new(move |payload| {
|
||||
@@ -1770,6 +1828,7 @@ pub struct WebGpuixRenderer {
|
||||
tree: Arc<Mutex<RetainedTree>>,
|
||||
selection: SharedSelection,
|
||||
event_callback: EventCallback,
|
||||
_copy_listener: Option<WebCopyListener>,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
@@ -1777,9 +1836,11 @@ pub struct WebGpuixRenderer {
|
||||
impl WebGpuixRenderer {
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(constructor)]
|
||||
pub fn new(event_callback: js_sys::Function) -> Self {
|
||||
let selection = SharedSelection::default();
|
||||
Self {
|
||||
tree: Arc::new(Mutex::new(RetainedTree::new())),
|
||||
selection: SharedSelection::default(),
|
||||
_copy_listener: WebCopyListener::new(selection.clone()),
|
||||
selection,
|
||||
event_callback: web_event_callback(event_callback),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ thread_local! {
|
||||
}
|
||||
|
||||
/// A zero-size canvas that clears the per-frame registries and installs the
|
||||
/// frame's copy and mouse-down listeners. Paint it FIRST in the root, before
|
||||
/// any text, so each frame holds exactly that frame's visible text elements
|
||||
/// in paint order.
|
||||
/// frame's mouse-down listener and the desktop copy listener. Paint it FIRST in
|
||||
/// the root, before any text, so each frame holds exactly that frame's visible
|
||||
/// text elements in paint order.
|
||||
pub fn selection_frame_reset(selection: SharedSelection) -> impl IntoElement {
|
||||
canvas(
|
||||
|_, _, _| (),
|
||||
@@ -71,6 +71,7 @@ pub fn selection_frame_reset(selection: SharedSelection) -> impl IntoElement {
|
||||
REGISTRY.with(|r| r.borrow_mut().clear());
|
||||
START_REGIONS.with(|r| r.borrow_mut().clear());
|
||||
PAINTED.with(|p| p.borrow_mut().clear());
|
||||
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
|
||||
register_copy_listener(window, &selection);
|
||||
register_down_listener(window, &selection);
|
||||
},
|
||||
@@ -441,6 +442,7 @@ fn register_down_listener(window: &mut Window, selection: &SharedSelection) {
|
||||
});
|
||||
let mut sel = selection.lock();
|
||||
if let Some((key, text, ix)) = hit {
|
||||
window.blur();
|
||||
match e.click_count {
|
||||
2 => {
|
||||
let range = selection::word_range(&text, ix);
|
||||
@@ -496,11 +498,7 @@ fn register_listeners(window: &mut Window, key: &Arc<str>, selection: &SharedSel
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the frame's single Cmd+C / Ctrl+C listener.
|
||||
///
|
||||
/// GPUIX has no keymap or action system, so this reads the raw keystroke.
|
||||
/// It lives on the frame reset rather than on each text element: registering it
|
||||
/// per element made one Cmd+C write the clipboard once per visible text node.
|
||||
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
|
||||
fn register_copy_listener(window: &mut Window, selection: &SharedSelection) {
|
||||
use gpui::{ClipboardItem, DispatchPhase, KeyDownEvent};
|
||||
|
||||
@@ -509,12 +507,11 @@ fn register_copy_listener(window: &mut Window, selection: &SharedSelection) {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
let m = &e.keystroke.modifiers;
|
||||
if e.keystroke.key != "c" || !(m.platform || m.control) {
|
||||
let modifiers = &e.keystroke.modifiers;
|
||||
if e.keystroke.key != "c" || !(modifiers.platform || modifiers.control) {
|
||||
return;
|
||||
}
|
||||
// Read out of the lock before touching platform code: the clipboard
|
||||
// backend is out of our control and must never run under our mutex.
|
||||
// Release the selection lock before entering the platform clipboard.
|
||||
let text = selection.lock().selected_text();
|
||||
if let Some(text) = text {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
||||
|
||||
Reference in new issue
Block a user