cadmus_core/view/home/
directories_bar.rs

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        // Top filler.
371        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        // Left filler.
381        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        // End of line filler.
452        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        // End of page filler.
475        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        // Right filler.
496        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        // Bottom filler.
512        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}