1use crate::symbols::MacSymbol;
4use crate::theme::MacTheme;
5use egui::{
6 Align2, Color32, CornerRadius, FontId, Id, Painter, Pos2, Rect, Response, Sense, Stroke,
7 StrokeKind, Ui, Vec2,
8};
9use std::fmt::Debug;
10use std::hash::Hash;
11
12#[derive(Debug, Clone)]
14pub struct MacPathComponent<'a> {
15 pub title: &'a str,
17 pub is_folder: bool,
19 pub symbol: Option<MacSymbol>,
21}
22
23impl<'a> MacPathComponent<'a> {
24 pub fn folder(title: &'a str) -> Self {
26 Self {
27 title,
28 is_folder: true,
29 symbol: None,
30 }
31 }
32
33 pub fn file(title: &'a str) -> Self {
35 Self {
36 title,
37 is_folder: false,
38 symbol: None,
39 }
40 }
41
42 pub fn with_symbol(mut self, symbol: MacSymbol) -> Self {
44 self.symbol = Some(symbol);
45 self
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct MacPathControlResponse {
52 pub response: Response,
54 pub clicked_segment: Option<usize>,
56 pub choose_clicked: bool,
58}
59
60pub struct MacPathControl<'a> {
62 id: Id,
63 components: Vec<MacPathComponent<'a>>,
64 show_choose_button: bool,
65 choose_label: &'a str,
66 width: Option<f32>,
67 enabled: bool,
68 theme: Option<&'a MacTheme>,
69}
70
71impl<'a> MacPathControl<'a> {
72 pub fn from_segments(id_source: impl Hash + Debug, segments: &'a [&'a str]) -> Self {
74 let n = segments.len();
75 let comps: Vec<_> = segments
76 .iter()
77 .enumerate()
78 .map(|(i, &title)| MacPathComponent {
79 title,
80 is_folder: i + 1 < n,
81 symbol: None,
82 })
83 .collect();
84 Self::new(id_source, &comps)
85 }
86
87 pub fn new(id_source: impl Hash + Debug, components: &[MacPathComponent<'a>]) -> Self {
89 #[allow(deprecated)]
90 let id = Id::new(id_source);
91 Self {
92 id,
93 components: components.to_vec(),
94 show_choose_button: false,
95 choose_label: "Choose...",
96 width: None,
97 enabled: true,
98 theme: None,
99 }
100 }
101
102 pub fn show_choose_button(mut self, show: bool) -> Self {
104 self.show_choose_button = show;
105 self
106 }
107
108 pub fn choose_label(mut self, label: &'a str) -> Self {
110 self.choose_label = label;
111 self
112 }
113
114 pub fn width(mut self, width: f32) -> Self {
116 self.width = Some(width);
117 self
118 }
119
120 pub fn enabled(mut self, enabled: bool) -> Self {
122 self.enabled = enabled;
123 self
124 }
125
126 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
128 self.theme = Some(theme);
129 self
130 }
131
132 pub fn show(self, ui: &mut Ui) -> MacPathControlResponse {
134 let default_theme = MacTheme::from_ctx(ui.ctx());
135 let theme = self.theme.unwrap_or(&default_theme);
136 let pcm = theme.path_control();
137
138 let width = self.width.unwrap_or_else(|| ui.available_width());
139 let height = pcm.height.points();
140 let sense = if self.enabled {
141 Sense::click()
142 } else {
143 Sense::hover()
144 };
145 let (rect, response) = ui.allocate_exact_size(Vec2::new(width, height), sense);
146
147 let painter = ui.painter_at(rect);
148 let corner_r = pcm.corner_radius.points();
149 let border_w = pcm.border_width.points();
150 let border_stroke = Stroke::new(border_w, theme.separator());
151 painter.rect(
152 rect,
153 CornerRadius::same(corner_r as u8),
154 theme.control_background(),
155 border_stroke,
156 StrokeKind::Inside,
157 );
158
159 let font_id = FontId::proportional(pcm.font_size.points());
160 let pad_h = pcm.padding_horizontal.points();
161 let seg_pad_h = pcm.segment_padding_horizontal.points();
162 let seg_spacing = pcm.segment_spacing.points();
163 let icon_sz = pcm.icon_size.points();
164 let icon_gap = pcm.icon_spacing.points();
165 let chev_sz = pcm.chevron_size.points();
166 let seg_r = CornerRadius::same(pcm.segment_corner_radius.points() as u8);
167
168 let mut choose_clicked = false;
169 let right_limit = if self.show_choose_button {
170 let lbl = self.choose_label.to_owned();
171 let choose_text_w = ui.fonts_mut(|f| {
172 f.layout_no_wrap(lbl, font_id.clone(), theme.label())
173 .size()
174 .x
175 });
176 let btn_w = choose_text_w + pcm.choose_button_padding.points() * 2.0;
177 let btn_h = pcm.choose_button_height.points();
178 let btn_pos = Pos2::new(
179 rect.right() - pad_h - btn_w,
180 rect.top() + (height - btn_h) * 0.5,
181 );
182 let btn_rect = Rect::from_min_size(btn_pos, Vec2::new(btn_w, btn_h));
183 let btn_resp = ui.interact(btn_rect, self.id.with("choose_btn"), sense);
184 if btn_resp.clicked() && self.enabled {
185 choose_clicked = true;
186 }
187 let down = btn_resp.is_pointer_button_down_on();
188 let btn_bg = if down {
189 theme.selected_control()
190 } else if btn_resp.hovered() {
191 theme.control()
192 } else {
193 theme.window_background()
194 };
195 let btn_fg = if down {
196 theme
197 .selected_control_background_style()
198 .primary_text_color(theme)
199 } else if self.enabled {
200 theme.label()
201 } else {
202 theme.placeholder_text()
203 };
204 painter.rect(btn_rect, seg_r, btn_bg, border_stroke, StrokeKind::Inside);
205 painter.text(
206 btn_rect.center(),
207 Align2::CENTER_CENTER,
208 self.choose_label,
209 font_id.clone(),
210 btn_fg,
211 );
212 btn_rect.left() - pad_h
213 } else {
214 rect.right() - pad_h
215 };
216
217 let mut cursor_x = rect.left() + pad_h;
218 let mut clicked_segment = None;
219 let count = self.components.len();
220 let text_color = if self.enabled {
221 theme.label()
222 } else {
223 theme.placeholder_text()
224 };
225
226 for (idx, comp) in self.components.iter().enumerate() {
227 if cursor_x >= right_limit {
228 break;
229 }
230 let title_str = comp.title.to_owned();
231 let title_w = ui.fonts_mut(|f| {
232 f.layout_no_wrap(title_str, font_id.clone(), theme.label())
233 .size()
234 .x
235 });
236 let seg_w = (seg_pad_h * 2.0 + icon_sz + icon_gap + title_w)
237 .min((right_limit - cursor_x).max(0.0));
238 let seg_rect = Rect::from_min_size(
239 Pos2::new(cursor_x, rect.top() + border_w),
240 Vec2::new(seg_w, height - border_w * 2.0),
241 );
242 let seg_resp = ui.interact(seg_rect, self.id.with(("seg", idx)), sense);
243 if seg_resp.clicked() && self.enabled {
244 clicked_segment = Some(idx);
245 }
246 if seg_resp.hovered() && self.enabled {
247 painter.rect_filled(seg_rect, seg_r, theme.control());
248 }
249
250 let seg_clip = seg_rect.intersect(Rect::from_min_max(
251 rect.min,
252 Pos2::new(right_limit, rect.max.y),
253 ));
254 let seg_painter = painter.with_clip_rect(seg_clip);
255 let icon_center =
256 Pos2::new(seg_rect.left() + seg_pad_h + icon_sz * 0.5, rect.center().y);
257 let icon_rect = Rect::from_center_size(icon_center, Vec2::splat(icon_sz));
258 paint_component_icon(&seg_painter, theme, comp, icon_rect);
259
260 let text_pos = Pos2::new(icon_rect.right() + icon_gap, rect.center().y);
261 seg_painter.text(
262 text_pos,
263 Align2::LEFT_CENTER,
264 comp.title,
265 font_id.clone(),
266 text_color,
267 );
268
269 cursor_x = seg_rect.right() + seg_spacing;
270 if idx + 1 < count && cursor_x + chev_sz < right_limit {
271 let chev_pos = Pos2::new(cursor_x + chev_sz * 0.5, rect.center().y);
272 let chev_font = FontId::proportional(chev_sz);
273 painter.text(
274 chev_pos,
275 Align2::CENTER_CENTER,
276 MacSymbol::ChevronRight.as_str(),
277 chev_font,
278 theme.secondary_label(),
279 );
280 cursor_x += chev_sz + seg_spacing;
281 }
282 }
283
284 MacPathControlResponse {
285 response,
286 clicked_segment,
287 choose_clicked,
288 }
289 }
290}
291
292fn paint_component_icon(
294 painter: &Painter,
295 theme: &MacTheme,
296 comp: &MacPathComponent<'_>,
297 icon_rect: Rect,
298) {
299 let pcm = theme.path_control();
300 let border_w = pcm.border_width.points();
301 let r = CornerRadius::same((border_w * 2.0) as u8);
302 if let Some(sym) = comp.symbol {
303 let font = FontId::proportional(pcm.font_size.points());
304 painter.text(
305 icon_rect.center(),
306 Align2::CENTER_CENTER,
307 sym.as_str(),
308 font,
309 theme.control_accent(),
310 );
311 } else if comp.is_folder {
312 let tab_w = pcm.folder_tab_width.points();
313 let tab_h = pcm.folder_tab_height.points();
314 let body_min = Pos2::new(
315 icon_rect.left() + border_w,
316 icon_rect.top() + tab_h + border_w,
317 );
318 let body_max = Pos2::new(
319 icon_rect.right() - border_w,
320 icon_rect.bottom() - border_w * 2.0,
321 );
322 let body_rect = Rect::from_min_max(body_min, body_max);
323 let tab_rect = Rect::from_min_size(
324 Pos2::new(body_rect.left(), body_rect.top() - tab_h),
325 Vec2::new(tab_w, tab_h + border_w),
326 );
327 painter.rect_filled(
328 tab_rect,
329 CornerRadius::same(border_w as u8),
330 theme.control_accent(),
331 );
332 painter.rect_filled(body_rect, r, theme.control_accent());
333 } else {
334 let inset_x = border_w * 2.0 + border_w;
335 let inset_y = border_w * 2.0;
336 let doc_min = Pos2::new(icon_rect.left() + inset_x, icon_rect.top() + inset_y);
337 let doc_max = Pos2::new(icon_rect.right() - inset_x, icon_rect.bottom() - inset_y);
338 let doc_rect = Rect::from_min_max(doc_min, doc_max);
339 painter.rect(
340 doc_rect,
341 r,
342 Color32::TRANSPARENT,
343 Stroke::new(border_w, theme.secondary_label()),
344 StrokeKind::Inside,
345 );
346 }
347}