mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-14 14:26:48 -04:00
Centre the app name and tagline on the icon in the grid
The grid card's text started at the top of its row and sat 11 pt below the icon's centre. The text is now measured (centered_lines) and the icon's slot is as tall as the text, so the two share a centre at any scale. The table already centred its rows on main. The icon now reports itself to accessibility, which the alignment test uses to find it. Ported onto the restyled card from the original PR. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b9397ff845
commit
f464b8145e
2 files changed
+64
-8
No files matched your search
@@ -5,7 +5,7 @@ use artcraft_launcher_core::platform::pick_asset;
|
||||
use artcraft_launcher_core::protocol::{Command, JobPhase};
|
||||
use artcraft_launcher_core::status::AppStatus;
|
||||
use artcraft_launcher_core::{AppGroup, SUITE, time};
|
||||
use egui::{Color32, CornerRadius, Rect, RichText, Sense, Stroke, Ui, vec2};
|
||||
use egui::{Color32, CornerRadius, FontId, Rect, RichText, Sense, Stroke, Ui, vec2};
|
||||
|
||||
use crate::app::{Dialog, LauncherUi, TitleBar};
|
||||
use crate::model::{self, AppEntry};
|
||||
@@ -366,9 +366,14 @@ impl LauncherUi {
|
||||
let ui = &mut child;
|
||||
ui.horizontal(|ui| {
|
||||
const ICON: f32 = 56.0;
|
||||
let (slot, _) = ui.allocate_exact_size(vec2(ICON, ICON), Sense::hover());
|
||||
let [name, tagline, like] = [FontId::new(22.0, theme::display()), FontId::proportional(13.5), FontId::proportional(12.0)];
|
||||
const GAP: f32 = 4.0;
|
||||
// The slot is as tall as the text beside it when that is taller, so the two share a centre.
|
||||
let block = lines_height(ui, &[&name, &tagline, &like], GAP);
|
||||
let (slot, icon_response) = ui.allocate_exact_size(vec2(ICON, block.max(ICON)), Sense::hover());
|
||||
icon_response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Image, true, format!("{} icon", app.name)));
|
||||
// The icon rises a little on hover, over a soft drop shadow (the site's only shadow).
|
||||
let icon_rect = slot.translate(vec2(0.0, -4.0 * lift));
|
||||
let icon_rect = Rect::from_center_size(slot.center(), vec2(ICON, ICON)).translate(vec2(0.0, -4.0 * lift));
|
||||
ui.painter().add(
|
||||
egui::epaint::Shadow { offset: [0, 6], blur: 14, spread: 0, color: Color32::from_black_alpha(71) }
|
||||
.as_shape(icon_rect.shrink(4.0), CornerRadius::same(12)),
|
||||
@@ -377,16 +382,17 @@ impl LauncherUi {
|
||||
egui::Image::new(icon).paint_at(ui, icon_rect);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 4.0;
|
||||
centered_lines(ui, &[&name, &tagline, &like], GAP, |ui| {
|
||||
// The name's row is as tall as its text, not as a button.
|
||||
ui.spacing_mut().interact_size.y = 0.0;
|
||||
ui.horizontal(|ui| {
|
||||
widgets::wordmark(ui, app, 22.0);
|
||||
if app.group == AppGroup::New {
|
||||
widgets::badge(ui, "New", t.app_ink(app));
|
||||
}
|
||||
});
|
||||
ui.add(egui::Label::new(RichText::new(app.tagline).size(13.5).color(t.muted)).truncate());
|
||||
ui.add(egui::Label::new(RichText::new(format!("Like {}", app.inspired_by())).size(12.0).color(t.faint)).truncate());
|
||||
ui.add(egui::Label::new(RichText::new(app.tagline).font(tagline.clone()).color(t.muted)).truncate());
|
||||
ui.add(egui::Label::new(RichText::new(format!("Like {}", app.inspired_by())).font(like.clone()).color(t.faint)).truncate());
|
||||
});
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
@@ -795,3 +801,20 @@ fn launcher_download(r: &artcraft_launcher_core::Release, p: artcraft_launcher_c
|
||||
};
|
||||
pick_asset(r, &[artcraft_launcher_core::catalog::LAUNCHER_ID], p, kinds).map(|(a, _)| a.url.clone())
|
||||
}
|
||||
|
||||
/// A column of single-line texts, one per font and `gap` apart, centred across the row it is in.
|
||||
/// `ui.vertical` starts at the top of the row instead: a row only centres what it knows the height of.
|
||||
/// Each line is measured as an empty galley, which is rounded to the pixel like a label, unlike `Fonts::row_height`.
|
||||
fn centered_lines(ui: &mut Ui, fonts: &[&FontId], gap: f32, add_contents: impl FnOnce(&mut Ui)) {
|
||||
let height = lines_height(ui, fonts, gap);
|
||||
ui.allocate_ui_with_layout(vec2(ui.available_width(), height), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
ui.spacing_mut().item_spacing.y = gap;
|
||||
add_contents(ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// The height of [`centered_lines`] with these fonts.
|
||||
fn lines_height(ui: &Ui, fonts: &[&FontId], gap: f32) -> f32 {
|
||||
let line = |font: &FontId| ui.fonts_mut(|f| f.layout_no_wrap(String::new(), font.clone(), Color32::PLACEHOLDER)).size().y;
|
||||
(fonts.iter().map(|font| line(font) + gap).sum::<f32>() - gap).max(0.0)
|
||||
}
|
||||
@@ -106,13 +106,17 @@ fn events() -> Vec<Event> {
|
||||
}
|
||||
|
||||
fn harness(view: ViewMode, theme: egui::Theme, manage: bool) -> (Harness<'static, Option<LauncherUi>>, Rc<RefCell<Vec<Command>>>) {
|
||||
harness_at(view, theme, manage, 1.0)
|
||||
}
|
||||
|
||||
fn harness_at(view: ViewMode, theme: egui::Theme, manage: bool, scale: f32) -> (Harness<'static, Option<LauncherUi>>, Rc<RefCell<Vec<Command>>>) {
|
||||
let sent = Rc::new(RefCell::new(Vec::new()));
|
||||
let sent2 = sent.clone();
|
||||
let mut backend = Some(Fake { events: events(), sent: sent2, manage, clock: Rc::new(Cell::new(NOW)) });
|
||||
let settings = Settings { view, notify: false, ..Settings::default() };
|
||||
// No `.wgpu()`: it gives every test its own GPU device, and those segfault on parallel test threads (see above).
|
||||
// The default renderer creates one only when a test renders.
|
||||
let harness = Harness::builder().with_size(egui::vec2(1180.0, 900.0)).with_theme(theme).build_ui_state(
|
||||
let harness = Harness::builder().with_size(egui::vec2(1180.0, 900.0)).with_pixels_per_point(scale).with_theme(theme).build_ui_state(
|
||||
move |ui, state: &mut Option<LauncherUi>| {
|
||||
if state.is_none()
|
||||
&& let Some(b) = backend.take()
|
||||
@@ -219,6 +223,35 @@ fn switching_views_and_filters() {
|
||||
h.get_by_label("PhotoCraft");
|
||||
}
|
||||
|
||||
/// How far the middle of these lines of text is below the middle of the app icon beside them, in points.
|
||||
fn below_icon_centre(h: &Harness<'_, Option<LauncherUi>>, lines: &[&str]) -> f32 {
|
||||
let text = lines.iter().map(|l| h.get_by_label(l).rect()).reduce(|a, b| a.union(b)).unwrap();
|
||||
let icon = h
|
||||
.query_all(egui_kittest::kittest::by().role(egui::accesskit::Role::Image))
|
||||
.map(|n| n.rect())
|
||||
.filter(|r| r.right() <= text.left() && r.top() < text.bottom() && r.bottom() > text.top())
|
||||
.max_by(|a, b| a.right().total_cmp(&b.right()))
|
||||
.unwrap();
|
||||
assert!(text.left() - icon.right() < 24.0, "no icon beside {lines:?}");
|
||||
text.center().y - icon.center().y
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_and_tagline_are_centred_on_the_icon() {
|
||||
for (scale, suffix) in [(1.0, "-1x"), (1.5, "-1.5x"), (2.0, "-2x")] {
|
||||
let (mut h, _) = harness_at(ViewMode::Grid, egui::Theme::Dark, true, scale);
|
||||
h.run_steps(8);
|
||||
snapshot(&mut h, &format!("grid-dark{suffix}"));
|
||||
let off = below_icon_centre(&h, &["PhotoCraft", "Image editing", "Like Adobe Photoshop®"]);
|
||||
assert!(off.abs() < 0.25, "grid at {scale}x: {off}");
|
||||
let (mut h, _) = harness_at(ViewMode::Table, egui::Theme::Light, true, scale);
|
||||
h.run_steps(8);
|
||||
snapshot(&mut h, &format!("table-light{suffix}"));
|
||||
let off = below_icon_centre(&h, &["PhotoCraft", "Image editing"]);
|
||||
assert!(off.abs() < 0.25, "table at {scale}x: {off}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The app names in the order the table shows them.
|
||||
fn order(h: &Harness<'_, Option<LauncherUi>>) -> Vec<String> {
|
||||
let names: Vec<&str> = SUITE.iter().map(|a| a.name).collect();
|
||||
|
||||
Reference in new issue
Block a user