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::{CardGrid, Column, ColumnWidth, ContextItem, Span, SpinnerStyle, Table, TableCell, TableRow, Text};
23use super::state::ROOT;
24use super::{FileManager, FileManagerMsg, child_key, root_name};
25
26const DATE_WIDTH: u16 = 16;
28
29const PERMISSIONS_WIDTH: u16 = 11;
31
32const NAME_WIDTH: u16 = 12;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40#[non_exhaustive]
41pub enum FileView {
42 #[default]
44 Tree,
45 List,
47 Icons,
49}
50
51type Card = (String, String, Option<String>, bool);
54
55pub(super) struct FlatRow {
57 pub(super) key: String,
59 pub(super) name: String,
61 pub(super) folder: bool,
63 pub(super) executable: bool,
65 pub(super) itself: bool,
67}
68
69impl<'a, Msg: Clone + 'static> FileManager<'a, Msg> {
70 pub(super) fn flat_rows(&self) -> Vec<FlatRow> {
72 let state = self.state;
73 let shown = state.folder();
74 let label = if shown == ROOT {
75 self.root_label.clone().unwrap_or_else(|| root_name(state.root()))
76 } else {
77 super::name_of(shown).to_owned()
78 };
79 let mut rows =
80 vec![FlatRow { key: shown.to_owned(), name: label, folder: true, executable: false, itself: true }];
81 if let Some(entries) = state.shown_children(shown) {
82 rows.extend(entries.into_iter().map(|entry| FlatRow {
83 key: child_key(shown, &entry.name),
84 name: entry.name.clone(),
85 folder: entry.folder,
86 executable: entry.executable,
87 itself: false,
88 }));
89 }
90 rows
91 }
92
93 fn flat_look(&self, row: &FlatRow) -> (String, Option<String>, bool) {
95 let mark = self.mark_of(&row.key);
96 let name = if row.key == ROOT { root_name(self.state.root()) } else { super::name_of(&row.key).to_owned() };
98 let (icon, tone) = self.sign_of(&mark, &row.key, &name, row.folder, row.executable);
99 let cut = !row.itself && self.state.is_cut(&row.key);
101 (icon, tone, self.disabled || cut || mark.is_faint())
102 }
103
104 fn flat_selection(&self, rows: &[FlatRow]) -> (Option<usize>, Vec<bool>) {
106 let state = self.state;
107 let selected = state.selected().and_then(|cursor| rows.iter().position(|row| row.key == cursor));
108 let checked = rows.iter().map(|row| !row.itself && state.chosen().contains(&row.key)).collect();
109 (selected, checked)
110 }
111
112 fn flat_activate(&self, row: &FlatRow) -> Msg {
115 if row.itself {
116 return (self.wrap)(FileManagerMsg::Leave);
117 }
118 if row.folder {
119 return (self.wrap)(FileManagerMsg::Enter(row.key.clone()));
120 }
121 match &self.on_open {
122 Some(open) => open(&self.state.path(&row.key)),
123 None => (self.wrap)(FileManagerMsg::Select(row.key.clone())),
125 }
126 }
127
128 fn flat_wiring(&self, rows: &[FlatRow]) -> Wiring<Msg> {
130 Wiring {
131 keys: rows.iter().map(|row| row.key.clone()).collect(),
132 activations: rows.iter().map(|row| self.flat_activate(row)).collect(),
133 chosen: self.state.chosen().to_vec(),
134 itself: rows.iter().map(|row| row.itself).collect(),
135 }
136 }
137
138 pub(super) fn table(&self, rows: &[FlatRow]) -> Table<Msg> {
140 let state = self.state;
141 let columns = vec![
142 Column::new(crate::t!("quvyta.file-manager.column-name")).min(NAME_WIDTH),
143 Column::new(crate::t!("quvyta.file-manager.column-size")).width(ColumnWidth::Fit).align(Align::End),
144 Column::new(crate::t!("quvyta.file-manager.column-modified")).width(ColumnWidth::Fixed(DATE_WIDTH)),
145 Column::new(crate::t!("quvyta.file-manager.column-permissions"))
146 .width(ColumnWidth::Fixed(PERMISSIONS_WIDTH)),
147 ];
148 let table_rows: Vec<TableRow> = rows
149 .iter()
150 .map(|row| {
151 let (icon, tone, faint) = self.flat_look(row);
152 let name = TableCell::new(row.name.clone()).icon(icon, tone.as_deref());
153 let details = if row.itself { None } else { state.details(&row.key).flatten() };
157 let (size, modified, permissions) = match details {
158 Some(details) => {
159 (details.size_text(row.folder), details.modified_text(), details.permissions_text(row.folder))
160 }
161 None => (String::new(), String::new(), String::new()),
162 };
163 TableRow::new([name, TableCell::new(size), TableCell::new(modified), TableCell::new(permissions)])
164 .faint(faint)
165 })
166 .collect();
167 let table = Table::new(columns, table_rows);
168 if self.disabled {
169 return table;
170 }
171 let (selected, checked) = self.flat_selection(rows);
172 let wiring = self.flat_wiring(rows);
173 let (select, activate, toggle) = (wiring.clone(), wiring.clone(), wiring.clone());
174 let (wrap, choose) = (Rc::clone(&self.wrap), Rc::clone(&self.wrap));
175 table
176 .selected(selected)
177 .checked(checked)
178 .on_select(move |index| select.select(index, &wrap))
179 .on_activate(move |index| activate.activate(index))
180 .on_toggle(move |index| choose(FileManagerMsg::Choose(toggle.toggled(index))))
181 .context_menu(self.flat_menu(rows))
182 }
183
184 pub(super) fn grid(&self, rows: &[FlatRow]) -> CardGrid<Msg> {
186 let cards: Arc<[Card]> = rows
187 .iter()
188 .map(|row| {
189 let (icon, tone, faint) = self.flat_look(row);
190 (row.name.clone(), icon, tone, faint)
191 })
192 .collect();
193 let built = Arc::clone(&cards);
194 let grid = CardGrid::new(rows.len()).card_width(18, 26).card_height(1).disabled(self.disabled).card(
195 move |ui, index| {
196 let Some((name, icon, tone, faint)) = built.get(index) else { return };
197 let glyph = ui.env().icons().glyph(icon).into_owned();
198 let mut mark = Span::new(format!("{glyph} "));
199 if let Some(tone) = tone {
200 mark = mark.color(tone.as_str());
201 }
202 let mut label = Span::new(name.clone());
203 if *faint {
204 (mark, label) = (mark.role("faint"), label.role("faint"));
205 }
206 ui.add(Text::rich([mark, label]).no_wrap());
207 },
208 );
209 if self.disabled {
210 return grid;
211 }
212 let (selected, checked) = self.flat_selection(rows);
213 let wiring = self.flat_wiring(rows);
214 let (select, activate, toggle) = (wiring.clone(), wiring.clone(), wiring.clone());
215 let (wrap, choose) = (Rc::clone(&self.wrap), Rc::clone(&self.wrap));
216 grid.selected(selected)
217 .checked(checked)
218 .on_select(move |index| select.select(index, &wrap))
219 .on_activate(move |index| activate.activate(index))
220 .on_toggle(move |index| choose(FileManagerMsg::Choose(toggle.toggled(index))))
221 .context_menu(self.flat_menu(rows))
222 }
223
224 fn flat_menu(&self, rows: &[FlatRow]) -> impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static {
226 let keys: Vec<String> = rows.iter().map(|row| row.key.clone()).collect();
227 let items = self.menu_for();
228 let leave = (self.state.folder() != ROOT).then(|| (self.wrap)(FileManagerMsg::Leave));
230 move |index| {
231 let mut own = keys.get(index).map(|key| items(key)).unwrap_or_default();
232 if index == 0
233 && let Some(leave) = &leave
234 {
235 own.insert(0, ContextItem::new(crate::t!("quvyta.file-manager.up"), leave.clone()));
236 own.insert(1, ContextItem::gap());
237 }
238 own
239 }
240 }
241
242 pub(super) fn foot(&self, ui: &mut View<'_, Msg>, rows: &[FlatRow]) {
247 let state = self.state;
248 let shown = state.folder();
249 let known = state.shown_children(shown);
250 let label = match &known {
251 None => String::new(),
252 Some(_) if state.folder_error(shown).is_some() => {
255 crate::t!("quvyta.file-manager.unreadable-short")
256 }
257 Some(_) if rows.len() <= 1 => crate::t!("quvyta.file-manager.empty"),
258 Some(_) => crate::t!("quvyta.file-manager.entries", n = rows.len() - 1),
259 };
260 ui.add(Reading { busy: state.is_loading(shown), label }).fill_width().height(Length::Cells(1));
261 }
262}
263
264struct Wiring<Msg> {
266 keys: Vec<String>,
267 activations: Vec<Msg>,
268 chosen: Vec<String>,
269 itself: Vec<bool>,
270}
271
272impl<Msg: Clone> Clone for Wiring<Msg> {
273 fn clone(&self) -> Self {
274 Self {
275 keys: self.keys.clone(),
276 activations: self.activations.clone(),
277 chosen: self.chosen.clone(),
278 itself: self.itself.clone(),
279 }
280 }
281}
282
283impl<Msg: Clone> Wiring<Msg> {
284 fn select(&self, index: usize, wrap: &Rc<dyn Fn(FileManagerMsg) -> Msg>) -> Msg {
286 let key = self.keys.get(index).cloned().unwrap_or_default();
287 wrap(FileManagerMsg::Select(key))
288 }
289
290 fn activate(&self, index: usize) -> Msg {
292 self.activations
293 .get(index)
294 .cloned()
295 .unwrap_or_else(|| self.activations.first().cloned().expect("a flat view always has the folder's own row"))
296 }
297
298 fn toggled(&self, index: usize) -> Vec<String> {
301 let mut chosen = self.chosen.clone();
302 let Some(key) = self.keys.get(index).filter(|_| self.itself.get(index) == Some(&false)) else {
303 return chosen;
304 };
305 match chosen.iter().position(|held| held == key) {
306 Some(at) => {
307 chosen.remove(at);
308 }
309 None => chosen.push(key.clone()),
310 }
311 chosen
312 }
313}
314
315struct Reading {
318 busy: bool,
319 label: String,
320}
321
322#[derive(Debug, Default)]
324struct Mark(DelayedIndicator);
325
326impl<Msg: 'static> Widget<Msg> for Reading {
327 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
328 Size::new(available.width, 1)
329 }
330
331 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
332 if area.is_empty() {
333 return;
334 }
335 let now = cx.now();
336 let mut mark = cx.memory::<Mark>().0;
337 let shown = mark.update(self.busy, now);
338 if let Some(change) = mark.next_change(self.busy, now) {
339 cx.request_frame_in(change);
340 }
341 cx.memory::<Mark>().0 = mark;
342 let faint = cx.style("list-header", None, &[]).text();
343 if !shown {
344 cx.text(area.x, area.y, &self.label, faint, area.width);
345 return;
346 }
347 let style = cx.style("spinner", None, &[]).text();
348 let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(Duration::ZERO));
349 let glyph = text::truncate(&cell.glyph, 1).into_owned();
350 cx.text(area.x, area.y, &glyph, cell.style, 1);
351 let word = crate::t!("quvyta.file-manager.reading");
352 cx.text(area.x + 2, area.y, &word, CellStyle { bg: None, ..faint }, area.width.saturating_sub(2));
353 }
354}