1use super::directory::Directory;
2use crate::color::TEXT_BUMP_SMALL;
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::font::{font_from_style, Font, Fonts, NORMAL_STYLE};
6use crate::framebuffer::{Framebuffer, UpdateMode};
7use crate::geom::{big_half, divide, small_half, CycleDir, Dir, Point, Rectangle};
8use crate::gesture::GestureEvent;
9use crate::unit::scale_by_dpi;
10use crate::view::filler::Filler;
11use crate::view::icon::{Icon, ICONS_PIXMAPS};
12use crate::view::{Align, Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER};
13use crate::view::{SMALL_BAR_HEIGHT, THICKNESS_MEDIUM};
14use std::collections::BTreeSet;
15use std::path::{Path, PathBuf};
16use tracing::warn;
17
18pub struct DirectoriesBar {
19 id: Id,
20 pub rect: Rectangle,
21 pub path: PathBuf,
22 pages: Vec<Vec<Box<dyn View>>>,
23 selection_page: Option<usize>,
24 current_page: usize,
25}
26
27#[derive(Debug, Clone)]
28struct Page<'a> {
29 start_index: usize,
30 end_index: usize,
31 lines: Vec<Line<'a>>,
32}
33
34impl<'a> Default for Page<'a> {
35 fn default() -> Page<'a> {
36 Page {
37 start_index: 0,
38 end_index: 0,
39 lines: Vec::new(),
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45struct Layout {
46 x_height: i32,
47 padding: i32,
48 max_line_width: i32,
49 max_lines: usize,
50}
51
52#[derive(Debug, Clone)]
53struct Line<'a> {
54 width: i32,
55 labels_count: usize,
56 items: Vec<Item<'a>>,
57}
58
59impl<'a> Default for Line<'a> {
60 fn default() -> Line<'a> {
61 Line {
62 width: 0,
63 labels_count: 0,
64 items: Vec::new(),
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
70enum Item<'a> {
71 Label {
72 path: &'a Path,
73 width: i32,
74 max_width: Option<i32>,
75 },
76 Icon {
77 name: &'a str,
78 width: i32,
79 },
80}
81
82impl<'a> Item<'a> {
83 #[inline]
84 fn width(&self) -> i32 {
85 match *self {
86 Item::Label { width, .. } | Item::Icon { width, .. } => width,
87 }
88 }
89}
90
91impl DirectoriesBar {
92 pub fn new<P: AsRef<Path>>(rect: Rectangle, path: P) -> DirectoriesBar {
93 DirectoriesBar {
94 id: ID_FEEDER.next(),
95 rect,
96 path: path.as_ref().to_path_buf(),
97 current_page: 0,
98 selection_page: None,
99 pages: vec![Vec::new()],
100 }
101 }
102
103 pub fn shift(&mut self, delta: Point) {
104 for children in &mut self.pages {
105 for child in children {
106 *child.rect_mut() += delta;
107 }
108 }
109 self.rect += delta;
110 }
111
112 pub fn dirs(&self) -> BTreeSet<PathBuf> {
113 self.pages
114 .iter()
115 .flatten()
116 .filter_map(|child| child.downcast_ref::<Directory>())
117 .map(|dir| &dir.path)
118 .cloned()
119 .collect()
120 }
121
122 pub fn go_to_page(&mut self, index: usize) {
123 self.current_page = index;
124 }
125
126 pub fn set_current_page(&mut self, dir: CycleDir) {
127 match dir {
128 CycleDir::Next if self.current_page < self.pages.len() - 1 => {
129 self.current_page += 1;
130 }
131 CycleDir::Previous if self.current_page > 0 => {
132 self.current_page -= 1;
133 }
134 _ => (),
135 }
136 }
137
138 pub fn update_selected(&mut self, current_directory: &Path) {
139 for (index, children) in self.pages.iter_mut().enumerate() {
140 for child in children.iter_mut() {
141 if let Some(dir) = child.downcast_mut::<Directory>() {
142 if dir.update_selected(current_directory) {
143 self.current_page = index;
144 self.selection_page = Some(index);
145 }
146 }
147 }
148 }
149 }
150
151 pub fn update_content(
152 &mut self,
153 directories: &BTreeSet<PathBuf>,
154 current_directory: &Path,
155 fonts: &mut Fonts,
156 ) {
157 let dpi = CURRENT_DEVICE.dpi;
158 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
159 let min_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32 - thickness;
160 let mut start_index = 0;
161 let mut font = font_from_style(fonts, &NORMAL_STYLE, dpi);
162 let x_height = font.x_heights.0 as i32;
163 let padding = font.em() as i32;
164 let vertical_padding = min_height - x_height;
165 let max_line_width = self.rect.width() as i32 - 2 * padding;
166 let max_lines = ((self.rect.height() as i32 - vertical_padding / 2)
167 / (x_height + vertical_padding / 2))
168 .max(1) as usize;
169 let layout = Layout {
170 x_height,
171 padding,
172 max_line_width,
173 max_lines,
174 };
175
176 let pages_count = self.pages.len();
177 self.pages.clear();
178 self.selection_page = None;
179
180 loop {
181 let mut has_selection = false;
182 let (children, end_index) = {
183 let page = self.make_page(start_index, &layout, directories, &mut font);
184 let children = self.make_children(
185 &page,
186 &layout,
187 current_directory,
188 directories,
189 &mut has_selection,
190 );
191 (children, page.end_index)
192 };
193 if has_selection {
194 self.selection_page = Some(self.pages.len());
195 }
196 self.pages.push(children);
197 if end_index == directories.len() {
198 break;
199 }
200 start_index = end_index;
201 }
202
203 let previous_position = if pages_count > 0 {
204 self.current_page as f32 / pages_count as f32
205 } else {
206 0.0
207 };
208
209 self.current_page = self
210 .selection_page
211 .unwrap_or_else(|| (previous_position * self.pages.len() as f32) as usize);
212 }
213
214 fn make_page<'a>(
215 &self,
216 start_index: usize,
217 layout: &Layout,
218 directories: &'a BTreeSet<PathBuf>,
219 font: &mut Font,
220 ) -> Page<'a> {
221 let Layout {
222 padding,
223 max_line_width,
224 max_lines,
225 ..
226 } = *layout;
227 let mut end_index = start_index;
228 let mut line = Line::default();
229 let mut page = Page::default();
230
231 if start_index > 0 {
232 let pixmap = ICONS_PIXMAPS.get("angle-left-small").unwrap();
233 line.width += pixmap.width as i32 + padding;
234 line.items.push(Item::Icon {
235 name: "angle-left-small",
236 width: pixmap.width as i32,
237 });
238 }
239
240 for dir in directories.iter().skip(start_index) {
241 let mut dir_width = font
242 .plan(dir.file_name().unwrap().to_string_lossy(), None, None)
243 .width;
244 let mut max_dir_width = None;
245
246 if dir_width > max_line_width {
247 max_dir_width = Some(max_line_width);
248 dir_width = max_line_width;
249 }
250
251 line.labels_count += 1;
252 line.width += dir_width;
253 end_index += 1;
254 let label = Item::Label {
255 path: dir.as_path(),
256 width: dir_width,
257 max_width: max_dir_width,
258 };
259 line.items.push(label);
260
261 if line.width >= max_line_width {
262 let mut next_line = Line::default();
263 if line.width > max_line_width {
264 if line.labels_count > 1 {
265 if let Some(item) = line.items.pop() {
266 line.width -= item.width() + padding;
267 line.labels_count -= 1;
268 next_line.width += item.width() + padding;
269 next_line.items.push(item);
270 next_line.labels_count += 1;
271 }
272 }
273 if line.labels_count == 1 {
274 let occupied_width = line.width - line.items.last().unwrap().width();
275 if let Some(&mut Item::Label {
276 ref mut width,
277 ref mut max_width,
278 ..
279 }) = line.items.last_mut()
280 {
281 *width = max_line_width - occupied_width;
282 *max_width = Some(*width);
283 }
284 line.width = max_line_width;
285 }
286 }
287 page.lines.push(line);
288 line = next_line;
289 if page.lines.len() >= max_lines {
290 break;
291 }
292 } else {
293 line.width += padding;
294 }
295 }
296
297 if page.lines.len() < max_lines {
298 page.lines.push(line);
299 } else {
300 end_index -= line.items.len();
301 }
302
303 if end_index < directories.len() {
304 if let Some(mut line) = page.lines.pop() {
305 let pixmap = ICONS_PIXMAPS.get("angle-right-small").unwrap();
306 line.width += pixmap.width as i32 + padding;
307
308 if line.labels_count > 1 {
309 while line.width > max_line_width {
310 if let Some(Item::Label { width, .. }) = line.items.pop() {
311 line.width -= width + padding;
312 line.labels_count -= 1;
313 end_index -= 1;
314 } else {
315 break;
316 }
317 }
318 } else {
319 let occupied_width = line.width - line.items.last().unwrap().width();
320 if let Some(&mut Item::Label {
321 ref mut width,
322 ref mut max_width,
323 ..
324 }) = line.items.last_mut()
325 {
326 *width = max_line_width - occupied_width;
327 *max_width = Some(*width);
328 }
329 line.width = max_line_width;
330 }
331
332 line.items.push(Item::Icon {
333 name: "angle-right-small",
334 width: pixmap.width as i32,
335 });
336 page.lines.push(line);
337 }
338 }
339
340 page.start_index = start_index;
341 page.end_index = end_index;
342 page
343 }
344
345 fn make_children(
346 &self,
347 page: &Page,
348 layout: &Layout,
349 current_directory: &Path,
350 directories: &BTreeSet<PathBuf>,
351 has_selection: &mut bool,
352 ) -> Vec<Box<dyn View>> {
353 let mut children = Vec::new();
354 let Layout {
355 x_height,
356 padding,
357 max_line_width,
358 max_lines,
359 } = *layout;
360 let background = TEXT_BUMP_SMALL[0];
361 let vertical_space = self.rect.height() as i32 - max_lines as i32 * x_height;
362 let baselines = divide(vertical_space, max_lines as i32 + 1);
363 let directories_count = directories.len();
364 let lines_count = page.lines.len();
365 let mut pos = pt!(
366 self.rect.min.x + small_half(padding),
367 self.rect.min.y + small_half(baselines[0])
368 );
369
370 let filler = Filler::new(
372 rect![
373 self.rect.min,
374 pt!(self.rect.max.x, self.rect.min.y + small_half(baselines[0]))
375 ],
376 background,
377 );
378 children.push(Box::new(filler) as Box<dyn View>);
379
380 let filler = Filler::new(
382 rect![
383 pt!(self.rect.min.x, self.rect.min.y + small_half(baselines[0])),
384 pt!(
385 self.rect.min.x + small_half(padding),
386 self.rect.max.y - big_half(baselines[max_lines])
387 )
388 ],
389 background,
390 );
391 children.push(Box::new(filler) as Box<dyn View>);
392
393 for (line_index, line) in page.lines.iter().enumerate() {
394 let paddings = if line_index == lines_count - 1 && page.end_index == directories_count {
395 vec![padding; line.items.len() + 1]
396 } else {
397 let horizontal_space =
398 (line.items.len() as i32 - 1) * padding + max_line_width - line.width;
399 let mut v = divide(horizontal_space, line.items.len() as i32 - 1);
400 v.insert(0, padding);
401 v.push(padding);
402 v
403 };
404
405 let rect_height =
406 big_half(baselines[line_index]) + x_height + small_half(baselines[line_index + 1]);
407
408 for (item_index, item) in line.items.iter().enumerate() {
409 let left_padding = big_half(paddings[item_index]);
410 let right_padding = small_half(paddings[item_index + 1]);
411 let rect_width = left_padding + item.width() + right_padding;
412 let sop = pos + pt!(rect_width, rect_height);
413
414 match *item {
415 Item::Label {
416 path, max_width, ..
417 } => {
418 let selected = current_directory.starts_with(path);
419 if selected {
420 *has_selection = true;
421 }
422 let child = Directory::new(
423 rect![pos, sop],
424 path.to_path_buf(),
425 selected,
426 Align::Left(left_padding),
427 max_width,
428 );
429 children.push(Box::new(child) as Box<dyn View>);
430 }
431 Item::Icon { name, .. } => {
432 let dir = if item_index == 0 {
433 CycleDir::Previous
434 } else {
435 CycleDir::Next
436 };
437 let child = Icon::new(name, rect![pos, sop], Event::Page(dir))
438 .background(background)
439 .align(Align::Left(left_padding));
440 children.push(Box::new(child) as Box<dyn View>);
441 }
442 }
443
444 pos.x += rect_width;
445 }
446
447 pos.x = self.rect.min.x + small_half(padding);
448 pos.y += rect_height;
449 }
450
451 if page.end_index == directories_count {
453 let last_width = page.lines[lines_count - 1].width;
454 let x_offset = last_width + small_half(padding);
455 let y_offset = baselines.iter().take(lines_count).sum::<i32>()
456 - big_half(baselines[lines_count - 1])
457 + (lines_count - 1) as i32 * x_height;
458 let height = big_half(baselines[lines_count - 1])
459 + x_height
460 + small_half(baselines[lines_count]);
461 let filler = Filler::new(
462 rect![
463 pt!(self.rect.min.x + x_offset, self.rect.min.y + y_offset),
464 pt!(
465 self.rect.max.x - big_half(padding),
466 self.rect.min.y + y_offset + height
467 )
468 ],
469 background,
470 );
471 children.push(Box::new(filler) as Box<dyn View>);
472 }
473
474 if lines_count < max_lines {
476 let y_offset = baselines.iter().take(lines_count + 1).sum::<i32>()
477 - big_half(baselines[lines_count])
478 + lines_count as i32 * x_height;
479 let filler = Filler::new(
480 rect![
481 pt!(
482 self.rect.min.x + small_half(padding),
483 self.rect.min.y + y_offset
484 ),
485 pt!(
486 self.rect.max.x - big_half(padding),
487 self.rect.max.y - big_half(baselines[max_lines])
488 )
489 ],
490 background,
491 );
492 children.push(Box::new(filler) as Box<dyn View>);
493 }
494
495 let filler = Filler::new(
497 rect![
498 pt!(
499 self.rect.max.x - big_half(padding),
500 self.rect.min.y + small_half(baselines[0])
501 ),
502 pt!(
503 self.rect.max.x,
504 self.rect.max.y - big_half(baselines[max_lines])
505 )
506 ],
507 background,
508 );
509 children.push(Box::new(filler) as Box<dyn View>);
510
511 let filler = Filler::new(
513 rect![
514 pt!(
515 self.rect.min.x,
516 self.rect.max.y - big_half(baselines[max_lines])
517 ),
518 self.rect.max
519 ],
520 background,
521 );
522 children.push(Box::new(filler) as Box<dyn View>);
523
524 children
525 }
526}
527
528impl View for DirectoriesBar {
529 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
530 fn handle_event(
531 &mut self,
532 evt: &Event,
533 _hub: &Hub,
534 _bus: &mut Bus,
535 rq: &mut RenderQueue,
536 _context: &mut Context,
537 ) -> bool {
538 match *evt {
539 Event::Gesture(GestureEvent::Swipe { dir, start, .. }) if self.rect.includes(start) => {
540 match dir {
541 Dir::West => {
542 self.set_current_page(CycleDir::Next);
543 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
544 true
545 }
546 Dir::East => {
547 self.set_current_page(CycleDir::Previous);
548 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
549 true
550 }
551 _ => false,
552 }
553 }
554 Event::Page(dir) => {
555 let current_page = self.current_page;
556 self.set_current_page(dir);
557 if self.current_page != current_page {
558 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
559 }
560 true
561 }
562 Event::Chapter(dir) => {
563 let pages_count = self.pages.len();
564 if pages_count > 1 {
565 let current_page = self.current_page;
566 match dir {
567 CycleDir::Previous => self.go_to_page(0),
568 CycleDir::Next => self.go_to_page(pages_count - 1),
569 }
570 if self.current_page != current_page {
571 let page = &mut self.pages[current_page];
572 let index = match dir {
573 CycleDir::Previous => 2,
574 CycleDir::Next => page.len() - 3,
575 };
576 if let Some(icon) = page[index].downcast_mut::<Icon>() {
577 icon.active = false;
578 } else {
579 warn!("Unexpected directory bar icon type");
580 }
581 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
582 }
583 }
584 true
585 }
586 _ => false,
587 }
588 }
589
590 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts, _rect), fields(rect = ?_rect)))]
591 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
592
593 fn rect(&self) -> &Rectangle {
594 &self.rect
595 }
596
597 fn rect_mut(&mut self) -> &mut Rectangle {
598 &mut self.rect
599 }
600
601 fn children(&self) -> &Vec<Box<dyn View>> {
602 &self.pages[self.current_page]
603 }
604
605 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
606 &mut self.pages[self.current_page]
607 }
608
609 fn id(&self) -> Id {
610 self.id
611 }
612}