Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
5 changes: 4 additions & 1 deletion examples/files/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
28 changes: 14 additions & 14 deletions examples/files/src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
},
Expand All @@ -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")
}
6 changes: 4 additions & 2 deletions examples/widget-gallery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
pub paths: Vec<PathBuf>,
/// The selected file format.
///
/// If there are multiple different formats available
Expand All @@ -37,7 +37,7 @@ pub struct FileInfo {
impl FileInfo {
/// Returns the underlying path.
pub fn path(&self) -> &Vec<PathBuf> {
&self.path
&self.paths
}
}

Expand Down
143 changes: 86 additions & 57 deletions src/file_action.rs
Original file line number Diff line number Diff line change
@@ -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},
};

Expand All @@ -12,68 +13,96 @@ pub fn open_file(
options: FileDialogOptions,
file_info_action: impl Fn(Option<FileInfo>) + 'static,
) {
let send = create_ext_action(
Scope::new(),
move |(path, paths): (Option<PathBuf>, Option<Vec<PathBuf>>)| {
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<PathBuf> {
vec![handle.path().to_path_buf()]
}

fn to_path_vecs(handles: Vec<rfd::FileHandle>) -> Vec<PathBuf> {
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<Box<dyn Future<Output = Option<Vec<PathBuf>>>>>,
(true, false) => {
Box::pin(dialog.pick_folder().map(|opt| opt.map(to_path_vec))) as Pin<Box<_>>
}
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<Box<_>>
}
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<Box<_>>
}
};

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<FileInfo>) + 'static) {
let send = create_ext_action(Scope::new(), move |path: Option<PathBuf>| {
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();
}
},
);
});
}
16 changes: 15 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down