forked from 1b-agent/opencadstudio-upstream
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:
1 parent
dd40e1f632
commit
5f08b77ebc
5 files changed
+147
No files matched your search
@@ -292,6 +292,90 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
Message::WebFieldPaste => {
|
||||
// The MText / inline-TEXT editors have their own web paste
|
||||
// paths — don't double-feed them.
|
||||
if self.mtext_editor.is_some() || self.text_inline.is_some() {
|
||||
return self.on_paste_shortcut();
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Task::perform(
|
||||
crate::sys::read_clipboard_text(),
|
||||
Message::WebFieldPasteText,
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::WebFieldPasteText(text) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(t) = &text {
|
||||
crate::sys::synthesize_typing(t);
|
||||
}
|
||||
let _ = text;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::WebFieldCopy => {
|
||||
// Walk the widget tree for the focused text input's visible
|
||||
// text. iced calls `text_input` then `focusable` back-to-back
|
||||
// on the same widget, so remembering the last text seen pairs
|
||||
// it with the focus check. (An empty field reports its
|
||||
// placeholder — that's iced's "visible text" contract.)
|
||||
use iced::advanced::widget::operation::{
|
||||
Focusable, Outcome, TextInput,
|
||||
};
|
||||
use iced::advanced::widget::{Id, Operation};
|
||||
#[derive(Default)]
|
||||
struct FocusedText {
|
||||
last_text: Option<String>,
|
||||
found: Option<String>,
|
||||
}
|
||||
impl Operation<Option<String>> for FocusedText {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: iced::Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.last_text = Some(state.text().to_owned());
|
||||
}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: iced::Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
let text = self.last_text.take();
|
||||
if state.is_focused() && self.found.is_none() {
|
||||
self.found = text;
|
||||
}
|
||||
}
|
||||
fn traverse(
|
||||
&mut self,
|
||||
operate: &mut dyn FnMut(&mut dyn Operation<Option<String>>),
|
||||
) {
|
||||
operate(self);
|
||||
}
|
||||
fn finish(&self) -> Outcome<Option<String>> {
|
||||
Outcome::Some(self.found.clone())
|
||||
}
|
||||
}
|
||||
iced::advanced::widget::operate(FocusedText::default())
|
||||
.map(Message::WebFieldCopyText)
|
||||
}
|
||||
|
||||
Message::WebFieldCopyText(text) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(t) = &text {
|
||||
if !t.is_empty() {
|
||||
crate::sys::write_clipboard_text(t);
|
||||
}
|
||||
}
|
||||
let _ = text;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SnapOverridePick(t) => {
|
||||
self.snap_override_popup = None;
|
||||
self.snapper.set_override(t);
|
||||
|
||||
Reference in new issue
Block a user