forked from 1b-agent/opencadstudio-upstream
fix(web): paste clipboard text into the TEXT / MText editors
Ctrl+V was globally bound to the entity paste command, so it never pasted into an open text editor, and even when routed there iced's clipboard read is a no-op on the web. Route Ctrl+V to the focused editor and, on the web, read the system clipboard via the browser's async Clipboard API (the keypress is a user gesture, so the read is permitted): - MText editor: insert at the caret (web + native). - TEXT field: append on the web; native lets the text_input paste itself. - No editor open: falls through to the entity paste command as before. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
54106ca9b0
commit
e1c5917644
5 files changed
+77
-1
No files matched your search
@@ -106,6 +106,9 @@ web-sys = { version = "0.3", features = [
|
||||
"Blob",
|
||||
"Url",
|
||||
"Response",
|
||||
# Async clipboard read for paste into the text/MText editors (iced's own
|
||||
# clipboard read is a no-op on the web).
|
||||
"Clipboard",
|
||||
] }
|
||||
# Browsers without WebGPU (e.g. Firefox by default) need wgpu's WebGL2 backend;
|
||||
# `webgl` enables it. `fira-sans` embeds the default UI font — the web has no
|
||||
|
||||
@@ -799,6 +799,14 @@ pub enum Message {
|
||||
PollWebFonts,
|
||||
/// Web: a per-script font finished fetching — `Ok(bytes)` or `Err(reason)`.
|
||||
WebFontLoaded(crate::scene::text::web_font::Script, Result<Vec<u8>, String>),
|
||||
/// Ctrl+V. Routed by `update`: into the open text/MText editor (via an async
|
||||
/// system-clipboard read, which is the only paste path that works on the
|
||||
/// web) or, with no editor open, the entity paste command.
|
||||
PasteShortcut,
|
||||
/// System-clipboard text read for the MText editor (`None` = empty/denied).
|
||||
MTextPasteClip(Option<String>),
|
||||
/// System-clipboard text read for the single-line TEXT editor.
|
||||
TextInlinePasteClip(Option<String>),
|
||||
OpenFile,
|
||||
/// File picker returned. `Some((path, size_in_bytes))` → start loading;
|
||||
/// `None` → user cancelled the dialog (no overlay shown).
|
||||
|
||||
@@ -4840,6 +4840,57 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// Ctrl+V. The MText editor and (on the web) the TEXT editor read the
|
||||
// system clipboard asynchronously — the only paste path that works
|
||||
// in the browser, where the synchronous clipboard the iced
|
||||
// text_input expects is empty. With no editor open it falls through
|
||||
// to the entity paste command.
|
||||
Message::PasteShortcut => {
|
||||
if self.mtext_editor.is_some() {
|
||||
// Web reads via the browser's async Clipboard API (iced's
|
||||
// sync clipboard read returns nothing there); native uses
|
||||
// iced's clipboard.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Task::perform(
|
||||
crate::sys::read_clipboard_text(),
|
||||
Message::MTextPasteClip,
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
return iced::clipboard::read().map(Message::MTextPasteClip);
|
||||
}
|
||||
if self.text_inline.is_some() {
|
||||
// Web: the iced text_input can't reach the async clipboard,
|
||||
// so paste it ourselves. Native: the focused text_input
|
||||
// already handled Ctrl+V — doing it here would duplicate.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Task::perform(
|
||||
crate::sys::read_clipboard_text(),
|
||||
Message::TextInlinePasteClip,
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
return Task::none();
|
||||
}
|
||||
Task::done(Message::Command("PASTECLIP".to_string()))
|
||||
}
|
||||
Message::MTextPasteClip(text) => {
|
||||
if let Some(text) = text.filter(|t| !t.is_empty()) {
|
||||
// CR/LF arrive as line breaks; MText keeps "\n", drop "\r".
|
||||
self.mtext_type(&text.replace('\r', ""));
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::TextInlinePasteClip(text) => {
|
||||
if let Some(text) = text.filter(|t| !t.is_empty()) {
|
||||
// Single-line field: collapse newlines, append at the end.
|
||||
let flat = text.replace(['\r', '\n'], " ");
|
||||
if let Some(ed) = self.text_inline.as_mut() {
|
||||
ed.value.push_str(&flat);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::TextInlineOk => {
|
||||
self.text_inline_commit();
|
||||
Task::none()
|
||||
|
||||
+1
-1
@@ -1769,7 +1769,7 @@ impl OpenCADStudio {
|
||||
"y" => Some(Message::Redo),
|
||||
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
||||
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
||||
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
||||
"v" => Some(Message::PasteShortcut),
|
||||
_ => None,
|
||||
},
|
||||
// Printable glyphs are already handled by the
|
||||
|
||||
+14
@@ -17,6 +17,20 @@ pub fn open_url(url: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Web: read text from the system clipboard via the async Clipboard API.
|
||||
/// iced's own `clipboard::read` is a no-op on the web (the browser clipboard is
|
||||
/// async + permission-gated), so the editor paste paths use this instead. The
|
||||
/// Ctrl+V keypress that drives it is a user gesture, so the read is permitted.
|
||||
/// Returns `None` when denied, empty, or unsupported.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn read_clipboard_text() -> Option<String> {
|
||||
let clipboard = web_sys::window()?.navigator().clipboard();
|
||||
let value = wasm_bindgen_futures::JsFuture::from(clipboard.read_text())
|
||||
.await
|
||||
.ok()?;
|
||||
value.as_string()
|
||||
}
|
||||
|
||||
/// Turn an `rfd` file handle into a `PathBuf` the rest of the app keys on.
|
||||
///
|
||||
/// Desktop returns the real filesystem path. The browser has no path, so we
|
||||
|
||||
Reference in new issue
Block a user