cadmus_core/view/file_chooser/
file_entry.rs1use 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
13pub struct FileEntry {
26 id: Id,
27 rect: Rectangle,
28 children: Vec<Box<dyn View>>,
29 data: FileEntryData,
30}
31
32impl FileEntry {
33 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 #[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}