diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ac2b7db1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# egui-file-dialog changelog + +## 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) +- Added attribute `operation_id` to `FileDialog::open` [#25](https://github.com/fluxxcode/egui-file-dialog/pull/25) + +### ✨ Features +- Implemented `operation_id` so the dialog can be used for multiple different actions in a single view [#25](https://github.com/fluxxcode/egui-file-dialog/pull/25) +- Added `FileDialog::anchor` to overwrite the window anchor [#11](https://github.com/fluxxcode/egui-file-dialog/pull/11) +- Added `FileDialog::title` to overwrite the window title [#12](https://github.com/fluxxcode/egui-file-dialog/pull/12) +- Added `FileDialog::resizable` to set if the window is resizable [#15](https://github.com/fluxxcode/egui-file-dialog/pull/15) +- Added `FileDialog::movable` to set if the window is movable [#15](https://github.com/fluxxcode/egui-file-dialog/pull/15) +- Added `FileDialog::id` to set the ID of the window [#16](https://github.com/fluxxcode/egui-file-dialog/pull/16) +- Added `FileDialog::fixed_pos` and `FileDialog::default_pos` to set the position of the window [#17](https://github.com/fluxxcode/egui-file-dialog/pull/17) +- Added `FileDialog::min_size` and `FileDialog::max_size` to set the minimum and maximum size of the window [#21](https://github.com/fluxxcode/egui-file-dialog/pull/21) +- Added `FileDialog::title_bar` to enable or disable the title bar of the window [#23](https://github.com/fluxxcode/egui-file-dialog/pull/23) + +### 🐛 Bug Fixes +- Fixed issue where no error message was displayed when creating a folder [#18](https://github.com/fluxxcode/egui-file-dialog/pull/18) +- Fixed an issue where the same disk can be loaded multiple times in a row on Windows [#26](https://github.com/fluxxcode/egui-file-dialog/pull/26) + +### 🔧 Changes +- Removed the version of `egui-file-dialog` in the examples [#8](https://github.com/fluxxcode/egui-file-dialog/pull/8) +- Use `ui.add_enabled` instead of custom `ui.rs` module [#22](https://github.com/fluxxcode/egui-file-dialog/pull/22) + +#### Dependency updates: +- Updated egui to version `0.26.0` [#24](https://github.com/fluxxcode/egui-file-dialog/pull/24) + +### 📚 Documentation +- Fix syntax highlighting on crates.io [#9](https://github.com/fluxxcode/egui-file-dialog/pull/9) +- Added dependency badge to `README.md` [#10](https://github.com/fluxxcode/egui-file-dialog/pull/10) +- Updated docs badge to use shields.io [#19](https://github.com/fluxxcode/egui-file-dialog/pull/19) + +## 2024-02-03 - v0.1.0 + +Initial release of the file dialog. + +The following features are included in this release: +- Select a file or a directory +- Save a file (Prompt user for a destination path) +- Create a new folder +- Navigation buttons to open the parent or previous directories +- Search for items in a directory +- Shortcut for user directories (Home, Documents, ...) and system disks +- Resizable window diff --git a/Cargo.toml b/Cargo.toml index e75975d4..22f1949b 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.1.0" +version = "0.2.0" edition = "2021" authors = ["fluxxcode"] repository = "https://github.com/fluxxcode/egui-file-dialog" @@ -17,7 +17,7 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -egui = "0.25.0" +egui = "0.26.0" # Used to fetch user folders directories = "5.0" diff --git a/README.md b/README.md index 2ad338c9..5faf7af4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # egui-file-dialog [![Latest version](https://img.shields.io/crates/v/egui-file-dialog.svg)](https://crates.io/crates/egui-file-dialog) -[![Documentation](https://docs.rs/egui-file-dialog/badge.svg)](https://docs.rs/egui-file-dialog) +[![Documentation](https://img.shields.io/docsrs/egui-file-dialog)](https://docs.rs/egui-file-dialog) +[![Dependency status](https://deps.rs/repo/github/fluxxcode/egui-file-dialog/status.svg)](https://deps.rs/repo/github/fluxxcode/egui-file-dialog) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fluxxcode/egui-file-dialog/blob/master/LICENSE) 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. @@ -31,11 +32,11 @@ Cargo.toml: ```toml [dependencies] eframe = "0.25.0" -egui-file-dialog = "0.1.0" +egui-file-dialog = "0.2.0" ``` main.rs: -```rs +```rust use std::path::PathBuf; use eframe::egui; diff --git a/examples/multiple_actions/Cargo.toml b/examples/multiple_actions/Cargo.toml new file mode 100644 index 00000000..e8a85dc2 --- /dev/null +++ b/examples/multiple_actions/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "multiple_actions" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eframe = { version = "0.26.0", default-features = false, features = ["glow"] } +egui-file-dialog = { path = "../../"} diff --git a/examples/multiple_actions/README.md b/examples/multiple_actions/README.md new file mode 100644 index 00000000..fd4e2ed2 --- /dev/null +++ b/examples/multiple_actions/README.md @@ -0,0 +1,7 @@ +This example shows how you can query multiple files from the user in one view. + +``` +cargo run -p multiple_actions +``` + +![](screenshot.png) diff --git a/examples/multiple_actions/screenshot.png b/examples/multiple_actions/screenshot.png new file mode 100644 index 00000000..3561a21d Binary files /dev/null and b/examples/multiple_actions/screenshot.png differ diff --git a/examples/multiple_actions/src/main.rs b/examples/multiple_actions/src/main.rs new file mode 100644 index 00000000..44c620cd --- /dev/null +++ b/examples/multiple_actions/src/main.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use eframe::egui; +use egui_file_dialog::{DialogMode, FileDialog}; + +struct MyApp { + file_dialog: FileDialog, + + selected_file_a: Option, + selected_file_b: Option, +} + +impl MyApp { + pub fn new(_cc: &eframe::CreationContext) -> Self { + Self { + file_dialog: FileDialog::new().id("egui_file_dialog"), + + selected_file_a: None, + selected_file_b: None, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("Select file a").clicked() { + let _ = self + .file_dialog + .open(DialogMode::SelectFile, true, Some("select_a")); + } + + if ui.button("Select file b").clicked() { + let _ = self + .file_dialog + .open(DialogMode::SelectFile, true, Some("select_b")); + } + + ui.label(format!("Selected file a: {:?}", self.selected_file_a)); + ui.label(format!("Selected file b: {:?}", self.selected_file_b)); + + self.file_dialog.update(ctx); + + if let Some(path) = self.file_dialog.selected() { + if self.file_dialog.operation_id() == Some("select_a") { + self.selected_file_a = Some(path.to_path_buf()); + } + + if self.file_dialog.operation_id() == Some("select_b") { + self.selected_file_b = Some(path.to_path_buf()); + } + } + }); + } +} + +fn main() -> eframe::Result<()> { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([1080.0, 720.0]), + ..Default::default() + }; + + eframe::run_native( + "My egui application", + options, + Box::new(|ctx| Box::new(MyApp::new(ctx))), + ) +} diff --git a/examples/sandbox/Cargo.toml b/examples/sandbox/Cargo.toml index 9764912e..5d1ec0e4 100644 --- a/examples/sandbox/Cargo.toml +++ b/examples/sandbox/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.25.0", default-features = false, features = ["glow"] } -egui-file-dialog = { version = "0.1.0", path = "../../"} +eframe = { version = "0.26.0", default-features = false, features = ["glow"] } +egui-file-dialog = { path = "../../"} diff --git a/examples/sandbox/src/main.rs b/examples/sandbox/src/main.rs index d0a00854..53e7d961 100644 --- a/examples/sandbox/src/main.rs +++ b/examples/sandbox/src/main.rs @@ -14,7 +14,7 @@ struct MyApp { impl MyApp { pub fn new(_cc: &eframe::CreationContext) -> Self { Self { - file_dialog: FileDialog::new(), + file_dialog: FileDialog::new().id("egui_file_dialog"), selected_directory: None, selected_file: None, diff --git a/examples/save_file/Cargo.toml b/examples/save_file/Cargo.toml index 85a7c9c1..2e44cd82 100644 --- a/examples/save_file/Cargo.toml +++ b/examples/save_file/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.25.0", default-features = false, features = ["glow"] } -egui-file-dialog = { version = "0.1.0", path = "../../"} +eframe = { version = "0.26.0", default-features = false, features = ["glow"] } +egui-file-dialog = { path = "../../"} diff --git a/examples/select_directory/Cargo.toml b/examples/select_directory/Cargo.toml index fd7097ce..66a6f762 100644 --- a/examples/select_directory/Cargo.toml +++ b/examples/select_directory/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.25.0", default-features = false, features = ["glow"] } -egui-file-dialog = { version = "0.1.0", path = "../../"} +eframe = { version = "0.26.0", default-features = false, features = ["glow"] } +egui-file-dialog = { path = "../../"} diff --git a/examples/select_file/Cargo.toml b/examples/select_file/Cargo.toml index 3ba49999..9636f081 100644 --- a/examples/select_file/Cargo.toml +++ b/examples/select_file/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.25.0", default-features = false, features = ["glow"] } -egui-file-dialog = { version = "0.1.0", path = "../../"} +eframe = { version = "0.26.0", default-features = false, features = ["glow"] } +egui-file-dialog = { path = "../../"} diff --git a/src/create_directory_dialog.rs b/src/create_directory_dialog.rs index 589e3aa7..7027c588 100644 --- a/src/create_directory_dialog.rs +++ b/src/create_directory_dialog.rs @@ -1,8 +1,6 @@ use std::fs; use std::path::{Path, PathBuf}; -use crate::ui; - pub struct CreateDirectoryResponse { /// Contains the path to the directory that was created. directory: Option, @@ -42,6 +40,8 @@ pub struct CreateDirectoryDialog { input: String, /// This contains the error message if the folder name is invalid error: Option, + /// If we should scroll to the error in the next frame + scroll_to_error: bool, } impl CreateDirectoryDialog { @@ -54,6 +54,7 @@ impl CreateDirectoryDialog { input: String::new(), error: None, + scroll_to_error: false, } } @@ -97,7 +98,10 @@ impl CreateDirectoryDialog { self.error = self.validate_input(); } - if ui::button_enabled_disabled(ui, "✔", self.error.is_none()) { + if ui + .add_enabled(self.error.is_none(), egui::Button::new("✔")) + .clicked() + { result = self.create_directory(); } @@ -108,7 +112,12 @@ impl CreateDirectoryDialog { if let Some(err) = &self.error { ui.add_space(5.0); - ui.label(err); + let response = ui.label(err); + + if self.scroll_to_error { + response.scroll_to_me(Some(egui::Align::Center)); + self.scroll_to_error = false; + } } result @@ -132,7 +141,7 @@ impl CreateDirectoryDialog { return CreateDirectoryResponse::new(dir.as_path()); } Err(err) => { - self.error = Some(format!("Error: {}", err)); + self.error = self.create_error(format!("Error: {}", err).as_str()); return CreateDirectoryResponse::new_empty(); } } @@ -141,7 +150,7 @@ impl CreateDirectoryDialog { // This error should not occur because the create_directory function is only // called when the dialog is open and the directory is set. // If this error occurs, there is most likely a bug in the code. - self.error = Some("No directory given".to_string()); + self.error = self.create_error("No directory given"); CreateDirectoryResponse::new_empty() } @@ -150,28 +159,34 @@ impl CreateDirectoryDialog { /// Returns None if the name is valid. Otherwise returns the error message. fn validate_input(&mut self) -> Option { if self.input.is_empty() { - return Some("Name of the folder can not be empty".to_string()); + return self.create_error("Name of the folder can not be empty"); } if let Some(mut x) = self.directory.clone() { x.push(self.input.as_str()); if x.is_dir() { - return Some("A directory with the name already exists".to_string()); + return self.create_error("A directory with the name already exists"); } if x.is_file() { - return Some("A file with the name already exists".to_string()); + return self.create_error("A file with the name already exists"); } } else { // This error should not occur because the validate_input function is only // called when the dialog is open and the directory is set. // If this error occurs, there is most likely a bug in the code. - return Some("No directory given".to_string()); + return self.create_error("No directory given"); } None } + /// Creates the specified error and sets to scroll to the error in the next frame. + fn create_error(&mut self, error: &str) -> Option { + self.scroll_to_error = true; + Some(error.to_string()) + } + /// Resets the dialog. /// Configuration variables are not changed. fn reset(&mut self) { @@ -179,5 +194,7 @@ impl CreateDirectoryDialog { self.init = false; self.directory = None; self.input.clear(); + self.error = None; + self.scroll_to_error = false; } } diff --git a/src/file_dialog.rs b/src/file_dialog.rs index ee26906c..89105a88 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -6,7 +6,6 @@ use std::{fs, io}; use crate::create_directory_dialog::CreateDirectoryDialog; use crate::data::{DirectoryContent, DirectoryEntry, Disks, UserDirectories}; -use crate::ui; /// Represents the mode the file dialog is currently in. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -72,6 +71,12 @@ pub struct FileDialog { /// If files are displayed in addition to directories. /// This option will be ignored when mode == DialogMode::SelectFile. show_files: bool, + /// This is an optional ID that can be set when opening the dialog to determine which + /// operation the dialog is used for. This is useful if the dialog is used multiple times + /// for different actions in the same view. The ID then makes it possible to distinguish + /// for which action the user has selected an item. + /// This ID is not used internally. + operation_id: Option, /// The user directories like Home or Documents. /// These are loaded once when the dialog is created or when the refresh() method is called. @@ -97,8 +102,29 @@ pub struct FileDialog { /// The currently used window title. /// This changes depending on the mode the dialog is in. window_title: String, + /// If set, the window title will be overwritten and set to the fixed value instead + /// of being set dynamically. + window_overwrite_title: Option, + /// The ID of the window. + window_id: Option, + /// The default position of the window. + window_default_pos: Option, + /// Sets the window position and prevents it from being dragged around. + window_fixed_pos: Option, /// The default size of the window. - default_window_size: egui::Vec2, + window_default_size: egui::Vec2, + /// The maximum size of the window. + window_max_size: Option, + /// The minimum size of the window. + window_min_size: egui::Vec2, + /// The anchor of the window. + window_anchor: Option<(egui::Align2, egui::Vec2)>, + /// If the window is resizable + window_resizable: bool, + /// If the window is movable + window_movable: bool, + /// If the title bar of the window is shown + window_title_bar: bool, /// The dialog that is shown when the user wants to create a new directory. create_directory_dialog: CreateDirectoryDialog, @@ -135,6 +161,7 @@ impl FileDialog { state: DialogState::Closed, initial_directory: std::env::current_dir().unwrap_or_default(), show_files: true, + operation_id: None, user_directories: UserDirectories::new(), system_disks: Disks::new_with_refreshed_list(), @@ -145,7 +172,17 @@ impl FileDialog { directory_error: None, window_title: String::from("Select directory"), - default_window_size: egui::Vec2::new(650.0, 370.0), + window_overwrite_title: None, + window_id: None, + window_default_pos: None, + 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_anchor: None, + window_resizable: true, + window_movable: true, + window_title_bar: true, create_directory_dialog: CreateDirectoryDialog::new(), @@ -171,9 +208,76 @@ impl FileDialog { 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_window_size(mut self, size: egui::Vec2) -> Self { - self.default_window_size = size; + 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 } @@ -189,8 +293,64 @@ impl FileDialog { /// /// Returns the result of the operation to load the initial directory. /// - /// The `show_files` parameter will be ignored when the mode equals `DialogMode::SelectFile`. - pub fn open(&mut self, mode: DialogMode, mut show_files: bool) -> io::Result<()> { + /// If you don't need to set the individual parameters, you can also use the shortcut + /// methods `select_directory`, `select_file` and `save_file`. + /// + /// # Arguments + /// + /// * `mode` - The mode in which the dialog should be opened + /// * `show_files` - If files should also be displayed to the user in addition to directories. + /// This is ignored if the mode is `DialogMode::SelectFile`. + /// * `operation_id` - Sets an ID for which operation the dialog was opened. + /// This is useful when the dialog can be used for various operations in a single view. + /// The ID can then be used to check which action the user selected an item for. + /// + /// # Examples + /// + /// The following example shows how the dialog can be used for multiple + /// actions using the `operation_id``. + /// + /// ``` + /// use std::path::PathBuf; + /// + /// use egui_file_dialog::{DialogMode, FileDialog}; + /// + /// struct MyApp { + /// file_dialog: FileDialog, + /// + /// selected_file_a: Option, + /// selected_file_b: Option, + /// } + /// + /// impl MyApp { + /// fn update(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + /// if ui.button("Select file a").clicked() { + /// let _ = self.file_dialog.open(DialogMode::SelectFile, true, Some("select_a")); + /// } + /// + /// if ui.button("Select file b").clicked() { + /// let _ = self.file_dialog.open(DialogMode::SelectFile, true, Some("select_b")); + /// } + /// + /// self.file_dialog.update(ctx); + /// + /// if let Some(path) = self.file_dialog.selected() { + /// if self.file_dialog.operation_id() == Some("select_a") { + /// self.selected_file_a = Some(path.to_path_buf()); + /// } + /// if self.file_dialog.operation_id() == Some("select_b") { + /// self.selected_file_b = Some(path.to_path_buf()); + /// } + /// } + /// } + /// } + /// ``` + pub fn open( + &mut self, + mode: DialogMode, + mut show_files: bool, + operation_id: Option<&str>, + ) -> io::Result<()> { self.reset(); // Try to use the parent directory if the initial directory is a file. @@ -214,12 +374,17 @@ impl FileDialog { self.mode = mode; self.state = DialogState::Open; self.show_files = show_files; + self.operation_id = operation_id.map(String::from); - self.window_title = match mode { - DialogMode::SelectDirectory => "📁 Select Folder".to_string(), - DialogMode::SelectFile => "📂 Open File".to_string(), - DialogMode::SaveFile => "📥 Save File".to_string(), - }; + if let Some(title) = &self.window_overwrite_title { + self.window_title = title.clone(); + } else { + self.window_title = match mode { + DialogMode::SelectDirectory => "📁 Select Folder".to_string(), + DialogMode::SelectFile => "📂 Open File".to_string(), + DialogMode::SaveFile => "📥 Save File".to_string(), + }; + } self.load_directory(&self.initial_directory.clone()) } @@ -232,7 +397,7 @@ impl FileDialog { /// /// The function ignores the result of the initial directory loading operation. pub fn select_directory(&mut self) { - let _ = self.open(DialogMode::SelectDirectory, false); + let _ = self.open(DialogMode::SelectDirectory, false, None); } /// Shortcut function to open the file dialog to prompt the user to select a file. @@ -241,7 +406,7 @@ impl FileDialog { /// /// The function ignores the result of the initial directory loading operation. pub fn select_file(&mut self) { - let _ = self.open(DialogMode::SelectFile, false); + let _ = self.open(DialogMode::SelectFile, false, None); } /// Shortcut function to open the file dialog to prompt the user to save a file. @@ -250,7 +415,7 @@ impl FileDialog { /// /// The function ignores the result of the initial directory loading operation. pub fn save_file(&mut self) { - let _ = self.open(DialogMode::SaveFile, true); + let _ = self.open(DialogMode::SaveFile, true, None); } /// Returns the mode the dialog is currently in. @@ -274,6 +439,13 @@ impl FileDialog { } } + /// Returns the ID of the operation for which the dialog is currently being used. + /// + /// See `FileDialog::open` more information. + pub fn operation_id(&self) -> Option<&str> { + self.operation_id.as_deref() + } + /// The main update method that should be called every frame if the dialog is to be visible. /// /// This function has no effect if the dialog state is currently not `DialogState::Open`. @@ -284,37 +456,31 @@ impl FileDialog { let mut is_open = true; - egui::Window::new(&self.window_title) - .open(&mut is_open) - .default_size(self.default_window_size) - .min_width(335.0) - .min_height(200.0) - .collapsible(false) - .show(ctx, |ui| { - egui::TopBottomPanel::top("fe_top_panel") - .resizable(false) - .show_inside(ui, |ui| { - self.ui_update_top_panel(ctx, ui); - }); - - egui::SidePanel::left("fe_left_panel") - .resizable(true) - .default_width(150.0) - .width_range(90.0..=250.0) - .show_inside(ui, |ui| { - self.ui_update_left_panel(ctx, ui); - }); + self.create_window(&mut is_open).show(ctx, |ui| { + egui::TopBottomPanel::top("fe_top_panel") + .resizable(false) + .show_inside(ui, |ui| { + self.ui_update_top_panel(ctx, ui); + }); - egui::TopBottomPanel::bottom("fe_bottom_panel") - .resizable(false) - .show_inside(ui, |ui| { - self.ui_update_bottom_panel(ctx, ui); - }); + egui::SidePanel::left("fe_left_panel") + .resizable(true) + .default_width(150.0) + .width_range(90.0..=250.0) + .show_inside(ui, |ui| { + self.ui_update_left_panel(ctx, ui); + }); - egui::CentralPanel::default().show_inside(ui, |ui| { - self.ui_update_central_panel(ui); + egui::TopBottomPanel::bottom("fe_bottom_panel") + .resizable(false) + .show_inside(ui, |ui| { + self.ui_update_bottom_panel(ctx, ui); }); + + egui::CentralPanel::default().show_inside(ui, |ui| { + self.ui_update_central_panel(ui); }); + }); // User closed the window without finishing the dialog if !is_open { @@ -324,6 +490,40 @@ impl FileDialog { self } + /// 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) + .open(is_open) + .default_size(self.window_default_size) + .min_size(self.window_min_size) + .resizable(self.window_resizable) + .movable(self.window_movable) + .title_bar(self.window_title_bar) + .collapsible(false); + + if let Some(id) = self.window_id { + window = window.id(id); + } + + if let Some(pos) = self.window_default_pos { + window = window.default_pos(pos); + } + + if let Some(pos) = self.window_fixed_pos { + window = window.fixed_pos(pos); + } + + if let Some((anchor, offset)) = self.window_anchor { + window = window.anchor(anchor, offset); + } + + if let Some(size) = self.window_max_size { + window = window.max_size(size); + } + + window + } + /// Updates the top panel of the dialog. Including the navigation buttons, /// the current path display, the reload button and the search field. fn ui_update_top_panel(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { @@ -332,37 +532,31 @@ impl FileDialog { ui.horizontal(|ui| { // Navigation buttons if let Some(x) = self.current_directory() { - if ui::button_sized_enabled_disabled(ui, NAV_BUTTON_SIZE, "⏶", x.parent().is_some()) - { + if self.ui_button_sized(ui, x.parent().is_some(), NAV_BUTTON_SIZE, "⏶") { let _ = self.load_parent_directory(); } } else { - let _ = ui::button_sized_enabled_disabled(ui, NAV_BUTTON_SIZE, "⏶", false); + let _ = self.ui_button_sized(ui, false, NAV_BUTTON_SIZE, "⏶"); } - if ui::button_sized_enabled_disabled( + if self.ui_button_sized( ui, + self.directory_offset + 1 < self.directory_stack.len(), NAV_BUTTON_SIZE, "⏴", - self.directory_offset + 1 < self.directory_stack.len(), ) { let _ = self.load_previous_directory(); } - if ui::button_sized_enabled_disabled( - ui, - NAV_BUTTON_SIZE, - "⏵", - self.directory_offset != 0, - ) { + if self.ui_button_sized(ui, self.directory_offset != 0, NAV_BUTTON_SIZE, "⏵") { let _ = self.load_next_directory(); } - if ui::button_sized_enabled_disabled( + if self.ui_button_sized( ui, + !self.create_directory_dialog.is_open(), NAV_BUTTON_SIZE, "+", - !self.create_directory_dialog.is_open(), ) { if let Some(x) = self.current_directory() { self.create_directory_dialog.open(x.to_path_buf()); @@ -535,8 +729,7 @@ impl FileDialog { DialogMode::SaveFile => "📥 Save", }; - if ui::button_sized_enabled_disabled(ui, BUTTON_SIZE, label, self.is_selection_valid()) - { + if self.ui_button_sized(ui, self.is_selection_valid(), BUTTON_SIZE, label) { match &self.mode { DialogMode::SelectDirectory | DialogMode::SelectFile => { // self.selected_item should always contain a value, @@ -743,11 +936,29 @@ impl FileDialog { self.system_disks = disks; } + /// Helper function to add a sized button that can be enabled or disabled + fn ui_button_sized( + &self, + ui: &mut egui::Ui, + enabled: bool, + size: egui::Vec2, + label: &str, + ) -> bool { + let mut clicked = false; + + ui.add_enabled_ui(enabled, |ui| { + clicked = ui.add_sized(size, egui::Button::new(label)).clicked(); + }); + + clicked + } + /// Resets the dialog to use default values. /// Configuration variables such as `initial_directory` are retained. fn reset(&mut self) { self.state = DialogState::Closed; self.show_files = true; + self.operation_id = None; self.system_disks = Disks::new_with_refreshed_list(); @@ -914,10 +1125,18 @@ impl FileDialog { /// The function deletes all directories from the `directory_stack` that are currently /// stored in the vector before the `directory_offset`. fn load_directory(&mut self, path: &Path) -> io::Result<()> { + let full_path = match fs::canonicalize(path) { + Ok(path) => path, + Err(err) => { + self.directory_error = Some(err.to_string()); + return Err(err); + } + }; + // Do not load the same directory again. // Use reload_directory if the content of the directory should be updated. if let Some(x) = self.current_directory() { - if x == path { + if x == full_path { return Ok(()); } } @@ -927,14 +1146,6 @@ impl FileDialog { .drain(self.directory_stack.len() - self.directory_offset..); } - let full_path = match fs::canonicalize(path) { - Ok(path) => path, - Err(err) => { - self.directory_error = Some(err.to_string()); - return Err(err); - } - }; - self.directory_stack.push(full_path); self.directory_offset = 0; diff --git a/src/lib.rs b/src/lib.rs index ed303999..a9d556d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,4 +58,3 @@ pub use file_dialog::{DialogMode, DialogState, FileDialog}; mod create_directory_dialog; mod data; -mod ui; diff --git a/src/ui.rs b/src/ui.rs deleted file mode 100644 index 173533ab..00000000 --- a/src/ui.rs +++ /dev/null @@ -1,43 +0,0 @@ -/// Adds a dynamically sized button to the UI that can be enabled or disabled. -/// Returns true if the button is clicked. Otherwise None is returned. -pub fn button_enabled_disabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> bool { - if !enabled { - let button = egui::Button::new(text) - .stroke(egui::Stroke::NONE) - .fill(get_disabled_fill_color(ui)); - - let _ = ui.add(button); - - return false; - } - - ui.add(egui::Button::new(text)).clicked() -} - -/// Adds a fixed sized button to the UI that can be enabled or disabled. -/// Returns true if the button is clicked. Otherwise None is returned. -pub fn button_sized_enabled_disabled( - ui: &mut egui::Ui, - size: egui::Vec2, - text: &str, - enabled: bool, -) -> bool { - if !enabled { - let button = egui::Button::new(text) - .stroke(egui::Stroke::NONE) - .fill(get_disabled_fill_color(ui)); - - let _ = ui.add_sized(size, button); - - return false; - } - - ui.add_sized(size, egui::Button::new(text)).clicked() -} - -/// Returns the fill color of disabled buttons -#[inline] -fn get_disabled_fill_color(ui: &egui::Ui) -> egui::Color32 { - let c = ui.style().visuals.widgets.noninteractive.bg_fill; - egui::Color32::from_rgba_premultiplied(c.r(), c.g(), c.b(), 100) -}