feat: in-place save (File System Access), image paste, print wiring, web capture

The last high-severity web gap plus a small integration bundle.

In-place save: with the File System Access API (Chromium/Edge), Save
now works like desktop. Plain Save on a tab with a retained handle
writes straight through — no picker, no download, no "(1)" copies.
First save or format change opens showSaveFilePicker AT the confirm
gesture (the worker serialization that follows can outlast the
transient-activation window; retained handles need no activation to
write). Picker cancel cancels the save like desktop; picker or write
failure falls back to the legacy download with a note; Firefox/Safari
keep the download flow untouched (feature-detected). Handles are
keyed by tab id in a thread-local registry (Message must be Send —
JsValue cannot ride in messages or App state); closing a tab drops
its handle; Save As re-picks. Dirty clears only on successful
persistence, same race guard as before.

Image paste: Ctrl+V of a screenshot inserts it as an image reference
— the clipboard reader falls back to image/* items when neither the
entity clipboard nor text has anything (precedence preserved), the
bytes enter the image source registry as pasted-<timestamp>.<ext>,
and attach reuses the IMAGE command's model-creation path verbatim.

Browser print: beforeprint snapshots the WebGL canvas into a print-
only <img> (the capture_canvas technique), so the browser's own print
menu produces the drawing instead of a blank page.

Automation capture on web: the capture op no longer calls the
synchronous GPU-map screenshot (dead on wasm); main's frame-parked
capture waits for a presented frame and reads via capture_canvas — the
same workaround the save-thumbnail path uses — delivering the PNG as a
download.

(cherry picked from commit 6e89b36d)

Ported onto main (#1717 review): the web capture rides main's existing
frame-parked capture (one ControlCaptureFrame path, ignoring the timer
tick on wasm) instead of a second parked-capture state; closing a tab
drops its file handle inside main's on_tab_close.
This commit is contained in:
Afonsop authored and Hakan Seven committed 2026-10-12 12:02:05 +03:00
1 parent 0459f20129
commit 835e8d469f
33 files changed
+1084 -29

No files matched your search

+63 -3
View File
@@ -6810,9 +6810,24 @@ impl OpenCADStudio {
self.focus_cmd_input()
}
Text::EmptyOrUnsupported => {
self.command_line
.push_error(crate::tr!("clipboard", "no-supported-content").as_ref());
Task::none()
// Web: no text — the clipboard may still hold an IMAGE
// (a pasted screenshot). Read it through the async
// Clipboard API and attach it the way the IMAGE
// command's pick does; only when that also finds
// nothing does the unsupported-content error surface.
// Native has no image route off the sync clipboard.
#[cfg(target_arch = "wasm32")]
return Task::perform(
crate::sys::read_clipboard_image(),
Message::SystemClipboardPasteImage,
);
#[cfg(not(target_arch = "wasm32"))]
{
self.command_line.push_error(
crate::tr!("clipboard", "no-supported-content").as_ref(),
);
Task::none()
}
}
Text::Unavailable => {
self.command_line
@@ -6832,6 +6847,19 @@ impl OpenCADStudio {
}
}
// Web: the clipboard-image fallback of a text-less paste either
// attaches the image or falls back to the same error the text
// path reports when nothing readable was on the clipboard.
#[cfg(target_arch = "wasm32")]
Message::SystemClipboardPasteImage(image) => match image {
Some((name, bytes)) => self.paste_clipboard_image(name, bytes),
None => {
self.command_line
.push_error(crate::tr!("clipboard", "no-supported-content").as_ref());
Task::none()
}
},
Message::SelectAllShortcut => {
let i = self.active_tab;
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
@@ -10275,6 +10303,38 @@ impl OpenCADStudio {
result,
),
#[cfg(target_arch = "wasm32")]
Message::WebSaveHandlePicked {
tab_id,
filename,
ext,
version,
picked,
} => self.on_web_save_handle_picked(tab_id, filename, ext, version, picked),
#[cfg(target_arch = "wasm32")]
Message::WebSaveWritten {
tab_id,
filename,
autosave_key,
version,
geometry_epoch,
edit_revision,
camera_generation,
result,
bytes,
} => self.on_web_save_written(
tab_id,
filename,
autosave_key,
version,
geometry_epoch,
edit_revision,
camera_generation,
result,
bytes,
),
#[cfg(target_arch = "wasm32")]
Message::WebSaveallSerialized { skipped, results } => {
self.on_web_saveall_serialized(skipped, results)