Merge pull request #4732 from SofiaAFigueredo/feat/4684-unify-home-screen-ui

UI: centre the Home screen and use the accent for the Discord button (#4684)
This commit is contained in:
BFlat
2026-11-07 09:07:26 +08:00
committed by GitHub
6 changed files with 119 additions and 29 deletions
+24 -15
View File
@@ -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
+33 -13
View File
@@ -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)]
+51
View File
@@ -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<f32> {
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}");
}
+1 -1
View File
@@ -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};
+7
View File
@@ -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)
+3
View File
@@ -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.