diff --git a/examples/custom-right-panel/Cargo.toml b/examples/custom-right-panel/Cargo.toml new file mode 100644 index 00000000..c932a8a6 --- /dev/null +++ b/examples/custom-right-panel/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "custom-right-panel" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eframe = { workspace = true } +egui-file-dialog = { path = "../../"} diff --git a/examples/custom-right-panel/src/main.rs b/examples/custom-right-panel/src/main.rs new file mode 100644 index 00000000..6293f947 --- /dev/null +++ b/examples/custom-right-panel/src/main.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +use eframe::egui; +use egui_file_dialog::{DialogMode, FileDialog}; + +struct MyApp { + file_dialog: FileDialog, + selected_items: Option>, +} + +impl MyApp { + pub fn new(_cc: &eframe::CreationContext) -> Self { + Self { + file_dialog: FileDialog::new(), + selected_items: 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 single").clicked() { + self.file_dialog.select_file(); + } + if ui.button("Select multiple").clicked() { + self.file_dialog.select_multiple(); + } + + ui.label("Selected items:"); + + if let Some(items) = &self.selected_items { + for item in items { + ui.label(format!("{:?}", item)); + } + } else { + ui.label("None"); + } + + self.file_dialog + .update_with_right_panel_ui(ctx, &mut |ui, dia| match dia.mode() { + DialogMode::SelectMultiple => { + ui.heading("Selected items"); + ui.separator(); + egui::ScrollArea::vertical() + .max_height(ui.available_height()) + .show(ui, |ui| { + for item in dia.active_selected_entries() { + ui.small(format!("{item:#?}")); + ui.separator(); + } + }); + } + _ => { + ui.heading("Active item"); + ui.small(format!("{:#?}", dia.active_entry())); + } + }); + + match self.file_dialog.mode() { + DialogMode::SelectMultiple => { + if let Some(items) = self.file_dialog.take_selected_multiple() { + self.selected_items = Some(items); + } + } + _ => { + if let Some(item) = self.file_dialog.take_selected() { + self.selected_items = Some(vec![item]); + } + } + } + }); + } +} + +fn main() -> eframe::Result<()> { + eframe::run_native( + "File dialog example", + eframe::NativeOptions::default(), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), + ) +} diff --git a/src/file_dialog.rs b/src/file_dialog.rs index 9604f002..71a0e8fa 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -180,6 +180,12 @@ impl Debug for dyn FileDialogModal + Send + Sync { } } +/// Callback type to inject a custom egui ui inside the file dialog's ui. +/// +/// Also gives access to the file dialog, since it would otherwise be inaccessible +/// inside the closure. +type FileDialogUiCallback<'a> = dyn FnMut(&mut egui::Ui, &mut FileDialog) + 'a; + impl FileDialog { // ------------------------------------------------------------------------ // Creation: @@ -383,7 +389,33 @@ impl FileDialog { } self.update_keybindings(ctx); - self.update_ui(ctx); + self.update_ui(ctx, None); + + self + } + + /// Do an [update](`Self::update`) with a custom right panel ui. + /// + /// Example use cases: + /// - Show custom information for a file (size, MIME type, etc.) + /// - Embed a preview, like a thumbnail for an image + /// - Add controls for custom open options, like open as read-only, etc. + /// + /// See [`active_entry`](Self::active_entry) to get the active directory entry + /// to show the information for. + /// + /// This function has no effect if the dialog state is currently not `DialogState::Open`. + pub fn update_with_right_panel_ui( + &mut self, + ctx: &egui::Context, + f: &mut FileDialogUiCallback, + ) -> &Self { + if self.state != DialogState::Open { + return self; + } + + self.update_keybindings(ctx); + self.update_ui(ctx, Some(f)); self } @@ -964,6 +996,26 @@ impl FileDialog { } } + /// Returns the currently active directory entry. + /// + /// This is either the currently highlighted entry, or the currently active directory + /// if nothing is being highlighted. + /// + /// For the [`DialogMode::SelectMultiple`] counterpart, + /// see [`FileDialog::active_selected_entries`]. + pub const fn active_entry(&self) -> Option<&DirectoryEntry> { + self.selected_item.as_ref() + } + + /// Returns an iterator over the currently selected entries in [`SelectMultiple`] mode. + /// + /// For the counterpart in single selection modes, see [`FileDialog::active_entry`]. + /// + /// [`SelectMultiple`]: DialogMode::SelectMultiple + pub fn active_selected_entries(&self) -> impl Iterator { + self.get_dir_content_filtered_iter().filter(|p| p.selected) + } + /// Returns the ID of the operation for which the dialog is currently being used. /// /// See `FileDialog::open` for more information. @@ -985,7 +1037,13 @@ impl FileDialog { /// UI methods impl FileDialog { /// Main update method of the UI - fn update_ui(&mut self, ctx: &egui::Context) { + /// + /// Takes an optional callback to show a custom right panel. + fn update_ui( + &mut self, + ctx: &egui::Context, + right_panel_fn: Option<&mut FileDialogUiCallback>, + ) { let mut is_open = true; if self.config.as_modal { @@ -1017,6 +1075,17 @@ impl FileDialog { }); } + // Optionally, show a custom right panel (see `update_with_custom_right_panel`) + if let Some(f) = right_panel_fn { + egui::SidePanel::right(self.window_id.with("right_panel")) + // Unlike the left panel, we have no control over the contents, so + // we don't restrict the width. It's up to the user to make the UI presentable. + .resizable(true) + .show_inside(ui, |ui| { + f(ui, self); + }); + } + egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel")) .resizable(false) .show_inside(ui, |ui| { @@ -2480,8 +2549,7 @@ impl FileDialog { } DialogMode::SelectMultiple => { let result: Vec = self - .get_dir_content_filtered_iter() - .filter(|p| p.selected) + .active_selected_entries() .map(crate::DirectoryEntry::to_path_buf) .collect();