Skip to content

Commit

Permalink
File picker
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Oct 3, 2024
1 parent 2cec30d commit a797230
Show file tree
Hide file tree
Showing 11 changed files with 1,084 additions and 17 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
widget.
- A rare deadlock occurring when multiple threads were racing to execute
`Dynamic<T>` change callbacks has been fixed.
- `Stack` no longer unwraps a `Resize` child if the resize widget is resizing in
the direction opposite of the Stack's orientation.

### Added

Expand Down Expand Up @@ -187,6 +189,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `impl FnMut(Duration) -> ControlFlow<Duration> + Send + Sync + 'static`
- `SharedCallback<Duration, ControlFlow<Duration>>`
- `SharedCallback`
- `Cushy::multi_click_threshold`/`Cushy::set_multi_click_threshold` provide
access to the setting used by Cushy widgets to detect whether two clicks are
related.
- `ClickCounter` is a new helper that simplifies handling actions based on how
many sequential clicks were observed.
- `Dimension::is_unbounded` is a new helper that returns true if neither the
start or end is bounded.
- `&String` and `Cow<'_, str>` now implement `MakeWidget`.
- `MessageBox` displays a prompt to the user in a `Modal` layer, above a
`WindowHandle`, or in an `App`. When shown above a window or app, the `rfd`
crate is used to use the native system dialogs.
- `FilePicker` displays a file picker to the user in a `Modal` layer, above a
`WindowHandle`, or in an `App`. When shown above a window or app, the `rfd`
crate is used to use the native system dialogs.

The `FilePicker` type supports these modes of operation:

- Saving a file
- Choosing a single file
- Choosing one or more files
- Choosing a single folder/directory
- Choosing one or more folders/directories


[139]: https://github.com/khonsulabs/cushy/issues/139
Expand Down
150 changes: 150 additions & 0 deletions examples/file-picker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use std::path::PathBuf;

use cushy::dialog::{FilePicker, PickFile};
use cushy::value::{Destination, Dynamic, Source};
use cushy::widget::{MakeWidget, MakeWidgetList};
use cushy::widgets::button::ButtonClick;
use cushy::widgets::checkbox::Checkable;
use cushy::widgets::layers::Modal;
use cushy::window::{PendingWindow, WindowHandle};
use cushy::{App, Open};

#[cushy::main]
fn main(app: &mut App) -> cushy::Result {
let modal = Modal::new();
let pending = PendingWindow::default();
let window = pending.handle();
let chosen_paths = Dynamic::<Vec<PathBuf>>::default();
let picker_mode = Dynamic::default();
let pick_multiple = Dynamic::new(false);
let results = chosen_paths.map_each(|paths| {
if paths.is_empty() {
"None".make_widget()
} else {
paths
.iter()
.map(|p| p.to_string_lossy().into_owned())
.into_rows()
.make_widget()
}
});

pending
.with_root(
picker_mode
.new_radio(PickerMode::SaveFile, "Save File")
.and(picker_mode.new_radio(PickerMode::PickFile, "Pick File"))
.and(picker_mode.new_radio(PickerMode::PickFolder, "Pick Folder"))
.into_columns()
.and(pick_multiple.to_checkbox("Select Multiple").with_enabled(
picker_mode.map_each(|kind| !matches!(kind, PickerMode::SaveFile)),
))
.and(picker_buttons(
&picker_mode,
&pick_multiple,
app,
&window,
&modal,
&chosen_paths,
))
.and("Result:")
.and(results)
.into_rows()
.centered()
.vertical_scroll()
.expand()
.and(modal)
.into_layers(),
)
.open(app)?;
Ok(())
}

#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)]
enum PickerMode {
#[default]
SaveFile,
PickFile,
PickFolder,
}

fn file_picker() -> FilePicker {
FilePicker::new()
.with_title("Pick a Rust source file")
.with_types([("Rust Source", ["rs"])])
}

fn display_single_result(
chosen_paths: &Dynamic<Vec<PathBuf>>,
) -> impl FnMut(Option<PathBuf>) + Send + 'static {
let chosen_paths = chosen_paths.clone();
move |path| {
chosen_paths.set(path.into_iter().collect());
}
}

fn display_multiple_results(
chosen_paths: &Dynamic<Vec<PathBuf>>,
) -> impl FnMut(Option<Vec<PathBuf>>) + Send + 'static {
let chosen_paths = chosen_paths.clone();
move |path| {
chosen_paths.set(path.into_iter().flatten().collect());
}
}

fn picker_buttons(
mode: &Dynamic<PickerMode>,
pick_multiple: &Dynamic<bool>,
app: &App,
window: &WindowHandle,
modal: &Modal,
chosen_paths: &Dynamic<Vec<PathBuf>>,
) -> impl MakeWidget {
"Show in Modal layer"
.into_button()
.on_click(show_picker_in(modal, chosen_paths, mode, pick_multiple))
.and("Show above window".into_button().on_click(show_picker_in(
window,
chosen_paths,
mode,
pick_multiple,
)))
.and("Show in app".into_button().on_click(show_picker_in(
app,
chosen_paths,
mode,
pick_multiple,
)))
.into_rows()
}

fn show_picker_in(
target: &(impl PickFile + Clone + Send + 'static),
chosen_paths: &Dynamic<Vec<PathBuf>>,
mode: &Dynamic<PickerMode>,
pick_multiple: &Dynamic<bool>,
) -> impl FnMut(Option<ButtonClick>) + Send + 'static {
let target = target.clone();
let chosen_paths = chosen_paths.clone();
let mode = mode.clone();
let pick_multiple = pick_multiple.clone();
move |_| {
match mode.get() {
PickerMode::SaveFile => {
file_picker().save_file(&target, display_single_result(&chosen_paths))
}
PickerMode::PickFile if pick_multiple.get() => {
file_picker().pick_files(&target, display_multiple_results(&chosen_paths))
}
PickerMode::PickFile => {
file_picker().pick_file(&target, display_single_result(&chosen_paths))
}
PickerMode::PickFolder if pick_multiple.get() => {
file_picker().pick_folders(&target, display_multiple_results(&chosen_paths))
}
PickerMode::PickFolder => {
file_picker().pick_folder(&target, display_single_result(&chosen_paths))
}
};
}
}
File renamed without changes.
22 changes: 22 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::marker::PhantomData;
use std::process::exit;
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use arboard::Clipboard;
use kludgine::app::winit::error::EventLoopError;
Expand Down Expand Up @@ -313,11 +314,16 @@ pub struct RuntimeGuard<'a>(Box<dyn BoxableGuard<'a> + 'a>);
trait BoxableGuard<'a> {}
impl<'a, T> BoxableGuard<'a> for T {}

struct AppSettings {
multi_click_threshold: Duration,
}

/// Shared resources for a GUI application.
#[derive(Clone)]
pub struct Cushy {
pub(crate) clipboard: Option<Arc<Mutex<Clipboard>>>,
pub(crate) fonts: FontCollection,
settings: Arc<Mutex<AppSettings>>,
runtime: BoxedRuntime,
}

Expand All @@ -328,10 +334,26 @@ impl Cushy {
.ok()
.map(|clipboard| Arc::new(Mutex::new(clipboard))),
fonts: FontCollection::default(),
settings: Arc::new(Mutex::new(AppSettings {
multi_click_threshold: Duration::from_millis(500),
})),
runtime,
}
}

/// Returns the duration between two mouse clicks that should be allowed to
/// elapse for the clicks to be considered separate actions.
#[must_use]
pub fn multi_click_threshold(&self) -> Duration {
self.settings.lock().multi_click_threshold
}

/// Sets the maximum time between sequential clicks that should be
/// considered the same action.
pub fn set_multi_click_threshold(&self, threshold: Duration) {
self.settings.lock().multi_click_threshold = threshold;
}

/// Returns a locked mutex guard to the OS's clipboard, if one was able to be
/// initialized when the window opened.
#[must_use]
Expand Down
2 changes: 1 addition & 1 deletion src/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl Drop for DebugContext {
}
}

trait Observable: Send + Sync {
trait Observable: Send {
fn label(&self) -> &str;
// fn alive(&self) -> bool;
fn widget(&self) -> &WidgetInstance;
Expand Down
Loading

0 comments on commit a797230

Please sign in to comment.