Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 100 additions & 8 deletions crates/gpui_web/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,11 @@ impl WebWindowInner {
key_char: key_char.clone(),
};

if is_paste_keystroke(&keystroke, this.is_mac) {
*this.pending_paste_keystroke.borrow_mut() = Some(keystroke);
return;
}

let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held,
Expand Down Expand Up @@ -455,6 +460,11 @@ impl WebWindowInner {
key_char,
};

if is_paste_keystroke(&keystroke, this.is_mac) {
this.pending_paste_keystroke.borrow_mut().take();
return;
}

let result = this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
if let Some(result) = result {
if !result.propagate {
Expand All @@ -464,11 +474,6 @@ impl WebWindowInner {
})
}

/// Paste is delivered through the DOM `paste` event rather than
/// `Platform::read_from_clipboard`: the browser's asynchronous clipboard
/// read API cannot fit that synchronous signature, while `ClipboardEvent`
/// exposes `clipboardData` synchronously inside the event. It fires for
/// any browser-initiated paste (keyboard, menu bar, context menu).
fn register_paste(self: &Rc<Self>) -> EventListenerHandle {
let this = Rc::clone(self);
self.listen_input("paste", move |event: JsValue| {
Expand All @@ -484,9 +489,18 @@ impl WebWindowInner {
}

event.prevent_default();
this.with_input_handler(|handler| {
handler.replace_text_in_range(None, &text);
});
let keystroke = this
.pending_paste_keystroke
.borrow_mut()
.take()
.unwrap_or_else(|| paste_keystroke(this.is_mac));
*this.pasting_clipboard_item.borrow_mut() = Some(gpui::ClipboardItem::new_string(text));
this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: false,
prefer_character_input: false,
}));
this.pasting_clipboard_item.borrow_mut().take();
})
}

Expand Down Expand Up @@ -675,6 +689,44 @@ fn is_modifier_only_key(key: &str) -> bool {
)
}

fn is_paste_keystroke(keystroke: &Keystroke, is_mac: bool) -> bool {
let Modifiers {
control,
alt,
shift,
platform,
function,
} = keystroke.modifiers;
if function || alt {
return false;
}

if is_mac {
keystroke.key == "v" && platform && !control
} else {
(keystroke.key == "v" && control && !platform)
|| (keystroke.key == "insert" && shift && !control && !platform)
}
}

fn paste_keystroke(is_mac: bool) -> Keystroke {
Keystroke {
modifiers: if is_mac {
Modifiers {
platform: true,
..Modifiers::default()
}
} else {
Modifiers {
control: true,
..Modifiers::default()
}
},
key: "v".to_string(),
key_char: None,
}
}

/// Whether a keystroke with these modifiers produces text to insert.
///
/// On macOS, Option participates in text entry (e.g. option-n composes "~"
Expand Down Expand Up @@ -726,3 +778,43 @@ fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point<Pixels> {
// offset_x/offset_y give position relative to the target element's padding edge
point(px(event.offset_x() as f32), px(event.offset_y() as f32))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn recognizes_browser_paste_keystrokes() {
assert!(is_paste_keystroke(&paste_keystroke(true), true));
assert!(is_paste_keystroke(&paste_keystroke(false), false));

let mut plain_key = paste_keystroke(false);
plain_key.modifiers = Modifiers::default();
assert!(!is_paste_keystroke(&plain_key, false));

let mut wrong_platform = paste_keystroke(true);
wrong_platform.modifiers = Modifiers {
control: true,
..Modifiers::default()
};
assert!(!is_paste_keystroke(&wrong_platform, true));
}

#[test]
fn recognizes_alternate_paste_bindings() {
let mut paste_without_formatting = paste_keystroke(false);
paste_without_formatting.modifiers.shift = true;
assert!(is_paste_keystroke(&paste_without_formatting, false));

let shift_insert = Keystroke {
modifiers: Modifiers {
shift: true,
..Modifiers::default()
},
key: "insert".to_string(),
key_char: None,
};
assert!(is_paste_keystroke(&shift_insert, false));
assert!(!is_paste_keystroke(&shift_insert, true));
}
}
5 changes: 4 additions & 1 deletion crates/gpui_web/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub struct WebPlatform {
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
prepared_window: Rc<RefCell<Option<PreparedWebWindow>>>,
window_lifecycle: Rc<Cell<WebWindowLifecycle>>,
pasting_clipboard_item: Rc<RefCell<Option<ClipboardItem>>>,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
_cursor_restore_listeners: Vec<EventListenerHandle>,
Expand Down Expand Up @@ -166,6 +167,7 @@ impl WebPlatform {
wgpu_context: Rc::new(RefCell::new(None)),
prepared_window: Rc::new(RefCell::new(None)),
window_lifecycle: Rc::new(Cell::new(WebWindowLifecycle::Available)),
pasting_clipboard_item: Rc::new(RefCell::new(None)),
cursor_visible,
last_cursor_css,
_cursor_restore_listeners: cursor_restore_listeners,
Expand Down Expand Up @@ -379,6 +381,7 @@ impl Platform for WebPlatform {
self.browser_window.clone(),
self.window_lifecycle.clone(),
self.active_window.clone(),
self.pasting_clipboard_item.clone(),
);
match window {
Ok(window) => {
Expand Down Expand Up @@ -552,7 +555,7 @@ impl Platform for WebPlatform {
}

fn read_from_clipboard(&self) -> Option<ClipboardItem> {
None
self.pasting_clipboard_item.borrow().clone()
}

fn write_to_clipboard(&self, item: ClipboardItem) {
Expand Down
16 changes: 11 additions & 5 deletions crates/gpui_web/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ use std::sync::Arc;
use std::{cell::Cell, cell::RefCell, rc::Rc};

use gpui::{
AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs,
Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowControls, WindowDecorations, WindowParams, px,
AnyWindowHandle, Bounds, Capslock, ClipboardItem, Decorations, DevicePixels,
DispatchEventResult, GpuSpecs, Keystroke, Modifiers, MouseButton, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton,
PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
WindowParams, px,
};
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig, wgpu};
use wasm_bindgen::prelude::*;
Expand Down Expand Up @@ -56,6 +57,8 @@ pub(crate) struct WebWindowInner {
pub(crate) last_physical_size: Cell<(u32, u32)>,
pub(crate) notify_scale: Cell<bool>,
pub(crate) is_composing: Cell<bool>,
pub(crate) pasting_clipboard_item: Rc<RefCell<Option<ClipboardItem>>>,
pub(crate) pending_paste_keystroke: RefCell<Option<Keystroke>>,
mql_handle: RefCell<Option<MqlHandle>>,
pending_physical_size: Cell<Option<(u32, u32)>>,
raf_id: Cell<Option<i32>>,
Expand Down Expand Up @@ -118,6 +121,7 @@ impl WebWindow {
browser_window: web_sys::Window,
lifecycle: Rc<Cell<WebWindowLifecycle>>,
active_window: Rc<RefCell<Option<AnyWindowHandle>>>,
pasting_clipboard_item: Rc<RefCell<Option<ClipboardItem>>>,
) -> anyhow::Result<Self> {
let document = browser_window
.document()
Expand Down Expand Up @@ -191,6 +195,8 @@ impl WebWindow {
last_physical_size: Cell::new((0, 0)),
notify_scale: Cell::new(false),
is_composing: Cell::new(false),
pasting_clipboard_item,
pending_paste_keystroke: RefCell::new(None),
mql_handle: RefCell::new(None),
pending_physical_size: Cell::new(None),
raf_id: Cell::new(None),
Expand Down
Loading