fix: warnings and icon rendering
- Remove unused imports (HashMap, TextEdit) - Remove unnecessary mut on fonts - Replace ✕ (U+2715) with × (U+00D7) for close/cross icons - Replace + (U+FF0B) with + (U+002B) for New Connection button - Replace ▲/▼ (U+25B2/U+25BC) with ↑/↓ (U+2191/U+2193) for sort arrows
This commit is contained in:
parent
9cb8e47a97
commit
f983f12bec
16 changed files with 1672 additions and 789 deletions
|
|
@ -1,33 +1,39 @@
|
|||
//! Переиспользуемая таблица файлов с нормальными колонками
|
||||
use egui::{Color32, Id, RichText, Ui};
|
||||
use crate::domain::file_entry::{EntryKind, FileEntry};
|
||||
use crate::ui::drag::DragPayload;
|
||||
use egui::{Grid, Id};
|
||||
use crate::ui::theme::*;
|
||||
|
||||
pub fn format_size(bytes: Option<u64>) -> String {
|
||||
match bytes {
|
||||
None => String::new(),
|
||||
Some(b) if b < 1024 => format!("{} B", b),
|
||||
Some(b) if b < 1024 * 1024 => format!("{:.1} KB", b as f64 / 1024.0),
|
||||
Some(b) if b < 1024 * 1024 * 1024 => {
|
||||
format!("{:.1} MB", b as f64 / (1024.0 * 1024.0))
|
||||
}
|
||||
Some(b) if b < 1024 * 1024 * 1024 => format!("{:.1} MB", b as f64 / (1024.0 * 1024.0)),
|
||||
Some(b) => format!("{:.2} GB", b as f64 / (1024.0 * 1024.0 * 1024.0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_time(ts: Option<i64>) -> String {
|
||||
match ts.and_then(|t| chrono::DateTime::from_timestamp(t, 0)) {
|
||||
Some(dt) => dt.format("%Y-%m-%d %H:%M").to_string(),
|
||||
Some(dt) => dt.format("%d.%m.%Y %H:%M").to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum SortCol { Name, Size, Kind, Modified }
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum SortDir { Asc, Desc }
|
||||
|
||||
pub struct FileTableResponse {
|
||||
pub double_clicked: Option<String>,
|
||||
pub clicked: Option<String>,
|
||||
}
|
||||
|
||||
pub fn file_table<F>(
|
||||
ui: &mut egui::Ui,
|
||||
ui: &mut Ui,
|
||||
id: &str,
|
||||
entries: &[FileEntry],
|
||||
selected: &mut Option<String>,
|
||||
|
|
@ -36,65 +42,222 @@ pub fn file_table<F>(
|
|||
where
|
||||
F: Fn(&FileEntry) -> Option<DragPayload>,
|
||||
{
|
||||
let mut double_clicked: Option<String> = None;
|
||||
let mut clicked: Option<String> = None;
|
||||
|
||||
let mut sorted = entries.to_vec();
|
||||
sorted.sort_by(|a, b| match (&a.kind, &b.kind) {
|
||||
(EntryKind::Dir, EntryKind::Dir) => a.name.cmp(&b.name),
|
||||
(EntryKind::Dir, _) => std::cmp::Ordering::Less,
|
||||
(_, EntryKind::Dir) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.cmp(&b.name),
|
||||
// Сортировка — читаем из egui memory
|
||||
let sort_id = Id::new((id, "sort"));
|
||||
let (sort_col, sort_dir) = ui.ctx().data_mut(|d| {
|
||||
*d.get_temp_mut_or(sort_id, (SortCol::Name, SortDir::Asc))
|
||||
});
|
||||
|
||||
let mut sorted = entries.to_vec();
|
||||
let dotdot_entry = sorted.iter().position(|e| e.name == "..");
|
||||
let dotdot = dotdot_entry.map(|i| sorted.remove(i));
|
||||
|
||||
sorted.sort_by(|a, b| {
|
||||
// папки всегда выше
|
||||
let dir_cmp = match (&b.kind, &a.kind) {
|
||||
(EntryKind::Dir, EntryKind::Dir) => std::cmp::Ordering::Equal,
|
||||
(EntryKind::Dir, _) => std::cmp::Ordering::Greater,
|
||||
(_, EntryKind::Dir) => std::cmp::Ordering::Less,
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
};
|
||||
if dir_cmp != std::cmp::Ordering::Equal { return dir_cmp; }
|
||||
|
||||
let ord = match sort_col {
|
||||
SortCol::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||
SortCol::Size => a.size.unwrap_or(0).cmp(&b.size.unwrap_or(0)),
|
||||
SortCol::Kind => format!("{:?}", a.kind).cmp(&format!("{:?}", b.kind)),
|
||||
SortCol::Modified => a.modified.unwrap_or(0).cmp(&b.modified.unwrap_or(0)),
|
||||
};
|
||||
if sort_dir == SortDir::Desc { ord.reverse() } else { ord }
|
||||
});
|
||||
|
||||
if let Some(dd) = dotdot {
|
||||
sorted.insert(0, dd);
|
||||
}
|
||||
|
||||
let mut double_clicked: Option<String> = None;
|
||||
let mut clicked: Option<String> = None;
|
||||
let mut new_sort: Option<(SortCol, SortDir)> = None;
|
||||
|
||||
// Заголовки колонок
|
||||
let header_frame = egui::Frame::none()
|
||||
.fill(BG_HEADER)
|
||||
.inner_margin(egui::Margin { left: 6.0, right: 6.0, top: 0.0, bottom: 0.0 });
|
||||
|
||||
header_frame.show(ui, |ui| {
|
||||
ui.set_min_height(22.0);
|
||||
ui.horizontal(|ui| {
|
||||
let name_r = sort_header(ui, "Name", 0.0, sort_col == SortCol::Name, sort_dir);
|
||||
if name_r {
|
||||
new_sort = Some(toggle_sort(SortCol::Name, sort_col, sort_dir));
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let mod_r = sort_header(ui, "Modified", 140.0, sort_col == SortCol::Modified, sort_dir);
|
||||
if mod_r { new_sort = Some(toggle_sort(SortCol::Modified, sort_col, sort_dir)); }
|
||||
|
||||
let type_r = sort_header(ui, "Type", 40.0, sort_col == SortCol::Kind, sort_dir);
|
||||
if type_r { new_sort = Some(toggle_sort(SortCol::Kind, sort_col, sort_dir)); }
|
||||
|
||||
let size_r = sort_header(ui, "Size", 70.0, sort_col == SortCol::Size, sort_dir);
|
||||
if size_r { new_sort = Some(toggle_sort(SortCol::Size, sort_col, sort_dir)); }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(s) = new_sort {
|
||||
ui.ctx().data_mut(|d| { d.insert_temp(sort_id, s); });
|
||||
}
|
||||
|
||||
// разделитель
|
||||
let (_, sep) = ui.allocate_space(egui::vec2(ui.available_width(), 1.0));
|
||||
ui.painter().rect_filled(sep, 0.0, BORDER);
|
||||
|
||||
// Список файлов
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
Grid::new(id)
|
||||
.striped(true)
|
||||
.min_col_width(60.0)
|
||||
.show(ui, |ui| {
|
||||
ui.strong("Name");
|
||||
ui.strong("Size");
|
||||
ui.strong("Type");
|
||||
ui.strong("Modified");
|
||||
ui.end_row();
|
||||
for entry in &sorted {
|
||||
let is_selected = selected.as_deref() == Some(&entry.name);
|
||||
let row_id = Id::new((id, "row", &entry.name));
|
||||
|
||||
for entry in &sorted {
|
||||
let is_selected = selected.as_ref() == Some(&entry.name);
|
||||
let row_rect = egui::Rect::from_min_size(
|
||||
ui.cursor().min,
|
||||
egui::vec2(ui.available_width(), ROW_H),
|
||||
);
|
||||
let is_hovered = ui.rect_contains_pointer(row_rect);
|
||||
|
||||
let response = if let Some(payload) = make_drag_payload(entry) {
|
||||
let drag_id = Id::new(("drag", id, &entry.name));
|
||||
ui.dnd_drag_source(drag_id, payload, |ui| {
|
||||
ui.selectable_label(is_selected, &entry.name)
|
||||
})
|
||||
.response
|
||||
} else {
|
||||
ui.selectable_label(is_selected, &entry.name)
|
||||
};
|
||||
let bg = if is_selected {
|
||||
BG_ROW_SEL
|
||||
} else if is_hovered {
|
||||
BG_ROW_HOVER
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
|
||||
if response.clicked() {
|
||||
*selected = Some(entry.name.clone());
|
||||
clicked = Some(entry.name.clone());
|
||||
}
|
||||
if response.double_clicked() {
|
||||
double_clicked = Some(entry.name.clone());
|
||||
}
|
||||
let response = egui::Frame::none()
|
||||
.fill(bg)
|
||||
.inner_margin(egui::Margin { left: 6.0, right: 6.0, top: 0.0, bottom: 0.0 })
|
||||
.show(ui, |ui| {
|
||||
ui.set_min_height(ROW_H);
|
||||
ui.horizontal_centered(|ui| {
|
||||
// иконка + имя
|
||||
let name_text = if entry.name == ".." {
|
||||
format!("⬆ ..")
|
||||
} else {
|
||||
match entry.kind {
|
||||
EntryKind::Dir => format!("📁 {}", entry.name),
|
||||
EntryKind::Symlink => format!("🔗 {}", entry.name),
|
||||
EntryKind::File => {
|
||||
let icon = file_icon(&entry.name);
|
||||
format!("{} {}", icon, entry.name)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ui.label(format_size(entry.size));
|
||||
ui.label(match entry.kind {
|
||||
EntryKind::Dir => "Dir",
|
||||
EntryKind::File => "File",
|
||||
EntryKind::Symlink => "Link",
|
||||
let name_col = if entry.name == ".." {
|
||||
TEXT_HINT
|
||||
} else if entry.kind == EntryKind::Dir {
|
||||
ACCENT
|
||||
} else {
|
||||
TEXT_PRIMARY
|
||||
};
|
||||
|
||||
if let Some(payload) = make_drag_payload(entry) {
|
||||
ui.dnd_drag_source(row_id, payload, |ui| {
|
||||
ui.label(
|
||||
RichText::new(&name_text).color(name_col).size(12.0)
|
||||
);
|
||||
});
|
||||
} else {
|
||||
ui.label(RichText::new(&name_text).color(name_col).size(12.0));
|
||||
}
|
||||
|
||||
// правые колонки
|
||||
ui.with_layout(
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.label(
|
||||
RichText::new(format_time(entry.modified))
|
||||
.color(TEXT_DIM)
|
||||
.size(11.0),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let (type_str, type_col) = match entry.kind {
|
||||
EntryKind::Dir => ("Dir", TEXT_DIM),
|
||||
EntryKind::File => ("File", TEXT_HINT),
|
||||
EntryKind::Symlink => ("Link", YELLOW),
|
||||
};
|
||||
let type_label = egui::Label::new(
|
||||
RichText::new(type_str).color(type_col).size(11.0)
|
||||
)
|
||||
.truncate();
|
||||
ui.add_sized([38.0, ROW_H], type_label);
|
||||
|
||||
ui.label(
|
||||
RichText::new(format_size(entry.size))
|
||||
.color(TEXT_DIM)
|
||||
.size(11.0),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
},
|
||||
);
|
||||
});
|
||||
ui.label(format_time(entry.modified));
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
})
|
||||
.response;
|
||||
|
||||
if response.clicked() {
|
||||
*selected = Some(entry.name.clone());
|
||||
clicked = Some(entry.name.clone());
|
||||
}
|
||||
if response.double_clicked() {
|
||||
double_clicked = Some(entry.name.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FileTableResponse {
|
||||
double_clicked,
|
||||
clicked,
|
||||
FileTableResponse { double_clicked, clicked }
|
||||
}
|
||||
|
||||
fn sort_header(ui: &mut Ui, label: &str, min_w: f32, active: bool, dir: SortDir) -> bool {
|
||||
let arrow = if active {
|
||||
if dir == SortDir::Asc { " ↑" } else { " ↓" }
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let full = format!("{}{}", label, arrow);
|
||||
let col = if active { ACCENT } else { TEXT_DIM };
|
||||
let btn = egui::Button::new(RichText::new(full).color(col).size(11.0).strong())
|
||||
.fill(Color32::TRANSPARENT)
|
||||
.min_size(egui::vec2(min_w, 20.0));
|
||||
ui.add(btn).clicked()
|
||||
}
|
||||
|
||||
fn toggle_sort(col: SortCol, cur_col: SortCol, cur_dir: SortDir) -> (SortCol, SortDir) {
|
||||
if col == cur_col {
|
||||
let new_dir = if cur_dir == SortDir::Asc { SortDir::Desc } else { SortDir::Asc };
|
||||
(col, new_dir)
|
||||
} else {
|
||||
(col, SortDir::Asc)
|
||||
}
|
||||
}
|
||||
|
||||
fn file_icon(name: &str) -> &'static str {
|
||||
let ext = name.rsplit('.').next().unwrap_or("").to_lowercase();
|
||||
match ext.as_str() {
|
||||
"rs" => "🦀",
|
||||
"toml" | "yaml" | "yml" | "json" => "⚙",
|
||||
"md" | "txt" | "log" => "📝",
|
||||
"png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" => "🖼",
|
||||
"mp4" | "mkv" | "avi" | "mov" => "🎬",
|
||||
"mp3" | "ogg" | "flac" | "wav" => "🎵",
|
||||
"zip" | "tar" | "gz" | "bz2" | "xz" | "7z" => "📦",
|
||||
"pdf" => "📕",
|
||||
"sh" | "bash" | "fish" | "zsh" => "⚡",
|
||||
"html" | "css" | "js" | "ts" => "🌐",
|
||||
"py" => "🐍",
|
||||
"java" | "class" | "jar" => "☕",
|
||||
_ => "📄",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue