diff --git a/ROADMAP.md b/ROADMAP.md index 25a23133..7efd344a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -150,7 +150,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist | M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | 🟡 | 15–25 | | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, regenerate in place) done; graph Design/Column/Marker designs pending | 6–10 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 | -| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux, content-sized dialogs and menus that scroll when longer than the window done; accessibility, Windows/Linux packaging pending | 18–28 | +| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces, a custom title bar on Windows/Linux with a Home button, content-sized dialogs and menus that scroll when longer than the window done; accessibility, Windows/Linux packaging pending | 18–28 | | — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance) | ⬜ | 60–90 | | — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 | 50–80 | | | **Total to full parity** (one agent; re-derived from the [Parity estimate](#parity-estimate) table) | | **~335–520** | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 86ea9d13..4b9dde39 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -127,7 +127,11 @@ pub fn fit(app: &mut VectorcraftApp, how: &str) { pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let full = ui.available_rect_before_wrap(); - if app.session.active().is_none() { + // A document that opened, closed or became active since Home was chosen replaces it. + if app.ui.home.is_some_and(|k| k != crate::menus::home_key(app)) { + app.ui.home = None; + } + if app.session.active().is_none() || app.ui.home.is_some() { home(app, ui, full); return; } diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 42fceecb..5126897f 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -27,8 +27,9 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); crate::brand::paint_mark(ui, r); ui.add_space(4.0); - if widgets::icon_button(ui, "house", "Home", false, 24.0).clicked() { - crate::dialogs::open_new_document(app); + let on_home = app.ui.home.is_some() || app.session.active().is_none(); + if widgets::icon_button(ui, "house", "Home", on_home, 24.0).clicked() { + app.run("app.home", json!({})).ok(); } ui.add_space(2.0); let menus_end = if app.native_menu { @@ -237,7 +238,8 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { for (i, d) in app.session.documents().iter().enumerate() { let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0); let title = tab_title(d, zoom, app.ui.view.outline); - let is_active = Some(i) == active; + // On the Home screen no tab is the current one. + let is_active = Some(i) == active && app.ui.home.is_none(); let galley = ui.painter().layout_no_wrap(title, theme::semibold(12.5), if is_active { t.text_strong } else { t.text_dim }); let w = galley.size().x + 50.0; let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(w, strip.height() - 1.0)); @@ -265,6 +267,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { app.status(e); } } else if let Some(i) = activate { + app.ui.home = None; app.session.set_active(i); } // Isolation mode breadcrumb bar. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1befbd13..70a26f24 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -46,6 +46,8 @@ mod tests_clipboard; #[cfg(test)] mod tests_docsetup; #[cfg(test)] +mod tests_home; +#[cfg(test)] mod tests_labels; #[cfg(test)] mod tests_nativeoptions; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 103fe999..81341c9e 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -111,6 +111,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ), ("file.documentSetup", "Document Setup…", "Cmd+Alt+P", "{}"), ("file.newDialog", "New…", "Cmd+N", "{} opens the New Document dialog"), + ("app.home", "Home", "", "{} shows the Home screen (new file presets, Open) over the open documents; choosing a document tab returns to it"), ("edit.preferences", "Preferences…", "Cmd+K", "{category?} open Preferences (engine: prefs.get / prefs.set / prefs.list)"), ("edit.keyboardShortcuts", "Keyboard Shortcuts…", "Cmd+Alt+Shift+K", "{}"), ("shortcuts.set", "Set Keyboard Shortcut", "", "{id: command id or tool:, shortcut: \"Cmd+Shift+K\" | \"\" (none) | null (default), force?}"), @@ -549,6 +550,10 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option { + app.ui.home = Some(home_key(app)); + Ok(Value::Null) + } "file.open" => match s("path") { Some(path) => io::open_path(app, &path).map(|_| Value::Null), None => io::open_dialog(app).map(|_| Value::Null), @@ -1919,6 +1924,12 @@ fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked } } +/// What the Home screen remembers when it opens (`app.home`): the active document and the +/// document count. When either changes, the Home screen gives way to the document. +pub(crate) fn home_key(app: &VectorcraftApp) -> (Option, usize) { + (app.session.active().map(|d| d.uid), app.session.documents().len()) +} + fn label_of(it: &Item) -> &'static str { match it { Item::Cmd(l, _, _) => l, diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 7a7d1303..4c7c3153 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -298,6 +298,11 @@ pub struct UiState { /// never used). #[serde(default)] pub dxf_import: Value, + /// The Home screen is shown over the open documents (`app.home`): the active document's uid + /// and the document count when it opened. Choosing a tab, or a document opening, closing or + /// becoming active, leaves it. + #[serde(skip)] + pub home: Option<(Option, usize)>, } impl UiState { @@ -359,6 +364,7 @@ impl Default for UiState { dxf_options: Value::Null, eps_options: Value::Null, dxf_import: Value::Null, + home: None, } } } diff --git a/crates/ui-egui/src/tests_home.rs b/crates/ui-egui/src/tests_home.rs new file mode 100644 index 00000000..c3208f53 --- /dev/null +++ b/crates/ui-egui/src/tests_home.rs @@ -0,0 +1,40 @@ +//! The Home icon shows the Home screen over open documents. + +use serde_json::json; + +use crate::{VectorcraftApp, canvas}; + +fn app_with_doc() -> VectorcraftApp { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app +} + +/// Draw one canvas frame; whether it drew the document (else the Home screen). +fn shows_document(app: &mut VectorcraftApp) -> bool { + app.canvas_rect = None; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + app.canvas_rect.is_some() +} + +#[test] +fn home_shows_over_open_documents_until_a_document_is_chosen() { + let mut app = app_with_doc(); + assert!(shows_document(&mut app)); + app.run("app.home", json!({})).unwrap(); + assert!(!shows_document(&mut app), "Home replaces the canvas"); + assert_eq!(app.session.documents().len(), 1, "the document stays open"); + assert!(app.ui.dialog.is_none(), "Home is not the New Document dialog"); + // Choosing the document's tab returns to it. + app.ui.home = None; + assert!(shows_document(&mut app)); + // A new document (from Home's presets, New… or Open) replaces Home too. + app.run("app.home", json!({})).unwrap(); + app.run("file.new", json!({})).unwrap(); + assert!(shows_document(&mut app)); + assert!(app.ui.home.is_none()); +}