diff --git a/crates/ui-egui/src/brushes_tab.rs b/crates/ui-egui/src/brushes_tab.rs index 7a8d56b3d0..703b6e0290 100644 --- a/crates/ui-egui/src/brushes_tab.rs +++ b/crates/ui-egui/src/brushes_tab.rs @@ -475,6 +475,22 @@ fn darker(c: Color32, f: f32) -> Color32 { Color32::from_rgba_unmultiplied((r as f32 * f).round() as u8, (g as f32 * f).round() as u8, (b as f32 * f).round() as u8, a) } +/// Where a card's tip thumbnail goes in its `cell`. `number_h` is the height of the size number +/// under it, `None` on compact cards (no number: the tip takes the cell whole). Otherwise the +/// number keeps a band at the bottom of the cell and the tip is centred in what's left above it, +/// at [`CARD_TIP_BASE`] × `scale` or that room's side, whichever is smaller, times the preset's +/// [`crate::brush_sections::thumb_scale`]: big brushes still read bigger than small ones, and +/// none of them reaches down over the number. +fn card_tip_rect(cell: egui::Rect, number_h: Option, size: f32, scale: f32) -> egui::Rect { + let Some(number_h) = number_h else { + return egui::Rect::from_center_size(cell.center(), egui::Vec2::splat((cell.width().min(cell.height()) - 2.0).max(1.0))); + }; + // The number sits 1 pt off the cell's bottom, with a 1 pt gap above it. + let room = egui::Rect::from_min_max(cell.min, pos2(cell.right(), (cell.bottom() - number_h - 2.0).max(cell.top()))); + let side = (CARD_TIP_BASE * scale).min(room.width().min(room.height())).max(0.0) * crate::brush_sections::thumb_scale(size); + egui::Rect::from_center_size(room.center(), egui::Vec2::splat(if side.is_finite() { side.max(1.0) } else { 1.0 })) +} + /// One preset in the Brush Preset picker: a 2 × 2 grid whose bottom row (the name) spans both /// columns — tip at the top left, stroke preview at the top right. A part that's off loses its /// cell (the other takes the room): with only the tip and the name on, the name moves into the @@ -545,18 +561,13 @@ fn preset_card( let tip_w = if tip_only { top.width() } else { (CARD_TIP_W * scale).min(top.width() * 0.5) }; let cell = egui::Rect::from_min_size(top.left_top(), vec2(tip_w, top_h)); let tip = brush_preview::tip_texture(ui.ctx(), &format!("brushes-tip:{}", p.name), &pb.tip, (pb.hardness, pb.angle, pb.roundness), 36, t.text); - let side = if compact { (cell.width().min(cell.height()) - 2.0).max(1.0) } else { CARD_TIP_BASE * scale * crate::brush_sections::thumb_scale(pb.size) }; - let center = if compact { cell.center() } else { pos2(cell.center().x, cell.center().y - 2.0 * scale_h) }; - let tr = egui::Rect::from_center_size(center, vec2(side, side)); + let number = + (!compact).then(|| ui.painter().layout_no_wrap(format!("{}", pb.size.round() as i64), egui::FontId::proportional(CARD_NUM_FONT), t.text_faint)); + let tr = card_tip_rect(cell, number.as_ref().map(|g| g.size().y), pb.size, scale); ui.painter().image(tip.id(), tr, full_uv(), Color32::WHITE); - if !compact { - ui.painter().text( - pos2(cell.center().x, cell.bottom() - 1.0), - egui::Align2::CENTER_BOTTOM, - format!("{}", pb.size.round() as i64), - egui::FontId::proportional(CARD_NUM_FONT), - t.text_faint, - ); + if let Some(g) = number { + let at = pos2(cell.center().x - g.size().x / 2.0, cell.bottom() - 1.0 - g.size().y); + ui.painter().galley(at, g, t.text_faint); } x = cell.right(); } diff --git a/crates/ui-egui/src/brushes_tab_tests.rs b/crates/ui-egui/src/brushes_tab_tests.rs index 3a225aa6c6..4c61ecc54d 100644 --- a/crates/ui-egui/src/brushes_tab_tests.rs +++ b/crates/ui-egui/src/brushes_tab_tests.rs @@ -265,3 +265,56 @@ fn the_brushes_tab_grid_is_centred_past_its_indent() { h.run_steps(6); rows_are_centred(&h, PANEL_LIST.indent, 2); } + +/// #4454: a card's size number sat under the tip thumbnail of any medium or large brush (the +/// tip took up to 44.8 pt of a 42 pt cell, the number anchored to the cell's bottom), so the +/// "30" of Hard Round and the "45" of Soft Round couldn't be read. At every footer scale that +/// shows numbers, in each card kind, the tip now stays above the number, and bigger brushes still +/// get bigger tips. +#[test] +fn card_tips_never_cover_their_size_number() { + let number_h = std::rc::Rc::new(std::cell::Cell::new(0.0_f32)); + let seen = number_h.clone(); + let mut h = Harness::new_ui(move |ui| { + let g = ui.painter().layout_no_wrap("45".into(), egui::FontId::proportional(CARD_NUM_FONT), Color32::WHITE); + seen.set(g.size().y); + }); + PhotocraftApp::setup_context(&h.ctx, Default::default()); + h.run(); + let number_h = number_h.get(); + assert!(number_h > 8.0, "the number has a real height: {number_h}"); + let mut scale = CARD_COMPACT_SCALE + 0.01; + while scale <= *crate::brush_picker::SCALE_RANGE.end() + 1e-3 { + let scale_h = scale.max(CARD_MIN_H_SCALE); + // The tip cell of a full card (name row under it), a tip-and-name card and a tip-only one, + // as [`preset_card`] sizes them in a wide column. + let cells = [ + vec2(CARD_TIP_W * scale, (CARD_MAX_H - CARD_PAD * 2.0 - CARD_NAME_H) * scale_h), + vec2(CARD_TIP_W * scale, (CARD_TIP_NAME_H - CARD_PAD * 2.0) * scale_h), + vec2((CARD_TIP_ONLY_W - 16.0) * scale, (CARD_TIP_ONLY_H - CARD_PAD * 2.0) * scale_h), + ]; + for size in cells { + let cell = Rect::from_min_size(egui::pos2(10.0, 20.0), size); + let number_top = cell.bottom() - 1.0 - number_h; + let mut last = 0.0; + for brush in [1.0, 5.0, 13.0, 30.0, 45.0, 100.0, 2500.0] { + let tip = card_tip_rect(cell, Some(number_h), brush, scale); + assert!(tip.bottom() <= number_top + 1e-3, "scale {scale}, cell {size:?}, {brush} px: tip {tip:?} reaches the number at {number_top}"); + assert!(cell.expand(1e-3).contains_rect(tip), "scale {scale}, {brush} px: tip {tip:?} leaves its cell {cell:?}"); + assert!(tip.width() >= last, "scale {scale}, {brush} px: a bigger brush gets a smaller tip"); + last = tip.width(); + } + } + scale += 0.05; + } + // The tip-only grid's cells are tall enough for the number: a big brush's tip still spans + // the cell's width. + let cell = Rect::from_min_size(egui::pos2(0.0, 0.0), vec2(CARD_TIP_ONLY_W - 16.0, CARD_TIP_ONLY_H - CARD_PAD * 2.0)); + assert!((card_tip_rect(cell, Some(number_h), 45.0, 1.0).width() - cell.width()).abs() < 1e-3); + // Compact cards: no number, the tip fills the cell; nonsense never makes a non-finite rect. + assert_eq!(card_tip_rect(cell, None, 45.0, 0.5).width(), cell.width() - 2.0); + for (n, size, scale) in [(f32::NAN, 30.0, 1.0), (500.0, f32::NAN, 1.0), (12.0, 30.0, f32::INFINITY), (12.0, -5.0, 0.0)] { + let r = card_tip_rect(cell, Some(n), size, scale); + assert!(r.min.is_finite() && r.max.is_finite(), "{n} {size} {scale}: {r:?}"); + } +}