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); it waits one presented
frame and reads via capture_canvas — the same workaround the save-
thumbnail path uses — delivering the PNG as a download.
This commit is contained in:
Afonsop committed 2026-09-21 11:17:23 +01:00
1 parent 369e2c8b5a
commit 6e89b36d44
34 files changed
+1138 -34

No files matched your search

+63 -5
View File
@@ -113,6 +113,13 @@ pub(super) struct State {
serial: u64,
events: VecDeque<Value>,
pub(super) routing: bool,
/// Web `capture` op's destination path, parked until the next presented
/// frame arrives: the canvas readback (sys::capture_canvas) only sees the
/// drawing before the browser clears the WebGL drawing buffer, so the op
/// stays pending across frames instead of using iced's GPU-mapped window
/// screenshot, which cannot run on wasm.
#[cfg(target_arch = "wasm32")]
pub(super) pending_capture: Option<String>,
}
impl State {
pub(super) fn new() -> Self {
@@ -895,12 +902,26 @@ impl OpenCADStudio {
}
}
"capture" => {
let window = self
.main_window
.ok_or_else(|| failure("gui_required", "Capture requires a GUI window"))?;
let path = string(req, "path")?.to_owned();
iced::window::screenshot(window)
.map(move |s| Message::ControlScreenshot(path.clone(), Some(s)))
// Web: iced's window screenshot needs a synchronous GPU map
// wasm cannot provide. Park the path until the next presented
// frame (the view's frames subscription turns it into
// ControlCaptureFrame) and read the canvas there — the same
// readback the save-thumbnail path uses. The op stays pending
// until ControlScreenshot lands from that frame.
#[cfg(target_arch = "wasm32")]
{
self.control.pending_capture = Some(path);
Task::none()
}
#[cfg(not(target_arch = "wasm32"))]
{
let window = self.main_window.ok_or_else(|| {
failure("gui_required", "Capture requires a GUI window")
})?;
iced::window::screenshot(window)
.map(move |s| Message::ControlScreenshot(path.clone(), Some(s)))
}
}
"stop" => {
self.control.enabled = false;
@@ -998,6 +1019,12 @@ impl OpenCADStudio {
if !self.active_save_jobs.is_empty() || self.pending_native_thumbnail_save.is_some() {
return;
}
// A web capture is still waiting for its presented frame; completing
// now would report the op done before ControlScreenshot exists.
#[cfg(target_arch = "wasm32")]
if self.control.pending_capture.is_some() {
return;
}
let p = self.control.pending.take().unwrap();
let previous = self.active_tab;
if let Some(index) = p
@@ -1089,6 +1116,20 @@ impl OpenCADStudio {
json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out})
}
/// Web capture frame: the just-presented frame's canvas contents are
/// still readable, so a parked `capture` op takes its screenshot here and
/// settles. Same window of validity the save-thumbnail flow reads the
/// canvas in — one frame after the request, before the buffer clears.
#[cfg(target_arch = "wasm32")]
pub(super) fn on_control_capture_frame(&mut self) -> Task<Message> {
if let Some(path) = self.control.pending_capture.take() {
let screenshot = crate::sys::capture_canvas();
self.control_screenshot(path, screenshot);
self.control_settle();
}
Task::none()
}
pub(super) fn control_screenshot(
&mut self,
path: String,
@@ -1151,6 +1192,23 @@ impl OpenCADStudio {
image::imageops::FilterType::Triangle,
);
}
#[cfg(target_arch = "wasm32")]
{
// No filesystem behind a web "path": encode the PNG in
// memory and hand it to the browser as a download, named
// after the request's file name — the delivery every other
// web output uses.
let mut png = Vec::new();
image
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.map_err(|e| e.to_string())?;
let download = std::path::Path::new(&path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.clone());
crate::sys::download_bytes(&download, &png);
}
#[cfg(not(target_arch = "wasm32"))]
image
.save_with_format(&path, image::ImageFormat::Png)
.map_err(|e| e.to_string())?;
+57
View File
@@ -724,6 +724,14 @@ pub(super) struct OpenCADStudio {
pending_native_thumbnail_save: Option<PendingNativeThumbnailSave>,
#[cfg(target_arch = "wasm32")]
pending_web_thumbnail_save: Option<PendingWebThumbnailSave>,
/// Web: whether the save in flight came from a plain Save
/// (Save/QSAVE or the save-before-close flow), so the tab's retained
/// File System Access handle (see io::fs_access) may be written
/// through directly; Save As opens the dialog modal and must re-pick.
/// Set true by `save_with_default_format`, false by
/// `open_save_dialog_window`.
#[cfg(target_arch = "wasm32")]
save_plain_reuse_fs_handle: bool,
/// One web autosave round is running; ticks that fire while it lasts are
/// dropped instead of queued (the next interval retries).
#[cfg(target_arch = "wasm32")]
@@ -1955,6 +1963,12 @@ pub enum Message {
ControlStep(String, Box<Message>),
ControlTaskDone(String),
ControlScreenshot(String, Option<iced::window::Screenshot>),
/// Web: a presented frame arrived while a control `capture` op is parked
/// waiting for one, so the canvas readback can run before the browser
/// clears the WebGL drawing buffer. Native uses iced's window screenshot
/// directly and never emits this.
#[cfg(target_arch = "wasm32")]
ControlCaptureFrame,
ControlToggle,
Tick(Instant),
/// Periodic drain of plugin-to-host requests that arrived outside a host
@@ -1978,6 +1992,12 @@ pub enum Message {
PasteShortcut,
/// Completion of a system text clipboard read requested by PASTECLIP.
SystemClipboardPaste(SystemClipboardText),
/// Web: completion of the clipboard-image read the paste fallback starts
/// when no text was found — `Some((name, bytes))` attaches the image,
/// `None` reports the usual unsupported-content error. Native never
/// constructs it.
#[cfg(target_arch = "wasm32")]
SystemClipboardPasteImage(Option<(String, Vec<u8>)>),
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
/// drawing objects otherwise (#236).
SelectAllShortcut,
@@ -2372,6 +2392,41 @@ pub enum Message {
camera_generation: u64,
result: Result<Vec<u8>, String>,
},
/// Web SAVE with the File System Access API: the showSaveFilePicker
/// promise settled. The picker ran in the save-confirm handler — the
/// user-gesture moment — because the serialization that follows may
/// outlast the transient-activation window; the retained handle (kept
/// in io::fs_access's tab-keyed registry, since a JsValue handle cannot
/// ride in a Send Message) is written to later without needing a
/// gesture. `Ok(Some(name))` = destination chosen (`name` is its real,
/// possibly renamed name); `Ok(None)` = the user dismissed the picker
/// (cancel the save, like desktop); `Err` = the picker itself failed
/// (fall back to the legacy download).
#[cfg(target_arch = "wasm32")]
WebSaveHandlePicked {
tab_id: u64,
filename: String,
ext: String,
version: acadrust::DxfVersion,
picked: Result<Option<String>, String>,
},
/// Web SAVE in-place completion: the retained FS Access handle's
/// createWritable write settled (on failure the task already fell back
/// to a download); run the shared save bookkeeping, including the OPFS
/// recent-copy store the download path performs. Same race-guard
/// markers as `WebSaveSerialized`.
#[cfg(target_arch = "wasm32")]
WebSaveWritten {
tab_id: u64,
filename: String,
autosave_key: String,
version: acadrust::DxfVersion,
geometry_epoch: u64,
edit_revision: u64,
camera_generation: u64,
result: Result<(), String>,
bytes: Vec<u8>,
},
/// Web WBLOCK: the serializer worker (or the inline fallback) produced
/// the block document bytes; download them under `filename`, then run
/// the same command-line bookkeeping as `WblockWriteFinished`.
@@ -3919,6 +3974,8 @@ impl OpenCADStudio {
#[cfg(target_arch = "wasm32")]
pending_web_thumbnail_save: None,
#[cfg(target_arch = "wasm32")]
save_plain_reuse_fs_handle: false,
#[cfg(target_arch = "wasm32")]
web_autosave_in_flight: false,
#[cfg(target_arch = "wasm32")]
web_autosave_warned: false,
+69
View File
@@ -117,6 +117,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// This tab is closing for good — drop its autosave recovery copy.
#[cfg(not(target_arch = "wasm32"))]
let _ = std::fs::remove_file(self.autosave_target(idx));
// …and, on the web, its retained in-place save handle: the
// next tab to reuse that id must start with a fresh pick
// instead of silently writing over the closed drawing's file.
#[cfg(target_arch = "wasm32")]
if let Some(closing_id) = self.tabs.get(idx).map(|t| t.id) {
crate::io::fs_access::drop_handle(closing_id);
}
// Only-tab case: when the lone non-start tab closes, fall
// back to the Start tab if it exists; otherwise spawn a
// fresh blank drawing (legacy behaviour).
@@ -1883,6 +1890,68 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
}
/// Attach a clipboard image read on the web (a pasted screenshot) the way
/// the IMAGE command's pick does: register the bytes name-keyed, decode
/// for the file's true pixel size, then commit the entity — but placed
/// directly at the cursor (0,0 before any pointer move) with the command's
/// default width, skipping the pick prompts a paste should not run.
#[cfg(target_arch = "wasm32")]
pub(super) fn paste_clipboard_image(&mut self, name: String, bytes: Vec<u8>) -> Task<Message> {
let i = self.active_tab;
if self.tabs[i].is_start {
self.command_line.push_error(
crate::tr!("common", "no-drawing-open-use-new-or-open-first").as_ref(),
);
return Task::none();
}
crate::scene::model::image_model::register_source(&name, std::sync::Arc::new(bytes));
let Some(decoded) = crate::scene::model::image_model::resolve_image(&name) else {
self.command_line.push_error(
crate::tr!("clipboard", "image-unsupported", name = name.clone()).as_ref(),
);
return Task::none();
};
let (pixels_w, pixels_h) = (decoded.source_width, decoded.source_height);
// Same default footprint as the IMAGE command's Enter placement: one
// world unit per 100 px (min 1), height from the true aspect ratio.
let width = (pixels_w as f64 / 100.0).max(1.0);
let aspect = if pixels_h == 0 {
1.0
} else {
pixels_w as f64 / pixels_h as f64
};
let height = width / aspect;
let origin = self.tabs[i].last_cursor_world;
let mut image = acadrust::entities::RasterImage::with_size(
&name,
acadrust::types::Vector3::new(origin.x, origin.y, origin.z),
pixels_w as f64,
pixels_h as f64,
width,
height,
);
image.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE
| acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
let short = std::path::Path::new(&name)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&name)
.to_string();
self.command_line.push_output(
crate::tr!(
"clipboard",
"pasted-image",
name = short,
width = pixels_w,
height = pixels_h
)
.as_ref(),
);
self.apply_cmd_result(crate::command::CmdResult::CommitAndExit(
acadrust::EntityType::RasterImage(image),
))
}
pub(super) fn on_qselect_open(&mut self) -> Task<Message> {
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
+9
View File
@@ -22,6 +22,15 @@ use iced::{mouse, Point, Task};
impl OpenCADStudio {
pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
// Every entry here is Save-As semantics (the SAVEAS command, the
// recovery save-as, the native failure redirects): the user is
// choosing a destination, so on the web the FS Access picker must
// open even when the tab already holds a handle (plain Save sets
// the flag back in save_with_default_format).
#[cfg(target_arch = "wasm32")]
{
self.save_plain_reuse_fs_handle = false;
}
// Default the format dropdown to the loaded file's own format — its
// DWG-vs-DXF kind (from the extension) and its version (from the parsed
// document) — so Save-As round-trips the format instead of silently
+257 -26
View File
@@ -1508,7 +1508,7 @@ impl OpenCADStudio {
#[cfg(target_arch = "wasm32")]
if self.web_session_focus_pending && self.web_autosave_restore_all_remaining == 0 {
self.web_session_focus_pending = false;
self.select_remembered_session_tab();
let _ = self.select_remembered_session_tab();
}
let mut recovery_report = recovery_needed.then(|| {
@@ -2193,10 +2193,12 @@ impl OpenCADStudio {
}
/// Web SAVE completion: the serializer worker (or inline fallback) has the
/// drawing bytes. The download and tab bookkeeping that used to run
/// synchronously inside `on_web_save_screenshot`, unchanged in semantics —
/// except the dirty flag is only cleared when the serialized snapshot is
/// still the tab's current state.
/// drawing bytes. When the tab holds a File System Access handle the
/// bytes go to it in place (createWritable needs no user gesture, so the
/// write may happen long after the pick); completion continues in
/// `on_web_save_written`. Otherwise the legacy download fires — the only
/// artifact of the save, launched even if the tab was closed while
/// serialization was in flight.
#[cfg(target_arch = "wasm32")]
pub(super) fn on_web_save_serialized(
&mut self,
@@ -2217,9 +2219,111 @@ impl OpenCADStudio {
return self.finish_web_save(false);
}
};
// In-place write through the handle picked at the confirm gesture
// (or an earlier one). The bookkeeping runs at write completion
// (WebSaveWritten) so a failed write cannot clear the dirty flag;
// the bytes ride back with the completion so the OPFS recent store
// gets the same copy the download path uses.
if let Some(picked_name) = crate::io::fs_access::handle_name(tab_id) {
let download_name = picked_name.clone();
return Task::perform(
async move {
let result = if let Err(error) =
crate::io::fs_access::write_handle(tab_id, &bytes).await
{
// Keep the save alive: the drawing still lands
// somewhere via the legacy download (which needs no
// activation), and the error travels with the
// completion message for the command-line note.
crate::sys::download_bytes(&download_name, &bytes);
Err(error)
} else {
Ok(())
};
(result, bytes)
},
move |(result, bytes)| Message::WebSaveWritten {
tab_id,
filename: picked_name,
autosave_key,
version,
geometry_epoch,
edit_revision,
camera_generation,
result,
bytes,
},
);
}
// The download is the only artifact of the save — fire it even if the
// tab was closed while serialization was in flight.
crate::sys::download_bytes(&filename, &bytes);
self.finalize_web_save(
tab_id,
filename,
autosave_key,
version,
geometry_epoch,
edit_revision,
camera_generation,
bytes,
)
}
/// Web SAVE in-place completion: the retained handle's write settled.
/// Success keeps the handle for the next plain Save; failure drops it
/// (the task already downloaded a copy — note that in the command line,
/// and re-pick on the next save instead of failing the same way again).
#[cfg(target_arch = "wasm32")]
pub(super) fn on_web_save_written(
&mut self,
tab_id: u64,
filename: String,
autosave_key: String,
version: acadrust::DxfVersion,
geometry_epoch: u64,
edit_revision: u64,
camera_generation: u64,
result: Result<(), String>,
bytes: Vec<u8>,
) -> Task<Message> {
if let Err(error) = result {
crate::io::fs_access::drop_handle(tab_id);
self.command_line.push_error(
crate::tf!(
"In-place save failed ({error}); a copy was downloaded instead"
)
.as_ref(),
);
}
self.finalize_web_save(
tab_id,
filename,
autosave_key,
version,
geometry_epoch,
edit_revision,
camera_generation,
bytes,
)
}
/// Shared completion half of the web save — download and in-place write
/// alike: recent-copy store task, tab bookkeeping under the race guard
/// (the dirty flag only clears when the persisted snapshot is still the
/// tab's current state), autosave-key retirement and the "Saved" line.
#[cfg(target_arch = "wasm32")]
fn finalize_web_save(
&mut self,
tab_id: u64,
filename: String,
autosave_key: String,
version: acadrust::DxfVersion,
geometry_epoch: u64,
edit_revision: u64,
camera_generation: u64,
bytes: Vec<u8>,
) -> Task<Message> {
let cache_name = std::path::Path::new(&filename)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
@@ -2992,6 +3096,14 @@ impl OpenCADStudio {
/// save-before-close flow — the version picker is reserved for Save As.
pub(in crate::app) fn save_with_default_format(&mut self, tab_idx: usize) -> Task<Message> {
self.active_tab = tab_idx;
// Every caller of this entry (Save/QSAVE, the save-before-close
// flow) is plain-save semantics: on the web, the tab's retained
// FS Access handle may be written through directly. Save As opens
// the dialog modal instead (open_save_dialog_window resets this).
#[cfg(target_arch = "wasm32")]
{
self.save_plain_reuse_fs_handle = true;
}
self.save_dialog_format = if self.tabs[tab_idx].recovery_save_as_required {
let document = &self.tabs[tab_idx].scene.document;
let is_dxf =
@@ -3088,7 +3200,9 @@ impl OpenCADStudio {
);
Task::batch([close, pick])
}
// Web: no filesystem — serialize and hand the browser a download.
// Web: no filesystem path — the destination is either a retained
// File System Access handle (in-place write), a fresh
// showSaveFilePicker pick, or the legacy download.
#[cfg(target_arch = "wasm32")]
{
let close = self.close_save_dialog_window();
@@ -3098,30 +3212,147 @@ impl OpenCADStudio {
self.sync_solid_models_for_save(i);
self.tabs[i].scene.sync_native_parametric_graph();
let tab_id = self.tabs[i].id;
let bounds =
crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID);
let Some(bounds) = bounds else {
return Task::batch([
close,
Task::done(Message::WebSaveScreenshot {
tab_id,
filename,
ext: ext.to_string(),
version,
bounds: None,
screenshot: None,
}),
]);
};
self.pending_web_thumbnail_save = Some(crate::app::PendingWebThumbnailSave {
// File System Access routing, decided HERE: showSaveFilePicker
// may only run while the page holds the Save click's transient
// user activation, and the thumbnail capture + worker
// serialization that follow can outlast that window. So the
// handle is secured at this gesture moment (or reused from a
// previous pick — retained handles need no activation to write)
// and only the write happens later.
let route = crate::io::fs_access::save_route(
crate::io::fs_access::supported(),
self.save_plain_reuse_fs_handle,
crate::io::fs_access::handle_ext(tab_id).as_deref(),
ext,
);
match route {
crate::io::fs_access::SaveRoute::WriteInPlace => {
// Plain Save on a matching handle — THE point: Ctrl+S
// updates the picked file, no dialog at all. The real
// name (the user may have renamed it in the picker)
// drives the save bookkeeping.
let picked_name =
crate::io::fs_access::handle_name(tab_id).unwrap_or(filename);
Task::batch([
close,
self.web_save_capture(tab_id, picked_name, ext.to_string(), version),
])
}
crate::io::fs_access::SaveRoute::PickHandle => {
// The picker IS the save dialog here: it opens inside
// the click's activation window, before any serialize
// task starts. Cancelling it cancels the save (same as
// a native save-dialog cancel); a picker failure falls
// back to the legacy download.
let suggested = filename;
let pick_suggested = suggested.clone();
Task::batch([
close,
Task::perform(
async move {
crate::io::fs_access::pick_save_handle(
tab_id,
&pick_suggested,
ext,
)
.await
},
move |picked| Message::WebSaveHandlePicked {
tab_id,
filename: suggested,
ext: ext.to_string(),
version,
picked,
},
),
])
}
// Firefox/Safari (no showSaveFilePicker): untouched legacy
// download flow.
crate::io::fs_access::SaveRoute::Download => {
Task::batch([
close,
self.web_save_capture(tab_id, filename, ext.to_string(), version),
])
}
}
}
}
/// Web save, destination-known half shared by the legacy download path
/// and both File System Access paths: queue the viewport thumbnail
/// capture (or skip straight to the serializer when the bounds are
/// unavailable), which continues in `on_web_save_screenshot`.
#[cfg(target_arch = "wasm32")]
fn web_save_capture(
&mut self,
tab_id: u64,
filename: String,
ext: String,
version: acadrust::DxfVersion,
) -> Task<Message> {
let bounds = crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID);
let Some(bounds) = bounds else {
return Task::done(Message::WebSaveScreenshot {
tab_id,
filename,
ext: ext.to_string(),
ext,
version,
bounds,
bounds: None,
screenshot: None,
});
self.thumbnail_capture_clean = true;
close
};
self.pending_web_thumbnail_save = Some(crate::app::PendingWebThumbnailSave {
tab_id,
filename,
ext,
version,
bounds,
});
self.thumbnail_capture_clean = true;
Task::none()
}
/// Web SAVE: the showSaveFilePicker promise from the confirm handler
/// settled. `Ok(Some(name))` continues the save against the picked
/// file's real name (the registry already retains the handle, replacing
/// any previous one — Save As re-picks); `Ok(None)` (dismissed picker)
/// cancels the save like a native save-dialog cancel; `Err` keeps the
/// save alive through the legacy download, with a note that the copy
/// was downloaded.
#[cfg(target_arch = "wasm32")]
pub(super) fn on_web_save_handle_picked(
&mut self,
tab_id: u64,
filename: String,
ext: String,
version: acadrust::DxfVersion,
picked: Result<Option<String>, String>,
) -> Task<Message> {
match picked {
Ok(Some(picked_name)) => {
self.web_save_capture(tab_id, picked_name, ext, version)
}
Ok(None) => {
// The picker was the save dialog: dismissing it means no
// save — the download fallback must NOT fire (that would
// surprise the user with a stray "drawing (1).dwg"). If
// this came from the unsaved-changes → save → close flow,
// re-show the confirmation so the tab isn't lost.
if self.save_dialog_for_unsaved && self.pending_close.is_some() {
return self.open_unsaved_dialog_window();
}
Task::none()
}
Err(error) => {
self.command_line.push_error(
crate::tf!(
"Save destination picker failed: {error}; downloading a copy instead"
)
.as_ref(),
);
self.web_save_capture(tab_id, filename, ext, version)
}
}
}
+67 -3
View File
@@ -488,6 +488,10 @@ impl OpenCADStudio {
self.control_screenshot(path, screenshot);
Task::none()
}
// Web: a presented frame arrived for a parked capture op (see
// control::on_control_capture_frame).
#[cfg(target_arch = "wasm32")]
Message::ControlCaptureFrame => self.on_control_capture_frame(),
Message::ControlToggle => {
self.control.enabled = !self.control.enabled;
Task::none()
@@ -5244,9 +5248,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
@@ -5266,6 +5285,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) {
@@ -8401,6 +8433,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)
+12
View File
@@ -2531,6 +2531,17 @@ impl OpenCADStudio {
} else {
Subscription::none()
};
// Web control capture: the parked op needs one presented frame so the
// canvas readback runs before the browser clears the WebGL drawing
// buffer (same timing requirement as the thumbnail capture above).
#[cfg(target_arch = "wasm32")]
let control_capture = if self.control.pending_capture.is_some() {
window::frames().map(|_| Message::ControlCaptureFrame)
} else {
Subscription::none()
};
#[cfg(not(target_arch = "wasm32"))]
let control_capture = Subscription::none();
// Blink the MText preview caret while the editor is open.
let caret_blink = if self.mtext_editor.is_some() {
iced::time::every(std::time::Duration::from_millis(530))
@@ -2735,6 +2746,7 @@ impl OpenCADStudio {
nav_settle,
gpu_probe,
thumbnail_capture,
control_capture,
caret_blink,
web_fonts,
autosave,