cadmus_core/view/file_chooser/
file_entry.rs

1use super::FileEntryData;
2use crate::color::{TEXT_NORMAL, WHITE};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
6use crate::framebuffer::Framebuffer;
7use crate::geom::Rectangle;
8use crate::gesture::GestureEvent;
9use crate::view::label::Label;
10use crate::view::{Align, Bus, EntryId, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
11use chrono::{DateTime, Local};
12
13/// A visual entry representing a file or directory in the file browser.
14///
15/// `FileEntry` displays file metadata in a horizontal layout with an icon, name, size, and date.
16/// It handles user interactions such as taps to select files and long presses to perform actions
17/// on directories.
18///
19/// # Fields
20///
21/// * `id` - Unique identifier for this view
22/// * `rect` - Bounding rectangle for the entire entry
23/// * `children` - Child views (labels for icon, name, size, and date)
24/// * `data` - File entry data containing metadata (name, size, date, directory flag, path)
25pub struct FileEntry {
26    id: Id,
27    rect: Rectangle,
28    children: Vec<Box<dyn View>>,
29    data: FileEntryData,
30}
31
32impl FileEntry {
33    /// Creates a new file entry with a horizontal layout displaying file metadata.
34    ///
35    /// # Layout
36    ///
37    /// The entry displays file information in a left-to-right layout:
38    /// - **Icon** (left): Directory folder (📁) or file (📄) emoji
39    /// - **Name** (center-left): File or directory name, truncated if necessary
40    /// - **Size** (center-right): Formatted file size (e.g., "1.5 MB") or "-" if unavailable
41    /// - **Date** (right): Last modified date in format "Mon DD, YYYY HH:MM" or "-" if unavailable
42    ///
43    /// Each element is separated by padding based on the font's em size.
44    /// The name field expands to fill available space between icon and size/date fields.
45    ///
46    /// # Arguments
47    ///
48    /// * `rect` - The bounding rectangle for the entire entry
49    /// * `data` - The file entry data containing name, size, modification date, and directory flag
50    /// * `context` - Mutable reference to the application context for font access
51    pub fn new(rect: Rectangle, data: FileEntryData, context: &mut Context) -> FileEntry {
52        let mut children: Vec<Box<dyn View>> = Vec::new();
53        let dpi = CURRENT_DEVICE.dpi;
54        let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
55        let padding = font.em() as i32;
56
57        let event = Some(Event::Select(EntryId::FileEntry(data.path.clone())));
58        let icon = if data.is_dir { "📁" } else { "📄" };
59        let size_text = data
60            .size
61            .map(Self::format_size)
62            .unwrap_or_else(|| "-".to_string());
63        let date_text = data
64            .modified
65            .map(Self::format_date)
66            .unwrap_or_else(|| "-".to_string());
67
68        let icon_plan = font.plan(icon, None, None);
69        let date_plan = font.plan(&date_text, None, None);
70        let size_plan = font.plan(&size_text, None, None);
71
72        let mut x = rect.min.x + padding;
73        let icon_width = icon_plan.width + padding;
74
75        let name_max_width = rect.width() as i32
76            - icon_width
77            - padding
78            - date_plan.width
79            - size_plan.width
80            - 4 * padding;
81
82        let name_plan = font.plan(&data.name, Some(name_max_width), None);
83
84        let icon_rect = rect![x, rect.min.y, x + icon_width, rect.max.y];
85        children.push(Box::new(
86            Label::new(icon_rect, icon.to_string(), Align::Left(0))
87                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
88                .event(event.clone()),
89        ));
90        x += icon_width;
91
92        let name_rect = rect![x, rect.min.y, x + name_plan.width + padding, rect.max.y];
93        children.push(Box::new(
94            Label::new(name_rect, data.name.clone(), Align::Left(0))
95                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
96                .event(event.clone()),
97        ));
98
99        let size_x = rect.max.x - date_plan.width - size_plan.width - 2 * padding;
100        let size_rect = rect![
101            size_x,
102            rect.min.y,
103            size_x + size_plan.width + padding,
104            rect.max.y
105        ];
106        children.push(Box::new(
107            Label::new(size_rect, size_text, Align::Left(0))
108                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
109                .event(event.clone()),
110        ));
111
112        let date_x = rect.max.x - date_plan.width - padding;
113        let date_rect = rect![date_x, rect.min.y, rect.max.x, rect.max.y];
114        children.push(Box::new(
115            Label::new(date_rect, date_text, Align::Left(0))
116                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
117                .event(event.clone()),
118        ));
119
120        FileEntry {
121            id: ID_FEEDER.next(),
122            rect,
123            children,
124            data,
125        }
126    }
127
128    fn format_size(size: u64) -> String {
129        const KB: u64 = 1024;
130        const MB: u64 = KB * 1024;
131        const GB: u64 = MB * 1024;
132
133        if size >= GB {
134            format!("{:.1} GB", size as f64 / GB as f64)
135        } else if size >= MB {
136            format!("{:.1} MB", size as f64 / MB as f64)
137        } else if size >= KB {
138            format!("{:.1} KB", size as f64 / KB as f64)
139        } else {
140            format!("{} B", size)
141        }
142    }
143
144    fn format_date(system_time: std::time::SystemTime) -> String {
145        let datetime: DateTime<Local> = system_time.into();
146        datetime.format("%b %d, %Y %H:%M").to_string()
147    }
148}
149
150impl View for FileEntry {
151    /// Handles events for the file entry.
152    ///
153    /// This method processes user interactions with the file entry:
154    /// - **Tap gesture**: If the tap is within the entry's bounds, it pushes a
155    ///   `Select(EntryId::FileEntry)` event with the file's path to the bus.
156    /// - **Other events**: Returns `false` and does not process other event types.
157    ///
158    /// # Arguments
159    ///
160    /// * `evt` - The event to handle
161    /// * `_hub` - Unused hub reference
162    /// * `bus` - The event bus to push generated events to
163    /// * `_rq` - Unused render queue reference
164    /// * `_context` - Unused context reference
165    ///
166    /// # Returns
167    ///
168    /// `true` if the event was handled, `false` otherwise.
169    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, _rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
170    fn handle_event(
171        &mut self,
172        evt: &Event,
173        _hub: &Hub,
174        bus: &mut Bus,
175        _rq: &mut RenderQueue,
176        _context: &mut Context,
177    ) -> bool {
178        match evt {
179            Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(*center) => {
180                bus.push_back(Event::Select(EntryId::FileEntry(self.data.path.clone())));
181                true
182            }
183            _ => false,
184        }
185    }
186
187    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts), fields(rect = ?_rect)))]
188    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
189        fb.draw_rectangle(&self.rect, WHITE);
190    }
191
192    fn rect(&self) -> &Rectangle {
193        &self.rect
194    }
195
196    fn rect_mut(&mut self) -> &mut Rectangle {
197        &mut self.rect
198    }
199
200    fn children(&self) -> &Vec<Box<dyn View>> {
201        &self.children
202    }
203
204    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
205        &mut self.children
206    }
207
208    fn id(&self) -> Id {
209        self.id
210    }
211}