1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! action_rail — the SHARED right-side, single-column, uniform-width action
//! toolbar. It is the ONE renderer behind BOTH the modeling context bar
//! ([`super::context_bar`]) and the in-sketch context bar
//! ([`super::sketch`]), so the two look and behave identically: change the
//! layout here and both update. It draws INTO a caller-owned `ui` (the caller
//! owns the top-right `Area`/`Frame` and any stacking), measures every caption
//! up front to size the column to the WIDEST button, and stretches every button
//! to that shared width (100%).
use std::collections::HashMap;
/// One button on an action rail.
pub struct ActionItem {
/// Stable key returned when clicked (and used for the verifier hit-rect).
pub key: String,
/// The caption drawn on the button.
pub label: String,
/// Hover tooltip.
pub tooltip: String,
/// Greyed-out + unclickable when false.
pub enabled: bool,
}
impl ActionItem {
pub fn new(key: impl Into<String>, label: impl Into<String>, tooltip: impl Into<String>) -> Self {
Self {
key: key.into(),
label: label.into(),
tooltip: tooltip.into(),
enabled: true,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
/// Draw a titled single-column toolbar of uniform-width buttons into `ui`.
/// Returns the clicked item's key (if any). Records each button's rect into
/// `hits` (keyed by item key) for the headed verifier. The card's width is the
/// widest button; the optional `title`/`subtitle` wrap within that width.
pub fn action_rail(
ui: &mut egui::Ui,
title: Option<&str>,
subtitle: Option<&str>,
items: &[ActionItem],
hits: &mut HashMap<String, egui::Rect>,
) -> Option<String> {
if items.is_empty() {
return None;
}
// Width = the widest button caption (+ button padding). Measured via the
// Button text style; titles are allowed to wrap within it.
let font = egui::TextStyle::Button.resolve(ui.style());
let measure = |ui: &egui::Ui, text: &str| -> f32 {
ui.ctx().fonts_mut(|f| {
f.layout_no_wrap(text.to_owned(), font.clone(), egui::Color32::PLACEHOLDER)
.size()
.x
})
};
// A caption's LEADING glyph is drawn as artwork, not as a character, so it
// is measured as a square of the text height rather than as text — the
// column must be sized to what is actually drawn. Without an icon font a
// character measured here would also be a box.
// Body, not Button: `icon_text::icon_button` sizes the artwork off Body, and
// the column must be measured at the size it is actually drawn.
let icon_w = ui.text_style_height(&egui::TextStyle::Body);
let mut widest = 0.0f32;
for item in items {
let (icon, rest) = crate::icon_text::split_caption(&item.label);
let text_w = measure(ui, rest);
widest = widest.max(if icon.is_some() {
text_w + icon_w + ui.spacing().item_spacing.x
} else {
text_w
});
}
let col_w = widest + 2.0 * ui.spacing().button_padding.x;
ui.set_width(col_w.max(1.0));
if let Some(title) = title {
ui.label(egui::RichText::new(title).strong());
}
if let Some(subtitle) = subtitle {
ui.label(egui::RichText::new(subtitle).weak().small());
}
ui.add_space(2.0);
let full = ui.available_width();
let h = ui.spacing().interact_size.y;
let mut clicked = None;
for item in items {
let resp = ui
.add_enabled_ui(item.enabled, |ui| {
let button = crate::icon_text::icon_button(ui, &item.label);
ui.add_sized([full, h], button)
})
.inner
.on_hover_text(&item.tooltip);
hits.insert(item.key.clone(), resp.rect);
if resp.clicked() {
clicked = Some(item.key.clone());
}
}
clicked
}