diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac2b7db1..9f353f26 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# egui-file-dialog changelog
+## 2024-02-18 - v0.3.0 - UI improvements
+### 🖥 UI
+- Updated bottom panel so that the dialog can also be resized in `DialogMode::SaveFile` or when selecting a file or directory with a long name [#32](https://github.com/fluxxcode/egui-file-dialog/pull/32)
+ - The error when saving a file is now displayed as a tooltip when hovering over the grayed out save button \
+ ![preview](media/changelog/v0.3.0/error_tooltip.png)
+ - Updated file name input to use all available space
+ - Added scroll area around the selected item, so that long file names can be displayed without making the dialog larger
+- The default minimum window size has been further reduced to `(340.0, 170.0)` [#32](https://github.com/fluxxcode/egui-file-dialog/pull/32)
+- Added an error icon to the error message when creating a new folder [#32](https://github.com/fluxxcode/egui-file-dialog/pull/32) \
+ ![preview](media/changelog/v0.3.0/error_icon.png)
+- Removable devices are now listed in a separate devices section [#34](https://github.com/fluxxcode/egui-file-dialog/pull/34)
+- Added mount point to the disk names on Windows [#38](https://github.com/fluxxcode/egui-file-dialog/pull/38)
+
+### 🔧 Changes
+- Restructure `file_dialog.rs` [#36](https://github.com/fluxxcode/egui-file-dialog/pull/36)
+
+### 📚 Documentation
+- Fix typos in the documentation [#29](https://github.com/fluxxcode/egui-file-dialog/pull/29)
+- Fix eframe version in the example in `README.md` [#30](https://github.com/fluxxcode/egui-file-dialog/pull/30)
+- Added "Planned features” section to `README.md` and minor improvements [#31](https://github.com/fluxxcode/egui-file-dialog/pull/31) (Renamed with [#35](https://github.com/fluxxcode/egui-file-dialog/pull/35))
+- Updated example screenshot in `README.md` to include new "Removable Devices" section [#34](https://github.com/fluxxcode/egui-file-dialog/pull/34)
+- Moved media files from `doc/img/` to `media/` [#37](https://github.com/fluxxcode/egui-file-dialog/pull/37)
+
## 2024-02-07 - v0.2.0 - API improvements
### 🚨 Breaking Changes
- Rename `FileDialog::default_window_size` to `FileDialog::default_size` [#14](https://github.com/fluxxcode/egui-file-dialog/pull/14)
diff --git a/Cargo.toml b/Cargo.toml
index 22f1949b..5124f48e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ members = [
[package]
name = "egui-file-dialog"
description = "An easy-to-use file dialog for egui"
-version = "0.2.0"
+version = "0.3.0"
edition = "2021"
authors = ["fluxxcode"]
repository = "https://github.com/fluxxcode/egui-file-dialog"
diff --git a/README.md b/README.md
index 5faf7af4..443965d1 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,13 @@
This repository provides an easy-to-use file dialog (a.k.a. file explorer, file picker) for [egui](https://github.com/emilk/egui). This makes it possible to use a file dialog directly in the egui application without having to rely on the file explorer of the operating system.
-
+
The goal for the future is that parts of the dialog can be dynamically adapted so that it can be used in different situations. One goal, for example, is that the labels can be dynamically adjusted to support different languages.
The project is currently in a very early version. Some planned features are still missing and some improvements still need to be made.
-**NOTE**: As long as version 1.0.0 has not yet been reached, even minor version increases may contain breaking changes.
+Check out [CHANGELOG.md](https://github.com/fluxxcode/egui-file-dialog/blob/develop/CHANGELOG.md) in the develop branch to find the latest features included in the next release.
**Currently only tested on Linux and Windows!**
@@ -25,14 +25,26 @@ The project is currently in a very early version. Some planned features are stil
- Shortcut for user directories (Home, Documents, ...) and system disks
- Resizable window
+## Planned features
+The following lists some of the features that are currently missing but are planned for the future!
+- Customize labels and enabled features
+- Selection of multiple directory items at once
+- Customizable file icons
+- Only show files with a specific file extension (The user can already filter files by file extension using the search, but there is currently no backend method for this or a dropdown to be able to select from predefined file extensions.)
+- Keyboard input
+- Context menus, for example for renaming, deleting or copying files or directories.
+- Option to show or hide hidden files and folders
+
## Example
+Detailed examples that can be run can be found in the [examples](https://github.com/fluxxcode/egui-file-dialog/tree/master/examples) folder.
+
The following example shows the basic use of the file dialog with [eframe](https://github.com/emilk/egui/tree/master/crates/eframe) to select a file.
Cargo.toml:
```toml
[dependencies]
-eframe = "0.25.0"
-egui-file-dialog = "0.2.0"
+eframe = "0.26.0"
+egui-file-dialog = "0.3.0"
```
main.rs:
diff --git a/doc/img/demo.png b/doc/img/demo.png
deleted file mode 100644
index 08e95d1f..00000000
Binary files a/doc/img/demo.png and /dev/null differ
diff --git a/media/changelog/v0.3.0/error_icon.png b/media/changelog/v0.3.0/error_icon.png
new file mode 100644
index 00000000..147b8d86
Binary files /dev/null and b/media/changelog/v0.3.0/error_icon.png differ
diff --git a/media/changelog/v0.3.0/error_tooltip.png b/media/changelog/v0.3.0/error_tooltip.png
new file mode 100644
index 00000000..c1f7955b
Binary files /dev/null and b/media/changelog/v0.3.0/error_tooltip.png differ
diff --git a/media/demo.png b/media/demo.png
new file mode 100644
index 00000000..c8bb8f94
Binary files /dev/null and b/media/demo.png differ
diff --git a/src/create_directory_dialog.rs b/src/create_directory_dialog.rs
index 7027c588..40c2cc7b 100644
--- a/src/create_directory_dialog.rs
+++ b/src/create_directory_dialog.rs
@@ -112,7 +112,15 @@ impl CreateDirectoryDialog {
if let Some(err) = &self.error {
ui.add_space(5.0);
- let response = ui.label(err);
+
+ let response = ui
+ .horizontal_wrapped(|ui| {
+ ui.spacing_mut().item_spacing.x = 0.0;
+
+ ui.colored_label(ui.style().visuals.error_fg_color, "⚠ ");
+ ui.label(err);
+ })
+ .response;
if self.scroll_to_error {
response.scroll_to_me(Some(egui::Align::Center));
diff --git a/src/data/disks.rs b/src/data/disks.rs
index b14d1d5d..aefc0138 100644
--- a/src/data/disks.rs
+++ b/src/data/disks.rs
@@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
pub struct Disk {
mount_point: PathBuf,
display_name: String,
+ is_removable: bool,
}
impl Disk {
@@ -14,7 +15,8 @@ impl Disk {
pub fn from_sysinfo_disk(disk: &sysinfo::Disk) -> Self {
Self {
mount_point: disk.mount_point().to_path_buf(),
- display_name: Self::gen_display_name(disk),
+ display_name: gen_display_name(disk),
+ is_removable: disk.is_removable(),
}
}
@@ -28,19 +30,8 @@ impl Disk {
&self.display_name
}
- fn gen_display_name(disk: &sysinfo::Disk) -> String {
- // TODO: Get display name of the devices.
- // Currently on Linux it returns "/dev/sda1" and on Windows it returns an
- // empty string for the C:\\ drive.
- let name = disk.name().to_str().unwrap_or_default().to_string();
-
- // Try using the mount point as the display name if the specified name
- // from sysinfo::Disk is empty or contains invalid characters
- if name.is_empty() {
- return disk.mount_point().to_str().unwrap_or_default().to_string();
- }
-
- name
+ pub fn is_removable(&self) -> bool {
+ self.is_removable
}
}
@@ -69,3 +60,40 @@ impl Disks {
self.disks.iter()
}
}
+
+#[cfg(windows)]
+fn gen_display_name(disk: &sysinfo::Disk) -> String {
+ // TODO: Get display name of the devices.
+ // Currently on Windows it returns an empty string for the C:\\ drive.
+
+ let mut name = disk.name().to_str().unwrap_or_default().to_string();
+ let mount_point = disk
+ .mount_point()
+ .to_str()
+ .unwrap_or_default()
+ .to_string()
+ .replace("\\", "");
+
+ // Try using the mount point as the display name if the specified name
+ // from sysinfo::Disk is empty or contains invalid characters
+ if name.is_empty() {
+ return mount_point;
+ }
+
+ name.push_str(format!(" ({})", mount_point).as_str());
+
+ name
+}
+
+#[cfg(not(windows))]
+fn gen_display_name(disk: &sysinfo::Disk) -> String {
+ let name = disk.name().to_str().unwrap_or_default().to_string();
+
+ // Try using the mount point as the display name if the specified name
+ // from sysinfo::Disk is empty or contains invalid characters
+ if name.is_empty() {
+ return disk.mount_point().to_str().unwrap_or_default().to_string();
+ }
+
+ name
+}
diff --git a/src/data/mod.rs b/src/data/mod.rs
index 4d6afc42..aa2fdc95 100644
--- a/src/data/mod.rs
+++ b/src/data/mod.rs
@@ -2,7 +2,7 @@ mod directory_content;
pub use directory_content::{DirectoryContent, DirectoryEntry};
mod disks;
-pub use disks::Disks;
+pub use disks::{Disk, Disks};
mod user_directories;
pub use user_directories::UserDirectories;
diff --git a/src/file_dialog.rs b/src/file_dialog.rs
index 89105a88..21b7a61e 100644
--- a/src/file_dialog.rs
+++ b/src/file_dialog.rs
@@ -5,7 +5,7 @@ use std::{fs, io};
use crate::create_directory_dialog::CreateDirectoryDialog;
-use crate::data::{DirectoryContent, DirectoryEntry, Disks, UserDirectories};
+use crate::data::{DirectoryContent, DirectoryEntry, Disk, Disks, UserDirectories};
/// Represents the mode the file dialog is currently in.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -154,6 +154,9 @@ impl Default for FileDialog {
}
impl FileDialog {
+ // ------------------------------------------------------------------------
+ // Creation:
+
/// Creates a new file dialog instance with default values.
pub fn new() -> Self {
FileDialog {
@@ -178,7 +181,7 @@ impl FileDialog {
window_fixed_pos: None,
window_default_size: egui::Vec2::new(650.0, 370.0),
window_max_size: None,
- window_min_size: egui::Vec2::new(355.0, 200.0),
+ window_min_size: egui::Vec2::new(340.0, 170.0),
window_anchor: None,
window_resizable: true,
window_movable: true,
@@ -196,96 +199,8 @@ impl FileDialog {
}
}
- /// Sets the first loaded directory when the dialog opens.
- /// If the path is a file, the file's parent directory is used. If the path then has no
- /// parent directory or cannot be loaded, the user will receive an error.
- /// However, the user directories and system disk allow the user to still select a file in
- /// the event of an error.
- ///
- /// Relative and absolute paths are allowed, but absolute paths are recommended.
- pub fn initial_directory(mut self, directory: PathBuf) -> Self {
- self.initial_directory = directory.clone();
- self
- }
-
- /// Overwrites the window title.
- ///
- /// By default, the title is set dynamically, based on the `DialogMode`
- /// the dialog is currently in.
- pub fn title(mut self, title: &str) -> Self {
- self.window_overwrite_title = Some(title.to_string());
- self
- }
-
- /// Sets the ID of the window.
- pub fn id(mut self, id: impl Into) -> Self {
- self.window_id = Some(id.into());
- self
- }
-
- /// Sets the default position of the window.
- pub fn default_pos(mut self, default_pos: impl Into) -> Self {
- self.window_default_pos = Some(default_pos.into());
- self
- }
-
- /// Sets the window position and prevents it from being dragged around.
- pub fn fixed_pos(mut self, pos: impl Into) -> Self {
- self.window_fixed_pos = Some(pos.into());
- self
- }
-
- /// Sets the default size of the window.
- pub fn default_size(mut self, size: impl Into) -> Self {
- self.window_default_size = size.into();
- self
- }
-
- /// Sets the maximum size of the window.
- pub fn max_size(mut self, max_size: impl Into) -> Self {
- self.window_max_size = Some(max_size.into());
- self
- }
-
- /// Sets the minimum size of the window.
- ///
- /// Specifying a smaller minimum size than the default can lead to unexpected behavior.
- pub fn min_size(mut self, min_size: impl Into) -> Self {
- self.window_min_size = min_size.into();
- self
- }
-
- /// Sets the anchor of the window.
- pub fn anchor(mut self, align: egui::Align2, offset: impl Into) -> Self {
- self.window_anchor = Some((align, offset.into()));
- self
- }
-
- /// Sets if the window is resizable.
- pub fn resizable(mut self, resizable: bool) -> Self {
- self.window_resizable = resizable;
- self
- }
-
- /// Sets if the window is movable.
- ///
- /// Has no effect if an anchor is set.
- pub fn movable(mut self, movable: bool) -> Self {
- self.window_movable = movable;
- self
- }
-
- /// Sets if the title bar of the window is shown.
- pub fn title_bar(mut self, title_bar: bool) -> Self {
- self.window_title_bar = title_bar;
- self
- }
-
- /// Sets the default file name when opening the dialog in `DialogMode::SaveFile` mode.
- pub fn default_file_name(mut self, name: &str) -> Self {
- self.default_file_name = name.to_string();
- self
- }
+ // -------------------------------------------------
+ // Open:
/// Opens the file dialog in the given mode with the given options.
/// This function resets the file dialog and takes care for the variables that need to be
@@ -308,7 +223,7 @@ impl FileDialog {
/// # Examples
///
/// The following example shows how the dialog can be used for multiple
- /// actions using the `operation_id``.
+ /// actions using the `operation_id`.
///
/// ```
/// use std::path::PathBuf;
@@ -418,6 +333,103 @@ impl FileDialog {
let _ = self.open(DialogMode::SaveFile, true, None);
}
+ // -------------------------------------------------
+ // Setter:
+
+ /// Sets the first loaded directory when the dialog opens.
+ /// If the path is a file, the file's parent directory is used. If the path then has no
+ /// parent directory or cannot be loaded, the user will receive an error.
+ /// However, the user directories and system disk allow the user to still select a file in
+ /// the event of an error.
+ ///
+ /// Relative and absolute paths are allowed, but absolute paths are recommended.
+ pub fn initial_directory(mut self, directory: PathBuf) -> Self {
+ self.initial_directory = directory.clone();
+ self
+ }
+
+ /// Overwrites the window title.
+ ///
+ /// By default, the title is set dynamically, based on the `DialogMode`
+ /// the dialog is currently in.
+ pub fn title(mut self, title: &str) -> Self {
+ self.window_overwrite_title = Some(title.to_string());
+ self
+ }
+
+ /// Sets the ID of the window.
+ pub fn id(mut self, id: impl Into) -> Self {
+ self.window_id = Some(id.into());
+ self
+ }
+
+ /// Sets the default position of the window.
+ pub fn default_pos(mut self, default_pos: impl Into) -> Self {
+ self.window_default_pos = Some(default_pos.into());
+ self
+ }
+
+ /// Sets the window position and prevents it from being dragged around.
+ pub fn fixed_pos(mut self, pos: impl Into) -> Self {
+ self.window_fixed_pos = Some(pos.into());
+ self
+ }
+
+ /// Sets the default size of the window.
+ pub fn default_size(mut self, size: impl Into) -> Self {
+ self.window_default_size = size.into();
+ self
+ }
+
+ /// Sets the maximum size of the window.
+ pub fn max_size(mut self, max_size: impl Into) -> Self {
+ self.window_max_size = Some(max_size.into());
+ self
+ }
+
+ /// Sets the minimum size of the window.
+ ///
+ /// Specifying a smaller minimum size than the default can lead to unexpected behavior.
+ pub fn min_size(mut self, min_size: impl Into) -> Self {
+ self.window_min_size = min_size.into();
+ self
+ }
+
+ /// Sets the anchor of the window.
+ pub fn anchor(mut self, align: egui::Align2, offset: impl Into) -> Self {
+ self.window_anchor = Some((align, offset.into()));
+ self
+ }
+
+ /// Sets if the window is resizable.
+ pub fn resizable(mut self, resizable: bool) -> Self {
+ self.window_resizable = resizable;
+ self
+ }
+
+ /// Sets if the window is movable.
+ ///
+ /// Has no effect if an anchor is set.
+ pub fn movable(mut self, movable: bool) -> Self {
+ self.window_movable = movable;
+ self
+ }
+
+ /// Sets if the title bar of the window is shown.
+ pub fn title_bar(mut self, title_bar: bool) -> Self {
+ self.window_title_bar = title_bar;
+ self
+ }
+
+ /// Sets the default file name when opening the dialog in `DialogMode::SaveFile` mode.
+ pub fn default_file_name(mut self, name: &str) -> Self {
+ self.default_file_name = name.to_string();
+ self
+ }
+
+ // -------------------------------------------------
+ // Getter:
+
/// Returns the mode the dialog is currently in.
pub fn mode(&self) -> DialogMode {
self.mode
@@ -441,7 +453,7 @@ impl FileDialog {
/// Returns the ID of the operation for which the dialog is currently being used.
///
- /// See `FileDialog::open` more information.
+ /// See `FileDialog::open` for more information.
pub fn operation_id(&self) -> Option<&str> {
self.operation_id.as_deref()
}
@@ -489,7 +501,10 @@ impl FileDialog {
self
}
+}
+/// UI methods
+impl FileDialog {
/// Creates a new egui window with the configured options.
fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> {
let mut window = egui::Window::new(&self.window_title)
@@ -532,11 +547,11 @@ impl FileDialog {
ui.horizontal(|ui| {
// Navigation buttons
if let Some(x) = self.current_directory() {
- if self.ui_button_sized(ui, x.parent().is_some(), NAV_BUTTON_SIZE, "⏶") {
+ if self.ui_button_sized(ui, x.parent().is_some(), NAV_BUTTON_SIZE, "⏶", None) {
let _ = self.load_parent_directory();
}
} else {
- let _ = self.ui_button_sized(ui, false, NAV_BUTTON_SIZE, "⏶");
+ let _ = self.ui_button_sized(ui, false, NAV_BUTTON_SIZE, "⏶", None);
}
if self.ui_button_sized(
@@ -544,11 +559,12 @@ impl FileDialog {
self.directory_offset + 1 < self.directory_stack.len(),
NAV_BUTTON_SIZE,
"⏴",
+ None,
) {
let _ = self.load_previous_directory();
}
- if self.ui_button_sized(ui, self.directory_offset != 0, NAV_BUTTON_SIZE, "⏵") {
+ if self.ui_button_sized(ui, self.directory_offset != 0, NAV_BUTTON_SIZE, "⏵", None) {
let _ = self.load_next_directory();
}
@@ -557,6 +573,7 @@ impl FileDialog {
!self.create_directory_dialog.is_open(),
NAV_BUTTON_SIZE,
"+",
+ None,
) {
if let Some(x) = self.current_directory() {
self.create_directory_dialog.open(x.to_path_buf());
@@ -679,9 +696,6 @@ impl FileDialog {
ui.add_space(ctx.style().spacing.item_spacing.y * 2.0);
self.ui_update_user_directories(ui);
-
- ui.add_space(ctx.style().spacing.item_spacing.y * 4.0);
-
self.ui_update_devices(ui);
});
});
@@ -693,6 +707,7 @@ impl FileDialog {
ui.add_space(5.0);
+ // ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
ui.horizontal(|ui| {
match &self.mode {
DialogMode::SelectDirectory => ui.label("Selected directory:"),
@@ -704,32 +719,51 @@ impl FileDialog {
DialogMode::SelectDirectory | DialogMode::SelectFile => {
if self.is_selection_valid() {
if let Some(x) = &self.selected_item {
- ui.colored_label(ui.style().visuals.selection.bg_fill, x.file_name());
+ use egui::containers::scroll_area::ScrollBarVisibility;
+
+ egui::containers::ScrollArea::horizontal()
+ .auto_shrink([false, false])
+ .stick_to_right(true)
+ .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
+ .show(ui, |ui| {
+ ui.colored_label(
+ ui.style().visuals.selection.bg_fill,
+ x.file_name(),
+ );
+ });
}
}
}
DialogMode::SaveFile => {
- let response = ui.add(egui::TextEdit::singleline(&mut self.file_name_input));
+ let response = ui.add(
+ egui::TextEdit::singleline(&mut self.file_name_input)
+ .desired_width(f32::INFINITY),
+ );
if response.changed() {
self.file_name_input_error = self.validate_file_name_input();
}
-
- if let Some(x) = &self.file_name_input_error {
- // TODO: Use error icon instead
- ui.label(x);
- }
}
};
});
+ if self.mode == DialogMode::SaveFile {
+ ui.add_space(ui.style().spacing.item_spacing.y * 2.0)
+ }
+
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
let label = match &self.mode {
DialogMode::SelectDirectory | DialogMode::SelectFile => "🗀 Open",
DialogMode::SaveFile => "📥 Save",
};
- if self.ui_button_sized(ui, self.is_selection_valid(), BUTTON_SIZE, label) {
+ if self.ui_button_sized(
+ ui,
+ self.is_selection_valid(),
+ BUTTON_SIZE,
+ label,
+ self.file_name_input_error.as_deref(),
+ ) {
match &self.mode {
DialogMode::SelectDirectory | DialogMode::SelectFile => {
// self.selected_item should always contain a value,
@@ -918,24 +952,45 @@ impl FileDialog {
}
}
- /// Updates the list of the system disks (Disks).
+ /// Updates the list of the system disks (Devices, Removable Devices).
fn ui_update_devices(&mut self, ui: &mut egui::Ui) {
- ui.label("Devices");
-
let disks = std::mem::take(&mut self.system_disks);
- for disk in disks.iter() {
- if ui
- .selectable_label(false, format!("🖴 {}", disk.display_name()))
- .clicked()
- {
- let _ = self.load_directory(disk.mount_point());
+ // Non removable devices like hard drives
+ for (i, disk) in disks.iter().filter(|x| !x.is_removable()).enumerate() {
+ if i == 0 {
+ ui.add_space(ui.style().spacing.item_spacing.y * 4.0);
+ ui.label("Devices");
}
+
+ self.ui_update_device_entry(ui, disk);
+ }
+
+ // Removable devices like USB sticks
+ for (i, disk) in disks.iter().filter(|x| x.is_removable()).enumerate() {
+ if i == 0 {
+ ui.add_space(ui.style().spacing.item_spacing.y * 4.0);
+ ui.label("Removable Devices");
+ }
+
+ self.ui_update_device_entry(ui, disk);
}
self.system_disks = disks;
}
+ /// Updates a device entry in the device list.
+ fn ui_update_device_entry(&mut self, ui: &mut egui::Ui, device: &Disk) {
+ let label = match device.is_removable() {
+ true => format!("💾 {}", device.display_name()),
+ false => format!("🖴 {}", device.display_name()),
+ };
+
+ if ui.selectable_label(false, label).clicked() {
+ let _ = self.load_directory(device.mount_point());
+ }
+ }
+
/// Helper function to add a sized button that can be enabled or disabled
fn ui_button_sized(
&self,
@@ -943,16 +998,32 @@ impl FileDialog {
enabled: bool,
size: egui::Vec2,
label: &str,
+ err_tooltip: Option<&str>,
) -> bool {
let mut clicked = false;
ui.add_enabled_ui(enabled, |ui| {
- clicked = ui.add_sized(size, egui::Button::new(label)).clicked();
+ let response = ui.add_sized(size, egui::Button::new(label));
+ clicked = response.clicked();
+
+ if let Some(err) = err_tooltip {
+ response.on_disabled_hover_ui(|ui| {
+ ui.horizontal_wrapped(|ui| {
+ ui.spacing_mut().item_spacing.x = 0.0;
+
+ ui.colored_label(ui.ctx().style().visuals.error_fg_color, "⚠ ");
+ ui.label(err);
+ });
+ });
+ }
});
clicked
}
+}
+/// Implementation
+impl FileDialog {
/// Resets the dialog to use default values.
/// Configuration variables such as `initial_directory` are retained.
fn reset(&mut self) {