diff --git a/Cargo.toml b/Cargo.toml index 9f5f104da..3f179da40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,8 @@ image-tiff = ["image/tiff"] image-webp = ["image/webp"] tokio = ["dep:tokio"] # rfd (file dialog) async runtime -rfd-async-std = ["dep:rfd", "rfd/async-std"] -rfd-tokio = ["dep:rfd", "rfd/tokio"] +rfd-async-std = ["rfd/async-std"] +rfd-tokio = ["rfd/tokio"] +rfd = ["dep:rfd"] crossbeam = ["dep:crossbeam", "floem_renderer/crossbeam"] localization = ["dep:fluent-bundle", "dep:unic-langid", "dep:sys-locale"] diff --git a/examples/files/Cargo.toml b/examples/files/Cargo.toml index 687a32385..47e04dc1a 100644 --- a/examples/files/Cargo.toml +++ b/examples/files/Cargo.toml @@ -4,4 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -floem = { path = "../..", features = ["rfd-async-std"] } +floem = { path = "../..", features = ["rfd"] } + +[target.'cfg(target_os = "linux")'.dependencies] +floem = { path = "../..", features = ["rfd", "rfd-async-std"] } diff --git a/examples/files/src/files.rs b/examples/files/src/files.rs index ea32118d7..7dc2af357 100644 --- a/examples/files/src/files.rs +++ b/examples/files/src/files.rs @@ -10,75 +10,75 @@ use floem::{ pub fn files_view() -> impl IntoView { let files = create_rw_signal("".to_string()); let view = h_stack(( - button("Select file").on_click_cont(move |_| { + button("Select file").action(move || { open_file( FileDialogOptions::new() .force_starting_directory("/") - .title("Select file") + .title("Select file (txt, rs, md)") .allowed_types(vec![FileSpec { name: "text", extensions: &["txt", "rs", "md"], }]), move |file_info| { if let Some(file) = file_info { - println!("Selected file: {:?}", file.path); + println!("Selected file: {:?}", file.paths); files.set(display_files(file)); } }, ); }), - button("Select multiple files").on_click_cont(move |_| { + button("Select multiple files").action(move || { open_file( FileDialogOptions::new() .multi_selection() - .title("Select file") + .title("Select multiple files (txt, rs, md)") .allowed_types(vec![FileSpec { name: "text", extensions: &["txt", "rs", "md"], }]), move |file_info| { if let Some(file) = file_info { - println!("Selected file: {:?}", file.path); + println!("Selected file: {:?}", file.paths); files.set(display_files(file)); } }, ); }), - button("Select folder").on_click_cont(move |_| { + button("Select folder").action(move || { open_file( FileDialogOptions::new() .select_directories() .title("Select Folder"), move |file_info| { if let Some(file) = file_info { - println!("Selected folder: {:?}", file.path); + println!("Selected folder: {:?}", file.paths); files.set(display_files(file)); } }, ); }), - button("Select multiple folder").on_click_cont(move |_| { + button("Select multiple folders").action(move || { open_file( FileDialogOptions::new() .select_directories() .multi_selection() - .title("Select multiple Folder"), + .title("Select multiple folders"), move |file_info| { if let Some(file) = file_info { - println!("Selected folder: {:?}", file.path); + println!("Selected folder: {:?}", file.paths); files.set(display_files(file)); } }, ); }), - button("Save file").on_click_cont(move |_| { + button("Save file").action(move || { save_as( FileDialogOptions::new() .default_name("floem.file") .title("Save file"), move |file_info| { if let Some(file) = file_info { - println!("Save file to: {:?}", file.path); + println!("Save file to: {:?}", file.paths); files.set(display_files(file)); } }, @@ -105,6 +105,6 @@ pub fn files_view() -> impl IntoView { } fn display_files(file: FileInfo) -> String { - let paths: Vec<&str> = file.path.iter().filter_map(|p| p.to_str()).collect(); + let paths: Vec<&str> = file.paths.iter().filter_map(|p| p.to_str()).collect(); paths.join("\n") } diff --git a/examples/widget-gallery/Cargo.toml b/examples/widget-gallery/Cargo.toml index 3013b5693..2c2ab0d3b 100644 --- a/examples/widget-gallery/Cargo.toml +++ b/examples/widget-gallery/Cargo.toml @@ -4,12 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -floem = { path = "../..", features = ["rfd-async-std", "vello"] } +floem = { path = "../..", features = ["rfd", "vello"] } strum = { workspace = true } files = { path = "../files/", optional = true } stacks = { path = "../stacks/", optional = true } +[target.'cfg(target_os = "linux")'.dependencies] +floem = { path = "../..", features = ["rfd", "rfd-async-std", "vello"] } + [features] default = ["full"] -vello = ["floem/vello"] full = ["dep:files", "dep:stacks"] diff --git a/src/action.rs b/src/action.rs index e4528fc01..ed9140f23 100644 --- a/src/action.rs +++ b/src/action.rs @@ -28,7 +28,7 @@ use crate::{ window_tracking::with_window, }; -#[cfg(any(feature = "rfd-async-std", feature = "rfd-tokio"))] +#[cfg(any(feature = "rfd-async-std", feature = "rfd-tokio", feature = "rfd"))] pub use crate::file_action::*; pub(crate) fn add_update_message(msg: UpdateMessage) { diff --git a/src/file.rs b/src/file.rs index f39e62c31..9739faff8 100644 --- a/src/file.rs +++ b/src/file.rs @@ -19,11 +19,11 @@ pub struct FileSpec { #[derive(Debug, Clone)] pub struct FileInfo { - /// The path to the selected file. + /// The path(s) to the selected file(s). /// /// On macOS, this is already rewritten to use the extension that the user selected /// with the `file format` property. - pub path: Vec, + pub paths: Vec, /// The selected file format. /// /// If there are multiple different formats available @@ -37,7 +37,7 @@ pub struct FileInfo { impl FileInfo { /// Returns the underlying path. pub fn path(&self) -> &Vec { - &self.path + &self.paths } } diff --git a/src/file_action.rs b/src/file_action.rs index 6b976e85e..459fa761f 100644 --- a/src/file_action.rs +++ b/src/file_action.rs @@ -1,9 +1,10 @@ -use std::path::PathBuf; +use std::{path::PathBuf, pin::Pin}; -use floem_reactive::Scope; +use floem_reactive::{Scope, SignalGet as _, create_updater, with_scope}; +use futures::FutureExt; use crate::{ - ext_event::create_ext_action, + ext_event::async_signal::FutureSignal, file::{FileDialogOptions, FileInfo}, }; @@ -12,68 +13,96 @@ pub fn open_file( options: FileDialogOptions, file_info_action: impl Fn(Option) + 'static, ) { - let send = create_ext_action( - Scope::new(), - move |(path, paths): (Option, Option>)| { - if paths.is_some() { - file_info_action(paths.map(|paths| FileInfo { - path: paths, - format: None, - })) - } else { - file_info_action(path.map(|path| FileInfo { - path: vec![path], - format: None, - })) - } - }, - ); - std::thread::spawn(move || { - let mut dialog = rfd::FileDialog::new(); - if let Some(path) = options.starting_directory.as_ref() { - dialog = dialog.set_directory(path); + let mut dialog = rfd::AsyncFileDialog::new(); + + if let Some(path) = options.starting_directory.as_ref() { + dialog = dialog.set_directory(path); + } + if let Some(title) = options.title.as_ref() { + dialog = dialog.set_title(title); + } + if let Some(allowed_types) = options.allowed_types.as_ref() { + dialog = allowed_types.iter().fold(dialog, |dialog, filter| { + dialog.add_filter(filter.name, filter.extensions) + }); + } + + fn to_path_vec(handle: rfd::FileHandle) -> Vec { + vec![handle.path().to_path_buf()] + } + + fn to_path_vecs(handles: Vec) -> Vec { + handles.iter().map(|h| h.path().to_path_buf()).collect() + } + + // Create the appropriate future based on options, mapping to unified return type + let future = match (options.select_directories, options.multi_selection) { + (true, true) => Box::pin(dialog.pick_folders().map(|opt| opt.map(to_path_vecs))) + as Pin>>>>, + (true, false) => { + Box::pin(dialog.pick_folder().map(|opt| opt.map(to_path_vec))) as Pin> } - if let Some(title) = options.title.as_ref() { - dialog = dialog.set_title(title); + (false, true) => { + Box::pin(dialog.pick_files().map(|opt| opt.map(to_path_vecs))) as Pin> } - if let Some(allowed_types) = options.allowed_types.as_ref() { - dialog = allowed_types.iter().fold(dialog, |dialog, filter| { - dialog.add_filter(filter.name, filter.extensions) - }); + (false, false) => { + Box::pin(dialog.pick_file().map(|opt| opt.map(to_path_vec))) as Pin> } + }; - if options.select_directories && options.multi_selection { - send((None, dialog.pick_folders())); - } else if options.select_directories && !options.multi_selection { - send((dialog.pick_folder(), None)); - } else if !options.select_directories && options.multi_selection { - send((None, dialog.pick_files())); - } else { - send((dialog.pick_file(), None)); - } + let scope = Scope::new(); + with_scope(scope, || { + let resource = FutureSignal::on_event_loop(future); + create_updater( + move || resource.get(), + move |paths| { + if let Some(paths) = paths { + if let Some(paths) = paths { + file_info_action(Some(FileInfo { + paths, + format: None, + })); + } + scope.dispose(); + } + }, + ); }); } /// Open a system file save dialog pub fn save_as(options: FileDialogOptions, file_info_action: impl Fn(Option) + 'static) { - let send = create_ext_action(Scope::new(), move |path: Option| { - file_info_action(path.map(|path| FileInfo { - path: vec![path], - format: None, - })) - }); - std::thread::spawn(move || { - let mut dialog = rfd::FileDialog::new(); - if let Some(path) = options.starting_directory.as_ref() { - dialog = dialog.set_directory(path); - } - if let Some(name) = options.default_name.as_ref() { - dialog = dialog.set_file_name(name); - } - if let Some(title) = options.title.as_ref() { - dialog = dialog.set_title(title); - } - let path = dialog.save_file(); - send(path); + let mut dialog = rfd::AsyncFileDialog::new(); + if let Some(path) = options.starting_directory.as_ref() { + dialog = dialog.set_directory(path); + } + if let Some(name) = options.default_name.as_ref() { + dialog = dialog.set_file_name(name); + } + if let Some(title) = options.title.as_ref() { + dialog = dialog.set_title(title); + } + + let future = dialog + .save_file() + .map(|opt| opt.map(|h| h.path().to_path_buf())); + + let scope = Scope::new(); + with_scope(scope, || { + let resource = FutureSignal::on_event_loop(future); + create_updater( + move || resource.get(), + move |path| { + if let Some(path) = path { + if let Some(path) = path { + file_info_action(Some(FileInfo { + paths: vec![path], + format: None, + })); + } + scope.dispose(); + } + }, + ); }); } diff --git a/src/lib.rs b/src/lib.rs index 97347b621..1ff36182a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,21 @@ pub mod easing; pub mod event; pub mod ext_event; pub mod file; -#[cfg(any(feature = "rfd-async-std", feature = "rfd-tokio"))] +#[cfg(all( + target_os = "linux", + feature = "rfd", + not(any(feature = "rfd-async-std", feature = "rfd-tokio")) +))] +compile_error!( + "On Linux, rfd requires an async runtime. \ + Please enable either the 'rfd-async-std' or 'rfd-tokio' feature." +); +#[cfg(all(feature = "rfd-async-std", feature = "rfd-tokio"))] +compile_error!( + "Cannot enable both 'rfd-async-std' and 'rfd-tokio' features simultaneously. \ + Please choose only one async runtime." +); +#[cfg(any(feature = "rfd-async-std", feature = "rfd-tokio", feature = "rfd"))] pub mod file_action; pub(crate) mod id; mod inspector;