armas_basic/components/
breadcrumb.rs1use crate::ext::ArmasContextExt;
23use egui::{Sense, Ui};
24
25const ITEM_GAP: f32 = 6.0; const SEPARATOR_SIZE: f32 = 14.0; pub struct Breadcrumb {
49 spacing: f32,
50}
51
52impl Breadcrumb {
53 #[must_use]
55 pub const fn new() -> Self {
56 Self { spacing: ITEM_GAP }
57 }
58
59 #[must_use]
61 pub const fn spacing(mut self, spacing: f32) -> Self {
62 self.spacing = spacing;
63 self
64 }
65
66 pub fn show<R>(
68 self,
69 ui: &mut Ui,
70 content: impl FnOnce(&mut BreadcrumbBuilder) -> R,
71 ) -> BreadcrumbResponse {
72 let mut clicked: Option<usize> = None;
73
74 let inner_response = ui.horizontal(|ui| {
75 ui.spacing_mut().item_spacing.x = self.spacing;
76
77 let mut builder = BreadcrumbBuilder {
78 ui,
79 spacing: self.spacing,
80 item_index: 0,
81 clicked: &mut clicked,
82 };
83
84 content(&mut builder);
85 });
86
87 BreadcrumbResponse {
88 response: inner_response.response,
89 clicked,
90 }
91 }
92}
93
94impl Default for Breadcrumb {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100pub struct BreadcrumbBuilder<'a> {
102 ui: &'a mut Ui,
103 spacing: f32,
104 item_index: usize,
105 clicked: &'a mut Option<usize>,
106}
107
108impl BreadcrumbBuilder<'_> {
109 pub fn item(&mut self, label: &str, icon: Option<&str>) -> ItemBuilder<'_> {
111 let theme = self.ui.ctx().armas_theme();
112
113 if self.item_index > 0 {
115 self.ui.add_space(self.spacing);
117
118 let (rect, _) = self
120 .ui
121 .allocate_exact_size(egui::vec2(SEPARATOR_SIZE, SEPARATOR_SIZE), Sense::hover());
122
123 if self.ui.is_rect_visible(rect) {
124 let painter = self.ui.painter();
125 let color = theme.muted_foreground();
126 let stroke = egui::Stroke::new(1.5, color);
127
128 let center = rect.center();
130 let half = SEPARATOR_SIZE * 0.2;
131 painter.line_segment(
132 [
133 egui::pos2(center.x - half, center.y - half * 1.5),
134 egui::pos2(center.x + half, center.y),
135 ],
136 stroke,
137 );
138 painter.line_segment(
139 [
140 egui::pos2(center.x + half, center.y),
141 egui::pos2(center.x - half, center.y + half * 1.5),
142 ],
143 stroke,
144 );
145 }
146
147 self.ui.add_space(self.spacing);
148 }
149
150 let item_builder = ItemBuilder {
151 ui: self.ui,
152 label: label.to_string(),
153 icon: icon.map(std::string::ToString::to_string),
154 is_current: false,
155 item_index: self.item_index,
156 clicked: self.clicked,
157 rendered: false,
158 };
159
160 self.item_index += 1;
161 item_builder
162 }
163}
164
165pub struct ItemBuilder<'a> {
167 ui: &'a mut Ui,
168 label: String,
169 icon: Option<String>,
170 is_current: bool,
171 item_index: usize,
172 clicked: &'a mut Option<usize>,
173 rendered: bool,
174}
175
176impl ItemBuilder<'_> {
177 #[must_use]
179 pub const fn current(mut self) -> Self {
180 self.is_current = true;
181 self
182 }
183
184 fn render(&mut self) {
185 if self.rendered {
186 return;
187 }
188 self.rendered = true;
189
190 let theme = self.ui.ctx().armas_theme();
191
192 let display_label = if let Some(icon) = &self.icon {
194 format!("{} {}", icon, self.label)
195 } else {
196 self.label.clone()
197 };
198
199 if self.is_current {
201 self.ui.label(
202 egui::RichText::new(&display_label)
203 .size(theme.typography.base)
204 .color(theme.foreground()),
205 );
206 } else {
207 let response = self.ui.add(
209 egui::Label::new(
210 egui::RichText::new(&display_label)
211 .size(theme.typography.base)
212 .color(theme.muted_foreground()),
213 )
214 .sense(Sense::click()),
215 );
216
217 if response.hovered() {
219 let rect = response.rect;
221 self.ui
222 .painter()
223 .rect_filled(rect, 0.0, egui::Color32::TRANSPARENT);
224 self.ui.painter().text(
225 rect.left_center(),
226 egui::Align2::LEFT_CENTER,
227 &display_label,
228 egui::FontId::proportional(theme.typography.base),
229 theme.foreground(),
230 );
231 }
232
233 if response.clicked() {
234 *self.clicked = Some(self.item_index);
235 }
236 }
237 }
238}
239
240impl Drop for ItemBuilder<'_> {
241 fn drop(&mut self) {
242 self.render();
243 }
244}
245
246pub struct BreadcrumbResponse {
248 pub response: egui::Response,
250 pub clicked: Option<usize>,
252}