cadmus_core/view/file_chooser/
breadcrumb.rs1use 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}