From 3d31b623a4b0aeb3a2b73326d8fcf87c611eab43 Mon Sep 17 00:00:00 2001 From: SofiaAFigueredo Date: Fri, 30 Oct 2026 16:55:39 +0000 Subject: [PATCH 1/3] UI: centre the Home screen and use the accent for the Discord button (#4684) Part of #4684 (unify the UI with PhotoCraft). The Home screen's welcome, New / Open buttons and Community block are centred, as PhotoCraft's Home screen is, and the large Discord button takes the theme's accent instead of Discord's own colour. Preset cards, Recent Files and the wording are unchanged. Still open from #4684: shortcut hints on the Home buttons, PhotoCraft's PRO and STUDIO themes, the Preferences layout and the title bar. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0113uEh3VQ6et4zDMZ9cqzkf --- crates/ui-egui/src/canvas/home.rs | 35 ++++++++++++++++++------------- crates/ui-egui/src/community.rs | 5 ++++- crates/ui-egui/src/tests/home.rs | 31 +++++++++++++++++++++++++++ docs/ui-parity.md | 3 +++ 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/crates/ui-egui/src/canvas/home.rs b/crates/ui-egui/src/canvas/home.rs index d73a5c5bb..672f2dd3d 100644 --- a/crates/ui-egui/src/canvas/home.rs +++ b/crates/ui-egui/src/canvas/home.rs @@ -25,18 +25,21 @@ pub(super) fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { /// The Home screen's column: welcome, New / Open, Recent Files, presets and community links. fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); - ui.label(egui::RichText::new(tl!("Welcome to VectorCraft")).font(theme::semibold(26.0)).color(t.text)); - ui.add_space(4.0); - ui.label(egui::RichText::new(tl!("Vector illustration — fast, open, scriptable.")).size(14.0).color(t.text_dim)); - ui.add_space(22.0); - ui.horizontal(|ui| { - if widgets::primary_button(ui, tl!("New file")).clicked() { - app.run("file.newDialog", json!({})).ok(); - } - ui.add_space(8.0); - if widgets::secondary_button(ui, tl!("Open")).clicked() { - app.run("file.open", json!({})).ok(); - } + // The welcome and the New / Open buttons sit centred, as PhotoCraft's Home screen does (#4684). + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new(tl!("Welcome to VectorCraft")).font(theme::semibold(26.0)).color(t.text)); + ui.add_space(4.0); + ui.label(egui::RichText::new(tl!("Vector illustration — fast, open, scriptable.")).size(14.0).color(t.text_dim)); + ui.add_space(22.0); + ui.horizontal(|ui| { + if widgets::primary_button(ui, tl!("New file")).clicked() { + app.run("file.newDialog", json!({})).ok(); + } + ui.add_space(8.0); + if widgets::secondary_button(ui, tl!("Open")).clicked() { + app.run("file.open", json!({})).ok(); + } + }); }); recent(app, ui); ui.add_space(28.0); @@ -54,9 +57,11 @@ fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) { } }); ui.add_space(28.0); - ui.label(egui::RichText::new(tl!("Community")).font(theme::semibold(14.0)).color(t.text)); - ui.add_space(10.0); - crate::community::links(app, ui); + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new(tl!("Community")).font(theme::semibold(14.0)).color(t.text)); + ui.add_space(10.0); + crate::community::links(app, ui); + }); } /// The Home screen's Recent Files (#663): the first of File › Open Recent Files, each its name and diff --git a/crates/ui-egui/src/community.rs b/crates/ui-egui/src/community.rs index 7ed5eb984..089a421ce 100644 --- a/crates/ui-egui/src/community.rs +++ b/crates/ui-egui/src/community.rs @@ -32,7 +32,10 @@ pub fn discord_button(app: &mut VectorcraftApp, ui: &mut Ui, large: bool) -> egu let at = egui::pos2(0.0, r.center().y - galley.size().y / 2.0); let ir = egui::Rect::from_center_size(r.left_center() + vec2(h * 0.35 + icon / 2.0, 0.0), vec2(icon, icon)); if large { - let fill = if resp.hovered() { DISCORD.gamma_multiply(0.85) } else { DISCORD }; + // The theme's accent, as the primary buttons use it, so the call to action matches the + // rest of the window (and PhotoCraft's, #4684); Discord's colour stays on the app-bar icon. + let t = Tokens::get(ui.ctx()); + let fill = if resp.hovered() { t.accent } else { t.accent_strong }; ui.painter().rect_filled(r, CornerRadius::same((h / 2.0) as u8), fill); icons::paint(ui, "message-circle", ir, Color32::WHITE); ui.painter().galley(egui::pos2(ir.right() + h * 0.2, at.y), galley, Color32::WHITE); diff --git a/crates/ui-egui/src/tests/home.rs b/crates/ui-egui/src/tests/home.rs index 96f50716d..249aa7f76 100644 --- a/crates/ui-egui/src/tests/home.rs +++ b/crates/ui-egui/src/tests/home.rs @@ -438,3 +438,34 @@ fn a_queued_reopen_keeps_the_roots_it_was_asked_in() { assert_eq!(app.session.documents().len(), 1, "the file outside the roots is not opened"); let _ = std::fs::remove_dir_all(d); } + +/// The x-centre of the first text shape whose text is `text` on a drawn Home screen, and the +/// window's centre (#4684). +fn text_centre_x(app: &mut VectorcraftApp, text: &str) -> Option<(f32, f32)> { + fn find(s: &egui::Shape, text: &str) -> Option { + match s { + egui::Shape::Text(t) if t.galley.text() == text => Some(t.pos.x + t.galley.size().x / 2.0), + egui::Shape::Vec(v) => v.iter().find_map(|s| find(s, text)), + _ => None, + } + } + app.canvas_rect = None; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 700.0)); + let raw = egui::RawInput { screen_rect: Some(screen), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| canvas::show(app, ui)); + out.textures_delta.clear(); + let x = out.shapes.iter().find_map(|c| find(&c.shape, text))?; + Some((x, screen.center().x)) +} + +/// The Home screen centres its welcome and its community block, as PhotoCraft's does (#4684). +#[test] +fn home_welcome_and_community_are_centred() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + for text in ["Welcome to VectorCraft", "Community"] { + let (x, centre) = text_centre_x(&mut app, text).unwrap_or_else(|| panic!("{text} is drawn")); + assert!((x - centre).abs() < 2.0, "{text} is centred: {x} vs {centre}"); + } +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 5ba97bacb..3bdb39930 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -39,6 +39,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | Home screen: welcome, New / Open and Community are centred and the large Discord button uses the theme accent, part of #4684; scores and estimates unchanged (estimated) | | 2026-11-06 | minor | Snap to Grid: a straight segment dragged with Direct Selection lands its anchors on the grid, not the pressed point (#3835); scores and estimates unchanged | | 2026-11-06 | minor | Drop Shadow: a swatch beside Color follows the hex and a double-click opens the Color Picker over the dialog, whose OK fills Color (#5364); scores and estimates unchanged | | 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: spinner arrows beside Opacity, the offsets, Blur and Darkness (#5367, #5368); scores and estimates unchanged | @@ -434,6 +435,8 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po ## Look and feel +- [x] Home screen (part of #4684, shared UI with PhotoCraft): the welcome, New / Open buttons and Community block are centred, and the large Discord button takes the theme's accent instead of Discord's colour; the layout and wording of the preset cards and Recent Files are unchanged. Still open from #4684: shortcut hints on the buttons, PhotoCraft's PRO and STUDIO themes, the Preferences layout (search box, theme swatches, Apply) and the title bar. + - [x] Recolor Artwork › Assign: double-click a row's new color to pick it in the Color Picker, in the model it was picked in (part of #4028); a Lab new color is kept but can't be typed yet (the picker's Lab fields work in RGB); Lab entry, swatch libraries and the row menu remain. - [x] Recolor Artwork Edit color chips scroll within the screen, preserving the wheel and actions (#3416); 5, 20, 200 and 400 colors, resizing and UI scaling, final-chip editing and preview/history covered by egui_kittest. From 9b8b1229b6ac8d96d610b8406f5077a570cd85ed Mon Sep 17 00:00:00 2001 From: SofiaAFigueredo Date: Fri, 30 Oct 2026 17:34:14 +0000 Subject: [PATCH 2/3] UI: centre the Home New / Open buttons and links too (#4684) The first commit centred the headings but left the New / Open buttons and the community links on the left, as `horizontal` fills the width. The buttons' row is now padded by its measured width, and the Home screen's links are centred (label only, no icon; About keeps its left-aligned icon rows). The test now measures where the centred text lies and checks the buttons, the Discord button and the links. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0113uEh3VQ6et4zDMZ9cqzkf --- crates/ui-egui/src/canvas/home.rs | 6 +++- crates/ui-egui/src/community.rs | 41 +++++++++++++++++++-------- crates/ui-egui/src/tests/home.rs | 30 ++++++++++++++++---- crates/ui-egui/src/widgets.rs | 2 +- crates/ui-egui/src/widgets/buttons.rs | 7 +++++ 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/crates/ui-egui/src/canvas/home.rs b/crates/ui-egui/src/canvas/home.rs index 672f2dd3d..cd2ac9c44 100644 --- a/crates/ui-egui/src/canvas/home.rs +++ b/crates/ui-egui/src/canvas/home.rs @@ -31,7 +31,11 @@ fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(4.0); ui.label(egui::RichText::new(tl!("Vector illustration — fast, open, scriptable.")).size(14.0).color(t.text_dim)); ui.add_space(22.0); + // `horizontal` fills the width, so the row's own width sets its left edge. + let gap = ui.spacing().item_spacing.x + 8.0; + let row = widgets::button_width(ui, tl!("New file")) + gap + widgets::button_width(ui, tl!("Open")); ui.horizontal(|ui| { + ui.add_space(((ui.available_width() - row) / 2.0).max(0.0)); if widgets::primary_button(ui, tl!("New file")).clicked() { app.run("file.newDialog", json!({})).ok(); } @@ -60,7 +64,7 @@ fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) { ui.vertical_centered(|ui| { ui.label(egui::RichText::new(tl!("Community")).font(theme::semibold(14.0)).color(t.text)); ui.add_space(10.0); - crate::community::links(app, ui); + crate::community::links_centered(app, ui); }); } diff --git a/crates/ui-egui/src/community.rs b/crates/ui-egui/src/community.rs index 089a421ce..034807b8b 100644 --- a/crates/ui-egui/src/community.rs +++ b/crates/ui-egui/src/community.rs @@ -53,27 +53,44 @@ pub fn discord_button(app: &mut VectorcraftApp, ui: &mut Ui, large: bool) -> egu resp } -/// One link row: icon, label, opens the command's URL. -fn link(app: &mut VectorcraftApp, ui: &mut Ui, icon: &str, label: &str, cmd: &str, url: &str) { +/// One link row: icon, label, opens the command's URL. `centered` drops the icon so the label +/// alone sits in the middle of the column. +fn link(app: &mut VectorcraftApp, ui: &mut Ui, icon: &str, label: &str, cmd: &str, url: &str, centered: bool) { let t = Tokens::get(ui.ctx()); - ui.horizontal(|ui| { - icons::icon(ui, icon, 16.0, t.icon); - let r = ui.add(egui::Button::new(egui::RichText::new(label).color(t.accent).size(13.0)).frame(false)).on_hover_text(url); - if r.clicked() { - app.open_link(cmd); - } - }); + let button = |ui: &mut Ui| ui.add(egui::Button::new(egui::RichText::new(label).color(t.accent).size(13.0)).frame(false)).on_hover_text(url); + let r = if centered { + button(ui) + } else { + ui.horizontal(|ui| { + icons::icon(ui, icon, 16.0, t.icon); + button(ui) + }) + .inner + }; + if r.clicked() { + app.open_link(cmd); + } } /// The Discord button and the website / app page / GitHub links. pub fn links(app: &mut VectorcraftApp, ui: &mut Ui) { + links_in(app, ui, false); +} + +/// [`links`] for a centred column (the Home screen's, as PhotoCraft's, #4684): the Discord button +/// and the links, each in the middle. +pub fn links_centered(app: &mut VectorcraftApp, ui: &mut Ui) { + links_in(app, ui, true); +} + +fn links_in(app: &mut VectorcraftApp, ui: &mut Ui, centered: bool) { discord_button(app, ui, true); ui.add_space(8.0); let l = app.session.execute("help.links", &serde_json::json!({})).unwrap_or_default(); let s = |k: &str| l[k].as_str().unwrap_or("").to_string(); - link(app, ui, "globe", tl!("ArtCraft website"), "help.website", &s("website")); - link(app, ui, "external-link", tl!("VectorCraft on getartcraft.com"), "help.appPage", &s("appPage")); - link(app, ui, "git-branch", tl!("Source code on GitHub"), "help.github", &s("github")); + link(app, ui, "globe", tl!("ArtCraft website"), "help.website", &s("website"), centered); + link(app, ui, "external-link", tl!("VectorCraft on getartcraft.com"), "help.appPage", &s("appPage"), centered); + link(app, ui, "git-branch", tl!("Source code on GitHub"), "help.github", &s("github"), centered); } #[cfg(test)] diff --git a/crates/ui-egui/src/tests/home.rs b/crates/ui-egui/src/tests/home.rs index 249aa7f76..cfe150038 100644 --- a/crates/ui-egui/src/tests/home.rs +++ b/crates/ui-egui/src/tests/home.rs @@ -440,11 +440,11 @@ fn a_queued_reopen_keeps_the_roots_it_was_asked_in() { } /// The x-centre of the first text shape whose text is `text` on a drawn Home screen, and the -/// window's centre (#4684). +/// window's centre (#4684). A centred galley's `pos` is its anchor, so its rect says where it lies. fn text_centre_x(app: &mut VectorcraftApp, text: &str) -> Option<(f32, f32)> { fn find(s: &egui::Shape, text: &str) -> Option { match s { - egui::Shape::Text(t) if t.galley.text() == text => Some(t.pos.x + t.galley.size().x / 2.0), + egui::Shape::Text(t) if t.galley.text() == text => Some(t.pos.x + t.galley.rect.center().x), egui::Shape::Vec(v) => v.iter().find_map(|s| find(s, text)), _ => None, } @@ -460,12 +460,32 @@ fn text_centre_x(app: &mut VectorcraftApp, text: &str) -> Option<(f32, f32)> { Some((x, screen.center().x)) } -/// The Home screen centres its welcome and its community block, as PhotoCraft's does (#4684). +/// The Home screen centres its welcome, its New / Open buttons and its community block, as +/// PhotoCraft's does (#4684). #[test] fn home_welcome_and_community_are_centred() { let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); - for text in ["Welcome to VectorCraft", "Community"] { + for text in ["Welcome to VectorCraft", "Community", "ArtCraft website", "VectorCraft on getartcraft.com", "Source code on GitHub"] { let (x, centre) = text_centre_x(&mut app, text).unwrap_or_else(|| panic!("{text} is drawn")); - assert!((x - centre).abs() < 2.0, "{text} is centred: {x} vs {centre}"); + assert!((x - centre).abs() < 6.0, "{text} is centred: {x} vs {centre}"); } + // The Discord button is centred; its label lies right of its icon, 0.375 of the button's 36 pt + // height right of the middle. + let (x, centre) = text_centre_x(&mut app, "Join our Discord").unwrap(); + assert!((x - centre - 0.375 * 36.0).abs() < 2.0, "the Discord button is centred: label at {x} about {centre}"); + // The two buttons (a label's width plus 32, at least 72, each) lie symmetrically about the + // centre: the first one's left edge and the second one's right edge are equally far from it. + let width = |label: &str| { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut w = 0.0; + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| w = crate::widgets::button_width(ui, label)); + out.textures_delta.clear(); + w + }; + let (new_x, centre) = text_centre_x(&mut app, "New file").unwrap(); + let (open_x, _) = text_centre_x(&mut app, "Open").unwrap(); + let left = new_x - width("New file") / 2.0; + let right = open_x + width("Open") / 2.0; + assert!((centre - left - (right - centre)).abs() < 6.0, "New / Open row is centred: {left}..{right} about {centre}"); } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index efde0d0b1..5a6daf1fa 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -28,7 +28,7 @@ pub use buttons::{ SPLIT_ARROW, flat_button, flat_button_fit, icon_button, icon_button_enabled, menu_item, menu_item_name, paint_icon_button, primary_button, primary_button_enabled, secondary_button, split_button, split_button_named, toggle_icon, }; -pub(crate) use buttons::{icon_button_info, primary_button_width, secondary_button_width}; +pub(crate) use buttons::{button_width, icon_button_info, primary_button_width, secondary_button_width}; #[cfg(test)] use chips::gradient_chip_colors; pub use chips::{ProxyClicks, chip_button, chip_button_named, fill_stroke_proxy, gradient_chip, paint_chip, proxy_chip, swatch_tile}; diff --git a/crates/ui-egui/src/widgets/buttons.rs b/crates/ui-egui/src/widgets/buttons.rs index 44ee3c477..a33f7cac8 100644 --- a/crates/ui-egui/src/widgets/buttons.rs +++ b/crates/ui-egui/src/widgets/buttons.rs @@ -101,6 +101,13 @@ fn flat_fill(ui: &Ui, rect: Rect, resp: &Response) { } } +/// The width [`primary_button`] and [`secondary_button`] give `text` (their label plus padding, at +/// least 72), for centring a row of them. +pub(crate) fn button_width(ui: &Ui, text: &str) -> f32 { + let galley = ui.painter().layout_no_wrap(tl!(text).to_string(), theme::semibold(12.5), Color32::PLACEHOLDER); + (galley.size().x + 32.0).max(72.0) +} + /// The accent call-to-action button (dialog OK/Create). pub fn primary_button(ui: &mut Ui, text: &str) -> Response { primary_button_enabled(ui, text, true) From 6173bd404f520eb43b9278c73528613d3bce158c Mon Sep 17 00:00:00 2001 From: SofiaAFigueredo Date: Fri, 6 Nov 2026 08:19:57 -0300 Subject: [PATCH 3/3] Tests: give the Home centring test a taller window (#4684) The Community links can fall below the first scroll viewport at 1000x700 when the fonts differ, so the test measured nothing. 1000x900 keeps every link visible. Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/tests/home.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/tests/home.rs b/crates/ui-egui/src/tests/home.rs index cfe150038..d1c20991a 100644 --- a/crates/ui-egui/src/tests/home.rs +++ b/crates/ui-egui/src/tests/home.rs @@ -452,7 +452,7 @@ fn text_centre_x(app: &mut VectorcraftApp, text: &str) -> Option<(f32, f32)> { app.canvas_rect = None; let ctx = egui::Context::default(); crate::theme::install_fonts(&ctx); - let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 700.0)); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 900.0)); let raw = egui::RawInput { screen_rect: Some(screen), ..Default::default() }; let mut out = ctx.run_ui(raw, |ui| canvas::show(app, ui)); out.textures_delta.clear();