From d8f6bacddb7ddadd6e8203104bf9da6c713f5ed9 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 11:12:36 +0700 Subject: [PATCH] Part of #663: Home screen Recent Files; a double-clicked preset creates the document - The Home screen lists the first six of File > Open Recent Files, each by name (a click opens it) and folder; one that can't be opened says so in the status bar. With none, there is no section. - In New Document, a double-click on a preset card creates the document from it; a click still only chooses it. The Create buttons no longer overwrite a create asked for earlier in the frame. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/canvas.rs | 30 ++++++++++ crates/ui-egui/src/dialogs/new_document.rs | 64 +++++++++++++++++++--- crates/ui-egui/src/tests_home.rs | 43 +++++++++++++++ 4 files changed, 131 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8496cebfa..b3e892234 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583), the Color panel keeping its height as warnings come and go so a drag on its spectrum no longer flickers near white (#578), fonts that font services such as Adobe Fonts load on Windows, listed through DirectWrite (#579), `.ai` files writing their layers and sublayers as PDF layers, hidden ones with their art, and PDF layers nested in layers importing as sublayers (#372), file dialogs on Linux shown off the UI thread so the compositor no longer finds the window not responding, and parented to the window on Linux and Windows (#592), anchors and handle ends picked from 2 px past their drawn size before the segments through them (#593), an image copied in a browser pasting as the picture, not its address (#597), the Free Transform widget with Constrain, Free Transform, Perspective Distort and Free Distort (#598), touch gestures: a two-finger tap undoes and a three-finger tap redoes (#585), the font menus' live preview staying on the text instead of flickering on and off every frame (#563), Edit Artboards showing the active artboard in the Properties panel with New Artboard, Delete Artboard and Exit (#530), the same DirectWrite fonts in `vectorcraft-cli` and its MCP server (#579), SVG layers keeping their names when wrapped and sublayers round-tripping through SVG (#372), a Layers panel that lays out only the rows in view and keeps a document's open rows in its file (#638), a panel dropped on the dock lights up the icon column it goes back to and pops out of its icon (#649), Image Trace's View modes and an Advanced section that stays as it was left (part of #641), Image Trace's Palette: Limited, Full Tone, Automatic and Document Library (part of #641), Image Trace's Create Strokes: line art traced as stroked centre lines (part of #641), a dual-GPU Mac falling back to its other GPU when one fails to start (#651), two fingers dragged on a touch screen panning the canvas (part of #449); see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG window (#493), panels, panel groups and the Tools panel undocking into floating panels that move, stack and dock again (#495), the 0.5.0 startup crash on dual-GPU Linux desktops under Wayland fixed: the desktop's own GPU by default and a restart on the next GPU when one can't show the window (#502), path editing while drawing: Cmd/Ctrl with the Pen borrowing Direct Selection and drawing on afterwards, Pen+Alt reshaping segments, handles of a whole selected path dragging, and handle drags no longer re-gathering snap targets every move (#494), the Control bar's and Properties panel's Convert buttons for a path selected as a whole with Direct Selection, the Pen and the anchor tools (#504), the Pen closing a path dragged out of its first anchor every time, without flicker (#501), the Paste menu checking the system clipboard on a background thread on Linux so an X11 clipboard owner that never answers no longer freezes the window (#510), PDF/`.ai` import keeping a type object's outline stroke on the type, wrapped lines as area type, barcode digits in place and layers in the file's stacking order (#508), the Italian interface (#514), EPS images with several data sources read in turn (#498), `restore` undoing dictionary changes in EPS import (part of #505), the macOS menu bar laid out as a Mac app's (the VectorCraft menu with Settings, Language and Appearance, the system's Hide, Services and Window items; owner's request), Object › Image Trace › Make tracing with the Default preset instead of asking for a preset's name (#543), the rulers' unit menu (#528), Live Corners on stars, polygons and pen paths (#511), the drawing tools snapping to Smart Guides as the Selection tool does, with Construction Guides (#506), the in-window menus' Select All, Cut, Copy and Paste acting on the text field they were opened from (#516), Windows rendering through DX12 before Vulkan (#545), Image Trace tracing colour layers in parallel and refusing traces too large for memory, with the canvas outlining huge selections by their bounds (#525), SVGs of chosen artboards holding only the art over each artboard (#550), the font menu scrolling only its list, under a search field and filters that stay put (#555), a click on an object of a selection making it the key object that Align aligns to and Distribute Spacing spaces from, drawn with a thicker outline (#541), Shape Builder merges taken from the planar map itself, so merged regions leave no inner edge, hairline gap or spur in their outline (#569), `.ai` written by the exports and the CLI (`--export`, `convert`) as Save As writes it (#526), SVG round trips keeping hidden objects and `data-*` attributes (#522), the gradient stop popover staying open after the click that opens it from the Gradient panel (#577), standard ligatures kept under small tracking and dropped only past about −8% / +22% of a word space or when the character asks for them (#583), the Color panel keeping its height as warnings come and go so a drag on its spectrum no longer flickers near white (#578), fonts that font services such as Adobe Fonts load on Windows, listed through DirectWrite (#579), `.ai` files writing their layers and sublayers as PDF layers, hidden ones with their art, and PDF layers nested in layers importing as sublayers (#372), file dialogs on Linux shown off the UI thread so the compositor no longer finds the window not responding, and parented to the window on Linux and Windows (#592), anchors and handle ends picked from 2 px past their drawn size before the segments through them (#593), an image copied in a browser pasting as the picture, not its address (#597), the Free Transform widget with Constrain, Free Transform, Perspective Distort and Free Distort (#598), touch gestures: a two-finger tap undoes and a three-finger tap redoes (#585), the font menus' live preview staying on the text instead of flickering on and off every frame (#563), Edit Artboards showing the active artboard in the Properties panel with New Artboard, Delete Artboard and Exit (#530), the same DirectWrite fonts in `vectorcraft-cli` and its MCP server (#579), SVG layers keeping their names when wrapped and sublayers round-tripping through SVG (#372), a Layers panel that lays out only the rows in view and keeps a document's open rows in its file (#638), a panel dropped on the dock lights up the icon column it goes back to and pops out of its icon (#649), Image Trace's View modes and an Advanced section that stays as it was left (part of #641), Image Trace's Palette: Limited, Full Tone, Automatic and Document Library (part of #641), Image Trace's Create Strokes: line art traced as stroked centre lines (part of #641), a dual-GPU Mac falling back to its other GPU when one fails to start (#651), two fingers dragged on a touch screen panning the canvas (part of #449), the Home screen's Recent Files and a double-clicked New Document preset creating the document (part of #663); see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index a29fb46f4..e431cbbbe 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1723,6 +1723,7 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { app.run("file.open", json!({})).ok(); } }); + recent(app, ui); ui.add_space(28.0); ui.label(egui::RichText::new(tl!("Quickly start a new file")).font(theme::semibold(14.0)).color(t.text)); ui.add_space(10.0); @@ -1743,6 +1744,35 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { crate::community::links(app, ui); } +/// The Home screen's Recent Files (#663): the first of File › Open Recent Files, each its name and +/// folder; a click opens it. Nothing when there are none. +fn recent(app: &mut VectorcraftApp, ui: &mut Ui) { + const SHOWN: usize = 6; + let files: Vec = crate::io::recent_files(app).iter().take(SHOWN).cloned().collect(); + if files.is_empty() { + return; + } + let t = Tokens::get(ui.ctx()); + ui.add_space(28.0); + ui.label(egui::RichText::new(tl!("Recent Files")).font(theme::semibold(14.0)).color(t.text)); + ui.add_space(8.0); + for path in files { + let p = std::path::Path::new(&path); + let name = p.file_name().map_or_else(|| path.clone(), |n| n.to_string_lossy().into_owned()); + let folder = p.parent().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default(); + ui.horizontal(|ui| { + crate::icons::icon(ui, "history", 16.0, t.icon); + let r = ui.add(egui::Button::new(egui::RichText::new(&name).color(t.accent).size(13.0)).frame(false)).on_hover_text(&path); + ui.add(egui::Label::new(egui::RichText::new(&folder).size(12.0).color(t.text_dim)).truncate()); + if r.clicked() + && let Err(e) = crate::io::open_path(app, &path) + { + app.status(e); + } + }); + } +} + fn kurbo_flatten(p: &BezPath, tol: f64, f: &mut impl FnMut(PathEl)) { vectorcraft_geom::kurbo::flatten(p.elements().iter().copied(), tol, f); } diff --git a/crates/ui-egui/src/dialogs/new_document.rs b/crates/ui-egui/src/dialogs/new_document.rs index 7939b963a..968870d95 100644 --- a/crates/ui-egui/src/dialogs/new_document.rs +++ b/crates/ui-egui/src/dialogs/new_document.rs @@ -190,7 +190,7 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.vertical(|ui| { ui.set_width(640.0); ui.set_min_height(HEIGHT - 32.0); - presets(app, ui, &mut d); + b.create |= presets(app, ui, &mut d); }); }); egui::Frame::NONE.fill(t.panel_darker).inner_margin(egui::Margin::same(18)).show(ui, |ui| { @@ -205,8 +205,9 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { b.finish(app, ctx, d); } -/// The category tabs and the chosen category's preset cards. -fn presets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { +/// The category tabs and the chosen category's preset cards: a click chooses one, a double-click +/// also creates it (true then). +fn presets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let names: Vec<&str> = newdoc::category_names().collect(); let cat = names.iter().position(|n| n.eq_ignore_ascii_case(&d.str("category"))).unwrap_or(0); @@ -227,20 +228,24 @@ fn presets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { if list.is_empty() { let hint = if cat == 1 { tl!("Presets you save with the Save Preset button appear here.") } else { tl!("Documents you create appear here.") }; ui.label(egui::RichText::new(hint).color(t.text_dim)); - return; + return false; } + let mut create = false; egui::ScrollArea::vertical().id_salt(("newdoc-presets", cat)).max_height(HEIGHT - 110.0).auto_shrink([false, false]).show(ui, |ui| { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing = egui::vec2(12.0, 12.0); let (w, h, preset) = (d.f64("width", 0.0), d.f64("height", 0.0), d.str("preset")); for s in &list { let selected = d.fields.contains_key(APPLIED) && s.name == preset && s.width == w && s.height == h; - if preset_card(ui, s, selected).clicked() { + let card = preset_card(ui, s, selected); + if card.clicked() || card.double_clicked() { apply(d, s); } + create |= card.double_clicked(); } }); }); + create } /// Is `name` a built-in preset's? Those are ours (translated); Recent and Saved hold the names of @@ -345,7 +350,7 @@ fn details(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, b: &mut // Close and Create at the bottom right. ui.add_space((HEIGHT - (ui.cursor().top() - top) - 28.0).max(12.0)); ui.allocate_ui_with_layout(egui::vec2(ui.available_width(), 28.0), egui::Layout::right_to_left(egui::Align::Center), |ui| { - b.create = widgets::primary_button(ui, tl!("Create")).clicked(); + b.create |= widgets::primary_button(ui, tl!("Create")).clicked(); ui.add_space(8.0); b.close = widgets::secondary_button(ui, tl!("Close")).clicked(); }); @@ -558,7 +563,7 @@ fn show_more(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.horizontal(|ui| { templates = widgets::secondary_button(ui, tl!("Templates…")).on_hover_text(tl!("New from Template")).clicked(); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - b.create = widgets::primary_button(ui, tl!("Create Document")).clicked(); + b.create |= widgets::primary_button(ui, tl!("Create Document")).clicked(); ui.add_space(8.0); // Cancel goes back to New Document. if widgets::secondary_button(ui, tl!("Cancel")).clicked() { @@ -610,6 +615,51 @@ mod tests { app.ui.dialog.as_ref().unwrap().fields.get(k).cloned().unwrap_or_default() } + /// A double-click on a preset card creates the document from it (#663); a click only chooses it. + #[test] + fn a_double_clicked_preset_creates_the_document() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.newDialog", json!({})).unwrap(); + set(&mut app, "category", json!("Mobile")); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + // Frames 50 ms apart, so two clicks a frame apart make a double-click. + let mut time = 0.0; + let mut run = |app: &mut VectorcraftApp, events| { + time += 0.05; + let screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 1000.0))); + let raw = egui::RawInput { screen_rect, events, time: Some(time), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| crate::dialogs::show(app, ui.ctx())); + out.textures_delta.clear(); + out + }; + run(&mut app, vec![]); + // The first card of the category, found by its size line. + let card = |app: &mut VectorcraftApp, run: &mut dyn FnMut(&mut VectorcraftApp, Vec) -> egui::FullOutput| { + let out = run(app, vec![]); + let mut texts = vec![]; + fn walk(s: &egui::Shape, out: &mut Vec<(String, egui::Rect)>) { + match s { + egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect())), + egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)), + _ => {} + } + } + out.shapes.iter().for_each(|c| walk(&c.shape, &mut texts)); + texts.into_iter().find(|(t, _)| t.starts_with("Phone 393")).map(|(_, r)| r.center()).unwrap() + }; + let at = card(&mut app, &mut run); + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + run(&mut app, vec![egui::Event::PointerMoved(at), press(true)]); + run(&mut app, vec![press(false)]); + assert!(app.ui.dialog.is_some() && app.session.documents().is_empty(), "a click only chooses it"); + assert!(field(&app, "preset").as_str().is_some_and(|p| p == "Phone 393×852"), "{:?}", field(&app, "preset")); + run(&mut app, vec![press(true)]); + run(&mut app, vec![press(false)]); + assert!(app.ui.dialog.is_none(), "the double-click created it: {:?}", app.ui.status); + assert_eq!(app.session.documents().len(), 1); + } + #[test] fn a_preset_fills_the_details_and_create_makes_the_document() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/tests_home.rs b/crates/ui-egui/src/tests_home.rs index 7df4afdf3..d998573f2 100644 --- a/crates/ui-egui/src/tests_home.rs +++ b/crates/ui-egui/src/tests_home.rs @@ -90,3 +90,46 @@ fn cmd_n_opens_the_new_document_dialog() { assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("newDocument")); assert_eq!(app.session.documents().len(), 1, "no document is made until the dialog's OK"); } + +/// One Home screen frame (no document open) with `events`: the texts drawn and where. +fn home_texts(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec) -> Vec<(String, egui::Rect)> { + fn walk(s: &egui::Shape, out: &mut Vec<(String, egui::Rect)>) { + match s { + egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect())), + egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)), + _ => {} + } + } + let raw = + egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 900.0))), events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + let mut texts = vec![]; + out.shapes.iter().for_each(|c| walk(&c.shape, &mut texts)); + texts +} + +/// The Home screen lists the first recent files (#663), each by name and folder; a click opens it, +/// and one that can't be opened says so. With none, there is no Recent Files section. +#[test] +fn home_lists_recent_files_and_a_click_opens_one() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let has = |texts: &[(String, egui::Rect)], s: &str| texts.iter().any(|(t, _)| t == s); + assert!(!has(&home_texts(&mut app, &ctx, vec![]), "Recent Files"), "none: no section"); + let dir = std::env::temp_dir().join("vectorcraft-home-recent"); + let missing = dir.join("Gone.svg").to_string_lossy().into_owned(); + app.ui.recent_files = (0..9).map(|i| dir.join(format!("Drawing {i}.svg")).to_string_lossy().into_owned()).collect(); + app.ui.recent_files.insert(0, missing); + let texts = home_texts(&mut app, &ctx, vec![]); + assert!(has(&texts, "Recent Files") && has(&texts, "Gone.svg") && has(&texts, "Drawing 4.svg"), "{texts:?}"); + assert!(!has(&texts, "Drawing 5.svg"), "the first six only"); + assert!(has(&texts, &dir.to_string_lossy()), "with its folder"); + let at = texts.iter().find(|(t, _)| t == "Gone.svg").map(|(_, r)| r.center()).unwrap(); + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + home_texts(&mut app, &ctx, vec![egui::Event::PointerMoved(at), press(true)]); + home_texts(&mut app, &ctx, vec![press(false)]); + assert!(app.session.documents().is_empty()); + assert!(!app.ui.status.is_empty(), "a file that can't be opened says so"); +}