Skip to content

Commit

Permalink
Add suport for XDG Desktop Portal on Linux & BSDs (#41)
Browse files Browse the repository at this point in the history
* use XDG Desktop Portal on Linux & BSDs

This new backend does not support MessageDialog nor
AsyncMessageDialog because there is no corresponding API in the
XDG Desktop Portal.

The GTK backend is still available with the new `gtk3` Cargo
feature.

Fixes #36

* replace smol with pollster

pollster is smaller than smol

* rename unwrap_or_warn to ok_or_warn

* reuse async functions to implement sync functions

* replace Option::ok with ok_or_warn

* impl From<FileHandle> for PathBuf

to reduce code duplication

* cargo fmt

* factor out file_chooser_proxy function

* add link to ashpd issue for RawWindowHandle

bilelmoussaoui/ashpd#40

* make gtk3 a default feature
  • Loading branch information
Be-ing committed Jan 18, 2022
1 parent 1ea9517 commit e331eff
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 7 deletions.
16 changes: 10 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ repository = "https://github.com/PolyMeilex/rfd"
documentation = "https://docs.rs/rfd"

[features]
default = ["parent"]
default = ["parent", "gtk3"]
parent = ["raw-window-handle"]
file-handle-inner = []
gtk3 = ["gtk-sys", "glib-sys", "gobject-sys", "lazy_static"]

[dev-dependencies]
futures = "0.3.12"
Expand All @@ -35,11 +36,14 @@ windows = { version = "0.30.0", features = [
"Win32_UI_WindowsAndMessaging",
] }

[target.'cfg(any(target_os = "freebsd", target_os = "linux"))'.dependencies]
gtk-sys = { version = "0.15.1", features = ["v3_20"] }
glib-sys = "0.15.1"
gobject-sys = "0.15.1"
lazy_static = "1.4.0"
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
ashpd = "0.2.0-beta-1"
pollster = "0.2"
log = "0.4"
gtk-sys = { version = "0.15.1", features = ["v3_20"], optional = true }
glib-sys = { version = "0.15.1", optional = true }
gobject-sys = { version = "0.15.1", optional = true }
lazy_static = { version = "1.4.0", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2.69"
Expand Down
15 changes: 15 additions & 0 deletions examples/msg.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
fn main() {
let res = "";
#[cfg(any(
target_os = "windows",
target_os = "macos",
all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk3"
)
))]
let res = rfd::MessageDialog::new()
.set_title("Msg!")
.set_description("Description!")
Expand Down
22 changes: 21 additions & 1 deletion src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,34 @@ use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;

#[cfg(any(target_os = "freebsd", target_os = "linux"))]
#[cfg(all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk3"
))]
mod gtk3;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_os = "windows")]
mod win_cid;
#[cfg(all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
not(feature = "gtk3")
))]
mod xdg_desktop_portal;

//
// Sync
Expand Down
216 changes: 216 additions & 0 deletions src/backend/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use std::path::PathBuf;

use crate::backend::DialogFutureType;
use crate::file_dialog::Filter;
use crate::{FileDialog, FileHandle};

use ashpd::desktop::file_chooser::{
FileChooserProxy, FileFilter, OpenFileOptions, SaveFileOptions,
};
// TODO: convert raw_window_handle::RawWindowHandle to ashpd::WindowIdentifier
// https://github.com/bilelmoussaoui/ashpd/issues/40
use ashpd::{zbus, WindowIdentifier};

use log::warn;
use pollster::block_on;

//
// Utility functions
//

fn add_filters_to_open_file_options(
filters: Vec<Filter>,
mut options: OpenFileOptions,
) -> OpenFileOptions {
for filter in &filters {
let mut ashpd_filter = FileFilter::new(&filter.name);
for file_extension in &filter.extensions {
ashpd_filter = ashpd_filter.glob(&format!("*.{}", file_extension));
}
options = options.add_filter(ashpd_filter);
}
options
}

fn add_filters_to_save_file_options(
filters: Vec<Filter>,
mut options: SaveFileOptions,
) -> SaveFileOptions {
for filter in &filters {
let mut ashpd_filter = FileFilter::new(&filter.name);
for file_extension in &filter.extensions {
ashpd_filter = ashpd_filter.glob(&format!("*.{}", file_extension));
}
options = options.add_filter(ashpd_filter);
}
options
}

// refer to https://github.com/flatpak/xdg-desktop-portal/issues/213
fn uri_to_pathbuf(uri: &str) -> Option<PathBuf> {
uri.strip_prefix("file://").map(PathBuf::from)
}

fn ok_or_warn<T, E: std::fmt::Debug>(result: Result<T, E>) -> Option<T> {
match result {
Err(e) => {
warn!("{:?}", e);
None
}
Ok(t) => Some(t),
}
}

async fn file_chooser_proxy<'a>() -> Option<FileChooserProxy<'a>> {
let connection = ok_or_warn(zbus::Connection::session().await)?;
ok_or_warn(FileChooserProxy::new(&connection).await)
}

//
// File Picker
//

use crate::backend::FilePickerDialogImpl;
impl FilePickerDialogImpl for FileDialog {
fn pick_file(self) -> Option<PathBuf> {
block_on(self.pick_file_async()).map(PathBuf::from)
}

fn pick_files(self) -> Option<Vec<PathBuf>> {
block_on(self.pick_files_async())
.map(|vec_file_handle| vec_file_handle.iter().map(PathBuf::from).collect())
}
}

use crate::backend::AsyncFilePickerDialogImpl;
impl AsyncFilePickerDialogImpl for FileDialog {
fn pick_file_async(self) -> DialogFutureType<Option<FileHandle>> {
Box::pin(async {
let proxy = file_chooser_proxy().await?;
let mut options = OpenFileOptions::default()
.accept_label("Pick file")
.multiple(false);
options = add_filters_to_open_file_options(self.filters, options);
let selected_files = proxy
.open_file(
&WindowIdentifier::default(),
&self.title.unwrap_or_else(|| "Pick a file".to_string()),
options,
)
.await;
if selected_files.is_err() {
return None;
}
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
})
}

fn pick_files_async(self) -> DialogFutureType<Option<Vec<FileHandle>>> {
Box::pin(async {
let proxy = file_chooser_proxy().await?;
let mut options = OpenFileOptions::default()
.accept_label("Pick file(s)")
.multiple(true);
options = add_filters_to_open_file_options(self.filters, options);
let selected_files = proxy
.open_file(
&WindowIdentifier::default(),
&self
.title
.unwrap_or_else(|| "Pick one or more files".to_string()),
options,
)
.await;
if selected_files.is_err() {
return None;
}
let selected_files = selected_files
.unwrap()
.uris()
.iter()
.filter_map(|string| uri_to_pathbuf(string))
.map(FileHandle::from)
.collect::<Vec<FileHandle>>();
if selected_files.is_empty() {
return None;
}
Some(selected_files)
})
}
}

//
// Folder Picker
//

use crate::backend::FolderPickerDialogImpl;
impl FolderPickerDialogImpl for FileDialog {
fn pick_folder(self) -> Option<PathBuf> {
block_on(self.pick_folder_async()).map(PathBuf::from)
}
}

use crate::backend::AsyncFolderPickerDialogImpl;
impl AsyncFolderPickerDialogImpl for FileDialog {
fn pick_folder_async(self) -> DialogFutureType<Option<FileHandle>> {
Box::pin(async {
let proxy = file_chooser_proxy().await?;
let mut options = OpenFileOptions::default()
.accept_label("Pick folder")
.multiple(false)
.directory(true);
options = add_filters_to_open_file_options(self.filters, options);
let selected_files = proxy
.open_file(
&WindowIdentifier::default(),
&self.title.unwrap_or_else(|| "Pick a folder".to_string()),
options,
)
.await;
if selected_files.is_err() {
return None;
}
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
})
}
}

//
// File Save
//

use crate::backend::FileSaveDialogImpl;
impl FileSaveDialogImpl for FileDialog {
fn save_file(self) -> Option<PathBuf> {
block_on(self.save_file_async()).map(PathBuf::from)
}
}

use crate::backend::AsyncFileSaveDialogImpl;
impl AsyncFileSaveDialogImpl for FileDialog {
fn save_file_async(self) -> DialogFutureType<Option<FileHandle>> {
Box::pin(async {
let proxy = file_chooser_proxy().await?;
let mut options = SaveFileOptions::default().accept_label("Save");
options = add_filters_to_save_file_options(self.filters, options);
if let Some(file_name) = self.file_name {
options = options.current_name(&file_name);
}
// TODO: impl zvariant::Type for PathBuf?
// if let Some(dir) = self.starting_directory {
// options.current_folder(dir);
// }
let selected_files = proxy
.save_file(
&WindowIdentifier::default(),
&self.title.unwrap_or_else(|| "Save file".to_string()),
options,
)
.await;
if selected_files.is_err() {
return None;
}
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
})
}
}
12 changes: 12 additions & 0 deletions src/file_handle/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,15 @@ impl From<PathBuf> for FileHandle {
Self(path)
}
}

impl From<FileHandle> for PathBuf {
fn from(file_handle: FileHandle) -> Self {
PathBuf::from(file_handle.path())
}
}

impl From<&FileHandle> for PathBuf {
fn from(file_handle: &FileHandle) -> Self {
PathBuf::from(file_handle.path())
}
}
30 changes: 30 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ pub use file_dialog::FileDialog;

pub use file_dialog::AsyncFileDialog;

#[cfg(any(
target_os = "windows",
target_os = "macos",
target_family = "wasm",
all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk3"
)
))]
mod message_dialog;

#[cfg(any(
target_os = "windows",
target_os = "macos",
target_family = "wasm",
all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk3"
)
))]
pub use message_dialog::{AsyncMessageDialog, MessageButtons, MessageDialog, MessageLevel};

0 comments on commit e331eff

Please sign in to comment.