Merge pull request #35 from harrisonoconnorhover/fix/ribbon-dropdown-chevron

Fix missing dropdown arrows on large ribbon buttons (#26)
This commit is contained in:
Brandon Thomas 2026-10-10 11:57:44 -04:00
commit bd0ccd41b5
2 changed files with 79 additions and 7 deletions

View File

@ -38,8 +38,10 @@ pub fn toggle_button(ui: &mut Ui, icon: Icon, on: bool, tip: &str) -> Response {
pub fn big_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str, dropdown: bool) -> Response {
let t = Tokens::get(ui.ctx());
let font = theme::ui_font(11.5);
let lines: Vec<&str> = label.split('\n').collect();
let text_w = lines.iter().map(|l| ui.painter().layout_no_wrap(l.to_string(), font.clone(), t.text).size().x).fold(0.0, f32::max);
let lines: Vec<_> = label.split('\n').map(|line| ui.painter().layout_no_wrap(line.to_string(), font.clone(), t.text)).collect();
let arrow_size = 10.0;
let arrow_space = if dropdown { 4.0 + arrow_size } else { 0.0 };
let text_w = lines.iter().enumerate().map(|(i, line)| line.size().x + if i + 1 == lines.len() { arrow_space } else { 0.0 }).fold(0.0, f32::max);
let w = (text_w + 12.0).max(44.0);
let (rect, resp) = ui.allocate_exact_size(vec2(w, 72.0), Sense::click());
if resp.is_pointer_button_down_on() {
@ -48,12 +50,20 @@ pub fn big_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str, dropdown: boo
ui.painter().rect_filled(rect, 5.0, t.hover);
}
icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.center().x, rect.top() + 21.0), vec2(30.0, 30.0)), icon, t.text);
for (i, l) in lines.iter().enumerate() {
let mut s = l.to_string();
if dropdown && i + 1 == lines.len() {
s.push_str(" ▾");
for (i, line) in lines.iter().enumerate() {
let has_arrow = dropdown && i + 1 == lines.len();
let line_w = line.size().x + if has_arrow { arrow_space } else { 0.0 };
let left = rect.center().x - line_w / 2.0;
let y = rect.top() + 46.0 + i as f32 * 13.0;
ui.painter().galley(pos2(left, y - line.size().y / 2.0), line.clone(), t.text);
if has_arrow {
icons::paint(
ui.painter(),
Rect::from_center_size(pos2(left + line_w - arrow_size / 2.0, y + 1.0), vec2(arrow_size, arrow_size)),
Icon::Chevron,
t.text_dim,
);
}
ui.painter().text(pos2(rect.center().x, rect.top() + 46.0 + i as f32 * 13.0), Align2::CENTER_CENTER, s, font.clone(), t.text);
}
resp.on_hover_text(tip)
}

View File

@ -0,0 +1,62 @@
//! Ribbon widget paint tests, using egui's bundled fonts on every platform.
use egui::{FontDefinitions, FontFamily, Rect, Shape, pos2, vec2};
use gridcraft_ui_egui::{icons::Icon, theme, widgets};
fn render_big_button(label: &str, dropdown: bool) -> (Rect, Vec<Shape>) {
let ctx = egui::Context::default();
let mut fonts = FontDefinitions::default();
let proportional = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
fonts.families.insert(FontFamily::Name(theme::UI.into()), proportional);
ctx.set_fonts(fonts);
let mut rect = Rect::NOTHING;
let mut output = ctx
.run_ui(egui::RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(400.0, 200.0))), ..Default::default() }, |ui| {
rect = widgets::big_button(ui, Icon::More, label, "test button", dropdown).rect
});
output.textures_delta.clear();
(rect, output.shapes.into_iter().map(|shape| shape.shape).collect())
}
#[test]
fn big_dropdown_paints_chevron_beside_final_label_line() {
for label in ["Paste", "Conditional\nFormatting", "Longer first line\nGo"] {
let (rect, shapes) = render_big_button(label, true);
let texts: Vec<_> = shapes.iter().filter_map(|shape| if let Shape::Text(text) = shape { Some(text) } else { None }).collect();
assert_eq!(texts.iter().map(|text| text.galley.job.text.as_str()).collect::<Vec<_>>(), label.split('\n').collect::<Vec<_>>());
let paths: Vec<_> = shapes.iter().filter_map(|shape| if let Shape::Path(path) = shape { Some(path) } else { None }).collect();
assert_eq!(paths.len(), 1, "dropdown indicator must be painted independently of font glyphs");
let chevron = paths[0];
assert_eq!(chevron.points.len(), 3);
assert!(chevron.points[0].x < chevron.points[1].x && chevron.points[1].x < chevron.points[2].x);
assert!(chevron.points[1].y > chevron.points[0].y && chevron.points[1].y > chevron.points[2].y);
let last = texts.last().unwrap();
let last_rect = last.galley.rect.translate(last.pos.to_vec2());
assert!(chevron.points[0].x > last_rect.right(), "arrow must not overlap the label");
assert!((chevron.points[1].y - last_rect.center().y).abs() < 3.0, "arrow must align with the final line");
assert!(rect.right() - chevron.points[2].x >= 6.0, "button must reserve space for the arrow and padding");
assert!(last_rect.left() - rect.left() >= 6.0);
assert!(((last_rect.left() + chevron.points[2].x) / 2.0 - rect.center().x).abs() < 2.0, "label and arrow must be centered together");
for text in texts.iter().take(texts.len() - 1) {
let text_rect = text.galley.rect.translate(text.pos.to_vec2());
assert!((text_rect.center().x - rect.center().x).abs() < 0.01, "earlier lines remain centered");
}
}
}
#[test]
fn big_button_without_dropdown_keeps_centered_labels_and_width() {
for label in ["Paste", "Conditional\nFormatting"] {
let (rect, shapes) = render_big_button(label, false);
assert!(!shapes.iter().any(|shape| matches!(shape, Shape::Path(_))), "plain button must not paint an arrow");
let mut widest: f32 = 0.0;
for shape in shapes {
if let Shape::Text(text) = shape {
let text_rect = text.galley.rect.translate(text.pos.to_vec2());
assert!((text_rect.center().x - rect.center().x).abs() < 0.01);
widest = widest.max(text_rect.width());
}
}
assert!((rect.width() - (widest + 12.0).max(44.0)).abs() < 0.01);
}
}