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 <noreply@anthropic.com>
This commit is contained in:
parent
54106ca9b0
commit
e1c5917644
5 changed files with 77 additions and 1 deletions
|
|
@ -106,6 +106,9 @@ web-sys = { version = "0.3", features = [
|
||||||
"Blob",
|
"Blob",
|
||||||
"Url",
|
"Url",
|
||||||
"Response",
|
"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;
|
# 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
|
# `webgl` enables it. `fira-sans` embeds the default UI font — the web has no
|
||||||
|
|
|
||||||
|
|
@ -799,6 +799,14 @@ pub enum Message {
|
||||||
PollWebFonts,
|
PollWebFonts,
|
||||||
/// Web: a per-script font finished fetching — `Ok(bytes)` or `Err(reason)`.
|
/// Web: a per-script font finished fetching — `Ok(bytes)` or `Err(reason)`.
|
||||||
WebFontLoaded(crate::scene::text::web_font::Script, Result<Vec<u8>, String>),
|
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,
|
OpenFile,
|
||||||
/// File picker returned. `Some((path, size_in_bytes))` → start loading;
|
/// File picker returned. `Some((path, size_in_bytes))` → start loading;
|
||||||
/// `None` → user cancelled the dialog (no overlay shown).
|
/// `None` → user cancelled the dialog (no overlay shown).
|
||||||
|
|
|
||||||
|
|
@ -4840,6 +4840,57 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
Task::none()
|
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 => {
|
Message::TextInlineOk => {
|
||||||
self.text_inline_commit();
|
self.text_inline_commit();
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -1769,7 +1769,7 @@ impl OpenCADStudio {
|
||||||
"y" => Some(Message::Redo),
|
"y" => Some(Message::Redo),
|
||||||
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
||||||
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
||||||
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
"v" => Some(Message::PasteShortcut),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
// Printable glyphs are already handled by the
|
// Printable glyphs are already handled by the
|
||||||
|
|
|
||||||
14
src/sys.rs
14
src/sys.rs
|
|
@ -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.
|
/// 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
|
/// Desktop returns the real filesystem path. The browser has no path, so we
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue