Skip to content

Commit 7936adb

Browse files
committed
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
1 parent 1ea9517 commit 7936adb

File tree

5 files changed

+360
-6
lines changed

5 files changed

+360
-6
lines changed

Cargo.toml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ documentation = "https://docs.rs/rfd"
1414
default = ["parent"]
1515
parent = ["raw-window-handle"]
1616
file-handle-inner = []
17+
gtk3 = ["gtk-sys", "glib-sys", "gobject-sys", "lazy_static"]
1718

1819
[dev-dependencies]
1920
futures = "0.3.12"
@@ -35,11 +36,14 @@ windows = { version = "0.30.0", features = [
3536
"Win32_UI_WindowsAndMessaging",
3637
] }
3738

38-
[target.'cfg(any(target_os = "freebsd", target_os = "linux"))'.dependencies]
39-
gtk-sys = { version = "0.15.1", features = ["v3_20"] }
40-
glib-sys = "0.15.1"
41-
gobject-sys = "0.15.1"
42-
lazy_static = "1.4.0"
39+
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
40+
ashpd = "0.2.0-beta-1"
41+
smol = "1.2"
42+
log = "0.4"
43+
gtk-sys = { version = "0.15.1", features = ["v3_20"], optional = true }
44+
glib-sys = { version = "0.15.1", optional = true }
45+
gobject-sys = { version = "0.15.1", optional = true }
46+
lazy_static = { version = "1.4.0", optional = true }
4347

4448
[target.'cfg(target_arch = "wasm32")'.dependencies]
4549
wasm-bindgen = "0.2.69"

examples/msg.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
fn main() {
2+
let res = "";
3+
#[cfg(any(
4+
target_os = "windows",
5+
target_os = "macos",
6+
all(
7+
any(
8+
target_os = "linux",
9+
target_os = "freebsd",
10+
target_os = "dragonfly",
11+
target_os = "netbsd",
12+
target_os = "openbsd"
13+
),
14+
feature = "gtk3"
15+
)
16+
))]
217
let res = rfd::MessageDialog::new()
318
.set_title("Msg!")
419
.set_description("Description!")

src/backend.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,27 @@ use std::future::Future;
33
use std::path::PathBuf;
44
use std::pin::Pin;
55

6-
#[cfg(any(target_os = "freebsd", target_os = "linux"))]
6+
#[cfg(all(
7+
any(
8+
target_os = "linux",
9+
target_os = "freebsd",
10+
target_os = "dragonfly",
11+
target_os = "netbsd",
12+
target_os = "openbsd"
13+
),
14+
not(feature = "gtk3")
15+
))]
16+
mod xdg_desktop_portal;
17+
#[cfg(all(
18+
any(
19+
target_os = "linux",
20+
target_os = "freebsd",
21+
target_os = "dragonfly",
22+
target_os = "netbsd",
23+
target_os = "openbsd"
24+
),
25+
feature = "gtk3"
26+
))]
727
mod gtk3;
828
#[cfg(target_os = "macos")]
929
mod macos;

src/backend/xdg_desktop_portal.rs

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
use std::path::PathBuf;
2+
3+
use crate::backend::DialogFutureType;
4+
use crate::file_dialog::Filter;
5+
use crate::{FileDialog, FileHandle};
6+
7+
use ashpd::desktop::file_chooser::{
8+
FileChooserProxy, FileFilter, OpenFileOptions, SaveFileOptions,
9+
};
10+
// TODO: convert raw_window_handle::RawWindowHandle to ashpd::WindowIdentifier
11+
use ashpd::{zbus, WindowIdentifier};
12+
13+
use smol::block_on;
14+
use log::warn;
15+
16+
//
17+
// Utility functions
18+
//
19+
20+
fn add_filters_to_open_file_options(
21+
filters: Vec<Filter>,
22+
mut options: OpenFileOptions,
23+
) -> OpenFileOptions {
24+
for filter in &filters {
25+
let mut ashpd_filter = FileFilter::new(&filter.name);
26+
for file_extension in &filter.extensions {
27+
ashpd_filter = ashpd_filter.glob(&format!("*.{}", file_extension));
28+
}
29+
options = options.add_filter(ashpd_filter);
30+
}
31+
options
32+
}
33+
34+
fn add_filters_to_save_file_options(
35+
filters: Vec<Filter>,
36+
mut options: SaveFileOptions,
37+
) -> SaveFileOptions {
38+
for filter in &filters {
39+
let mut ashpd_filter = FileFilter::new(&filter.name);
40+
for file_extension in &filter.extensions {
41+
ashpd_filter = ashpd_filter.glob(&format!("*.{}", file_extension));
42+
}
43+
options = options.add_filter(ashpd_filter);
44+
}
45+
options
46+
}
47+
48+
// refer to https://github.com/flatpak/xdg-desktop-portal/issues/213
49+
fn uri_to_pathbuf(uri: &str) -> Option<PathBuf> {
50+
uri.strip_prefix("file://").map(PathBuf::from)
51+
}
52+
53+
fn unwrap_or_warn<T, E: std::fmt::Debug>(result: Result<T, E>) -> Option<T> {
54+
match result {
55+
Err(e) => {
56+
warn!("{:?}", e);
57+
None
58+
},
59+
Ok(t) => Some(t),
60+
}
61+
}
62+
63+
//
64+
// File Picker
65+
//
66+
67+
use crate::backend::FilePickerDialogImpl;
68+
impl FilePickerDialogImpl for FileDialog {
69+
fn pick_file(self) -> Option<PathBuf> {
70+
let connection = unwrap_or_warn(block_on(zbus::Connection::session()))?;
71+
let proxy = unwrap_or_warn(block_on(FileChooserProxy::new(&connection)))?;
72+
let mut options = OpenFileOptions::default()
73+
.accept_label("Pick file")
74+
.multiple(false);
75+
options = add_filters_to_open_file_options(self.filters, options);
76+
let selected_files = block_on(proxy.open_file(
77+
&WindowIdentifier::default(),
78+
&self.title.unwrap_or_else(|| "Pick a file".to_string()),
79+
options,
80+
));
81+
if selected_files.is_err() {
82+
return None;
83+
}
84+
uri_to_pathbuf(&selected_files.unwrap().uris()[0])
85+
}
86+
87+
fn pick_files(self) -> Option<Vec<PathBuf>> {
88+
let connection = unwrap_or_warn(block_on(zbus::Connection::session()))?;
89+
let proxy = unwrap_or_warn(block_on(FileChooserProxy::new(&connection)))?;
90+
let mut options = OpenFileOptions::default()
91+
.accept_label("Pick file")
92+
.multiple(true);
93+
options = add_filters_to_open_file_options(self.filters, options);
94+
let selected_files = block_on(proxy.open_file(
95+
&WindowIdentifier::default(),
96+
&self.title.unwrap_or_else(|| "Pick a file".to_string()),
97+
options,
98+
));
99+
if selected_files.is_err() {
100+
return None;
101+
}
102+
let selected_files = selected_files
103+
.unwrap()
104+
.uris()
105+
.iter()
106+
.filter_map(|string| uri_to_pathbuf(string))
107+
.collect::<Vec<PathBuf>>();
108+
if selected_files.is_empty() {
109+
return None;
110+
}
111+
Some(selected_files)
112+
}
113+
}
114+
115+
use crate::backend::AsyncFilePickerDialogImpl;
116+
impl AsyncFilePickerDialogImpl for FileDialog {
117+
fn pick_file_async(self) -> DialogFutureType<Option<FileHandle>> {
118+
Box::pin(async {
119+
let connection = unwrap_or_warn(zbus::Connection::session().await)?;
120+
let proxy = unwrap_or_warn(FileChooserProxy::new(&connection).await)?;
121+
let mut options = OpenFileOptions::default()
122+
.accept_label("Pick file")
123+
.multiple(false);
124+
options = add_filters_to_open_file_options(self.filters, options);
125+
let selected_files = proxy
126+
.open_file(
127+
&WindowIdentifier::default(),
128+
&self.title.unwrap_or_else(|| "Pick a file".to_string()),
129+
options,
130+
)
131+
.await;
132+
if selected_files.is_err() {
133+
return None;
134+
}
135+
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
136+
})
137+
}
138+
139+
fn pick_files_async(self) -> DialogFutureType<Option<Vec<FileHandle>>> {
140+
Box::pin(async {
141+
let connection = unwrap_or_warn(zbus::Connection::session().await)?;
142+
let proxy = unwrap_or_warn(FileChooserProxy::new(&connection).await)?;
143+
let mut options = OpenFileOptions::default()
144+
.accept_label("Pick file(s)")
145+
.multiple(true);
146+
options = add_filters_to_open_file_options(self.filters, options);
147+
let selected_files = proxy
148+
.open_file(
149+
&WindowIdentifier::default(),
150+
&self
151+
.title
152+
.unwrap_or_else(|| "Pick one or more files".to_string()),
153+
options,
154+
)
155+
.await;
156+
if selected_files.is_err() {
157+
return None;
158+
}
159+
let selected_files = selected_files
160+
.unwrap()
161+
.uris()
162+
.iter()
163+
.filter_map(|string| uri_to_pathbuf(string))
164+
.map(FileHandle::from)
165+
.collect::<Vec<FileHandle>>();
166+
if selected_files.is_empty() {
167+
return None;
168+
}
169+
Some(selected_files)
170+
})
171+
}
172+
}
173+
174+
//
175+
// Folder Picker
176+
//
177+
178+
use crate::backend::FolderPickerDialogImpl;
179+
impl FolderPickerDialogImpl for FileDialog {
180+
fn pick_folder(self) -> Option<PathBuf> {
181+
let connection = unwrap_or_warn(block_on(zbus::Connection::session()))?;
182+
let proxy = unwrap_or_warn(block_on(FileChooserProxy::new(&connection)))?;
183+
let mut options = OpenFileOptions::default()
184+
.accept_label("Pick folder")
185+
.multiple(false)
186+
.directory(true);
187+
options = add_filters_to_open_file_options(self.filters, options);
188+
let selected_files = block_on(proxy
189+
.open_file(
190+
&WindowIdentifier::default(),
191+
&self.title.unwrap_or_else(|| "Pick a folder".to_string()),
192+
options,
193+
));
194+
if selected_files.is_err() {
195+
return None;
196+
}
197+
uri_to_pathbuf(&selected_files.unwrap().uris()[0])
198+
}
199+
}
200+
201+
use crate::backend::AsyncFolderPickerDialogImpl;
202+
impl AsyncFolderPickerDialogImpl for FileDialog {
203+
fn pick_folder_async(self) -> DialogFutureType<Option<FileHandle>> {
204+
Box::pin(async {
205+
let connection = zbus::Connection::session().await.ok()?;
206+
let proxy = FileChooserProxy::new(&connection).await.ok()?;
207+
let mut options = OpenFileOptions::default()
208+
.accept_label("Pick folder")
209+
.multiple(false)
210+
.directory(true);
211+
options = add_filters_to_open_file_options(self.filters, options);
212+
let selected_files = proxy
213+
.open_file(
214+
&WindowIdentifier::default(),
215+
&self.title.unwrap_or_else(|| "Pick a folder".to_string()),
216+
options,
217+
)
218+
.await;
219+
if selected_files.is_err() {
220+
return None;
221+
}
222+
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
223+
})
224+
}
225+
}
226+
227+
//
228+
// File Save
229+
//
230+
231+
use crate::backend::FileSaveDialogImpl;
232+
impl FileSaveDialogImpl for FileDialog {
233+
fn save_file(self) -> Option<PathBuf> {
234+
let connection = block_on(zbus::Connection::session()).ok()?;
235+
let proxy = block_on(FileChooserProxy::new(&connection)).ok()?;
236+
let mut options = SaveFileOptions::default().accept_label("Save");
237+
options = add_filters_to_save_file_options(self.filters, options);
238+
if let Some(file_name) = self.file_name {
239+
options = options.current_name(&file_name);
240+
}
241+
// TODO: impl zvariant::Type for PathBuf?
242+
// if let Some(dir) = self.starting_directory {
243+
// options.current_folder(dir);
244+
// }
245+
let selected_files = block_on(proxy.save_file(
246+
&WindowIdentifier::default(),
247+
&self.title.unwrap_or_else(|| "Save file".to_string()),
248+
options,
249+
));
250+
if selected_files.is_err() {
251+
return None;
252+
}
253+
uri_to_pathbuf(&selected_files.unwrap().uris()[0])
254+
}
255+
}
256+
257+
use crate::backend::AsyncFileSaveDialogImpl;
258+
impl AsyncFileSaveDialogImpl for FileDialog {
259+
fn save_file_async(self) -> DialogFutureType<Option<FileHandle>> {
260+
Box::pin(async {
261+
let connection = zbus::Connection::session().await.ok()?;
262+
let proxy = FileChooserProxy::new(&connection).await.ok()?;
263+
let mut options = SaveFileOptions::default().accept_label("Save");
264+
options = add_filters_to_save_file_options(self.filters, options);
265+
if let Some(file_name) = self.file_name {
266+
options = options.current_name(&file_name);
267+
}
268+
// TODO: impl zvariant::Type for PathBuf?
269+
// if let Some(dir) = self.starting_directory {
270+
// options.current_folder(dir);
271+
// }
272+
let selected_files = proxy
273+
.save_file(
274+
&WindowIdentifier::default(),
275+
&self.title.unwrap_or_else(|| "Save file".to_string()),
276+
options,
277+
)
278+
.await;
279+
if selected_files.is_err() {
280+
return None;
281+
}
282+
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
283+
})
284+
}
285+
}

src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,36 @@ pub use file_dialog::FileDialog;
1010

1111
pub use file_dialog::AsyncFileDialog;
1212

13+
#[cfg(any(
14+
target_os = "windows",
15+
target_os = "macos",
16+
target_family = "wasm",
17+
all(
18+
any(
19+
target_os = "linux",
20+
target_os = "freebsd",
21+
target_os = "dragonfly",
22+
target_os = "netbsd",
23+
target_os = "openbsd"
24+
),
25+
feature = "gtk3"
26+
)
27+
))]
1328
mod message_dialog;
1429

30+
#[cfg(any(
31+
target_os = "windows",
32+
target_os = "macos",
33+
target_family = "wasm",
34+
all(
35+
any(
36+
target_os = "linux",
37+
target_os = "freebsd",
38+
target_os = "dragonfly",
39+
target_os = "netbsd",
40+
target_os = "openbsd"
41+
),
42+
feature = "gtk3"
43+
)
44+
))]
1545
pub use message_dialog::{AsyncMessageDialog, MessageButtons, MessageDialog, MessageLevel};

0 commit comments

Comments
 (0)