Skip to content

Commit 404e88e

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 404e88e

File tree

4 files changed

+333
-6
lines changed

4 files changed

+333
-6
lines changed

Cargo.toml

Lines changed: 8 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,13 @@ 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+
gtk-sys = { version = "0.15.1", features = ["v3_20"], optional = true }
43+
glib-sys = { version = "0.15.1", optional = true }
44+
gobject-sys = { version = "0.15.1", optional = true }
45+
lazy_static = { version = "1.4.0", optional = true }
4346

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

src/backend.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,34 @@ 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+
feature = "gtk3"
15+
))]
716
mod gtk3;
817
#[cfg(target_os = "macos")]
918
mod macos;
1019
#[cfg(target_arch = "wasm32")]
1120
mod wasm;
1221
#[cfg(target_os = "windows")]
1322
mod win_cid;
23+
#[cfg(all(
24+
any(
25+
target_os = "linux",
26+
target_os = "freebsd",
27+
target_os = "dragonfly",
28+
target_os = "netbsd",
29+
target_os = "openbsd"
30+
),
31+
not(feature = "gtk3")
32+
))]
33+
mod xdg_desktop_portal;
1434

1535
//
1636
// Sync

src/backend/xdg_desktop_portal.rs

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

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)