1use std::rc::Rc;
13use std::sync::Arc;
14use std::time::Duration;
15
16use crate::geometry::{Rect, Size};
17use crate::style::CellStyle;
18use crate::text;
19use crate::widget::{Align, Length, MeasureCx, PaintCx, View, Widget};
20
21use super::super::delayed::DelayedIndicator;
22use super::super::{
23 CardGrid, Column, ColumnWidth, ContextItem, RowDrop, Span, SpinnerStyle, Table, TableCell, TableRow, Text, TreeDrop,
24};
25use super::state::ROOT;
26use super::{FileManager, FileManagerMsg, child_key, root_name};
27
28const DATE_WIDTH: u16 = 16;
30
31const PERMISSIONS_WIDTH: u16 = 11;
33
34const NAME_WIDTH: u16 = 12;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42#[non_exhaustive]
43pub enum FileView {
44 #[default]
46 Tree,
47 List,
49 Icons,
51}
52
53type Card = (String, String, Option<String>, bool);
56
57pub(super) struct FlatRow {
59 pub(super) key: String,
61 pub(super) name: String,
63 pub(super) folder: bool,
65 pub(super) executable: bool,
67 pub(super) itself: bool,
69}
70
71impl<'a, Msg: Clone + 'static> FileManager<'a, Msg> {
72 pub(super) fn flat_rows(&self) -> Vec<FlatRow> {
74 let state = self.state;
75 let shown = state.folder();
76 let label = if shown == ROOT {
77 self.root_label.clone().unwrap_or_else(|| root_name(state.root()))
78 } else {
79 super::name_of(shown).to_owned()
80 };
81 let mut rows =
82 vec![FlatRow { key: shown.to_owned(), name: label, folder: true, executable: false, itself: true }];
83 if let Some(entries) = state.shown_children(shown) {
84 rows.extend(entries.into_iter().map(|entry| FlatRow {
85 key: child_key(shown, &entry.name),
86 name: entry.name.clone(),
87 folder: entry.folder,
88 executable: entry.executable,
89 itself: false,
90 }));
91 }
92 rows
93 }
94
95 fn flat_look(&self, row: &FlatRow) -> (String, Option<String>, bool) {
97 let mark = self.mark_of(&row.key);
98 let name = if row.key == ROOT { root_name(self.state.root()) } else { super::name_of(&row.key).to_owned() };
100 let (icon, tone) = self.sign_of(&mark, &row.key, &name, row.folder, row.executable);
101 let cut = !row.itself && self.state.is_cut(&row.key);
103 (icon, tone, self.disabled || cut || mark.is_faint())
104 }
105
106 fn flat_selection(&self, rows: &[FlatRow]) -> (Option<usize>, Vec<usize>) {
109 let state = self.state;
110 let selected = state.selected().and_then(|cursor| rows.iter().position(|row| row.key == cursor));
111 let chosen = rows
112 .iter()
113 .enumerate()
114 .filter(|(_, row)| !row.itself && state.chosen().contains(&row.key))
115 .map(|(index, _)| index)
116 .collect();
117 (selected, chosen)
118 }
119
120 fn flat_activate(&self, row: &FlatRow) -> Msg {
123 if row.itself {
124 return (self.wrap)(FileManagerMsg::Leave);
125 }
126 if row.folder {
127 return (self.wrap)(FileManagerMsg::Enter(row.key.clone()));
128 }
129 match &self.on_open {
130 Some(open) => open(&self.state.path(&row.key)),
131 None => (self.wrap)(FileManagerMsg::Select(row.key.clone())),
133 }
134 }
135
136 fn flat_wiring(&self, rows: &[FlatRow]) -> Wiring<Msg> {
138 Wiring {
139 keys: rows.iter().map(|row| row.key.clone()).collect(),
140 activations: rows.iter().map(|row| self.flat_activate(row)).collect(),
141 itself: rows.iter().map(|row| row.itself).collect(),
142 folders: rows.iter().map(|row| row.folder && !row.itself).collect(),
143 up: (self.state.folder() != ROOT).then(|| super::parent_key(self.state.folder()).to_owned()),
144 }
145 }
146
147 pub(super) fn table(&self, rows: &[FlatRow]) -> Table<Msg> {
149 let state = self.state;
150 let columns = vec![
151 Column::new(crate::t!("quvyta.file-manager.column-name")).min(NAME_WIDTH),
152 Column::new(crate::t!("quvyta.file-manager.column-size")).width(ColumnWidth::Fit).align(Align::End),
153 Column::new(crate::t!("quvyta.file-manager.column-modified")).width(ColumnWidth::Fixed(DATE_WIDTH)),
154 Column::new(crate::t!("quvyta.file-manager.column-permissions"))
155 .width(ColumnWidth::Fixed(PERMISSIONS_WIDTH)),
156 ];
157 let table_rows: Vec<TableRow> = rows
158 .iter()
159 .map(|row| {
160 let (icon, tone, faint) = self.flat_look(row);
161 let name = TableCell::new(row.name.clone()).icon(icon, tone.as_deref());
162 let details = if row.itself { None } else { state.details(&row.key).flatten() };
166 let (size, modified, permissions) = match details {
167 Some(details) => {
168 (details.size_text(row.folder), details.modified_text(), details.permissions_text(row.folder))
169 }
170 None => (String::new(), String::new(), String::new()),
171 };
172 TableRow::new([name, TableCell::new(size), TableCell::new(modified), TableCell::new(permissions)])
173 .faint(faint)
174 })
175 .collect();
176 let table = Table::new(columns, table_rows);
177 if self.disabled {
178 return table;
179 }
180 let (selected, chosen) = self.flat_selection(rows);
181 let wiring = self.flat_wiring(rows);
182 let [select, activate, choose, drop, copy, accepts] = std::array::from_fn(|_| wiring.clone());
183 let [wrap, choosing, dropping, copying] = std::array::from_fn(|_| Rc::clone(&self.wrap));
184 table
185 .selected(selected)
186 .activate_on(self.open_on)
187 .multi_select(&chosen, move |indexes| choosing(FileManagerMsg::Choose(choose.keys_of(&indexes))))
188 .box_select(true)
189 .droppable(
190 move |dropped| dropping(FileManagerMsg::Drop(drop.drop(&dropped))),
191 move |index| accepts.takes_drop(index),
192 )
193 .on_copy_drop(move |dropped| copying(FileManagerMsg::DropCopy(copy.drop(&dropped))))
194 .on_select(move |index| select.select(index, &wrap))
195 .on_activate(move |index| activate.activate(index))
196 .context_menu(self.flat_menu(rows))
197 }
198
199 pub(super) fn grid(&self, rows: &[FlatRow]) -> CardGrid<Msg> {
201 let cards: Arc<[Card]> = rows
202 .iter()
203 .map(|row| {
204 let (icon, tone, faint) = self.flat_look(row);
205 (row.name.clone(), icon, tone, faint)
206 })
207 .collect();
208 let built = Arc::clone(&cards);
209 let grid = CardGrid::new(rows.len()).card_width(18, 26).card_height(1).disabled(self.disabled).card(
210 move |ui, index| {
211 let Some((name, icon, tone, faint)) = built.get(index) else { return };
212 let glyph = ui.env().icons().glyph(icon).into_owned();
213 let mut mark = Span::new(format!("{glyph} "));
214 if let Some(tone) = tone {
215 mark = mark.color(tone.as_str());
216 }
217 let mut label = Span::new(name.clone());
218 if *faint {
219 (mark, label) = (mark.role("faint"), label.role("faint"));
220 }
221 ui.add(Text::rich([mark, label]).no_wrap());
222 },
223 );
224 if self.disabled {
225 return grid;
226 }
227 let (selected, chosen) = self.flat_selection(rows);
228 let wiring = self.flat_wiring(rows);
229 let [select, activate, choose, drop, copy, accepts] = std::array::from_fn(|_| wiring.clone());
230 let [wrap, choosing, dropping, copying] = std::array::from_fn(|_| Rc::clone(&self.wrap));
231 grid.selected(selected)
232 .activate_on(self.open_on)
233 .multi_select(&chosen, move |indexes| choosing(FileManagerMsg::Choose(choose.keys_of(&indexes))))
234 .box_select(true)
235 .droppable(
236 move |dropped| dropping(FileManagerMsg::Drop(drop.drop(&dropped))),
237 move |index| accepts.takes_drop(index),
238 )
239 .on_copy_drop(move |dropped| copying(FileManagerMsg::DropCopy(copy.drop(&dropped))))
240 .on_select(move |index| select.select(index, &wrap))
241 .on_activate(move |index| activate.activate(index))
242 .context_menu(self.flat_menu(rows))
243 }
244
245 fn flat_menu(&self, rows: &[FlatRow]) -> impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static {
247 let keys: Vec<String> = rows.iter().map(|row| row.key.clone()).collect();
248 let items = self.menu_for();
249 let leave = (self.state.folder() != ROOT).then(|| (self.wrap)(FileManagerMsg::Leave));
251 move |index| {
252 let mut own = keys.get(index).map(|key| items(key)).unwrap_or_default();
253 if index == 0
254 && let Some(leave) = &leave
255 {
256 own.insert(0, ContextItem::new(crate::t!("quvyta.file-manager.up"), leave.clone()));
257 own.insert(1, ContextItem::gap());
258 }
259 own
260 }
261 }
262
263 pub(super) fn foot(&self, ui: &mut View<'_, Msg>, rows: &[FlatRow]) {
268 let state = self.state;
269 let shown = state.folder();
270 let known = state.shown_children(shown);
271 let label = match &known {
272 None => String::new(),
273 Some(_) if state.folder_error(shown).is_some() => {
276 crate::t!("quvyta.file-manager.unreadable-short")
277 }
278 Some(_) if rows.len() <= 1 => crate::t!("quvyta.file-manager.empty"),
279 Some(_) => crate::t!("quvyta.file-manager.entries", n = rows.len() - 1),
280 };
281 ui.add(Reading { busy: state.is_loading(shown), label }).fill_width().height(Length::Cells(1));
282 }
283}
284
285struct Wiring<Msg> {
287 keys: Vec<String>,
288 activations: Vec<Msg>,
289 itself: Vec<bool>,
290 folders: Vec<bool>,
292 up: Option<String>,
295}
296
297impl<Msg: Clone> Clone for Wiring<Msg> {
298 fn clone(&self) -> Self {
299 Self {
300 keys: self.keys.clone(),
301 activations: self.activations.clone(),
302 itself: self.itself.clone(),
303 folders: self.folders.clone(),
304 up: self.up.clone(),
305 }
306 }
307}
308
309impl<Msg: Clone> Wiring<Msg> {
310 fn select(&self, index: usize, wrap: &Rc<dyn Fn(FileManagerMsg) -> Msg>) -> Msg {
312 let key = self.keys.get(index).cloned().unwrap_or_default();
313 wrap(FileManagerMsg::Select(key))
314 }
315
316 fn activate(&self, index: usize) -> Msg {
318 self.activations
319 .get(index)
320 .cloned()
321 .unwrap_or_else(|| self.activations.first().cloned().expect("a flat view always has the folder's own row"))
322 }
323
324 fn keys_of(&self, indexes: &[usize]) -> Vec<String> {
327 indexes
328 .iter()
329 .filter(|index| self.itself.get(**index) == Some(&false))
330 .filter_map(|index| self.keys.get(*index).cloned())
331 .collect()
332 }
333
334 fn takes_drop(&self, index: usize) -> bool {
338 if self.itself.get(index) == Some(&true) {
339 return self.up.is_some();
340 }
341 self.folders.get(index).copied().unwrap_or(false)
342 }
343
344 fn drop(&self, dropped: &RowDrop) -> TreeDrop {
347 let into = if self.itself.get(dropped.into) == Some(&true) {
348 self.up.clone()
349 } else {
350 self.keys.get(dropped.into).cloned()
351 };
352 TreeDrop { keys: self.keys_of(&dropped.rows), into }
353 }
354}
355
356struct Reading {
359 busy: bool,
360 label: String,
361}
362
363#[derive(Debug, Default)]
365struct Mark(DelayedIndicator);
366
367impl<Msg: 'static> Widget<Msg> for Reading {
368 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
369 Size::new(available.width, 1)
370 }
371
372 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
373 if area.is_empty() {
374 return;
375 }
376 let now = cx.now();
377 let mut mark = cx.memory::<Mark>().0;
378 let shown = mark.update(self.busy, now);
379 if let Some(change) = mark.next_change(self.busy, now) {
380 cx.request_frame_in(change);
381 }
382 cx.memory::<Mark>().0 = mark;
383 let faint = cx.style("list-header", None, &[]).text();
384 if !shown {
385 cx.text(area.x, area.y, &self.label, faint, area.width);
386 return;
387 }
388 let style = cx.style("spinner", None, &[]).text();
389 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(Duration::ZERO));
390 let glyph = text::truncate(&cell.glyph, 1).into_owned();
391 cx.text(area.x, area.y, &glyph, cell.style, 1);
392 let word = crate::t!("quvyta.file-manager.reading");
393 cx.text(area.x + 2, area.y, &word, CellStyle { bg: None, ..faint }, area.width.saturating_sub(2));
394 }
395}