diff --git a/crates/ui-egui/src/canvas/home.rs b/crates/ui-egui/src/canvas/home.rs index d73a5c5bb..cd2ac9c44 100644 --- a/crates/ui-egui/src/canvas/home.rs +++ b/crates/ui-egui/src/canvas/home.rs @@ -25,18 +25,25 @@ 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); + // `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(); + } + 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 +61,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_centered(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..034807b8b 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); @@ -50,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 96f50716d..d1c20991a 100644 --- a/crates/ui-egui/src/tests/home.rs +++ b/crates/ui-egui/src/tests/home.rs @@ -438,3 +438,54 @@ 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). 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.rect.center().x), + 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, 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(); + let x = out.shapes.iter().find_map(|c| find(&c.shape, text))?; + Some((x, screen.center().x)) +} + +/// 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", "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() < 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) 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.