cadmus_core/view/file_chooser/
breadcrumb.rs

1use crate::color::TEXT_NORMAL;
2use crate::context::Context;
3use crate::device::CURRENT_DEVICE;
4use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
5use crate::framebuffer::Framebuffer;
6use crate::geom::Rectangle;
7use crate::gesture::GestureEvent;
8use crate::unit::scale_by_dpi;
9use crate::view::{Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
10use std::path::{Path, PathBuf};
11
12pub struct Breadcrumb {
13    id: Id,
14    rect: Rectangle,
15    children: Vec<Box<dyn View>>,
16    path: PathBuf,
17}
18
19struct BreadcrumbEntry {
20    id: Id,
21    rect: Rectangle,
22    children: Vec<Box<dyn View>>,
23    path: Option<PathBuf>,
24    text: String,
25    is_current: bool,
26}
27
28impl BreadcrumbEntry {
29    fn new(rect: Rectangle, path: Option<PathBuf>, text: String, is_current: bool) -> Self {
30        BreadcrumbEntry {
31            id: ID_FEEDER.next(),
32            rect,
33            children: Vec::new(),
34            path,
35            text,
36            is_current,
37        }
38    }
39}
40
41impl View for BreadcrumbEntry {
42    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, _rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
43    fn handle_event(
44        &mut self,
45        evt: &Event,
46        _hub: &Hub,
47        bus: &mut Bus,
48        _rq: &mut RenderQueue,
49        _context: &mut Context,
50    ) -> bool {
51        match evt {
52            Event::Gesture(GestureEvent::Tap(center))
53                if self.rect.includes(*center) && !self.is_current =>
54            {
55                if let Some(p) = &self.path {
56                    bus.push_back(Event::SelectDirectory(p.clone()));
57                }
58                true
59            }
60            _ => false,
61        }
62    }
63
64    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts), fields(rect = ?_rect)))]
65    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
66        let dpi = CURRENT_DEVICE.dpi;
67        let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
68
69        let plan = font.plan(&self.text, None, None);
70        let dx = (self.rect.width() as i32 - plan.width as i32) / 2;
71        let dy = (self.rect.height() as i32 - font.x_heights.0 as i32) / 2;
72        let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy);
73
74        font.render(fb, TEXT_NORMAL[1], &plan, pt);
75    }
76
77    fn rect(&self) -> &Rectangle {
78        &self.rect
79    }
80
81    fn rect_mut(&mut self) -> &mut Rectangle {
82        &mut self.rect
83    }
84
85    fn children(&self) -> &Vec<Box<dyn View>> {
86        &self.children
87    }
88
89    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
90        &mut self.children
91    }
92
93    fn id(&self) -> Id {
94        self.id
95    }
96}
97
98struct ComponentData {
99    path: PathBuf,
100    text: String,
101    width: i32,
102    is_current: bool,
103}
104
105impl Breadcrumb {
106    pub fn new(rect: Rectangle, path: &Path) -> Breadcrumb {
107        let id = ID_FEEDER.next();
108        let children = Vec::new();
109        Breadcrumb {
110            id,
111            rect,
112            children,
113            path: path.to_path_buf(),
114        }
115    }
116
117    fn build_path_components(path: &Path) -> Vec<PathBuf> {
118        let mut components: Vec<PathBuf> = Vec::new();
119        let mut current = path;
120
121        while let Some(parent) = current.parent() {
122            components.push(current.to_path_buf());
123            current = parent;
124        }
125        components.push(current.to_path_buf());
126        components.reverse();
127        components
128    }
129
130    fn create_component_data(
131        components: &[PathBuf],
132        font: &mut crate::font::Font,
133    ) -> Vec<ComponentData> {
134        let mut component_data: Vec<ComponentData> = Vec::new();
135
136        for (i, component_path) in components.iter().enumerate() {
137            let name = component_path
138                .file_name()
139                .unwrap_or_else(|| {
140                    if component_path.as_os_str() == "/" {
141                        std::ffi::OsStr::new("/")
142                    } else {
143                        component_path.as_os_str()
144                    }
145                })
146                .to_string_lossy()
147                .to_string();
148
149            let text = if i == components.len() - 1 {
150                name.clone()
151            } else if name == "/" {
152                "/ ".to_string()
153            } else {
154                format!("{} / ", name)
155            };
156
157            let width = font.plan(&text, None, None).width;
158            let is_current = i == components.len() - 1;
159
160            component_data.push(ComponentData {
161                path: component_path.clone(),
162                text,
163                width,
164                is_current,
165            });
166        }
167
168        component_data
169    }
170
171    fn calculate_start_index(
172        component_data: &[ComponentData],
173        available_width: i32,
174        font: &mut crate::font::Font,
175    ) -> usize {
176        let total_width: i32 = component_data.iter().map(|c| c.width).sum();
177
178        if total_width <= available_width {
179            return 0;
180        }
181
182        let ellipsis_text = "... / ";
183        let ellipsis_width = font.plan(ellipsis_text, None, None).width;
184
185        let mut accumulated_width = ellipsis_width;
186        let mut start_idx = component_data.len();
187
188        for i in (0..component_data.len()).rev() {
189            if accumulated_width + component_data[i].width > available_width {
190                break;
191            }
192            accumulated_width += component_data[i].width;
193            start_idx = i;
194        }
195
196        start_idx
197    }
198
199    fn add_ellipsis_entry(&mut self, ellipsis_width: i32, padding: i32) {
200        let ellipsis_rect = rect![
201            self.rect.min.x + padding,
202            self.rect.min.y,
203            self.rect.min.x + padding + ellipsis_width,
204            self.rect.max.y
205        ];
206
207        let ellipsis_entry = BreadcrumbEntry::new(ellipsis_rect, None, "... / ".to_string(), false);
208
209        self.children
210            .push(Box::new(ellipsis_entry) as Box<dyn View>);
211    }
212
213    fn create_breadcrumb_entries(
214        &mut self,
215        component_data: &[ComponentData],
216        start_index: usize,
217        padding: i32,
218        font: &mut crate::font::Font,
219    ) {
220        let mut x = self.rect.min.x + padding;
221
222        if start_index > 0 {
223            let ellipsis_text = "... / ";
224            x += font.plan(ellipsis_text, None, None).width;
225        }
226
227        for data in component_data.iter().skip(start_index) {
228            let segment_rect = rect![x, self.rect.min.y, x + data.width, self.rect.max.y];
229
230            let entry = BreadcrumbEntry::new(
231                segment_rect,
232                Some(data.path.clone()),
233                data.text.clone(),
234                data.is_current,
235            );
236
237            self.children.push(Box::new(entry) as Box<dyn View>);
238
239            x += data.width;
240        }
241    }
242
243    pub fn set_path(&mut self, path: &Path, fonts: &mut Fonts) {
244        self.path = path.to_path_buf();
245        self.children.clear();
246
247        let dpi = CURRENT_DEVICE.dpi;
248        let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
249        let padding = scale_by_dpi(8.0, dpi) as i32;
250
251        let components = Self::build_path_components(path);
252        let component_data = Self::create_component_data(&components, font);
253
254        let available_width = self.rect.width() as i32 - 2 * padding;
255        let start_index = Self::calculate_start_index(&component_data, available_width, font);
256
257        if start_index > 0 {
258            let ellipsis_width = font.plan("... / ", None, None).width as i32;
259            self.add_ellipsis_entry(ellipsis_width, padding);
260        }
261
262        self.create_breadcrumb_entries(&component_data, start_index, padding, font);
263    }
264}
265
266impl View for Breadcrumb {
267    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
268    fn handle_event(
269        &mut self,
270        _evt: &Event,
271        _hub: &Hub,
272        _bus: &mut Bus,
273        _rq: &mut RenderQueue,
274        _context: &mut Context,
275    ) -> bool {
276        false
277    }
278
279    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
280    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
281
282    fn rect(&self) -> &Rectangle {
283        &self.rect
284    }
285
286    fn rect_mut(&mut self) -> &mut Rectangle {
287        &mut self.rect
288    }
289
290    fn children(&self) -> &Vec<Box<dyn View>> {
291        &self.children
292    }
293
294    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
295        &mut self.children
296    }
297
298    fn id(&self) -> Id {
299        self.id
300    }
301}