From 43d9542c3d344e2dc5d0c0ba17c8a26c2aa16e45 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Thu, 5 Nov 2026 00:24:14 +0000 Subject: [PATCH] =?UTF-8?q?#1537:=20review=20=E2=80=94=20a=20browser's=20M?= =?UTF-8?q?od+Shift+V=20paste=20event=20pastes=20text=20only,=20once=20(Co?= =?UTF-8?q?deRabbit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the web the key no longer pastes as well: the paste event that follows carries the text. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/keys/canvas.rs | 3 +++ crates/ui-egui/src/keys/shortcuts.rs | 10 +++++++++- crates/ui-egui/src/keys/tests.rs | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/keys/canvas.rs b/crates/ui-egui/src/keys/canvas.rs index f5890f382c..944c7f8df8 100644 --- a/crates/ui-egui/src/keys/canvas.rs +++ b/crates/ui-egui/src/keys/canvas.rs @@ -510,9 +510,12 @@ fn handle_event(app: &mut WordApp, ctx: &egui::Context, e: egui::Event) { egui::Event::Paste(t) => { // Mod+Alt+V arrives as a paste too (the shell reads the clipboard): Paste Special. let m = ctx.input(|i| i.modifiers); + // A browser sends Mod+Shift+V as a paste too: Keep Text Only, as the key does on the + // desktop (#1537). (egui-winit sends that chord as a key, and Shift+Insert has no Mod.) let id = match (m.alt, m.shift) { (true, true) => "edit.pasteText", (true, false) => "edit.pasteSpecial", + (false, true) if m.command => "edit.pasteText", _ => "edit.paste", }; let _ = app.run(id, json!({"text": t})); diff --git a/crates/ui-egui/src/keys/shortcuts.rs b/crates/ui-egui/src/keys/shortcuts.rs index 81819e78bc..1e0e56d30d 100644 --- a/crates/ui-egui/src/keys/shortcuts.rs +++ b/crates/ui-egui/src/keys/shortcuts.rs @@ -134,7 +134,15 @@ pub(super) fn dispatch(app: &mut WordApp, key: Key, m: Modifiers) -> bool { } // Mod+Shift+V pastes the formatting Mod+Shift+C picked up; with none picked up it pastes // the clipboard as text only, as Word for Windows does (#1537, #4943). - let id = if spec.id == "edit.pasteFormat" && app.session.painter.is_none() { "edit.pasteText" } else { spec.id }; + let id = if spec.id == "edit.pasteFormat" && app.session.painter.is_none() { + // On the web the browser's paste event that follows carries the text: paste once, there. + if cfg!(target_arch = "wasm32") { + return true; + } + "edit.pasteText" + } else { + spec.id + }; let _ = app.run(id, json!({})); return true; } diff --git a/crates/ui-egui/src/keys/tests.rs b/crates/ui-egui/src/keys/tests.rs index 528f3bf83c..f8ee0655bd 100644 --- a/crates/ui-egui/src/keys/tests.rs +++ b/crates/ui-egui/src/keys/tests.rs @@ -133,6 +133,21 @@ fn only_plain_paste_releases_paste_a_clipboard_picture() { assert!(!is_paste_special_release(Key::V, m(true, true, true)) && !is_paste_special_release(Key::V, m(true, false, false))); } +/// #1537: a browser sends Mod+Shift+V as a paste event; it pastes text only, in the destination's +/// formatting, though the clipboard also holds bold HTML. Plain Mod+V keeps the HTML's bold. +#[test] +fn a_mod_shift_paste_event_keeps_text_only() { + for (modifiers, bold) in [(Modifiers::COMMAND | Modifiers::SHIFT, false), (Modifiers::COMMAND, true)] { + let services = crate::Services { clipboard_html: Some(Box::new(|| Some("

external

".into()))), ..Default::default() }; + let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), services); + let ctx = egui::Context::default(); + mods_frame(&ctx, &mut a, modifiers, vec![egui::Event::Paste("external".into())]); + assert_eq!(a.session.doc.plain_text(wordcraft_doc::StoryRef::Body), "external", "{modifiers:?}"); + let state = a.session.run("format.state", &json!({})).unwrap(); + assert_eq!(state.get("bold").and_then(serde_json::Value::as_bool) == Some(true), bold, "{modifiers:?}: {state}"); + } +} + /// An empty document whose clipboard holds a 4 × 4 picture. fn picture_app() -> WordApp { use crate::paste_picture::ClipboardPicture;