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:
loki5512344 2026-06-19 16:53:51 +02:00
parent 9cb8e47a97
commit f983f12bec
16 changed files with 1672 additions and 789 deletions

View file

@ -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" => "☕",
_ => "📄",
}
}