feat(web): copy/paste in text fields via the browser clipboard (#346)

iced's clipboard rides window_clipboard, which has no wasm backend — a
focused text_input captured Ctrl+C/V on the web and did nothing.

Ctrl+V (captured, wasm only): read the async browser clipboard (the
keypress is a user gesture, so it's permitted) and replay the text as
synthetic KeyboardEvents on the winit canvas — the only route into a
focused text_input, since iced exposes no insert-text operation.
Control characters are dropped and the length is capped. The MText and
inline-TEXT editors keep their dedicated paste paths (no double feed),
and an unfocused Ctrl+V still runs PASTECLIP.

Ctrl+C (captured, wasm only): a widget operation pairs each text_input
with its focus state (iced fires both callbacks back-to-back on the
same widget) to collect the focused field's visible text, then writes
it with navigator.clipboard.writeText. Whole-field text, not the
selection — iced doesn't expose the selected range.

Native behaviour is unchanged.

Closes #346

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-22 12:47:19 +03:00
1 parent dd40e1f632
commit 5f08b77ebc
5 files changed
+147

No files matched your search

+38
View File
@@ -31,6 +31,44 @@ pub async fn read_clipboard_text() -> Option<String> {
value.as_string()
}
/// Web: write text to the system clipboard (fire-and-forget). Backs Ctrl+C in
/// plain text_input fields, whose iced-internal copy is a no-op on the web
/// (#346). The triggering keypress is a user gesture, so the write is allowed.
#[cfg(target_arch = "wasm32")]
pub fn write_clipboard_text(text: &str) {
if let Some(window) = web_sys::window() {
let _ = window.navigator().clipboard().write_text(text);
}
}
/// Web: replay `text` into the focused iced widget as synthetic KeyboardEvents
/// dispatched on the winit canvas. This is the only route INTO a focused
/// text_input on the web — iced's clipboard read is a no-op there and it
/// exposes no insert-text operation (#346). Control characters are dropped
/// and the length capped so a runaway clipboard can't wedge the event loop.
#[cfg(target_arch = "wasm32")]
pub fn synthesize_typing(text: &str) {
let Some(canvas) = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.query_selector("canvas").ok().flatten())
else {
return;
};
for ch in text.chars().filter(|c| !c.is_control()).take(1024) {
let init = web_sys::KeyboardEventInit::new();
init.set_key(&ch.to_string());
init.set_bubbles(true);
init.set_cancelable(true);
for kind in ["keydown", "keyup"] {
if let Ok(ev) =
web_sys::KeyboardEvent::new_with_keyboard_event_init_dict(kind, &init)
{
let _ = canvas.dispatch_event(&ev);
}
}
}
}
/// 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