Skip to content

Commit 300cb6f

Browse files
hacknusbircnijannistpl
authored
feat: implement information view and preview for files (#184)
* initial implementation of file information display * revert previous prototype and create information_panel.rs * add meta data grid * update meta_data * revert public config and selected item * only read 1000 characters of text files and implement Markdown as an example * add separators * introduce info_panel feature * upload test_image * implement metadata, markdown and image preview * metadata in ScrollArea and only display size for files, not directories * clean up, add doc and add folder icon as preview * removed profiles, not used anymore * image crate needs to be loaded always, because of the Image metadata loaded in directory_content.rs * only load image crate when using info_panel feature * only load text content once. show icon if no preview is available * extend preview for jpeg and jpg * extension to lowercase, so it does not matter how they are stored on the file system. added color space * rapidly decreased loading times of directories using image-meta crate instead of image crate * indexmap instead of hashmap, so that metadata is always in the same order * cargo clippy * remove unused doc * Update Cargo.toml Co-authored-by: Nicolas <bircni@icloud.com> * workflow fix * reformat * text content is now only loaded for the selected file and then stored in the ActiveDirectory * not hard-coding max chars of text preview * fix width and compilation errors * implement custom metadata loader * remove unused dependency * code review by bircni * required for image loader * add for cargo machete. image crate is required per documentation of egui loaders * remove md render * use picked instead of selected * rename example * rename example * create metadata struct * rustfmt * implement content_mut * fix typo * implement `InfoPanelEntry` struct * change feature name to information_view * move format_pixels() to information_panel.rs * add screenshot and remove test_image * remove print * change date formatting * Update examples/select_file_with_information_view/README.md Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * Update src/data/information_panel.rs Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * Update src/data/information_panel.rs Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * add docs * introduced path_buf variable * code review * rename handler * set image max width to be as high as the panel is wide * show icon for all files * don't set width of meta grid * adapt for latest example structure * Revert "don't set width of meta grid" This reverts commit 0c4eb84. * add comment for image crate * clippy * remove images if more than 10 are loaded * move information_panel.rs * rustfmt * better doc for example * update README.md * implement `forget_all_stored_images` * implement `forget_all_stored_images` in `MyApp::update()` * update Cargo.toml * Update examples/README.md Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * pick file rename * remove hardcoded icon width * id with `FileDialog` as parent * fix id * windows should always take up the same amount of space * Update src/information_panel.rs Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * Update src/information_panel.rs Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * extract image display function * rustfmt * all previews should have the same size * Update examples/pick_file_with_information_view.rs Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * Update Cargo.toml Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com> * fix typo --------- Co-authored-by: Nicolas <bircni@icloud.com> Co-authored-by: Jannis <55352293+fluxxcode@users.noreply.github.com>
1 parent 042efb2 commit 300cb6f

File tree

10 files changed

+630
-11
lines changed

10 files changed

+630
-11
lines changed

Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,28 @@ dunce = "1.0.5"
2121
sysinfo = { version = "0.32", default-features = false, features = ["disk"] }
2222
# persistent storage
2323
serde = { version = "1", features = ["derive"], optional = true }
24+
# meta-data storage
25+
indexmap = { version = "2.6.0", features = ["serde"], optional = true }
26+
27+
# info panel meta-data display
28+
image-meta = { version = "0.1.2", optional = true }
29+
chrono = { version = "0.4.38", optional = true }
2430

2531
[dev-dependencies]
2632
eframe = { version = "0.29.1", default-features = false, features = [
2733
"glow",
2834
"persistence",
2935
] }
30-
egui-file-dialog = { path = "." }
36+
egui-file-dialog = { path = "." , features = ["information_view"] }
37+
egui_extras = { version = "0.29", features = ["all_loaders"] }
38+
# required by the egui loaders
39+
image = { version = "0.25.5", features = ["bmp", "jpeg", "gif", "png", "tiff", "rayon"] }
3140

3241
[features]
3342
default = ["serde", "default_fonts"]
3443
serde = ["dep:serde"]
3544
default_fonts = ["egui/default_fonts"]
45+
information_view = ["dep:chrono", "image-meta", "indexmap"]
3646

3747
[lints.rust]
3848
unsafe_code = "warn"
@@ -54,3 +64,4 @@ struct_field_names = { level = "allow", priority = 13 }
5464
missing_fields_in_debug = { level = "allow", priority = 14 }
5565
missing_errors_doc = { level = "allow", priority = 15 }
5666
module_name_repetitions = { level = "allow", priority = 16 }
67+
cast_precision_loss = { level = "allow", priority = 17 }

examples/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,25 @@ cargo run --example save_file
7676
```
7777

7878
![Screenshot](../media/examples/save_file.png)
79+
80+
81+
## Pick File with Information View
82+
83+
Example showing how to pick a file and display file information using the `InformationView`.
84+
85+
Requires the feature `information_view` as well as these dependencies:
86+
87+
```toml
88+
[dependencies]
89+
egui-file-dialog = { version = "*", features = ["information_view"] }
90+
egui_extras = { version = "0.29", features = ["all_loaders"] }
91+
# required by the egui loaders
92+
image = { version = "0.25.5", features = ["bmp", "jpeg", "gif", "png", "tiff", "rayon"] }
93+
```
94+
95+
```shell
96+
cargo run --example pick_file_with_information_view
97+
```
98+
99+
![Screenshot](../media/examples/information_view.png)
100+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::path::PathBuf;
2+
3+
use eframe::egui;
4+
use egui_file_dialog::information_panel::InformationPanel;
5+
use egui_file_dialog::{DialogState, FileDialog};
6+
7+
struct MyApp {
8+
file_dialog: FileDialog,
9+
information_panel: InformationPanel,
10+
selected_file: Option<PathBuf>,
11+
}
12+
13+
impl MyApp {
14+
pub fn new(_cc: &eframe::CreationContext) -> Self {
15+
Self {
16+
file_dialog: FileDialog::new(),
17+
information_panel: InformationPanel::default()
18+
.add_file_preview("csv", |ui, item| {
19+
ui.label("CSV preview:");
20+
if let Some(mut content) = item.content() {
21+
egui::ScrollArea::vertical()
22+
.max_height(ui.available_height())
23+
.show(ui, |ui| {
24+
ui.add(egui::TextEdit::multiline(&mut content).code_editor());
25+
});
26+
}
27+
})
28+
// add additional metadata loader
29+
.add_metadata_loader("pdf", |other_meta_data, path| {
30+
// as a simple example, just show the Filename of the PDF
31+
other_meta_data.insert("PDF Filename".to_string(), format!("{path:?}"));
32+
}),
33+
selected_file: None,
34+
}
35+
}
36+
}
37+
38+
impl eframe::App for MyApp {
39+
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
40+
egui::CentralPanel::default().show(ctx, |ui| {
41+
if ui.button("Select file").clicked() {
42+
self.file_dialog.pick_file();
43+
}
44+
45+
self.file_dialog.set_right_panel_width(200.0);
46+
47+
if let Some(path) = self
48+
.file_dialog
49+
.update_with_right_panel_ui(ctx, &mut |ui, dia| {
50+
self.information_panel.ui(ui, dia);
51+
})
52+
.picked()
53+
{
54+
self.selected_file = Some(path.to_path_buf());
55+
}
56+
57+
match self.file_dialog.state() {
58+
DialogState::Closed | DialogState::Cancelled => {
59+
self.information_panel.forget_all_stored_images(ui);
60+
}
61+
_ => {}
62+
}
63+
64+
ui.label(format!("Selected file: {:?}", self.selected_file));
65+
});
66+
}
67+
}
68+
69+
fn main() -> eframe::Result<()> {
70+
eframe::run_native(
71+
"File dialog example",
72+
eframe::NativeOptions::default(),
73+
Box::new(|ctx| {
74+
egui_extras::install_image_loaders(&ctx.egui_ctx);
75+
Ok(Box::new(MyApp::new(ctx)))
76+
}),
77+
)
78+
}
1.45 MB
Loading

src/config/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ pub struct FileDialogConfig {
188188
/// If the search input in the top panel should be visible.
189189
pub show_search: bool,
190190

191+
/// Set the width of the right panel, if used
192+
pub right_panel_width: Option<f32>,
193+
191194
/// If the sidebar with the shortcut directories such as
192195
/// “Home”, “Documents” etc. should be visible.
193196
pub show_left_panel: bool,
@@ -260,6 +263,7 @@ impl Default for FileDialogConfig {
260263
show_system_files_option: true,
261264
show_search: true,
262265

266+
right_panel_width: None,
263267
show_left_panel: true,
264268
show_pinned_folders: true,
265269
show_places: true,

src/data/directory_content.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1+
use crate::config::{FileDialogConfig, FileFilter};
2+
use egui::mutex::Mutex;
13
use std::path::{Path, PathBuf};
24
use std::sync::{mpsc, Arc};
35
use std::time::SystemTime;
46
use std::{fs, io, thread};
57

6-
use egui::mutex::Mutex;
7-
8-
use crate::config::{FileDialogConfig, FileFilter};
9-
108
/// Contains the metadata of a directory item.
9+
#[derive(Debug, Default, Clone)]
10+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
11+
pub struct Metadata {
12+
pub size: Option<u64>,
13+
pub last_modified: Option<SystemTime>,
14+
pub created: Option<SystemTime>,
15+
pub file_type: Option<String>,
16+
}
17+
18+
/// Contains the information of a directory item.
1119
///
12-
/// This struct is mainly there so that the metadata can be loaded once and not that
20+
/// This struct is mainly there so that the information and metadata can be loaded once and not that
1321
/// a request has to be sent to the OS every frame using, for example, `path.is_file()`.
1422
#[derive(Debug, Default, Clone)]
1523
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1624
pub struct DirectoryEntry {
1725
path: PathBuf,
26+
metadata: Metadata,
1827
is_directory: bool,
1928
is_system_file: bool,
2029
icon: String,
@@ -25,15 +34,30 @@ pub struct DirectoryEntry {
2534
impl DirectoryEntry {
2635
/// Creates a new directory entry from a path
2736
pub fn from_path(config: &FileDialogConfig, path: &Path) -> Self {
37+
let mut metadata = Metadata::default();
38+
39+
if let Ok(md) = fs::metadata(path) {
40+
metadata.size = Some(md.len());
41+
metadata.last_modified = md.modified().ok();
42+
metadata.created = md.created().ok();
43+
metadata.file_type = Some(format!("{:?}", md.file_type()));
44+
}
45+
2846
Self {
2947
path: path.to_path_buf(),
48+
metadata,
3049
is_directory: path.is_dir(),
3150
is_system_file: !path.is_dir() && !path.is_file(),
3251
icon: gen_path_icon(config, path),
3352
selected: false,
3453
}
3554
}
3655

56+
/// Returns the metadata of the directory entry.
57+
pub const fn metadata(&self) -> &Metadata {
58+
&self.metadata
59+
}
60+
3761
/// Checks if the path of the current directory entry matches the other directory entry.
3862
pub fn path_eq(&self, other: &Self) -> bool {
3963
other.as_path() == self.as_path()

src/data/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ mod disks;
55
pub use disks::{Disk, Disks};
66

77
mod user_directories;
8+
89
pub use user_directories::UserDirectories;

src/file_dialog.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ impl FileDialog {
442442
self
443443
}
444444

445+
/// Sets the width of the right panel.
446+
pub fn set_right_panel_width(&mut self, width: f32) {
447+
self.config.right_panel_width = Some(width);
448+
}
449+
450+
/// Clears the width of the right panel by setting it to None.
451+
pub fn clear_right_panel_width(&mut self) {
452+
self.config.right_panel_width = None;
453+
}
454+
445455
/// Do an [update](`Self::update`) with a custom right panel ui.
446456
///
447457
/// Example use cases:
@@ -1168,6 +1178,11 @@ impl FileDialog {
11681178
pub fn state(&self) -> DialogState {
11691179
self.state.clone()
11701180
}
1181+
1182+
/// Get the window Id
1183+
pub const fn get_window_id(&self) -> egui::Id {
1184+
self.window_id
1185+
}
11711186
}
11721187

11731188
/// UI methods
@@ -1213,13 +1228,16 @@ impl FileDialog {
12131228

12141229
// Optionally, show a custom right panel (see `update_with_custom_right_panel`)
12151230
if let Some(f) = right_panel_fn {
1216-
egui::SidePanel::right(self.window_id.with("right_panel"))
1231+
let mut right_panel = egui::SidePanel::right(self.window_id.with("right_panel"))
12171232
// Unlike the left panel, we have no control over the contents, so
12181233
// we don't restrict the width. It's up to the user to make the UI presentable.
1219-
.resizable(true)
1220-
.show_inside(ui, |ui| {
1221-
f(ui, self);
1222-
});
1234+
.resizable(true);
1235+
if let Some(width) = self.config.right_panel_width {
1236+
right_panel = right_panel.default_width(width);
1237+
}
1238+
right_panel.show_inside(ui, |ui| {
1239+
f(ui, self);
1240+
});
12231241
}
12241242

12251243
egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel"))

0 commit comments

Comments
 (0)