Skip to content

Commit

Permalink
feat: implement file truncating (fluxxcode#203)
Browse files Browse the repository at this point in the history
* implement file truncating in the middle to prevent filenames from being displayed over two lines

---------

Co-authored-by: Jannis <[email protected]>
  • Loading branch information
hacknus and fluxxcode authored Nov 24, 2024
1 parent 91ce1a8 commit 4be7bfe
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
133 changes: 117 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 @@ -2165,20 +2174,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 @@ -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::<String>()
)
}
}

/// Keybindings
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 4be7bfe

Please sign in to comment.