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