Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement file truncating #203

Merged
merged 13 commits into from
Nov 24, 2024
3 changes: 3 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub struct FileDialogConfig {
/// This prevents the application from blocking when loading large directories
/// or from slow hard drives.
pub load_via_thread: bool,
/// If we should truncate the filenames in the middle
pub truncate_filenames: bool,

/// The icon that is used to display error messages.
pub err_icon: String,
Expand Down Expand Up @@ -217,6 +219,7 @@ impl Default for FileDialogConfig {
directory_separator: String::from(">"),
canonicalize_paths: true,
load_via_thread: true,
truncate_filenames: true,

err_icon: String::from("⚠"),
warn_icon: String::from("⚠"),
Expand Down
99 changes: 83 additions & 16 deletions src/file_dialog.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
use egui::text::{CCursor, CCursorRange};
use std::fmt::Debug;
use std::io;
use std::path::{Path, PathBuf};

use crate::config::{
FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, FileFilter,
Filter, QuickAccess,
Expand All @@ -12,6 +7,10 @@ use crate::data::{
DirectoryContent, DirectoryContentState, DirectoryEntry, Disk, Disks, UserDirectories,
};
use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal};
use egui::text::{CCursor, CCursorRange};
use std::fmt::Debug;
use std::io;
use std::path::{Path, PathBuf};

/// Represents the mode the file dialog is currently in.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
Expand Down Expand Up @@ -629,6 +628,16 @@ impl FileDialog {
self
}

/// Sets if long filenames should be truncated in the middle.
/// The extension, if available, will be preserved.
///
/// Warning! If this is disabled, the scroll-to-selection might not work correctly and have
/// an offset for large directories.
pub const fn truncate_filenames(mut self, truncate_filenames: bool) -> Self {
self.config.truncate_filenames = truncate_filenames;
self
}

/// Sets the icon that is used to display errors.
pub fn err_icon(mut self, icon: &str) -> Self {
self.config.err_icon = icon.to_string();
Expand Down Expand Up @@ -2170,20 +2179,35 @@ impl FileDialog {
batch_select_item_b: &mut Option<DirectoryEntry>,
) -> bool {
let file_name = item.file_name();
let primary_selected = self.is_primary_selected(item);
let pinned = self.is_pinned(item);

let mut primary_selected = false;
if let Some(x) = &self.selected_item {
primary_selected = x.path_eq(item);
}
let icons = if pinned {
format!("{} {} ", item.icon(), self.config.pinned_icon)
} else {
format!("{} ", item.icon())
};

let pinned = self.is_pinned(item);
let label = if pinned {
format!("{} {} {}", item.icon(), self.config.pinned_icon, file_name)
let icons_width = Self::calc_text_width(ui, &icons);

// Calc available width for the file name and include a small margin
let available_width = ui.available_width() - icons_width - 15.0;

let truncate = self.config.truncate_filenames
&& available_width < Self::calc_text_width(ui, file_name);

let text = if truncate {
Self::truncate_filename(ui, item, available_width)
} else {
format!("{} {}", item.icon(), file_name)
file_name.to_owned()
};

let re = ui.selectable_label(primary_selected || item.selected, label);
let mut re =
ui.selectable_label(primary_selected || item.selected, format!("{icons}{text}"));

if truncate {
re = re.on_hover_text(file_name);
}

if item.is_dir() {
self.ui_update_path_context_menu(&re, item);
Expand Down Expand Up @@ -2398,15 +2422,52 @@ impl FileDialog {
}
}

/// Calculate the width of the specified text using the current font configuration.
/// Calculates the width of a single char.
fn calc_char_width(ui: &egui::Ui, char: char) -> f32 {
ui.fonts(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char))
}

/// Calculates the width of the specified text using the current font configuration.
/// Does not take new lines or text breaks into account!
fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 {
let mut width = 0.0;

for char in text.chars() {
width += ui.fonts(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char));
width += Self::calc_char_width(ui, char);
}

width
}

fn truncate_filename(ui: &egui::Ui, item: &DirectoryEntry, max_length: f32) -> String {
let path = item.as_path();
let file_stem = path.file_stem().and_then(|f| f.to_str()).unwrap_or("");
let extension = path.extension().map_or("...".to_owned(), |ext| {
format!("...{}", ext.to_str().unwrap_or(""))
});

let reserved = Self::calc_text_width(ui, &extension);

if max_length <= reserved {
return extension;
}

let mut width = reserved;
let mut shortened_stem = String::new();

for char in file_stem.chars() {
let w = Self::calc_char_width(ui, char);

if width + w > max_length {
break;
}

shortened_stem.push(char);
width += w;
}

format!("{shortened_stem}{extension}")
}
}

/// Keybindings
Expand Down Expand Up @@ -2666,6 +2727,12 @@ impl FileDialog {
.any(|p| path.path_eq(p))
}

fn is_primary_selected(&self, item: &DirectoryEntry) -> bool {
self.selected_item
.as_ref()
.map_or(false, |x| x.path_eq(item))
}

/// Resets the dialog to use default values.
/// Configuration variables are retained.
fn reset(&mut self) {
Expand Down