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 = if data.is_dir {
58            Some(Event::SelectDirectory(data.path.clone()))
59        } else {
60            Some(Event::Select(EntryId::FileEntry(data.path.clone())))
61        };
62        let hold_event = if data.is_dir {
63            Some(Event::Hold(EntryId::FileEntry(data.path.clone())))
64        } else {
65            None
66        };
67
68        let icon = if data.is_dir { "📁" } else { "📄" };
69        let size_text = data
70            .size
71            .map(Self::format_size)
72            .unwrap_or_else(|| "-".to_string());
73        let date_text = data
74            .modified
75            .map(Self::format_date)
76            .unwrap_or_else(|| "-".to_string());
77
78        let icon_plan = font.plan(icon, None, None);
79        let date_plan = font.plan(&date_text, None, None);
80        let size_plan = font.plan(&size_text, None, None);
81
82        let mut x = rect.min.x + padding;
83        let icon_width = icon_plan.width + padding;
84
85        let name_max_width = rect.width() as i32
86            - icon_width
87            - padding
88            - date_plan.width
89            - size_plan.width
90            - 4 * padding;
91
92        let name_plan = font.plan(&data.name, Some(name_max_width), None);
93
94        let icon_rect = rect![x, rect.min.y, x + icon_width, rect.max.y];
95        children.push(Box::new(
96            Label::new(icon_rect, icon.to_string(), Align::Left(0))
97                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
98                .event(event.clone())
99                .hold_event(hold_event.clone()),
100        ));
101        x += icon_width;
102
103        let name_rect = rect![x, rect.min.y, x + name_plan.width + padding, rect.max.y];
104        children.push(Box::new(
105            Label::new(name_rect, data.name.clone(), Align::Left(0))
106                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
107                .event(event.clone())
108                .hold_event(hold_event.clone()),
109        ));
110
111        let size_x = rect.max.x - date_plan.width - size_plan.width - 2 * padding;
112        let size_rect = rect![
113            size_x,
114            rect.min.y,
115            size_x + size_plan.width + padding,
116            rect.max.y
117        ];
118        children.push(Box::new(
119            Label::new(size_rect, size_text, Align::Left(0))
120                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
121                .event(event.clone())
122                .hold_event(hold_event.clone()),
123        ));
124
125        let date_x = rect.max.x - date_plan.width - padding;
126        let date_rect = rect![date_x, rect.min.y, rect.max.x, rect.max.y];
127        children.push(Box::new(
128            Label::new(date_rect, date_text, Align::Left(0))
129                .scheme([WHITE, TEXT_NORMAL[1], TEXT_NORMAL[2]])
130                .event(event.clone())
131                .hold_event(hold_event.clone()),
132        ));
133
134        FileEntry {
135            id: ID_FEEDER.next(),
136            rect,
137            children,
138            data,
139        }
140    }
141
142    fn format_size(size: u64) -> String {
143        const KB: u64 = 1024;
144        const MB: u64 = KB * 1024;
145        const GB: u64 = MB * 1024;
146
147        if size >= GB {
148            format!("{:.1} GB", size as f64 / GB as f64)
149        } else if size >= MB {
150            format!("{:.1} MB", size as f64 / MB as f64)
151        } else if size >= KB {
152            format!("{:.1} KB", size as f64 / KB as f64)
153        } else {
154            format!("{} B", size)
155        }
156    }
157
158    fn format_date(system_time: std::time::SystemTime) -> String {
159        let datetime: DateTime<Local> = system_time.into();
160        datetime.format("%b %d, %Y %H:%M").to_string()
161    }
162}
163
164impl View for FileEntry {
165    /// Handles events for the file entry.
166    ///
167    /// This method processes user interactions with the file entry:
168    /// - **Tap gesture**: If the tap is within the entry's bounds, it pushes either a
169    ///   `SelectDirectory` event (for directories) or a `Select` event (for files) to the bus.
170    /// - **Hold gesture** (short): If the hold is within the entry's bounds and the entry
171    ///   represents a directory, it pushes a `Hold` event to the bus.
172    /// - **Other events**: Returns `false` and does not process other event types.
173    ///
174    /// # Arguments
175    ///
176    /// * `evt` - The event to handle
177    /// * `_hub` - Unused hub reference
178    /// * `bus` - The event bus to push generated events to
179    /// * `_rq` - Unused render queue reference
180    /// * `_context` - Unused context reference
181    ///
182    /// # Returns
183    ///
184    /// `true` if the event was handled, `false` otherwise.
185    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, _rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
186    fn handle_event(
187        &mut self,
188        evt: &Event,
189        _hub: &Hub,
190        bus: &mut Bus,
191        _rq: &mut RenderQueue,
192        _context: &mut Context,
193    ) -> bool {
194        match evt {
195            Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(*center) => {
196                if self.data.is_dir {
197                    bus.push_back(Event::SelectDirectory(self.data.path.clone()));
198                } else {
199                    bus.push_back(Event::Select(EntryId::FileEntry(self.data.path.clone())));
200                }
201                true
202            }
203            Event::Gesture(GestureEvent::HoldFingerShort(center, _id))
204                if self.rect.includes(*center) && self.data.is_dir =>
205            {
206                bus.push_back(Event::Hold(EntryId::FileEntry(self.data.path.clone())));
207                true
208            }
209            _ => false,
210        }
211    }
212
213    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts), fields(rect = ?_rect)))]
214    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
215        fb.draw_rectangle(&self.rect, WHITE);
216    }
217
218    fn rect(&self) -> &Rectangle {
219        &self.rect
220    }
221
222    fn rect_mut(&mut self) -> &mut Rectangle {
223        &mut self.rect
224    }
225
226    fn children(&self) -> &Vec<Box<dyn View>> {
227        &self.children
228    }
229
230    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
231        &mut self.children
232    }
233
234    fn id(&self) -> Id {
235        self.id
236    }
237}