Merge main into Select-Color-Redesign

Resolve conflicts with the new gradient and graphic-attributes colour
targets: keep main's targets and menu handling, routed through the new
ColorPicker state; adapt dock/view.rs to `color_picker`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-12 19:29:37 +00:00
commit 0904661192
220 files changed
+37313 -5826

No files matched your search

+394 -21
View File
@@ -14,6 +14,7 @@ pub(crate) mod commands;
pub(crate) mod dim_viewport;
#[cfg(test)]
mod dimension_preview_tests;
mod dock;
mod document;
mod drafting_settings;
pub(crate) mod expr_eval;
@@ -814,6 +815,11 @@ pub(super) struct OpenCADStudio {
render_mode_preview: Option<codec::entities::ViewportRenderMode>,
/// Whether the Properties panel is shown on the left (PROPERTIES).
show_properties: bool,
/// The Layer Manager pallet (LAYERS) is open.
pub(crate) show_layers: bool,
/// Whether the Graphic Attributes palette is shown (GRAPHICATTRIBUTES).
show_graphic_attributes: bool,
graphic_attributes: crate::ui::window::graphic_attributes::GraphicAttributesState,
/// Docked Insert Block panel visibility.
pub(crate) show_block_palette: bool,
/// Node graph overlay over the viewport.
@@ -840,17 +846,33 @@ pub(super) struct OpenCADStudio {
pub(crate) sheet_set_import_parent: Option<String>,
/// General edge-stack dock layout for the side panels.
pub(crate) dock: crate::ui::dock::DockState,
/// Which panel is currently floated at full height (hovered, or a pinned
/// panel on top).
pub(crate) dock_expanded: Option<crate::ui::dock::PanelId>,
/// Panel currently being dragged between sides / reordered.
pub(crate) dock_dragging: Option<crate::ui::dock::PanelId>,
/// Panel currently being width-resized.
pub(crate) dock_resizing: Option<crate::ui::dock::PanelId>,
/// Last pointer position during a drag / resize.
/// Pallet revealed although it auto-hides (its edge's group, or its
/// floating frame): hovered, being dragged, or just opened. Cleared when
/// the pointer leaves.
pub(crate) dock_peek: Option<crate::ui::dock::PanelId>,
/// Reveal or hide waiting out the hover delay, with the generation its
/// timer carries; any newer hover supersedes it.
pub(crate) dock_hover_pending: Option<crate::ui::dock::HoverPending>,
pub(crate) dock_hover_gen: u64,
/// What the pointer is dragging in the dock (pallet, group, width,
/// splitter, floating size, Layer Manager column).
pub(crate) dock_drag: Option<crate::ui::dock::DockDrag>,
/// Pallet whose strip icon (or floating frame) the pointer is over.
pub(crate) dock_icon_hover: Option<crate::ui::dock::PanelId>,
/// Last pointer position during a dock drag.
pub(crate) dock_drag_last: Option<iced::Point>,
/// Live drag target (side + index), shown as a highlight while dragging.
pub(crate) dock_drag_target: Option<(crate::app::config::DockSide, usize)>,
/// Docked panel whose title bar the pointer is over (shows pin / close).
pub(crate) dock_title_hover: Option<crate::ui::dock::PanelId>,
/// Edge whose icon-strip pallet menu (+) is open.
pub(crate) dock_edge_menu: Option<crate::app::config::DockSide>,
/// Floating pallet whose title-bar right-click menu is open.
pub(crate) dock_float_menu: Option<crate::ui::dock::PanelId>,
/// Pointer position over a floating title bar (bar-local), and where
/// its open menu was asked for.
pub(crate) dock_float_pointer: iced::Point,
pub(crate) dock_float_menu_at: iced::Point,
/// Group whose edge band (group grip) the pointer is over.
pub(crate) dock_grip_hover: Option<(crate::app::config::DockSide, usize)>,
/// Reference-table column currently being width-resized (column index),
/// with the last pointer position. Mirrors the Layers Name-column drag.
pub(crate) xref_col_drag: Option<usize>,
@@ -890,6 +912,56 @@ 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")]
web_autosave_in_flight: bool,
/// Web autosave storage already reported a failure — stay quiet until a
/// round succeeds again, so a full quota does not spam every tick.
#[cfg(target_arch = "wasm32")]
web_autosave_warned: bool,
/// Crash-recovery snapshots found at boot (newest first) that the restore
/// prompt still has to offer, one at a time.
#[cfg(target_arch = "wasm32")]
web_autosave_pending: Vec<crate::io::web_autosave::WebAutosaveEntry>,
/// Snapshots a "Restore all" click still expects to settle (opened,
/// failed, cancelled, unreadable, or rejected by the open queue). Each
/// settle funnels through `queue_next_web_autosave_prompt`, which counts
/// this down; reaching zero arms `web_session_focus_pending`.
#[cfg(target_arch = "wasm32")]
web_autosave_restore_all_remaining: usize,
/// A Restore-all batch finished settling — apply the remembered
/// session's active tab at the next message boundary, after the last
/// restored open has actually inserted its tab (`web_autosave_opened`
/// runs before that insertion, so applying there would pick against a
/// tab list still missing the last restore).
#[cfg(target_arch = "wasm32")]
web_session_focus_pending: bool,
/// The tab list the previous session remembered (localStorage), loaded
/// after the boot autosave scan. Advisory only: it feeds the restore
/// modal's session line and the post-restore tab selection — nothing is
/// ever auto-opened from it.
#[cfg(target_arch = "wasm32")]
web_session: Option<crate::io::web_session::WebSession>,
/// The snapshot whose recovered open is in flight: `(autosave key,
/// open id)`. The OPFS copy is the only one, so it is deleted only
/// once the open with exactly that id succeeds — identity, never the
/// display name (same-named files share one stored copy on web).
#[cfg(target_arch = "wasm32")]
web_autosave_consumed: Option<(String, u64)>,
/// Autosave keys a web save wanted to remove while a snapshot round was
/// still writing (the round's late write would resurrect the snapshot
/// with a fresh mtime); the removes run when the round finishes.
#[cfg(target_arch = "wasm32")]
web_autosave_clear_after: Vec<String>,
// ── Floating panel windows ────────────────────────────────────────────
/// The open Select Color panel, if any.
color_picker: Option<crate::ui::color_select::ColorPicker>,
@@ -937,9 +1009,6 @@ pub(super) struct OpenCADStudio {
modal_drag_last: Option<Point>,
/// True while the modal title bar is held (a drag is in progress).
modal_dragging: bool,
/// Layer Manager: dragging the Name-column divider (width follows the
/// shared ModalDragMove flow).
layer_col_dragging: bool,
/// Layer Manager Name column width in px, adjusted by the divider drag.
layer_name_col_w: f32,
/// How far the user has dragged the modal's corner resize grip from the
@@ -1560,6 +1629,25 @@ pub(super) struct PendingExternalChange {
set_current_path: bool,
}
/// One tab's slot in a web SAVEALL round (`saveall_web`): the tab identity,
/// download name and race-guard markers snapshot when serialization started,
/// plus the writer result — the future fills `result` in before the
/// completion message is built. The web counterpart of native's
/// `SaveOutcome`, minus the path/continuation bookkeeping SAVEALL never had.
#[derive(Debug, Clone)]
#[cfg(target_arch = "wasm32")]
pub struct WebSaveallEntry {
tab_id: u64,
path: PathBuf,
filename: String,
ext: String,
version: codec::DxfVersion,
geometry_epoch: u64,
edit_revision: u64,
camera_generation: u64,
result: Result<Vec<u8>, String>,
}
#[derive(Debug, Clone)]
#[cfg(not(target_arch = "wasm32"))]
pub struct SaveOutcome {
@@ -1593,6 +1681,9 @@ pub enum UiColor {
/// Where a colour chosen in the standalone palette window should be applied.
#[derive(Debug, Clone)]
pub enum ColorPickTarget {
Gradient(u8),
GraphicAttributesLine,
GraphicAttributesSolid,
DimStyle(DsField),
MLeader(&'static str),
Table(u8, &'static str),
@@ -1946,7 +2037,6 @@ pub enum ModalKind {
PluginManager,
UpdateNotice,
DonationPrompt,
Layers,
LayerStateManager,
LayerTranslator,
DrawingUnits,
@@ -2014,6 +2104,11 @@ pub enum ModalKind {
XrefHelp,
/// Sheet set wizard, properties and sheet / subset forms.
SheetSet,
/// Web: a crash-recovery autosave snapshot newer than the drawing's last
/// save awaits a restore/discard decision. Distinct from the corrupt-file
/// `RecoveryPrompt` — nothing is wrong with the drawing itself.
#[cfg(target_arch = "wasm32")]
AutosaveRestore,
}
/// A property group controlled by a layer state's restore mask.
@@ -2135,6 +2230,7 @@ pub enum PasteFocus {
#[derive(Debug, Clone)]
pub enum Message {
GraphicAttributes(crate::ui::window::graphic_attributes::GraphicAttributesMsg),
SpaceMouseWake,
SpaceMouseFrame(iced::time::Instant),
SpaceMouseFocus(iced::window::Id, bool),
@@ -2153,6 +2249,10 @@ pub enum Message {
ControlStep(String, Box<Message>),
ControlTaskDone(String),
ControlScreenshot(String, Option<iced::window::Screenshot>),
/// A frame has been drawn (or the anti-hang timer expired) for a parked
/// `capture` request: take the screenshot now, so it shows the latest
/// state instead of the previous frame (C11).
ControlCaptureFrame { redrew: bool },
ControlToggle,
/// Node graph overlay interaction.
Graph(crate::ui::node_graph::GraphMsg),
@@ -2180,6 +2280,12 @@ pub enum Message {
PasteShortcutResolved(PasteFocus),
/// 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,
@@ -2200,6 +2306,12 @@ pub enum Message {
OpenPathPicked(Option<(PathBuf, u64)>),
/// A file was dragged from the desktop and dropped on the window (#344).
FileDropped(PathBuf),
/// Web only: a drawing dragged onto the page — the DOM drop listener has
/// already read the bytes; this opens through the web-picker flow (#344).
/// The third field tags an autosave-restore file with its snapshot key so
/// the snapshot is deleted only when this exact open succeeds.
#[cfg(target_arch = "wasm32")]
WebFileDropped(String, Vec<u8>, Option<String>),
/// Web only: Ctrl+V captured by a focused text field, whose iced-internal
/// paste is inert there — read the async clipboard instead (#346).
WebFieldPaste,
@@ -2262,6 +2374,39 @@ pub enum Message {
/// Web: an asynchronous OPFS copy written after Save is ready for recents.
#[cfg(target_arch = "wasm32")]
WebRecentStored(Result<PathBuf, String>),
/// Web: the `?file=` boot deep link settled — its bytes entered the
/// guarded open queue, or a notice/error text for the command line
/// (recent copy missing, refused or failed fetch).
#[cfg(target_arch = "wasm32")]
WebLinkSettled(crate::io::web_link::LinkOutcome),
/// Web: the boot scan finished — these autosave snapshots are newer than
/// the drawings' last saved copies (newest first).
#[cfg(target_arch = "wasm32")]
WebAutosavesScanned(Result<Vec<crate::io::web_autosave::WebAutosaveEntry>, String>),
/// Web: restore the offered autosave snapshot into a recovered tab.
#[cfg(target_arch = "wasm32")]
WebAutosaveRestore,
/// Web: restore EVERY pending autosave snapshot — each is read and
/// enqueued through the guarded open queue with its own key (the poll
/// opens them sequentially); a snapshot is still consumed only after its
/// own open succeeds.
#[cfg(target_arch = "wasm32")]
WebAutosaveRestoreAll,
/// Web: delete the offered autosave snapshot (and offer the next, if any).
#[cfg(target_arch = "wasm32")]
WebAutosaveDiscard,
/// Web: close the restore prompt WITHOUT deleting the offered snapshot
/// (Escape/✕): it is simply re-offered on the next boot. Only the
/// Discard button deletes.
#[cfg(target_arch = "wasm32")]
WebAutosaveDefer,
/// Web: the offered snapshot's bytes are in; queue them for the guarded
/// open poll. The OPFS copy is consumed only once that open succeeds.
#[cfg(target_arch = "wasm32")]
WebAutosaveBytes(String, String, Result<Vec<u8>, String>),
/// Web: one autosave round finished for all dirty tabs (serially).
#[cfg(target_arch = "wasm32")]
WebAutosaveSnapshotFinished(Result<(), String>),
SaveFile,
SaveAs,
// ── Custom Save-As dialog ─────────────────────────────────────────────
@@ -2439,6 +2584,19 @@ pub enum Message {
LayerTranslatorLoad,
/// A target set was read from `path`.
LayerTranslatorLoaded(std::path::PathBuf),
/// Web: a standards drawing picked and parsed in the browser, with the
/// target layers already extracted — a name-only path has nothing behind
/// it for `load_targets` to read. `Ok(None)` = cancelled.
#[cfg(target_arch = "wasm32")]
LayerTranslatorLoadedWeb(
Result<
Option<(
String,
Vec<crate::modules::draw::layers::laytrans::TargetLayer>,
)>,
String,
>,
),
LayerTranslatorSelectFrom(String),
LayerTranslatorSelectTo(String),
/// Pair the two selected layers.
@@ -2544,6 +2702,89 @@ pub enum Message {
bounds: Option<iced::Rectangle>,
screenshot: Option<iced::window::Screenshot>,
},
/// Web SAVE: the serializer worker (or the inline fallback) produced the
/// drawing bytes; download them and finish the save bookkeeping. The
/// epoch/revision/camera markers snapshot the tab when serialization
/// started — the web counterpart of native's `snapshot_is_current`, so a
/// save that raced user edits does not wipe the newer dirty flag.
#[cfg(target_arch = "wasm32")]
WebSaveSerialized {
tab_id: u64,
filename: String,
autosave_key: String,
version: codec::DxfVersion,
geometry_epoch: u64,
edit_revision: u64,
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: codec::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: codec::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`.
#[cfg(target_arch = "wasm32")]
WebWblockSerialized {
block_name: String,
path: std::path::PathBuf,
filename: String,
result: Result<Vec<u8>, String>,
},
/// Web SAVEALL round completion: one entry per tab serialized on the
/// worker (or the inline fallback). Downloads fire per entry; dirty
/// flags clear only for tabs whose serialized snapshot is still current
/// (the same race guard a single web Save uses).
#[cfg(target_arch = "wasm32")]
WebSaveallSerialized {
skipped: usize,
results: Vec<WebSaveallEntry>,
},
/// Web ARCHIVE/ETRANSMIT completion: the serializer worker (or the
/// inline fallback) produced the drawing bytes; assemble the transmittal
/// ZIP from the dependencies collected at command time, download it and
/// report — including the skipped-dependency lines.
#[cfg(target_arch = "wasm32")]
WebArchiveSerialized {
cmd: String,
zip_name: String,
filename: String,
deps: Vec<(String, Vec<u8>)>,
skipped: Vec<String>,
result: Result<Vec<u8>, String>,
},
/// Retry the failed save after the other application releases the file.
#[cfg(not(target_arch = "wasm32"))]
SaveFileInUseRetry,
@@ -2573,6 +2814,19 @@ pub enum Message {
/// dispatch, this accepts an interactive verb and all of its arguments on
/// the same line (`BOX 0,0,0 10,10,0 10`).
ScriptLine(String),
/// Web SCRIPT: the picked .scr file's name and text. `None` = the
/// browser dialog was cancelled.
#[cfg(target_arch = "wasm32")]
ScriptFilePicked(Option<(String, String)>),
/// Web CENTERLTYPEFILE: the picked .lin/.dnl file's name and text.
/// `None` = cancelled.
#[cfg(target_arch = "wasm32")]
CenterlineLinetypeFilePicked(Option<(String, String)>),
/// Web DATALINK: the picked .csv file's name and raw bytes — registered
/// in the picked-source registry, then fed to the same table-insert path
/// the native typed-path command uses. `None` = cancelled.
#[cfg(target_arch = "wasm32")]
DataLinkCsvPicked(Option<(String, Vec<u8>)>),
/// Append one typed character to the command-line input from the
/// global key-press subscription. Used when the text-input widget
/// itself isn't focused (focus parked on viewport / button / etc.)
@@ -2603,6 +2857,12 @@ pub enum Message {
shortcut: String,
extend_selection: bool,
},
/// A text field captured SPACE while it is rebound away from
/// COMMANDSPACE; run the binding only if the command input has focus.
CommandLineSpaceProbe,
/// The command input has focus: drop the space it just typed and run the
/// SPACE binding.
CommandLineSpaceResolved,
/// A widget captured Up/Down; resolve it only if the command input owns
/// keyboard focus.
CommandLineArrowProbe {
@@ -2856,6 +3116,8 @@ pub enum Message {
ToggleViewCube,
/// Toggle the Properties panel visibility (PROPERTIES).
ToggleProperties,
/// Toggle the Graphic Attributes panel visibility (GRAPHICATTRIBUTES).
ToggleGraphicAttributes,
/// Toggle the document file tabs at the top (FILETAB).
ToggleFileTabs,
/// Toggle the layout tabs at the bottom (LAYOUTTAB).
@@ -3667,6 +3929,8 @@ pub enum Message {
/// Send current layout to the system printer (via lp / lpr).
PrintToPrinter,
/// Callback from the async printer job.
/// Web never constructs it — printing opens the PDF preview instead.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
PrintResult(Result<String, String>),
/// Open the full Plot / Print dialog (seeds state from the layout).
PlotDialogOpen,
@@ -4001,7 +4265,9 @@ pub enum Message {
DataExtractionBrowseSettings,
DataExtractionBrowseSettingsResult(Option<std::path::PathBuf>),
DataExtractionAddDrawings,
DataExtractionAddDrawingsResult(Vec<std::path::PathBuf>),
/// Picked source drawings — `Err` carries a pick/parse failure (web
/// parses each file on the worker at pick time).
DataExtractionAddDrawingsResult(Result<Vec<std::path::PathBuf>, String>),
DataExtractionAddFolder,
DataExtractionAddFolderResult(Option<std::path::PathBuf>),
DataExtractionClearSources,
@@ -4028,6 +4294,8 @@ pub enum Message {
/// Trigger OBJ import: show open-file dialog.
ObjImport,
/// Callback after the user picks (or cancels) the OBJ file path.
/// Web never constructs it — the browser handle's bytes are read directly.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
ObjImportPath(Option<std::path::PathBuf>),
ObjImportFinished(
u64,
@@ -4270,6 +4538,9 @@ impl OpenCADStudio {
render_mode_menu_open: false,
render_mode_preview: None,
show_properties: true,
show_layers: false,
show_graphic_attributes: false,
graphic_attributes: Default::default(),
show_block_palette: false,
show_node_graph: false,
property_target_override: None,
@@ -4284,11 +4555,18 @@ impl OpenCADStudio {
block_palette: Default::default(),
xref_manager: Default::default(),
dock: Default::default(),
dock_expanded: None,
dock_dragging: None,
dock_resizing: None,
dock_peek: None,
dock_hover_pending: None,
dock_hover_gen: 0,
dock_float_menu: None,
dock_float_pointer: iced::Point::ORIGIN,
dock_float_menu_at: iced::Point::ORIGIN,
dock_drag: None,
dock_drag_last: None,
dock_drag_target: None,
dock_title_hover: None,
dock_edge_menu: None,
dock_grip_hover: None,
dock_icon_hover: None,
xref_col_drag: None,
xref_col_last: None,
xref_split_drag: false,
@@ -4304,6 +4582,24 @@ impl OpenCADStudio {
pending_native_thumbnail_save: None,
#[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,
#[cfg(target_arch = "wasm32")]
web_autosave_pending: Vec::new(),
#[cfg(target_arch = "wasm32")]
web_autosave_restore_all_remaining: 0,
#[cfg(target_arch = "wasm32")]
web_session_focus_pending: false,
#[cfg(target_arch = "wasm32")]
web_session: None,
#[cfg(target_arch = "wasm32")]
web_autosave_consumed: None,
#[cfg(target_arch = "wasm32")]
web_autosave_clear_after: Vec::new(),
color_picker: None,
recent_colors: Vec::new(),
active_modal: None,
@@ -4324,7 +4620,6 @@ impl OpenCADStudio {
modal_offset: iced::Vector::ZERO,
modal_drag_last: None,
modal_dragging: false,
layer_col_dragging: false,
layer_name_col_w: 130.0,
modal_resize: iced::Vector::ZERO,
modal_content_size: None,
@@ -4861,6 +5156,16 @@ impl OpenCADStudio {
fn boot_web() -> (Self, Task<Message>) {
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut s = Self::new();
// A web host that intends to automate can opt in before the first
// control request by setting `opencadstudio.automation` to "1" in
// localStorage (the same pre-load mechanism the settings use): the
// startup donation prompt then never queues (#1349).
if web_sys::window().and_then(|window| window.local_storage().ok().flatten()).and_then(
|storage| storage.get_item("opencadstudio.automation").ok().flatten(),
) == Some("1".to_string())
{
s.control.automation_seen = true;
}
s.queue_startup_prompts();
let focus = s.focus_cmd_input();
let primary_font =
@@ -4880,9 +5185,64 @@ impl OpenCADStudio {
Message::DiscussionsFetched,
);
let thumbs_fetch = s.refresh_recent_thumbs();
// Crash-recovery scan: OPFS snapshots newer than a drawing's last
// saved copy come back as a restore prompt (stale ones are deleted).
let autosave_scan = Task::perform(
crate::io::web_autosave::scan(),
Message::WebAutosavesScanned,
);
// URL deep link (`?file=…`): a shareable/embeddable link that opens a
// drawing at boot. An http(s) URL is fetched with the same capped
// streaming reader the remote-image fetch uses and handed to the
// guarded drop-open queue; a bare name reads the OPFS recent copy the
// same way `open_recent_web` does. Everything else (other schemes,
// non-.dwg/.dxf URL tails) is refused with a console warning — the
// same scheme gate the image fetch applies. Pure parsing/validation
// lives in `io::web_link` and is unit-tested there.
let deep_link: Task<Message> = {
let search = web_sys::window()
.and_then(|window| window.location().search().ok())
.unwrap_or_default();
match crate::io::web_link::parse_link(&search) {
crate::io::web_link::ParsedLink::Target(target) => {
// Opening a URL from the query string is an implicit trust
// decision: name the origin before the fetch starts (info
// only — the bytes still go through the parse + recovery
// machinery like any other open). Same-origin links stay
// quiet: that drawing already comes from this site.
if let crate::io::web_link::LinkTarget::Url { url, name } = &target {
if !crate::io::web_link::same_origin(url) {
let origin = crate::io::web_link::display_origin(url)
.unwrap_or_else(|| url.clone());
s.command_line
.push_info(&format!("Opening {name} from {origin}…"));
}
}
Task::perform(crate::io::web_link::open_link(target), |outcome| {
Message::WebLinkSettled(outcome)
})
}
crate::io::web_link::ParsedLink::Ignored(refusal, value) => {
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
&crate::io::web_link::refusal_warning(refusal, &value),
));
Task::none()
}
crate::io::web_link::ParsedLink::Absent => Task::none(),
}
};
(
s,
Task::batch([focus, fonts, patrons, videos, discussions, thumbs_fetch]),
Task::batch([
focus,
fonts,
patrons,
videos,
discussions,
thumbs_fetch,
autosave_scan,
deep_link,
]),
)
}
}
@@ -4940,6 +5300,19 @@ impl Drop for OpenCADStudio {
/// unavailable for now (see issue #45). Native keeps the multi-window `run`.
#[cfg(target_arch = "wasm32")]
pub fn run_web() -> iced::Result {
// winit never emits DroppedFile on the web, so the document drag & drop
// listeners are installed here and feed the control poll (#344).
crate::io::web_drop::install();
// Best-effort, fire-and-forget: ask the browser not to evict the origin's
// storage under pressure — settings (localStorage) and the OPFS recent
// copies / autosave snapshots live there. A denial changes nothing.
wasm_bindgen_futures::spawn_local(async {
if let Some(window) = web_sys::window() {
if let Ok(promise) = window.navigator().storage().persist() {
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
}
}
});
iced::application(
OpenCADStudio::boot_web,
OpenCADStudio::update,