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