use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use crate::geometry::{Rect, Size};
use crate::style::CellStyle;
use crate::text;
use crate::widget::{Align, Length, MeasureCx, PaintCx, View, Widget};
use super::super::delayed::DelayedIndicator;
use super::super::{
CardGrid, Column, ColumnWidth, ContextItem, RowDrop, Span, SpinnerStyle, Table, TableCell, TableRow, Text, TreeDrop,
};
use super::state::ROOT;
use super::{FileManager, FileManagerMsg, child_key, root_name};
const DATE_WIDTH: u16 = 16;
const PERMISSIONS_WIDTH: u16 = 11;
const NAME_WIDTH: u16 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum FileView {
#[default]
Tree,
List,
Icons,
}
type Card = (String, String, Option<String>, bool);
pub(super) struct FlatRow {
pub(super) key: String,
pub(super) name: String,
pub(super) folder: bool,
pub(super) executable: bool,
pub(super) itself: bool,
}
impl<'a, Msg: Clone + 'static> FileManager<'a, Msg> {
pub(super) fn flat_rows(&self) -> Vec<FlatRow> {
let state = self.state;
let shown = state.folder();
let label = if shown == ROOT {
self.root_label.clone().unwrap_or_else(|| root_name(state.root()))
} else {
super::name_of(shown).to_owned()
};
let mut rows =
vec![FlatRow { key: shown.to_owned(), name: label, folder: true, executable: false, itself: true }];
if let Some(entries) = state.shown_children(shown) {
rows.extend(entries.into_iter().map(|entry| FlatRow {
key: child_key(shown, &entry.name),
name: entry.name.clone(),
folder: entry.folder,
executable: entry.executable,
itself: false,
}));
}
rows
}
fn flat_look(&self, row: &FlatRow) -> (String, Option<String>, bool) {
let mark = self.mark_of(&row.key);
let name = if row.key == ROOT { root_name(self.state.root()) } else { super::name_of(&row.key).to_owned() };
let (icon, tone) = self.sign_of(&mark, &row.key, &name, row.folder, row.executable);
let cut = !row.itself && self.state.is_cut(&row.key);
(icon, tone, self.disabled || cut || mark.is_faint())
}
fn flat_selection(&self, rows: &[FlatRow]) -> (Option<usize>, Vec<usize>) {
let state = self.state;
let selected = state.selected().and_then(|cursor| rows.iter().position(|row| row.key == cursor));
let chosen = rows
.iter()
.enumerate()
.filter(|(_, row)| !row.itself && state.chosen().contains(&row.key))
.map(|(index, _)| index)
.collect();
(selected, chosen)
}
fn flat_activate(&self, row: &FlatRow) -> Msg {
if row.itself {
return (self.wrap)(FileManagerMsg::Leave);
}
if row.folder {
return (self.wrap)(FileManagerMsg::Enter(row.key.clone()));
}
match &self.on_open {
Some(open) => open(&self.state.path(&row.key)),
None => (self.wrap)(FileManagerMsg::Select(row.key.clone())),
}
}
fn flat_wiring(&self, rows: &[FlatRow]) -> Wiring<Msg> {
Wiring {
keys: rows.iter().map(|row| row.key.clone()).collect(),
activations: rows.iter().map(|row| self.flat_activate(row)).collect(),
itself: rows.iter().map(|row| row.itself).collect(),
folders: rows.iter().map(|row| row.folder && !row.itself).collect(),
}
}
pub(super) fn table(&self, rows: &[FlatRow]) -> Table<Msg> {
let state = self.state;
let columns = vec![
Column::new(crate::t!("quvyta.file-manager.column-name")).min(NAME_WIDTH),
Column::new(crate::t!("quvyta.file-manager.column-size")).width(ColumnWidth::Fit).align(Align::End),
Column::new(crate::t!("quvyta.file-manager.column-modified")).width(ColumnWidth::Fixed(DATE_WIDTH)),
Column::new(crate::t!("quvyta.file-manager.column-permissions"))
.width(ColumnWidth::Fixed(PERMISSIONS_WIDTH)),
];
let table_rows: Vec<TableRow> = rows
.iter()
.map(|row| {
let (icon, tone, faint) = self.flat_look(row);
let name = TableCell::new(row.name.clone()).icon(icon, tone.as_deref());
let details = if row.itself { None } else { state.details(&row.key).flatten() };
let (size, modified, permissions) = match details {
Some(details) => {
(details.size_text(row.folder), details.modified_text(), details.permissions_text(row.folder))
}
None => (String::new(), String::new(), String::new()),
};
TableRow::new([name, TableCell::new(size), TableCell::new(modified), TableCell::new(permissions)])
.faint(faint)
})
.collect();
let table = Table::new(columns, table_rows);
if self.disabled {
return table;
}
let (selected, chosen) = self.flat_selection(rows);
let wiring = self.flat_wiring(rows);
let [select, activate, choose, drop, copy, accepts] = std::array::from_fn(|_| wiring.clone());
let [wrap, choosing, dropping, copying] = std::array::from_fn(|_| Rc::clone(&self.wrap));
table
.selected(selected)
.activate_on(self.open_on)
.multi_select(&chosen, move |indexes| choosing(FileManagerMsg::Choose(choose.keys_of(&indexes))))
.box_select(true)
.droppable(
move |dropped| dropping(FileManagerMsg::Drop(drop.drop(&dropped))),
move |index| accepts.takes_drop(index),
)
.on_copy_drop(move |dropped| copying(FileManagerMsg::DropCopy(copy.drop(&dropped))))
.on_select(move |index| select.select(index, &wrap))
.on_activate(move |index| activate.activate(index))
.context_menu(self.flat_menu(rows))
}
pub(super) fn grid(&self, rows: &[FlatRow]) -> CardGrid<Msg> {
let cards: Arc<[Card]> = rows
.iter()
.map(|row| {
let (icon, tone, faint) = self.flat_look(row);
(row.name.clone(), icon, tone, faint)
})
.collect();
let built = Arc::clone(&cards);
let grid = CardGrid::new(rows.len()).card_width(18, 26).card_height(1).disabled(self.disabled).card(
move |ui, index| {
let Some((name, icon, tone, faint)) = built.get(index) else { return };
let glyph = ui.env().icons().glyph(icon).into_owned();
let mut mark = Span::new(format!("{glyph} "));
if let Some(tone) = tone {
mark = mark.color(tone.as_str());
}
let mut label = Span::new(name.clone());
if *faint {
(mark, label) = (mark.role("faint"), label.role("faint"));
}
ui.add(Text::rich([mark, label]).no_wrap());
},
);
if self.disabled {
return grid;
}
let (selected, chosen) = self.flat_selection(rows);
let wiring = self.flat_wiring(rows);
let [select, activate, choose, drop, copy, accepts] = std::array::from_fn(|_| wiring.clone());
let [wrap, choosing, dropping, copying] = std::array::from_fn(|_| Rc::clone(&self.wrap));
grid.selected(selected)
.activate_on(self.open_on)
.multi_select(&chosen, move |indexes| choosing(FileManagerMsg::Choose(choose.keys_of(&indexes))))
.box_select(true)
.droppable(
move |dropped| dropping(FileManagerMsg::Drop(drop.drop(&dropped))),
move |index| accepts.takes_drop(index),
)
.on_copy_drop(move |dropped| copying(FileManagerMsg::DropCopy(copy.drop(&dropped))))
.on_select(move |index| select.select(index, &wrap))
.on_activate(move |index| activate.activate(index))
.context_menu(self.flat_menu(rows))
}
fn flat_menu(&self, rows: &[FlatRow]) -> impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static {
let keys: Vec<String> = rows.iter().map(|row| row.key.clone()).collect();
let items = self.menu_for();
let leave = (self.state.folder() != ROOT).then(|| (self.wrap)(FileManagerMsg::Leave));
move |index| {
let mut own = keys.get(index).map(|key| items(key)).unwrap_or_default();
if index == 0
&& let Some(leave) = &leave
{
own.insert(0, ContextItem::new(crate::t!("quvyta.file-manager.up"), leave.clone()));
own.insert(1, ContextItem::gap());
}
own
}
}
pub(super) fn foot(&self, ui: &mut View<'_, Msg>, rows: &[FlatRow]) {
let state = self.state;
let shown = state.folder();
let known = state.shown_children(shown);
let label = match &known {
None => String::new(),
Some(_) if state.folder_error(shown).is_some() => {
crate::t!("quvyta.file-manager.unreadable-short")
}
Some(_) if rows.len() <= 1 => crate::t!("quvyta.file-manager.empty"),
Some(_) => crate::t!("quvyta.file-manager.entries", n = rows.len() - 1),
};
ui.add(Reading { busy: state.is_loading(shown), label }).fill_width().height(Length::Cells(1));
}
}
struct Wiring<Msg> {
keys: Vec<String>,
activations: Vec<Msg>,
itself: Vec<bool>,
folders: Vec<bool>,
}
impl<Msg: Clone> Clone for Wiring<Msg> {
fn clone(&self) -> Self {
Self {
keys: self.keys.clone(),
activations: self.activations.clone(),
itself: self.itself.clone(),
folders: self.folders.clone(),
}
}
}
impl<Msg: Clone> Wiring<Msg> {
fn select(&self, index: usize, wrap: &Rc<dyn Fn(FileManagerMsg) -> Msg>) -> Msg {
let key = self.keys.get(index).cloned().unwrap_or_default();
wrap(FileManagerMsg::Select(key))
}
fn activate(&self, index: usize) -> Msg {
self.activations
.get(index)
.cloned()
.unwrap_or_else(|| self.activations.first().cloned().expect("a flat view always has the folder's own row"))
}
fn keys_of(&self, indexes: &[usize]) -> Vec<String> {
indexes
.iter()
.filter(|index| self.itself.get(**index) == Some(&false))
.filter_map(|index| self.keys.get(*index).cloned())
.collect()
}
fn takes_drop(&self, index: usize) -> bool {
self.folders.get(index).copied().unwrap_or(false)
}
fn drop(&self, dropped: &RowDrop) -> TreeDrop {
TreeDrop { keys: self.keys_of(&dropped.rows), into: self.keys.get(dropped.into).cloned() }
}
}
struct Reading {
busy: bool,
label: String,
}
#[derive(Debug, Default)]
struct Mark(DelayedIndicator);
impl<Msg: 'static> Widget<Msg> for Reading {
fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
Size::new(available.width, 1)
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
if area.is_empty() {
return;
}
let now = cx.now();
let mut mark = cx.memory::<Mark>().0;
let shown = mark.update(self.busy, now);
if let Some(change) = mark.next_change(self.busy, now) {
cx.request_frame_in(change);
}
cx.memory::<Mark>().0 = mark;
let faint = cx.style("list-header", None, &[]).text();
if !shown {
cx.text(area.x, area.y, &self.label, faint, area.width);
return;
}
let style = cx.style("spinner", None, &[]).text();
let cell = cx.animation(SpinnerStyle::Dots.animation(), style, Some(Duration::ZERO));
let glyph = text::truncate(&cell.glyph, 1).into_owned();
cx.text(area.x, area.y, &glyph, cell.style, 1);
let word = crate::t!("quvyta.file-manager.reading");
cx.text(area.x + 2, area.y, &word, CellStyle { bg: None, ..faint }, area.width.saturating_sub(2));
}
}