diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec2cdf8..65ac1302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Implement non blocking directory loading [#177](https://github.com/fluxxcode/egui-file-dialog/pull/177) - Only update visible items in the central panel if the search value is empty and the create directory dialog is currently closed [#181](https://github.com/fluxxcode/egui-file-dialog/pull/181) - Improve CI [#186](https://github.com/fluxxcode/egui-file-dialog/pull/186) (thanks [@bircni](https://github.com/bircni)!) +- Files and folders are now truncated in the middle and no longer divided onto separate lines. This can be disabled using `FileDialog::truncate_filenames` [#203](https://github.com/fluxxcode/egui-file-dialog/pull/203) (thanks [@hacknus](https://github.com/hacknus)!) ### 📚 Documentation - Updated `README.md` to include latest features [#176](https://github.com/fluxxcode/egui-file-dialog/pull/176) diff --git a/src/config/mod.rs b/src/config/mod.rs index b4d1efff..ee4a47ec 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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, @@ -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("⚠"), diff --git a/src/file_dialog.rs b/src/file_dialog.rs index 54cfa60a..451cf73a 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -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, @@ -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)] @@ -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(); @@ -2165,20 +2174,35 @@ impl FileDialog { batch_select_item_b: &mut Option, ) -> 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); @@ -2395,15 +2419,86 @@ 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 { + const TRUNCATE_STR: &str = "..."; + + let path = item.as_path(); + + let file_stem = if path.is_file() { + path.file_stem().and_then(|f| f.to_str()).unwrap_or("") + } else { + item.file_name() + }; + + let extension = if path.is_file() { + path.extension().map_or(String::new(), |ext| { + format!(".{}", ext.to_str().unwrap_or("")) + }) + } else { + String::new() + }; + + let extension_width = Self::calc_text_width(ui, &extension); + let reserved = extension_width + Self::calc_text_width(ui, TRUNCATE_STR); + + if max_length <= reserved { + return format!("{TRUNCATE_STR}{extension}"); + } + + let mut width = reserved; + let mut front = String::new(); + let mut back = String::new(); + + for (i, char) in file_stem.chars().enumerate() { + let w = Self::calc_char_width(ui, char); + + if width + w > max_length { + break; + } + + front.push(char); + width += w; + + let back_index = file_stem.len() - i - 1; + + if back_index <= i { + break; + } + + if let Some(char) = file_stem.chars().nth(back_index) { + let w = Self::calc_char_width(ui, char); + + if width + w > max_length { + break; + } + + back.push(char); + width += w; + } + } + + format!( + "{front}{TRUNCATE_STR}{}{extension}", + back.chars().rev().collect::() + ) + } } /// Keybindings @@ -2663,6 +2758,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) {