├── .editorconifg ├── .github └── workflows │ ├── cargo-deny.yml │ ├── changelog.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── deny.toml ├── examples ├── async.rs ├── message-custom-buttons │ ├── .gitignore │ ├── Cargo.toml │ ├── app.exe.manifest │ ├── build.rs │ ├── manifest.rc │ └── src │ │ └── main.rs ├── msg.rs ├── save.rs ├── simple.rs └── winit-example │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Trunk.toml │ ├── index.html │ └── src │ └── main.rs └── src ├── backend.rs ├── backend ├── gtk3.rs ├── gtk3 │ ├── file_dialog.rs │ ├── file_dialog │ │ └── dialog_ffi.rs │ ├── gtk_future.rs │ ├── message_dialog.rs │ └── utils.rs ├── linux │ ├── async_command.rs │ ├── mod.rs │ └── zenity.rs ├── macos.rs ├── macos │ ├── file_dialog.rs │ ├── file_dialog │ │ └── panel_ffi.rs │ ├── message_dialog.rs │ ├── modal_future.rs │ ├── utils.rs │ └── utils │ │ ├── focus_manager.rs │ │ ├── policy_manager.rs │ │ └── user_alert.rs ├── wasm.rs ├── wasm │ ├── file_dialog.rs │ └── style.css ├── win_cid.rs ├── win_cid │ ├── file_dialog.rs │ ├── file_dialog │ │ ├── com.rs │ │ ├── dialog_ffi.rs │ │ └── dialog_future.rs │ ├── message_dialog.rs │ ├── thread_future.rs │ └── utils.rs ├── win_xp.rs.old └── xdg_desktop_portal.rs ├── file_dialog.rs ├── file_handle ├── mod.rs ├── native.rs └── web.rs ├── lib.rs ├── message_dialog.rs └── oneshot.rs /.editorconifg: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | max_line_length = 100 10 | 11 | [*.{rs, toml}] 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = true 16 | indent_size = 4 17 | 18 | [Dockerfile] 19 | indent_size = 4 -------------------------------------------------------------------------------- /.github/workflows/cargo-deny.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/EmbarkStudios/cargo-deny 2 | # To run locally: `cargo install cargo-deny && cargo deny check` 3 | 4 | name: cargo-deny 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - name: Ubuntu GTK 15 | target: x86_64-unknown-linux-gnu 16 | flags: "--no-default-features --features gtk3" 17 | - name: Ubuntu XDG 18 | target: x86_64-unknown-linux-gnu 19 | flags: "--no-default-features --features xdg-portal --exclude syn" 20 | - name: Windows 21 | target: x86_64-pc-windows-msvc 22 | flags: "" 23 | - name: Windows CC6 24 | target: x86_64-pc-windows-msvc 25 | flags: "--features common-controls-v6" 26 | - name: macOS 27 | target: x86_64-apple-darwin 28 | flags: "" 29 | - name: WASM32 30 | target: wasm32-unknown-unknown 31 | flags: "" 32 | 33 | name: ${{ matrix.name }} 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | - uses: EmbarkStudios/cargo-deny-action@v2 39 | with: 40 | log-level: error 41 | command: check 42 | arguments: ${{ matrix.flags }} --target ${{ matrix.target }} 43 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Changelog check 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | types: [ opened, synchronize, reopened, labeled, unlabeled ] 7 | 8 | jobs: 9 | Changelog-Entry-Check: 10 | name: Check Changelog Action 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: tarides/changelog-check-action@v2 14 | with: 15 | changelog: CHANGELOG.md 16 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - name: Ubuntu GTK 16 | os: ubuntu-latest 17 | target: x86_64-unknown-linux-gnu 18 | flags: '--no-default-features --features gtk3' 19 | - name: Ubuntu XDG 20 | os: ubuntu-latest 21 | target: x86_64-unknown-linux-gnu 22 | flags: '--no-default-features --features xdg-portal,tokio' 23 | - name: Windows 24 | os: windows-latest 25 | target: x86_64-pc-windows-msvc 26 | flags: '' 27 | - name: Windows CC6 28 | os: windows-latest 29 | target: x86_64-pc-windows-msvc 30 | flags: '--features common-controls-v6' 31 | - name: macOS 32 | os: macos-latest 33 | target: aarch64-apple-darwin 34 | flags: '' 35 | - name: WASM32 36 | os: ubuntu-latest 37 | target: wasm32-unknown-unknown 38 | flags: '' 39 | 40 | name: ${{ matrix.name }} 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: "[Ubuntu GTK] install dependencies" 45 | if: matrix.name == 'Ubuntu GTK' 46 | run: sudo apt update && sudo apt install libgtk-3-dev 47 | - name: "[Ubuntu XDG] install dependencies" 48 | if: matrix.name == 'Ubuntu XDG' 49 | run: sudo apt update && sudo apt install libwayland-dev 50 | - name: "[WASM] rustup" 51 | if: matrix.name == 'WASM32' 52 | run: rustup target add wasm32-unknown-unknown 53 | - uses: Swatinem/rust-cache@v2 54 | - name: Build 55 | run: cargo build --target ${{ matrix.target }} ${{ matrix.flags }} 56 | - name: Test 57 | # FIXME 58 | if: matrix.name != 'WASM32' 59 | run: cargo test --target ${{ matrix.target }} ${{ matrix.flags }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased 4 | 5 | ## 0.15.3 6 | - Update `objc2` to v0.6. 7 | - Update `ashpd` to 0.11. 8 | 9 | ## 0.15.1 10 | - Update `ashpd` to 0.10. 11 | - Fix issue where with no filter added no files are selectable on Windows (#211). 12 | 13 | ## 0.15.0 14 | - Move from `objc` crates to `objc2` crates. 15 | - Fix `AsyncFileDialog` blocking the executor on Windows (#191) 16 | - Add `TDF_SIZE_TO_CONTENT` to `TaskDialogIndirect` config so that it can display longer text without truncating/wrapping (80 characters instead of 55) (#202) 17 | - Fix `xdg-portal` backend not accepting special characters in message dialogs 18 | - Make `set_parent` require `HasWindowHandle + HasDisplayHandle` 19 | - Add support for `set_parent` in XDG Portals 20 | - Update `ashpd` to 0.9. 21 | - Add support for files without an extension in XDG Portal filters 22 | - Derive `Clone` for `FileHandle` 23 | 24 | ## 0.14.0 25 | - i18n for GTK and XDG Portal 26 | - Use XDG Portal as default 27 | - Use zenity as a fallback for XDG Portal 28 | - Update `raw-window-handle` to 0.6. 29 | - Update `winit` in example to 0.29. 30 | - Update `ashpd` to 0.8. 31 | - Update wasm CSS to respect the color scheme (including dark mode) 32 | - Fix macOS sync backend incorrectly setting the parent window 33 | - Add `FileDialog/AsyncFileDialog::set_can_create_directories`, supported on macOS only. 34 | 35 | ## 0.13.0 36 | - **[Breaking]** Users of the `xdg-portal` feature must now also select the `tokio` 37 | or `async-std` feature 38 | - [macOS] Use NSOpenPanel.message instead of title #166 39 | 40 | ## 0.12.1 41 | - Fix `FileHandle::inner` (under feature `file-handle-inner`) on wasm 42 | 43 | ## 0.12.0 44 | - Add title support for WASM (#132) 45 | - Add Create folder button to `pick_folder` on macOS (#127) 46 | - Add support for Yes/No/Cancel buttons (#123) 47 | - Change a string method signatures #117 48 | - WASM `save_file` (#134) 49 | - Update `gtk-sys` to `0.18` (#143) 50 | - Update `ashpd` to `0.6` (#133) 51 | - Replace windows with `windows-sys` (#118) 52 | - Make zenity related deps optional (#141) 53 | 54 | ## 0.11.3 55 | - Zenity message dialogs for xdg portal backend 56 | 57 | ## 0.10.1 58 | - Update `gtk-sys` to `0.16` and `windows-rs` to `0.44` 59 | 60 | ## 0.10.0 61 | - fix(FileDialog::set_directory): fallback to default if path is empty 62 | 63 | ## 0.9.0 64 | - feat: customize button text, Close #74 65 | - feat: Add support for selecting multiple folders, fixes #73 66 | 67 | ## 0.8.4 68 | - XDG: decode URI before converting to PathBuf #70 69 | 70 | ## 0.8.3 71 | - Windows-rs update 0.37 72 | 73 | ## 0.8.2 74 | - Windows-rs update 0.35 75 | 76 | ## 0.8.1 77 | - Macos parent for sync FileDialog (#58) 78 | - Windows-rs update 0.33 79 | 80 | ## 0.8.0 81 | - `parent` feature was removed, it is always on now 82 | - New feature `xdg-portal` 83 | - Now you have to choose one of the features `gtk3` or `xdg-portal`, gtk is on by default 84 | - `window` crate got updated to 0.32 85 | 86 | ## 0.7.0 87 | - Safe Rust XDG Desktop Portal support 88 | 89 | ## 0.6.3 90 | 91 | - Update `windows` crate to 0.30. 92 | 93 | ## 0.6.2 94 | - Strip Win32 namespaces from directory paths 95 | 96 | ## 0.6.0 97 | - FreeBSD support 98 | - Port to windows-rs 99 | - Update RawWindowHandle to 0.4 100 | 101 | ## 0.4.4 102 | 103 | - Fix `set_directory` on some windows setups (#22) 104 | - Implement `set_file_name` on MacOS (#21) 105 | 106 | ## 0.4.3 107 | 108 | - `set_parent` support for `MessageDialog` on windows 109 | 110 | ## 0.4.2 111 | 112 | - GTK save dialog now sets current_name correctly (#18) 113 | 114 | ## 0.4.1 115 | 116 | - Update gtk 117 | 118 | ## 0.4.0 119 | 120 | - **[Breaking]** Fix misspeled `OkCancel` in `MessageButtons` (#12) 121 | - `set_parent` support for Windows (#14) 122 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rfd" 3 | version = "0.15.3" 4 | edition = "2021" 5 | 6 | authors = ["Poly "] 7 | description = "Rusty File Dialog" 8 | keywords = ["file", "ui", "dialog"] 9 | license = "MIT" 10 | repository = "https://github.com/PolyMeilex/rfd" 11 | documentation = "https://docs.rs/rfd" 12 | 13 | [features] 14 | default = ["xdg-portal", "async-std"] 15 | file-handle-inner = [] 16 | gtk3 = ["gtk-sys", "glib-sys", "gobject-sys"] 17 | xdg-portal = ["ashpd", "urlencoding", "pollster"] 18 | # Use async-std for xdg-portal 19 | async-std = ["ashpd?/async-std"] 20 | # Use tokio for xdg-portal 21 | tokio = ["ashpd?/tokio"] 22 | common-controls-v6 = ["windows-sys/Win32_UI_Controls"] 23 | 24 | [dev-dependencies] 25 | futures = "0.3.12" 26 | 27 | [dependencies] 28 | raw-window-handle = "0.6" 29 | log = "0.4" 30 | 31 | [target.'cfg(target_os = "macos")'.dependencies] 32 | block2 = "0.6.0" 33 | dispatch2 = "0.3.0" 34 | objc2 = "0.6.0" 35 | objc2-foundation = { version = "0.3.0", default-features = false, features = [ 36 | "std", 37 | "NSArray", 38 | "NSEnumerator", 39 | "NSString", 40 | "NSThread", 41 | "NSURL", 42 | ] } 43 | objc2-app-kit = { version = "0.3.0", default-features = false, features = [ 44 | "std", 45 | "block2", 46 | "NSAlert", 47 | "NSApplication", 48 | "NSButton", 49 | "NSControl", 50 | "NSOpenPanel", 51 | "NSPanel", 52 | "NSResponder", 53 | "NSRunningApplication", 54 | "NSSavePanel", 55 | "NSView", 56 | "NSWindow", 57 | ] } 58 | objc2-core-foundation = { version = "0.3.0", default-features = false, features = [ 59 | "std", 60 | "CFBase", 61 | "CFDate", 62 | "CFString", 63 | "CFURL", 64 | "CFUserNotification", 65 | ] } 66 | 67 | [target.'cfg(target_os = "windows")'.dependencies] 68 | windows-sys = { version = "0.59", features = [ 69 | "Win32_Foundation", 70 | "Win32_System_Com", 71 | "Win32_UI_Shell_Common", 72 | "Win32_UI_Shell", 73 | "Win32_UI_WindowsAndMessaging", 74 | ] } 75 | 76 | [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies] 77 | # XDG Desktop Portal 78 | ashpd = { version = "0.11", optional = true, default-features = false, features = [ 79 | "raw_handle", 80 | ] } 81 | urlencoding = { version = "2.1.0", optional = true } 82 | pollster = { version = "0.4", optional = true } 83 | # GTK 84 | gtk-sys = { version = "0.18.0", features = ["v3_24"], optional = true } 85 | glib-sys = { version = "0.18.0", optional = true } 86 | gobject-sys = { version = "0.18.0", optional = true } 87 | 88 | [target.'cfg(target_arch = "wasm32")'.dependencies] 89 | wasm-bindgen = "0.2.69" 90 | js-sys = "0.3.46" 91 | web-sys = { version = "0.3.46", features = [ 92 | 'Document', 93 | 'Element', 94 | 'HtmlInputElement', 95 | 'HtmlButtonElement', 96 | 'HtmlAnchorElement', 97 | 'Window', 98 | 'File', 99 | 'FileList', 100 | 'FileReader', 101 | 'Blob', 102 | 'BlobPropertyBag', 103 | 'Url', 104 | ] } 105 | wasm-bindgen-futures = "0.4.19" 106 | 107 | [[example]] 108 | name = "simple" 109 | [[example]] 110 | name = "async" 111 | 112 | [package.metadata.docs.rs] 113 | features = ["file-handle-inner"] 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bartłomiej Maryńczak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![img](https://github.com/PolyMeilex/rfd/assets/20758186/9bef59fa-60f0-448c-b9db-44ab436ee611) 2 | 3 | 4 | [![version](https://img.shields.io/crates/v/rfd.svg)](https://crates.io/crates/rfd) 5 | [![Documentation](https://docs.rs/rfd/badge.svg)](https://docs.rs/rfd) 6 | [![dependency status](https://deps.rs/crate/rfd/0.15.1/status.svg)](https://deps.rs/crate/rfd/0.15.3) 7 | 8 | Rusty File Dialogs is a cross platform Rust library for using native file open/save dialogs. 9 | It provides both asynchronous and synchronous APIs. Supported platforms: 10 | 11 | * Windows 12 | * macOS 13 | * Linux & BSDs (GTK3 or XDG Desktop Portal) 14 | * WASM32 (async only) 15 | 16 | Refer to the [documentation](https://docs.rs/rfd) for more details. 17 | 18 | 19 | ## Platform-specific notes 20 | 21 | ### Linux 22 | Please refer to [Linux & BSD backends](https://docs.rs/rfd/latest/rfd/#linux--bsd-backends) for information about the needed dependencies to be able to compile on Linux. 23 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("target OS not detected"); 3 | 4 | match target_os.as_str() { 5 | "macos" => println!("cargo:rustc-link-lib=framework=AppKit"), 6 | "windows" => {} 7 | _ => { 8 | let gtk = std::env::var_os("CARGO_FEATURE_GTK3").is_some(); 9 | let xdg = std::env::var_os("CARGO_FEATURE_XDG_PORTAL").is_some(); 10 | 11 | if gtk && xdg { 12 | panic!("You can't enable both `gtk3` and `xdg-portal` features at once"); 13 | } else if !gtk && !xdg { 14 | panic!("You need to choose at least one backend: `gtk3` or `xdg-portal` features"); 15 | } 16 | 17 | if xdg { 18 | let tokio = std::env::var_os("CARGO_FEATURE_TOKIO").is_some(); 19 | let async_std = std::env::var_os("CARGO_FEATURE_ASYNC_STD").is_some(); 20 | if !tokio && !async_std { 21 | panic!("One of the `tokio` or `async-std` features must be enabled to use `xdg-portal`"); 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # https://embarkstudios.github.io/cargo-deny/ 2 | 3 | [graph] 4 | targets = [ 5 | { triple = "aarch64-apple-darwin" }, 6 | { triple = "aarch64-linux-android" }, 7 | { triple = "wasm32-unknown-unknown" }, 8 | { triple = "x86_64-apple-darwin" }, 9 | { triple = "x86_64-pc-windows-msvc" }, 10 | { triple = "x86_64-unknown-linux-gnu" }, 11 | { triple = "x86_64-unknown-linux-musl" }, 12 | ] 13 | exclude = ["gtk-sys"] 14 | 15 | [advisories] 16 | version = 2 17 | yanked = "deny" 18 | ignore = [] 19 | 20 | [bans] 21 | multiple-versions = "deny" 22 | wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed 23 | deny = [] 24 | skip = [] 25 | skip-tree = [] 26 | 27 | [licenses] 28 | version = 2 29 | unused-allowed-license = "allow" 30 | allow = [ 31 | "MIT", 32 | "Unicode-3.0", 33 | "ISC", 34 | "Apache-2.0", 35 | "Apache-2.0 WITH LLVM-exception", 36 | ] 37 | -------------------------------------------------------------------------------- /examples/async.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Spawn dialog on main thread 3 | let task = rfd::AsyncFileDialog::new().pick_file(); 4 | 5 | // Await somewhere else 6 | execute(async { 7 | let file = task.await; 8 | 9 | if let Some(file) = file { 10 | // If you are on native platform you can just get the path 11 | #[cfg(not(target_arch = "wasm32"))] 12 | println!("{:?}", file.path()); 13 | 14 | // If you care about wasm support you just read() the file 15 | file.read().await; 16 | } 17 | }); 18 | 19 | loop {} 20 | } 21 | 22 | use std::future::Future; 23 | 24 | #[cfg(not(target_arch = "wasm32"))] 25 | fn execute + Send + 'static>(f: F) { 26 | // this is stupid... use any executor of your choice instead 27 | std::thread::spawn(move || futures::executor::block_on(f)); 28 | } 29 | #[cfg(target_arch = "wasm32")] 30 | fn execute + 'static>(f: F) { 31 | wasm_bindgen_futures::spawn_local(f); 32 | } 33 | -------------------------------------------------------------------------------- /examples/message-custom-buttons/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/message-custom-buttons/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "message-custom-buttons" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | rfd = { version = "0.11.3", path = "../..", features = ["common-controls-v6"] } 8 | 9 | [build-dependencies] 10 | embed-resource = "2.1" 11 | -------------------------------------------------------------------------------- /examples/message-custom-buttons/app.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/message-custom-buttons/build.rs: -------------------------------------------------------------------------------- 1 | extern crate embed_resource; 2 | 3 | fn main() { 4 | #[cfg(target_os = "windows")] 5 | embed_resource::compile("manifest.rc", embed_resource::NONE); 6 | } 7 | -------------------------------------------------------------------------------- /examples/message-custom-buttons/manifest.rc: -------------------------------------------------------------------------------- 1 | #define RT_MANIFEST 24 2 | 1 RT_MANIFEST "app.exe.manifest" -------------------------------------------------------------------------------- /examples/message-custom-buttons/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let res = rfd::MessageDialog::new() 3 | .set_title("Msg!") 4 | .set_description("Description!") 5 | .set_level(rfd::MessageLevel::Warning) 6 | .set_buttons(rfd::MessageButtons::OkCancelCustom( 7 | "Got it!".to_string(), 8 | "No!".to_string(), 9 | )) 10 | .show(); 11 | println!("{res}"); 12 | 13 | let res = rfd::MessageDialog::new() 14 | .set_title("Do you want to save the changes you made?") 15 | .set_description("Your changes will be lost if you don't save them.") 16 | .set_level(rfd::MessageLevel::Warning) 17 | .set_buttons(rfd::MessageButtons::YesNoCancelCustom( 18 | "Save".to_string(), 19 | "Don't Save".to_string(), 20 | "Cancel".to_string(), 21 | )) 22 | .show(); 23 | println!("{res}"); 24 | } 25 | -------------------------------------------------------------------------------- /examples/msg.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read}; 2 | 3 | fn main() { 4 | #[cfg(not(feature = "gtk3"))] 5 | let res = ""; 6 | #[cfg(any( 7 | target_os = "windows", 8 | target_os = "macos", 9 | all( 10 | any( 11 | target_os = "linux", 12 | target_os = "freebsd", 13 | target_os = "dragonfly", 14 | target_os = "netbsd", 15 | target_os = "openbsd" 16 | ), 17 | feature = "gtk3" 18 | ) 19 | ))] 20 | let res = rfd::MessageDialog::new() 21 | .set_title("Msg!") 22 | .set_description("Description!") 23 | .set_buttons(rfd::MessageButtons::OkCancel) 24 | .set_level(rfd::MessageLevel::Error) 25 | .show(); 26 | println!("res: {}, Ctrl+D", res); 27 | 28 | let mut stdin = io::stdin(); 29 | let mut buffer: Vec = vec![]; 30 | stdin.read_to_end(&mut buffer).unwrap(); 31 | 32 | #[cfg(any( 33 | target_os = "windows", 34 | target_os = "macos", 35 | all( 36 | any( 37 | target_os = "linux", 38 | target_os = "freebsd", 39 | target_os = "dragonfly", 40 | target_os = "netbsd", 41 | target_os = "openbsd" 42 | ), 43 | feature = "gtk3" 44 | ) 45 | ))] 46 | futures::executor::block_on(async move { 47 | let res = rfd::AsyncMessageDialog::new() 48 | .set_title("Msg!") 49 | .set_description("Description!") 50 | .set_buttons(rfd::MessageButtons::OkCancel) 51 | .show() 52 | .await; 53 | println!("res: {}", res); 54 | }); 55 | 56 | println!("Ctrl+D"); 57 | let mut stdin = io::stdin(); 58 | let mut buffer: Vec = vec![]; 59 | stdin.read_to_end(&mut buffer).unwrap(); 60 | println!(); 61 | } 62 | -------------------------------------------------------------------------------- /examples/save.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | fn main() { 3 | let path = std::env::current_dir().unwrap(); 4 | 5 | let res = rfd::FileDialog::new() 6 | .set_file_name("foo.txt") 7 | .set_directory(&path) 8 | .save_file(); 9 | 10 | println!("The user choose: {:#?}", res); 11 | } 12 | 13 | #[cfg(target_arch = "wasm32")] 14 | fn main() { 15 | // On wasm only async dialogs are possible 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | fn main() { 3 | let path = std::env::current_dir().unwrap(); 4 | 5 | let res = rfd::FileDialog::new() 6 | .add_filter("text", &["txt", "rs"]) 7 | .add_filter("rust", &["rs", "toml"]) 8 | .set_directory(&path) 9 | .pick_files(); 10 | 11 | println!("The user choose: {:#?}", res); 12 | } 13 | 14 | #[cfg(target_arch = "wasm32")] 15 | fn main() { 16 | // On wasm only async dialogs are possible 17 | } 18 | -------------------------------------------------------------------------------- /examples/winit-example/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist -------------------------------------------------------------------------------- /examples/winit-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winit-example" 3 | version = "0.1.0" 4 | authors = ["Poly "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | rfd={path="../../"} 9 | 10 | wasm-bindgen="0.2.69" 11 | web-sys={version="0.3.46",features = [ 12 | 'Document', 13 | 'Window', 14 | ]} 15 | 16 | wasm-bindgen-futures = "0.4.19" 17 | winit = "0.29" 18 | futures = {version="0.3.10",features=["thread-pool"]} 19 | -------------------------------------------------------------------------------- /examples/winit-example/Trunk.toml: -------------------------------------------------------------------------------- 1 | [watch] 2 | watch = ["../../src"] -------------------------------------------------------------------------------- /examples/winit-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/winit-example/src/main.rs: -------------------------------------------------------------------------------- 1 | use winit::{ 2 | event::{self, WindowEvent}, 3 | event_loop::EventLoopBuilder, 4 | keyboard::{KeyCode, PhysicalKey}, 5 | }; 6 | 7 | use wasm_bindgen::prelude::*; 8 | 9 | #[wasm_bindgen] 10 | extern "C" { 11 | fn alert(s: &str); 12 | } 13 | 14 | fn main() { 15 | let event_loop = EventLoopBuilder::::with_user_event() 16 | .build() 17 | .expect("Failed"); 18 | let builder = winit::window::WindowBuilder::new(); 19 | 20 | let window = builder.build(&event_loop).unwrap(); 21 | 22 | #[cfg(target_arch = "wasm32")] 23 | { 24 | use winit::platform::web::WindowExtWebSys; 25 | 26 | web_sys::window() 27 | .and_then(|win| win.document()) 28 | .and_then(|doc| doc.body()) 29 | .and_then(|body| { 30 | if let Some(canvas) = window.canvas() { 31 | body.append_child(&canvas.into()).ok() 32 | } else { 33 | None 34 | } 35 | }) 36 | .expect("couldn't append canvas to document body"); 37 | } 38 | let event_loop_proxy = event_loop.create_proxy(); 39 | let executor = Executor::new(); 40 | 41 | event_loop 42 | .run(move |event, target| match event { 43 | event::Event::UserEvent(name) => { 44 | #[cfg(target_arch = "wasm32")] 45 | alert(&name); 46 | #[cfg(not(target_arch = "wasm32"))] 47 | println!("{}", name); 48 | } 49 | event::Event::WindowEvent { event, .. } => match event { 50 | WindowEvent::CloseRequested { .. } => target.exit(), 51 | WindowEvent::KeyboardInput { 52 | event: 53 | event::KeyEvent { 54 | state: event::ElementState::Pressed, 55 | physical_key: PhysicalKey::Code(KeyCode::KeyS), 56 | .. 57 | }, 58 | .. 59 | } => { 60 | let dialog = rfd::AsyncFileDialog::new() 61 | .add_filter("midi", &["mid", "midi"]) 62 | .add_filter("rust", &["rs", "toml"]) 63 | .set_parent(&window) 64 | .save_file(); 65 | 66 | let event_loop_proxy = event_loop_proxy.clone(); 67 | executor.execut(async move { 68 | let file = dialog.await; 69 | 70 | let file = if let Some(file) = file { 71 | file.write(b"Hi! This is a test file").await.unwrap(); 72 | Some(file) 73 | } else { 74 | None 75 | }; 76 | 77 | event_loop_proxy 78 | .send_event(format!("saved file name: {:#?}", file)) 79 | .ok(); 80 | }); 81 | } 82 | WindowEvent::KeyboardInput { 83 | event: 84 | event::KeyEvent { 85 | state: event::ElementState::Pressed, 86 | physical_key: PhysicalKey::Code(KeyCode::KeyF), 87 | .. 88 | }, 89 | .. 90 | } => { 91 | let dialog = rfd::AsyncFileDialog::new() 92 | .add_filter("midi", &["mid", "midi"]) 93 | .add_filter("rust", &["rs", "toml"]) 94 | .set_parent(&window) 95 | .pick_file(); 96 | 97 | let event_loop_proxy = event_loop_proxy.clone(); 98 | executor.execut(async move { 99 | let files = dialog.await; 100 | 101 | // let names: Vec = files.into_iter().map(|f| f.file_name()).collect(); 102 | let names = files; 103 | 104 | event_loop_proxy.send_event(format!("{:#?}", names)).ok(); 105 | }); 106 | } 107 | WindowEvent::DroppedFile(file_path) => { 108 | let dialog = rfd::AsyncMessageDialog::new() 109 | .set_title("File dropped") 110 | .set_description(format!("file path was: {:#?}", file_path)) 111 | .set_buttons(rfd::MessageButtons::YesNo) 112 | .set_parent(&window) 113 | .show(); 114 | 115 | let event_loop_proxy = event_loop_proxy.clone(); 116 | executor.execut(async move { 117 | let val = dialog.await; 118 | event_loop_proxy.send_event(format!("Msg: {}", val)).ok(); 119 | }); 120 | } 121 | WindowEvent::KeyboardInput { 122 | event: 123 | event::KeyEvent { 124 | state: event::ElementState::Pressed, 125 | physical_key: PhysicalKey::Code(KeyCode::KeyM), 126 | .. 127 | }, 128 | .. 129 | } => { 130 | let dialog = rfd::AsyncMessageDialog::new() 131 | .set_title("Msg!") 132 | .set_description("Description!") 133 | .set_buttons(rfd::MessageButtons::YesNo) 134 | .set_parent(&window) 135 | .show(); 136 | 137 | let event_loop_proxy = event_loop_proxy.clone(); 138 | executor.execut(async move { 139 | let val = dialog.await; 140 | event_loop_proxy.send_event(format!("Msg: {}", val)).ok(); 141 | }); 142 | } 143 | _ => {} 144 | }, 145 | _ => {} 146 | }) 147 | .unwrap(); 148 | } 149 | 150 | use std::future::Future; 151 | 152 | struct Executor { 153 | #[cfg(not(target_arch = "wasm32"))] 154 | pool: futures::executor::ThreadPool, 155 | } 156 | 157 | impl Executor { 158 | fn new() -> Self { 159 | Self { 160 | #[cfg(not(target_arch = "wasm32"))] 161 | pool: futures::executor::ThreadPool::new().unwrap(), 162 | } 163 | } 164 | 165 | #[cfg(not(target_arch = "wasm32"))] 166 | fn execut + Send + 'static>(&self, f: F) { 167 | self.pool.spawn_ok(f); 168 | } 169 | #[cfg(target_arch = "wasm32")] 170 | fn execut + 'static>(&self, f: F) { 171 | wasm_bindgen_futures::spawn_local(f); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/backend.rs: -------------------------------------------------------------------------------- 1 | use crate::message_dialog::MessageDialogResult; 2 | use crate::FileHandle; 3 | use std::future::Future; 4 | use std::path::PathBuf; 5 | use std::pin::Pin; 6 | 7 | #[cfg(all( 8 | any( 9 | target_os = "linux", 10 | target_os = "freebsd", 11 | target_os = "dragonfly", 12 | target_os = "netbsd", 13 | target_os = "openbsd" 14 | ), 15 | not(feature = "gtk3") 16 | ))] 17 | mod linux; 18 | 19 | #[cfg(all( 20 | any( 21 | target_os = "linux", 22 | target_os = "freebsd", 23 | target_os = "dragonfly", 24 | target_os = "netbsd", 25 | target_os = "openbsd" 26 | ), 27 | feature = "gtk3" 28 | ))] 29 | mod gtk3; 30 | #[cfg(target_os = "macos")] 31 | mod macos; 32 | #[cfg(target_arch = "wasm32")] 33 | mod wasm; 34 | #[cfg(target_os = "windows")] 35 | mod win_cid; 36 | #[cfg(all( 37 | any( 38 | target_os = "linux", 39 | target_os = "freebsd", 40 | target_os = "dragonfly", 41 | target_os = "netbsd", 42 | target_os = "openbsd" 43 | ), 44 | not(feature = "gtk3") 45 | ))] 46 | mod xdg_desktop_portal; 47 | 48 | // 49 | // Sync 50 | // 51 | 52 | /// Dialog used to pick file/files 53 | #[cfg(not(target_arch = "wasm32"))] 54 | pub trait FilePickerDialogImpl { 55 | fn pick_file(self) -> Option; 56 | fn pick_files(self) -> Option>; 57 | } 58 | 59 | /// Dialog used to save file 60 | #[cfg(not(target_arch = "wasm32"))] 61 | pub trait FileSaveDialogImpl { 62 | fn save_file(self) -> Option; 63 | } 64 | 65 | /// Dialog used to pick folder 66 | #[cfg(not(target_arch = "wasm32"))] 67 | pub trait FolderPickerDialogImpl { 68 | fn pick_folder(self) -> Option; 69 | fn pick_folders(self) -> Option>; 70 | } 71 | 72 | pub trait MessageDialogImpl { 73 | fn show(self) -> MessageDialogResult; 74 | } 75 | 76 | // 77 | // Async 78 | // 79 | 80 | // Return type of async dialogs: 81 | #[cfg(not(target_arch = "wasm32"))] 82 | pub type DialogFutureType = Pin + Send>>; 83 | #[cfg(target_arch = "wasm32")] 84 | pub type DialogFutureType = Pin>>; 85 | 86 | /// Dialog used to pick file/files 87 | pub trait AsyncFilePickerDialogImpl { 88 | fn pick_file_async(self) -> DialogFutureType>; 89 | fn pick_files_async(self) -> DialogFutureType>>; 90 | } 91 | 92 | /// Dialog used to pick folder 93 | pub trait AsyncFolderPickerDialogImpl { 94 | fn pick_folder_async(self) -> DialogFutureType>; 95 | fn pick_folders_async(self) -> DialogFutureType>>; 96 | } 97 | 98 | /// Dialog used to pick folder 99 | pub trait AsyncFileSaveDialogImpl { 100 | fn save_file_async(self) -> DialogFutureType>; 101 | } 102 | 103 | pub trait AsyncMessageDialogImpl { 104 | fn show_async(self) -> DialogFutureType; 105 | } 106 | -------------------------------------------------------------------------------- /src/backend/gtk3.rs: -------------------------------------------------------------------------------- 1 | mod file_dialog; 2 | mod message_dialog; 3 | 4 | mod gtk_future; 5 | 6 | mod utils; 7 | 8 | pub(self) trait AsGtkDialog { 9 | fn gtk_dialog_ptr(&self) -> *mut gtk_sys::GtkDialog; 10 | unsafe fn show(&self); 11 | } 12 | -------------------------------------------------------------------------------- /src/backend/gtk3/file_dialog.rs: -------------------------------------------------------------------------------- 1 | pub mod dialog_ffi; 2 | 3 | use dialog_ffi::GtkFileDialog; 4 | 5 | use std::path::PathBuf; 6 | 7 | use super::utils::GtkGlobalThread; 8 | use crate::backend::DialogFutureType; 9 | use crate::{FileDialog, FileHandle}; 10 | 11 | use super::gtk_future::GtkDialogFuture; 12 | 13 | // 14 | // File Picker 15 | // 16 | 17 | use crate::backend::FilePickerDialogImpl; 18 | impl FilePickerDialogImpl for FileDialog { 19 | fn pick_file(self) -> Option { 20 | GtkGlobalThread::instance().run_blocking(move || { 21 | let dialog = GtkFileDialog::build_pick_file(&self); 22 | 23 | if dialog.run() == gtk_sys::GTK_RESPONSE_ACCEPT { 24 | dialog.get_result() 25 | } else { 26 | None 27 | } 28 | }) 29 | } 30 | 31 | fn pick_files(self) -> Option> { 32 | GtkGlobalThread::instance().run_blocking(move || { 33 | let dialog = GtkFileDialog::build_pick_files(&self); 34 | 35 | if dialog.run() == gtk_sys::GTK_RESPONSE_ACCEPT { 36 | Some(dialog.get_results()) 37 | } else { 38 | None 39 | } 40 | }) 41 | } 42 | } 43 | 44 | use crate::backend::AsyncFilePickerDialogImpl; 45 | impl AsyncFilePickerDialogImpl for FileDialog { 46 | fn pick_file_async(self) -> DialogFutureType> { 47 | let builder = move || GtkFileDialog::build_pick_file(&self); 48 | 49 | let future = GtkDialogFuture::new(builder, |dialog, res_id| { 50 | if res_id == gtk_sys::GTK_RESPONSE_ACCEPT { 51 | dialog.get_result().map(FileHandle::wrap) 52 | } else { 53 | None 54 | } 55 | }); 56 | 57 | Box::pin(future) 58 | } 59 | 60 | fn pick_files_async(self) -> DialogFutureType>> { 61 | let builder = move || GtkFileDialog::build_pick_files(&self); 62 | 63 | let future = GtkDialogFuture::new(builder, |dialog, res_id| { 64 | if res_id == gtk_sys::GTK_RESPONSE_ACCEPT { 65 | Some( 66 | dialog 67 | .get_results() 68 | .into_iter() 69 | .map(FileHandle::wrap) 70 | .collect(), 71 | ) 72 | } else { 73 | None 74 | } 75 | }); 76 | 77 | Box::pin(future) 78 | } 79 | } 80 | 81 | // 82 | // Folder Picker 83 | // 84 | 85 | use crate::backend::FolderPickerDialogImpl; 86 | impl FolderPickerDialogImpl for FileDialog { 87 | fn pick_folder(self) -> Option { 88 | GtkGlobalThread::instance().run_blocking(move || { 89 | let dialog = GtkFileDialog::build_pick_folder(&self); 90 | 91 | if dialog.run() == gtk_sys::GTK_RESPONSE_ACCEPT { 92 | dialog.get_result() 93 | } else { 94 | None 95 | } 96 | }) 97 | } 98 | 99 | fn pick_folders(self) -> Option> { 100 | GtkGlobalThread::instance().run_blocking(move || { 101 | let dialog = GtkFileDialog::build_pick_folders(&self); 102 | 103 | if dialog.run() == gtk_sys::GTK_RESPONSE_ACCEPT { 104 | Some(dialog.get_results()) 105 | } else { 106 | None 107 | } 108 | }) 109 | } 110 | } 111 | 112 | use crate::backend::AsyncFolderPickerDialogImpl; 113 | impl AsyncFolderPickerDialogImpl for FileDialog { 114 | fn pick_folder_async(self) -> DialogFutureType> { 115 | let builder = move || GtkFileDialog::build_pick_folder(&self); 116 | 117 | let future = GtkDialogFuture::new(builder, |dialog, res_id| { 118 | if res_id == gtk_sys::GTK_RESPONSE_ACCEPT { 119 | dialog.get_result().map(FileHandle::wrap) 120 | } else { 121 | None 122 | } 123 | }); 124 | 125 | Box::pin(future) 126 | } 127 | 128 | fn pick_folders_async(self) -> DialogFutureType>> { 129 | let builder = move || GtkFileDialog::build_pick_folders(&self); 130 | 131 | let future = GtkDialogFuture::new(builder, |dialog, res_id| { 132 | if res_id == gtk_sys::GTK_RESPONSE_ACCEPT { 133 | Some( 134 | dialog 135 | .get_results() 136 | .into_iter() 137 | .map(FileHandle::wrap) 138 | .collect(), 139 | ) 140 | } else { 141 | None 142 | } 143 | }); 144 | 145 | Box::pin(future) 146 | } 147 | } 148 | 149 | // 150 | // File Save 151 | // 152 | 153 | use crate::backend::FileSaveDialogImpl; 154 | impl FileSaveDialogImpl for FileDialog { 155 | fn save_file(self) -> Option { 156 | GtkGlobalThread::instance().run_blocking(move || { 157 | let dialog = GtkFileDialog::build_save_file(&self); 158 | 159 | if dialog.run() == gtk_sys::GTK_RESPONSE_ACCEPT { 160 | dialog.get_result() 161 | } else { 162 | None 163 | } 164 | }) 165 | } 166 | } 167 | 168 | use crate::backend::AsyncFileSaveDialogImpl; 169 | impl AsyncFileSaveDialogImpl for FileDialog { 170 | fn save_file_async(self) -> DialogFutureType> { 171 | let builder = move || GtkFileDialog::build_save_file(&self); 172 | 173 | let future = GtkDialogFuture::new(builder, |dialog, res_id| { 174 | if res_id == gtk_sys::GTK_RESPONSE_ACCEPT { 175 | dialog.get_result().map(FileHandle::wrap) 176 | } else { 177 | None 178 | } 179 | }); 180 | 181 | Box::pin(future) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/backend/gtk3/file_dialog/dialog_ffi.rs: -------------------------------------------------------------------------------- 1 | use super::super::AsGtkDialog; 2 | use crate::FileDialog; 3 | use gtk_sys::GtkFileChooserNative; 4 | 5 | use std::{ 6 | ffi::{CStr, CString}, 7 | ops::Deref, 8 | path::{Path, PathBuf}, 9 | ptr, 10 | }; 11 | 12 | #[repr(i32)] 13 | pub enum GtkFileChooserAction { 14 | Open = 0, 15 | Save = 1, 16 | SelectFolder = 2, 17 | // CreateFolder = 3, 18 | } 19 | 20 | pub struct GtkFileDialog { 21 | pub ptr: *mut GtkFileChooserNative, 22 | } 23 | 24 | impl GtkFileDialog { 25 | fn new(title: &str, action: GtkFileChooserAction) -> Self { 26 | let title = CString::new(title).unwrap(); 27 | 28 | let ptr = unsafe { 29 | let dialog = gtk_sys::gtk_file_chooser_native_new( 30 | title.as_ptr(), 31 | ptr::null_mut(), 32 | action as i32, 33 | // passing null for the texts will use the default text, which has full support for i18n 34 | std::ptr::null(), 35 | std::ptr::null(), 36 | ); 37 | dialog as _ 38 | }; 39 | 40 | Self { ptr } 41 | } 42 | 43 | fn add_filters(&mut self, filters: &[crate::file_dialog::Filter]) { 44 | for f in filters.iter() { 45 | if let Ok(name) = CString::new(f.name.as_str()) { 46 | unsafe { 47 | let filter = gtk_sys::gtk_file_filter_new(); 48 | 49 | let paterns: Vec<_> = f 50 | .extensions 51 | .iter() 52 | .filter_map(|e| CString::new(format!("*.{}", e)).ok()) 53 | .collect(); 54 | 55 | gtk_sys::gtk_file_filter_set_name(filter, name.as_ptr()); 56 | 57 | for p in paterns.iter() { 58 | gtk_sys::gtk_file_filter_add_pattern(filter, p.as_ptr()); 59 | } 60 | 61 | gtk_sys::gtk_file_chooser_add_filter(self.ptr as _, filter); 62 | } 63 | } 64 | } 65 | } 66 | 67 | fn set_file_name(&self, name: Option<&str>) { 68 | if let Some(name) = name { 69 | if let Ok(name) = CString::new(name) { 70 | unsafe { 71 | gtk_sys::gtk_file_chooser_set_filename(self.ptr as _, name.as_ptr()); 72 | } 73 | } 74 | } 75 | } 76 | 77 | fn set_current_name(&self, name: Option<&str>) { 78 | if let Some(name) = name { 79 | if let Ok(name) = CString::new(name) { 80 | unsafe { 81 | gtk_sys::gtk_file_chooser_set_current_name(self.ptr as _, name.as_ptr()); 82 | } 83 | } 84 | } 85 | } 86 | 87 | fn set_path(&self, path: Option<&Path>) { 88 | if let Some(path) = path { 89 | if let Some(path) = path.to_str() { 90 | if let Ok(path) = CString::new(path) { 91 | unsafe { 92 | gtk_sys::gtk_file_chooser_set_current_folder(self.ptr as _, path.as_ptr()); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | pub fn get_result(&self) -> Option { 100 | let cstr = unsafe { 101 | let chosen_filename = gtk_sys::gtk_file_chooser_get_filename(self.ptr as _); 102 | CStr::from_ptr(chosen_filename).to_str() 103 | }; 104 | 105 | if let Ok(cstr) = cstr { 106 | Some(PathBuf::from(cstr.to_owned())) 107 | } else { 108 | None 109 | } 110 | } 111 | 112 | pub fn get_results(&self) -> Vec { 113 | #[derive(Debug)] 114 | struct FileList(*mut glib_sys::GSList); 115 | 116 | impl Iterator for FileList { 117 | type Item = glib_sys::GSList; 118 | fn next(&mut self) -> Option { 119 | let curr_ptr = self.0; 120 | 121 | if !curr_ptr.is_null() { 122 | let curr = unsafe { *curr_ptr }; 123 | 124 | self.0 = curr.next; 125 | 126 | Some(curr) 127 | } else { 128 | None 129 | } 130 | } 131 | } 132 | 133 | let chosen_filenames = 134 | unsafe { gtk_sys::gtk_file_chooser_get_filenames(self.ptr as *mut _) }; 135 | 136 | let paths: Vec = FileList(chosen_filenames) 137 | .filter_map(|item| { 138 | let cstr = unsafe { CStr::from_ptr(item.data as _).to_str() }; 139 | 140 | if let Ok(cstr) = cstr { 141 | Some(PathBuf::from(cstr.to_owned())) 142 | } else { 143 | None 144 | } 145 | }) 146 | .collect(); 147 | 148 | paths 149 | } 150 | 151 | pub fn run(&self) -> i32 { 152 | unsafe { gtk_sys::gtk_native_dialog_run(self.ptr as *mut _) } 153 | } 154 | } 155 | 156 | impl GtkFileDialog { 157 | pub fn build_pick_file(opt: &FileDialog) -> Self { 158 | let mut dialog = GtkFileDialog::new( 159 | opt.title.as_deref().unwrap_or("Open File"), 160 | GtkFileChooserAction::Open, 161 | ); 162 | 163 | dialog.add_filters(&opt.filters); 164 | dialog.set_path(opt.starting_directory.as_deref()); 165 | 166 | if let (Some(mut path), Some(file_name)) = 167 | (opt.starting_directory.to_owned(), opt.file_name.as_deref()) 168 | { 169 | path.push(file_name); 170 | dialog.set_file_name(path.deref().to_str()); 171 | } else { 172 | dialog.set_file_name(opt.file_name.as_deref()); 173 | } 174 | 175 | dialog 176 | } 177 | 178 | pub fn build_save_file(opt: &FileDialog) -> Self { 179 | let mut dialog = GtkFileDialog::new( 180 | opt.title.as_deref().unwrap_or("Save File"), 181 | GtkFileChooserAction::Save, 182 | ); 183 | 184 | unsafe { gtk_sys::gtk_file_chooser_set_do_overwrite_confirmation(dialog.ptr as _, 1) }; 185 | 186 | dialog.add_filters(&opt.filters); 187 | dialog.set_path(opt.starting_directory.as_deref()); 188 | 189 | if let (Some(mut path), Some(file_name)) = 190 | (opt.starting_directory.to_owned(), opt.file_name.as_deref()) 191 | { 192 | path.push(file_name); 193 | if path.exists() { 194 | // the user edited an existing document 195 | dialog.set_file_name(path.deref().to_str()); 196 | } else { 197 | // the user just created a new document 198 | dialog.set_current_name(opt.file_name.as_deref()); 199 | } 200 | } else { 201 | // the user just created a new document 202 | dialog.set_current_name(opt.file_name.as_deref()); 203 | } 204 | 205 | dialog 206 | } 207 | 208 | pub fn build_pick_folder(opt: &FileDialog) -> Self { 209 | let dialog = GtkFileDialog::new( 210 | opt.title.as_deref().unwrap_or("Select Folder"), 211 | GtkFileChooserAction::SelectFolder, 212 | ); 213 | dialog.set_path(opt.starting_directory.as_deref()); 214 | 215 | if let (Some(mut path), Some(file_name)) = 216 | (opt.starting_directory.to_owned(), opt.file_name.as_deref()) 217 | { 218 | path.push(file_name); 219 | dialog.set_file_name(path.deref().to_str()); 220 | } else { 221 | dialog.set_file_name(opt.file_name.as_deref()); 222 | } 223 | 224 | dialog 225 | } 226 | 227 | pub fn build_pick_folders(opt: &FileDialog) -> Self { 228 | let dialog = GtkFileDialog::new( 229 | opt.title.as_deref().unwrap_or("Select Folder"), 230 | GtkFileChooserAction::SelectFolder, 231 | ); 232 | unsafe { gtk_sys::gtk_file_chooser_set_select_multiple(dialog.ptr as _, 1) }; 233 | dialog.set_path(opt.starting_directory.as_deref()); 234 | 235 | if let (Some(mut path), Some(file_name)) = 236 | (opt.starting_directory.to_owned(), opt.file_name.as_deref()) 237 | { 238 | path.push(file_name); 239 | dialog.set_file_name(path.deref().to_str()); 240 | } else { 241 | dialog.set_file_name(opt.file_name.as_deref()); 242 | } 243 | 244 | dialog 245 | } 246 | 247 | pub fn build_pick_files(opt: &FileDialog) -> Self { 248 | let mut dialog = GtkFileDialog::new( 249 | opt.title.as_deref().unwrap_or("Open File"), 250 | GtkFileChooserAction::Open, 251 | ); 252 | 253 | unsafe { gtk_sys::gtk_file_chooser_set_select_multiple(dialog.ptr as _, 1) }; 254 | dialog.add_filters(&opt.filters); 255 | dialog.set_path(opt.starting_directory.as_deref()); 256 | 257 | if let (Some(mut path), Some(file_name)) = 258 | (opt.starting_directory.to_owned(), opt.file_name.as_deref()) 259 | { 260 | path.push(file_name); 261 | dialog.set_file_name(path.deref().to_str()); 262 | } else { 263 | dialog.set_file_name(opt.file_name.as_deref()); 264 | } 265 | 266 | dialog 267 | } 268 | } 269 | 270 | impl AsGtkDialog for GtkFileDialog { 271 | fn gtk_dialog_ptr(&self) -> *mut gtk_sys::GtkDialog { 272 | self.ptr as *mut _ 273 | } 274 | 275 | unsafe fn show(&self) { 276 | gtk_sys::gtk_native_dialog_show(self.ptr as *mut _); 277 | } 278 | } 279 | 280 | impl Drop for GtkFileDialog { 281 | fn drop(&mut self) { 282 | unsafe { 283 | gtk_sys::gtk_native_dialog_destroy(self.ptr as _); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/backend/gtk3/gtk_future.rs: -------------------------------------------------------------------------------- 1 | use super::utils::GtkGlobalThread; 2 | 3 | use std::pin::Pin; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | use std::task::{Context, Poll, Waker}; 7 | 8 | use super::AsGtkDialog; 9 | 10 | struct FutureState { 11 | waker: Option, 12 | data: Option, 13 | dialog: Option, 14 | } 15 | 16 | unsafe impl Send for FutureState {} 17 | 18 | pub(super) struct GtkDialogFuture { 19 | state: Arc>>, 20 | } 21 | 22 | unsafe impl Send for GtkDialogFuture {} 23 | 24 | impl GtkDialogFuture { 25 | pub fn new(build: B, cb: F) -> Self 26 | where 27 | B: FnOnce() -> D + Send + 'static, 28 | F: Fn(&mut D, i32) -> R + Send + 'static, 29 | { 30 | let state = Arc::new(Mutex::new(FutureState { 31 | waker: None, 32 | data: None, 33 | dialog: None, 34 | })); 35 | 36 | { 37 | let state = state.clone(); 38 | let callback = { 39 | let state = state.clone(); 40 | 41 | move |res_id| { 42 | let mut state = state.lock().unwrap(); 43 | 44 | if let Some(mut dialog) = state.dialog.take() { 45 | state.data = Some(cb(&mut dialog, res_id)); 46 | } 47 | 48 | if let Some(waker) = state.waker.take() { 49 | waker.wake(); 50 | } 51 | } 52 | }; 53 | 54 | GtkGlobalThread::instance().run(move || { 55 | let mut state = state.lock().unwrap(); 56 | state.dialog = Some(build()); 57 | 58 | unsafe { 59 | let dialog = state.dialog.as_ref().unwrap(); 60 | dialog.show(); 61 | 62 | let ptr = dialog.gtk_dialog_ptr(); 63 | connect_response(ptr as *mut _, callback); 64 | } 65 | }); 66 | } 67 | 68 | Self { state } 69 | } 70 | } 71 | 72 | impl std::future::Future for GtkDialogFuture { 73 | type Output = R; 74 | 75 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 76 | let mut state = self.state.lock().unwrap(); 77 | 78 | if state.data.is_some() { 79 | Poll::Ready(state.data.take().unwrap()) 80 | } else { 81 | state.waker = Some(cx.waker().clone()); 82 | Poll::Pending 83 | } 84 | } 85 | } 86 | 87 | use gobject_sys::GCallback; 88 | use gtk_sys::{GtkDialog, GtkResponseType}; 89 | use std::ffi::c_void; 90 | use std::os::raw::c_char; 91 | 92 | unsafe fn connect_raw( 93 | receiver: *mut gobject_sys::GObject, 94 | signal_name: *const c_char, 95 | trampoline: GCallback, 96 | closure: *mut F, 97 | ) { 98 | use std::mem; 99 | 100 | use glib_sys::gpointer; 101 | 102 | unsafe extern "C" fn destroy_closure(ptr: *mut c_void, _: *mut gobject_sys::GClosure) { 103 | // destroy 104 | let _ = Box::::from_raw(ptr as *mut _); 105 | } 106 | assert_eq!(mem::size_of::<*mut F>(), mem::size_of::()); 107 | assert!(trampoline.is_some()); 108 | let handle = gobject_sys::g_signal_connect_data( 109 | receiver, 110 | signal_name, 111 | trampoline, 112 | closure as *mut _, 113 | Some(destroy_closure::), 114 | 0, 115 | ); 116 | assert!(handle > 0); 117 | } 118 | 119 | unsafe fn connect_response(dialog: *mut GtkDialog, f: F) { 120 | use std::mem::transmute; 121 | 122 | unsafe extern "C" fn response_trampoline( 123 | _this: *mut gtk_sys::GtkDialog, 124 | res: GtkResponseType, 125 | f: glib_sys::gpointer, 126 | ) { 127 | let f: &F = &*(f as *const F); 128 | 129 | f(res); 130 | } 131 | let f: Box = Box::new(f); 132 | connect_raw( 133 | dialog as *mut _, 134 | b"response\0".as_ptr() as *const _, 135 | Some(transmute::<_, unsafe extern "C" fn()>( 136 | response_trampoline:: as *const (), 137 | )), 138 | Box::into_raw(f), 139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /src/backend/gtk3/message_dialog.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::ptr; 3 | 4 | use super::gtk_future::GtkDialogFuture; 5 | use super::utils::GtkGlobalThread; 6 | use super::AsGtkDialog; 7 | 8 | use crate::message_dialog::{MessageButtons, MessageDialog, MessageLevel}; 9 | use crate::MessageDialogResult; 10 | 11 | pub struct GtkMessageDialog { 12 | buttons: MessageButtons, 13 | ptr: *mut gtk_sys::GtkDialog, 14 | } 15 | 16 | impl GtkMessageDialog { 17 | pub fn new(opt: MessageDialog) -> Self { 18 | let level = match opt.level { 19 | MessageLevel::Info => gtk_sys::GTK_MESSAGE_INFO, 20 | MessageLevel::Warning => gtk_sys::GTK_MESSAGE_WARNING, 21 | MessageLevel::Error => gtk_sys::GTK_MESSAGE_ERROR, 22 | }; 23 | 24 | let buttons = match opt.buttons { 25 | MessageButtons::Ok => gtk_sys::GTK_BUTTONS_OK, 26 | MessageButtons::OkCancel => gtk_sys::GTK_BUTTONS_OK_CANCEL, 27 | MessageButtons::YesNo => gtk_sys::GTK_BUTTONS_YES_NO, 28 | MessageButtons::YesNoCancel => gtk_sys::GTK_BUTTONS_NONE, 29 | MessageButtons::OkCustom(_) => gtk_sys::GTK_BUTTONS_NONE, 30 | MessageButtons::OkCancelCustom(_, _) => gtk_sys::GTK_BUTTONS_NONE, 31 | MessageButtons::YesNoCancelCustom(_, _, _) => gtk_sys::GTK_BUTTONS_NONE, 32 | }; 33 | 34 | let custom_buttons = match &opt.buttons { 35 | MessageButtons::YesNoCancel => vec![ 36 | Some((CString::new("Yes").unwrap(), gtk_sys::GTK_RESPONSE_YES)), 37 | Some((CString::new("No").unwrap(), gtk_sys::GTK_RESPONSE_NO)), 38 | Some(( 39 | CString::new("Cancel").unwrap(), 40 | gtk_sys::GTK_RESPONSE_CANCEL, 41 | )), 42 | None, 43 | ], 44 | MessageButtons::OkCustom(ok_text) => vec![ 45 | Some(( 46 | CString::new(ok_text.as_bytes()).unwrap(), 47 | gtk_sys::GTK_RESPONSE_OK, 48 | )), 49 | None, 50 | ], 51 | MessageButtons::OkCancelCustom(ok_text, cancel_text) => vec![ 52 | Some(( 53 | CString::new(ok_text.as_bytes()).unwrap(), 54 | gtk_sys::GTK_RESPONSE_OK, 55 | )), 56 | Some(( 57 | CString::new(cancel_text.as_bytes()).unwrap(), 58 | gtk_sys::GTK_RESPONSE_CANCEL, 59 | )), 60 | ], 61 | MessageButtons::YesNoCancelCustom(yes_text, no_text, cancel_text) => vec![ 62 | Some(( 63 | CString::new(yes_text.as_bytes()).unwrap(), 64 | gtk_sys::GTK_RESPONSE_YES, 65 | )), 66 | Some(( 67 | CString::new(no_text.as_bytes()).unwrap(), 68 | gtk_sys::GTK_RESPONSE_NO, 69 | )), 70 | Some(( 71 | CString::new(cancel_text.as_bytes()).unwrap(), 72 | gtk_sys::GTK_RESPONSE_CANCEL, 73 | )), 74 | None, 75 | ], 76 | _ => vec![], 77 | }; 78 | 79 | let s: &str = &opt.title; 80 | let title = CString::new(s).unwrap(); 81 | let s: &str = &opt.description; 82 | let description = CString::new(s).unwrap(); 83 | 84 | let ptr = unsafe { 85 | let dialog = gtk_sys::gtk_message_dialog_new( 86 | ptr::null_mut(), 87 | gtk_sys::GTK_DIALOG_MODAL, 88 | level, 89 | buttons, 90 | b"%s\0".as_ptr() as *mut _, 91 | title.as_ptr(), 92 | ) as *mut gtk_sys::GtkDialog; 93 | 94 | set_child_labels_selectable(dialog); 95 | // Also set the window title, otherwise it would be empty 96 | gtk_sys::gtk_window_set_title(dialog as _, title.as_ptr()); 97 | 98 | for custom_button in custom_buttons { 99 | if let Some((custom_button_cstr, response_id)) = custom_button { 100 | gtk_sys::gtk_dialog_add_button( 101 | dialog, 102 | custom_button_cstr.as_ptr(), 103 | response_id, 104 | ); 105 | } 106 | } 107 | 108 | dialog 109 | }; 110 | 111 | unsafe { 112 | gtk_sys::gtk_message_dialog_format_secondary_text(ptr as *mut _, description.as_ptr()); 113 | } 114 | 115 | Self { 116 | ptr, 117 | buttons: opt.buttons, 118 | } 119 | } 120 | 121 | pub fn run(self) -> MessageDialogResult { 122 | let res = unsafe { gtk_sys::gtk_dialog_run(self.ptr) }; 123 | 124 | use MessageButtons::*; 125 | match (&self.buttons, res) { 126 | (Ok | OkCancel, gtk_sys::GTK_RESPONSE_OK) => MessageDialogResult::Ok, 127 | (Ok | OkCancel | YesNoCancel, gtk_sys::GTK_RESPONSE_CANCEL) => { 128 | MessageDialogResult::Cancel 129 | } 130 | (YesNo | YesNoCancel, gtk_sys::GTK_RESPONSE_YES) => MessageDialogResult::Yes, 131 | (YesNo | YesNoCancel, gtk_sys::GTK_RESPONSE_NO) => MessageDialogResult::No, 132 | (OkCustom(custom), gtk_sys::GTK_RESPONSE_OK) => { 133 | MessageDialogResult::Custom(custom.to_owned()) 134 | } 135 | (OkCancelCustom(custom, _), gtk_sys::GTK_RESPONSE_OK) => { 136 | MessageDialogResult::Custom(custom.to_owned()) 137 | } 138 | (OkCancelCustom(_, custom), gtk_sys::GTK_RESPONSE_CANCEL) => { 139 | MessageDialogResult::Custom(custom.to_owned()) 140 | } 141 | (YesNoCancelCustom(custom, _, _), gtk_sys::GTK_RESPONSE_YES) => { 142 | MessageDialogResult::Custom(custom.to_owned()) 143 | } 144 | (YesNoCancelCustom(_, custom, _), gtk_sys::GTK_RESPONSE_NO) => { 145 | MessageDialogResult::Custom(custom.to_owned()) 146 | } 147 | (YesNoCancelCustom(_, _, custom), gtk_sys::GTK_RESPONSE_CANCEL) => { 148 | MessageDialogResult::Custom(custom.to_owned()) 149 | } 150 | _ => MessageDialogResult::Cancel, 151 | } 152 | } 153 | } 154 | 155 | unsafe fn is_label(type_instance: *const gobject_sys::GTypeInstance) -> bool { 156 | (*(*type_instance).g_class).g_type == gtk_sys::gtk_label_get_type() 157 | } 158 | 159 | /// Sets the child labels of a widget selectable 160 | unsafe fn set_child_labels_selectable(dialog: *mut gtk_sys::GtkDialog) { 161 | let area = gtk_sys::gtk_message_dialog_get_message_area(dialog as _); 162 | let mut children = gtk_sys::gtk_container_get_children(area as _); 163 | while !children.is_null() { 164 | let child = (*children).data; 165 | if is_label(child as _) { 166 | gtk_sys::gtk_label_set_selectable(child as _, 1); 167 | } 168 | children = (*children).next; 169 | } 170 | } 171 | 172 | impl Drop for GtkMessageDialog { 173 | fn drop(&mut self) { 174 | unsafe { 175 | gtk_sys::gtk_widget_destroy(self.ptr as *mut _); 176 | } 177 | } 178 | } 179 | 180 | impl AsGtkDialog for GtkMessageDialog { 181 | fn gtk_dialog_ptr(&self) -> *mut gtk_sys::GtkDialog { 182 | self.ptr as *mut _ 183 | } 184 | unsafe fn show(&self) { 185 | gtk_sys::gtk_widget_show_all(self.ptr as *mut _); 186 | } 187 | } 188 | 189 | use crate::backend::MessageDialogImpl; 190 | 191 | impl MessageDialogImpl for MessageDialog { 192 | fn show(self) -> MessageDialogResult { 193 | GtkGlobalThread::instance().run_blocking(move || { 194 | let dialog = GtkMessageDialog::new(self); 195 | dialog.run() 196 | }) 197 | } 198 | } 199 | 200 | use crate::backend::AsyncMessageDialogImpl; 201 | use crate::backend::DialogFutureType; 202 | 203 | impl AsyncMessageDialogImpl for MessageDialog { 204 | fn show_async(self) -> DialogFutureType { 205 | let builder = move || GtkMessageDialog::new(self); 206 | 207 | let future = GtkDialogFuture::new(builder, |_, res| match res { 208 | gtk_sys::GTK_RESPONSE_OK => MessageDialogResult::Ok, 209 | gtk_sys::GTK_RESPONSE_CANCEL => MessageDialogResult::Cancel, 210 | gtk_sys::GTK_RESPONSE_YES => MessageDialogResult::Yes, 211 | gtk_sys::GTK_RESPONSE_NO => MessageDialogResult::No, 212 | gtk_sys::GTK_RESPONSE_DELETE_EVENT => MessageDialogResult::Cancel, 213 | _ => unreachable!(), 214 | }); 215 | Box::pin(future) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/backend/gtk3/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | use std::sync::OnceLock; 4 | use std::sync::{Arc, Condvar, Mutex}; 5 | use std::thread::spawn; 6 | 7 | static GTK_THREAD: OnceLock = OnceLock::new(); 8 | 9 | /// GTK functions are not thread-safe, and must all be called from the thread that initialized GTK. To ensure this, we 10 | /// spawn one thread the first time a GTK dialog is opened and keep it open for the entire lifetime of the application, 11 | /// as GTK cannot be de-initialized or re-initialized on another thread. You're stuck on the thread on which you first 12 | /// initialize GTK. 13 | pub struct GtkGlobalThread { 14 | running: Arc, 15 | } 16 | 17 | impl GtkGlobalThread { 18 | /// Return the global, lazily-initialized instance of the global GTK thread. 19 | pub(super) fn instance() -> &'static Self { 20 | GTK_THREAD.get_or_init(|| Self::new()) 21 | } 22 | 23 | fn new() -> Self { 24 | // When the GtkGlobalThread is eventually dropped, we will set `running` to false and wake up the loop so 25 | // gtk_main_iteration unblocks and we exit the thread on the next iteration. 26 | let running = Arc::new(AtomicBool::new(true)); 27 | let thread_running = Arc::clone(&running); 28 | 29 | spawn(move || { 30 | let initialized = 31 | unsafe { gtk_sys::gtk_init_check(ptr::null_mut(), ptr::null_mut()) == 1 }; 32 | if !initialized { 33 | return; 34 | } 35 | 36 | loop { 37 | if !thread_running.load(Ordering::Acquire) { 38 | break; 39 | } 40 | 41 | unsafe { 42 | gtk_sys::gtk_main_iteration(); 43 | } 44 | } 45 | }); 46 | 47 | Self { 48 | running: Arc::new(AtomicBool::new(true)), 49 | } 50 | } 51 | 52 | /// Run a function on the GTK thread, blocking on the result which is then passed back. 53 | pub(super) fn run_blocking< 54 | T: Send + Clone + std::fmt::Debug + 'static, 55 | F: FnOnce() -> T + Send + 'static, 56 | >( 57 | &self, 58 | cb: F, 59 | ) -> T { 60 | let data: Arc<(Mutex>, _)> = Arc::new((Mutex::new(None), Condvar::new())); 61 | let thread_data = Arc::clone(&data); 62 | let mut cb = Some(cb); 63 | unsafe { 64 | connect_idle(move || { 65 | // connect_idle takes a FnMut; convert our FnOnce into that by ensuring we only call it once 66 | let res = cb.take().expect("Callback should only be called once")(); 67 | 68 | // pass the result back to the main thread 69 | let (lock, cvar) = &*thread_data; 70 | *lock.lock().unwrap() = Some(res); 71 | cvar.notify_all(); 72 | 73 | glib_sys::GFALSE 74 | }); 75 | }; 76 | 77 | // wait for GTK thread to execute the callback and place the result into `data` 78 | let lock_res = data 79 | .1 80 | .wait_while(data.0.lock().unwrap(), |res| res.is_none()) 81 | .unwrap(); 82 | lock_res.as_ref().unwrap().clone() 83 | } 84 | 85 | /// Launch a function on the GTK thread without blocking. 86 | pub(super) fn run(&self, cb: F) { 87 | let mut cb = Some(cb); 88 | unsafe { 89 | connect_idle(move || { 90 | cb.take().expect("Callback should only be called once")(); 91 | glib_sys::GFALSE 92 | }); 93 | }; 94 | } 95 | } 96 | 97 | impl Drop for GtkGlobalThread { 98 | fn drop(&mut self) { 99 | self.running.store(false, Ordering::Release); 100 | unsafe { glib_sys::g_main_context_wakeup(std::ptr::null_mut()) }; 101 | } 102 | } 103 | 104 | unsafe fn connect_idle glib_sys::gboolean + Send + 'static>(f: F) { 105 | unsafe extern "C" fn response_trampoline glib_sys::gboolean + Send + 'static>( 106 | f: glib_sys::gpointer, 107 | ) -> glib_sys::gboolean { 108 | let f: &mut F = &mut *(f as *mut F); 109 | 110 | f() 111 | } 112 | let f_box: Box = Box::new(f); 113 | 114 | unsafe extern "C" fn destroy_closure(ptr: *mut std::ffi::c_void) { 115 | // destroy 116 | let _ = Box::::from_raw(ptr as *mut _); 117 | } 118 | 119 | glib_sys::g_idle_add_full( 120 | glib_sys::G_PRIORITY_DEFAULT_IDLE, 121 | Some(response_trampoline::), 122 | Box::into_raw(f_box) as glib_sys::gpointer, 123 | Some(destroy_closure::), 124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /src/backend/linux/async_command.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | pin::Pin, 4 | sync::{Arc, Mutex}, 5 | task::{Context, Poll, Waker}, 6 | }; 7 | 8 | struct State { 9 | waker: Option, 10 | data: Option>, 11 | } 12 | 13 | pub struct AsyncCommand { 14 | state: Arc>, 15 | } 16 | 17 | impl AsyncCommand { 18 | pub fn spawn(mut command: std::process::Command) -> Self { 19 | let state = Arc::new(Mutex::new(State { 20 | waker: None, 21 | data: None, 22 | })); 23 | 24 | std::thread::spawn({ 25 | let state = state.clone(); 26 | move || { 27 | let output = command.output(); 28 | 29 | let mut state = state.lock().unwrap(); 30 | state.data = Some(output); 31 | 32 | if let Some(waker) = state.waker.take() { 33 | waker.wake(); 34 | } 35 | } 36 | }); 37 | 38 | Self { state } 39 | } 40 | } 41 | 42 | impl std::future::Future for AsyncCommand { 43 | type Output = io::Result; 44 | 45 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 46 | let mut state = self.state.lock().unwrap(); 47 | 48 | if state.data.is_some() { 49 | Poll::Ready(state.data.take().unwrap()) 50 | } else { 51 | state.waker = Some(cx.waker().clone()); 52 | Poll::Pending 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/backend/linux/mod.rs: -------------------------------------------------------------------------------- 1 | mod async_command; 2 | pub(crate) mod zenity; 3 | -------------------------------------------------------------------------------- /src/backend/linux/zenity.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt::Display, path::PathBuf, process::Command}; 2 | 3 | use crate::{ 4 | file_dialog::Filter, 5 | message_dialog::{MessageButtons, MessageLevel}, 6 | FileDialog, MessageDialogResult, 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub enum ZenityError { 11 | Io(std::io::Error), 12 | FromUtf8Error(std::string::FromUtf8Error), 13 | } 14 | 15 | impl Error for ZenityError {} 16 | 17 | impl Display for ZenityError { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | match self { 20 | ZenityError::Io(io) => write!(f, "{io}"), 21 | ZenityError::FromUtf8Error(err) => err.fmt(f), 22 | } 23 | } 24 | } 25 | 26 | impl From for ZenityError { 27 | fn from(value: std::io::Error) -> Self { 28 | Self::Io(value) 29 | } 30 | } 31 | 32 | impl From for ZenityError { 33 | fn from(value: std::string::FromUtf8Error) -> Self { 34 | Self::FromUtf8Error(value) 35 | } 36 | } 37 | 38 | pub type ZenityResult = Result; 39 | 40 | fn command() -> Command { 41 | let mut cmd = Command::new("zenity"); 42 | cmd.arg("--no-markup"); 43 | cmd 44 | } 45 | 46 | fn add_filters(command: &mut Command, filters: &[Filter]) { 47 | for f in filters.iter() { 48 | command.arg("--file-filter"); 49 | 50 | let extensions: Vec<_> = f 51 | .extensions 52 | .iter() 53 | .map(|ext| format!("*.{}", ext)) 54 | .collect(); 55 | 56 | command.arg(format!("{} | {}", f.name, extensions.join(" "))); 57 | } 58 | } 59 | 60 | fn add_filename(command: &mut Command, file_name: &Option) { 61 | if let Some(name) = file_name.as_ref() { 62 | command.arg("--filename"); 63 | command.arg(name); 64 | } 65 | } 66 | 67 | async fn run(command: Command) -> ZenityResult> { 68 | let res = super::async_command::AsyncCommand::spawn(command).await?; 69 | let buffer = String::from_utf8(res.stdout)?; 70 | 71 | Ok((res.status.success() || !buffer.is_empty()).then_some(buffer)) 72 | } 73 | 74 | pub async fn pick_file(dialog: &FileDialog) -> ZenityResult> { 75 | let mut command = command(); 76 | command.arg("--file-selection"); 77 | 78 | add_filters(&mut command, &dialog.filters); 79 | add_filename(&mut command, &dialog.file_name); 80 | 81 | run(command).await.map(|res| { 82 | res.map(|buffer| { 83 | let trimed = buffer.trim(); 84 | trimed.into() 85 | }) 86 | }) 87 | } 88 | 89 | pub async fn pick_files(dialog: &FileDialog) -> ZenityResult> { 90 | let mut command = command(); 91 | command.args(["--file-selection", "--multiple"]); 92 | 93 | add_filters(&mut command, &dialog.filters); 94 | add_filename(&mut command, &dialog.file_name); 95 | 96 | run(command).await.map(|res| { 97 | res.map(|buffer| { 98 | let list = buffer.trim().split('|').map(PathBuf::from).collect(); 99 | list 100 | }) 101 | .unwrap_or_default() 102 | }) 103 | } 104 | 105 | pub async fn pick_folder(dialog: &FileDialog) -> ZenityResult> { 106 | let mut command = command(); 107 | command.args(["--file-selection", "--directory"]); 108 | 109 | add_filters(&mut command, &dialog.filters); 110 | add_filename(&mut command, &dialog.file_name); 111 | 112 | run(command).await.map(|res| { 113 | res.map(|buffer| { 114 | let trimed = buffer.trim(); 115 | trimed.into() 116 | }) 117 | }) 118 | } 119 | 120 | pub async fn pick_folders(dialog: &FileDialog) -> ZenityResult> { 121 | let mut command = command(); 122 | command.args(["--file-selection", "--directory", "--multiple"]); 123 | 124 | add_filters(&mut command, &dialog.filters); 125 | add_filename(&mut command, &dialog.file_name); 126 | 127 | run(command).await.map(|res| { 128 | res.map(|buffer| { 129 | let list = buffer.trim().split('|').map(PathBuf::from).collect(); 130 | list 131 | }) 132 | .unwrap_or_default() 133 | }) 134 | } 135 | 136 | pub async fn save_file(dialog: &FileDialog) -> ZenityResult> { 137 | let mut command = command(); 138 | command.args(["--file-selection", "--save", "--confirm-overwrite"]); 139 | 140 | add_filters(&mut command, &dialog.filters); 141 | add_filename(&mut command, &dialog.file_name); 142 | 143 | run(command).await.map(|res| { 144 | res.map(|buffer| { 145 | let trimed = buffer.trim(); 146 | trimed.into() 147 | }) 148 | }) 149 | } 150 | 151 | pub async fn message( 152 | level: &MessageLevel, 153 | btns: &MessageButtons, 154 | title: &str, 155 | description: &str, 156 | ) -> ZenityResult { 157 | let cmd = match level { 158 | MessageLevel::Info => "--info", 159 | MessageLevel::Warning => "--warning", 160 | MessageLevel::Error => "--error", 161 | }; 162 | 163 | let ok_label = match btns { 164 | MessageButtons::Ok => None, 165 | MessageButtons::OkCustom(ok) => Some(ok), 166 | _ => None, 167 | }; 168 | 169 | let mut command = command(); 170 | command.args([cmd, "--title", title, "--text", description]); 171 | 172 | if let Some(ok) = ok_label { 173 | command.args(["--ok-label", ok]); 174 | } 175 | 176 | run(command).await.map(|res| match res { 177 | Some(_) => MessageDialogResult::Ok, 178 | None => MessageDialogResult::Cancel, 179 | }) 180 | } 181 | 182 | pub async fn question( 183 | btns: &MessageButtons, 184 | title: &str, 185 | description: &str, 186 | ) -> ZenityResult { 187 | let mut command = command(); 188 | command.args(["--question", "--title", title, "--text", description]); 189 | 190 | match btns { 191 | MessageButtons::OkCancel => { 192 | command.args(["--ok-label", "Ok"]); 193 | command.args(["--cancel-label", "Cancel"]); 194 | } 195 | MessageButtons::OkCancelCustom(ok, cancel) => { 196 | command.args(["--ok-label", ok.as_str()]); 197 | command.args(["--cancel-label", cancel.as_str()]); 198 | } 199 | MessageButtons::YesNoCancel => { 200 | command.args(["--extra-button", "No"]); 201 | command.args(["--cancel-label", "Cancel"]); 202 | } 203 | MessageButtons::YesNoCancelCustom(yes, no, cancel) => { 204 | command.args(["--ok-label", yes.as_str()]); 205 | command.args(["--cancel-label", cancel.as_str()]); 206 | command.args(["--extra-button", no.as_str()]); 207 | } 208 | _ => {} 209 | } 210 | 211 | run(command).await.map(|res| match btns { 212 | MessageButtons::OkCancel => match res { 213 | Some(_) => MessageDialogResult::Ok, 214 | None => MessageDialogResult::Cancel, 215 | }, 216 | MessageButtons::YesNo => match res { 217 | Some(_) => MessageDialogResult::Yes, 218 | None => MessageDialogResult::No, 219 | }, 220 | MessageButtons::OkCancelCustom(ok, cancel) => match res { 221 | Some(_) => MessageDialogResult::Custom(ok.clone()), 222 | None => MessageDialogResult::Custom(cancel.clone()), 223 | }, 224 | MessageButtons::YesNoCancel => match res { 225 | Some(output) if output.is_empty() => MessageDialogResult::Yes, 226 | Some(_) => MessageDialogResult::No, 227 | None => MessageDialogResult::Cancel, 228 | }, 229 | MessageButtons::YesNoCancelCustom(yes, no, cancel) => match res { 230 | Some(output) if output.is_empty() => MessageDialogResult::Custom(yes.clone()), 231 | Some(_) => MessageDialogResult::Custom(no.clone()), 232 | None => MessageDialogResult::Custom(cancel.clone()), 233 | }, 234 | _ => MessageDialogResult::Cancel, 235 | }) 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use crate::FileDialog; 241 | 242 | #[test] 243 | #[ignore] 244 | fn message() { 245 | pollster::block_on(super::message( 246 | &crate::message_dialog::MessageLevel::Info, 247 | &crate::message_dialog::MessageButtons::Ok, 248 | "hi", 249 | "me", 250 | )) 251 | .unwrap(); 252 | pollster::block_on(super::message( 253 | &crate::message_dialog::MessageLevel::Warning, 254 | &crate::message_dialog::MessageButtons::Ok, 255 | "hi", 256 | "me", 257 | )) 258 | .unwrap(); 259 | pollster::block_on(super::message( 260 | &crate::message_dialog::MessageLevel::Error, 261 | &crate::message_dialog::MessageButtons::Ok, 262 | "hi", 263 | "me", 264 | )) 265 | .unwrap(); 266 | } 267 | 268 | #[test] 269 | #[ignore] 270 | fn question() { 271 | pollster::block_on(super::question( 272 | &crate::message_dialog::MessageButtons::OkCancel, 273 | "hi", 274 | "me", 275 | )) 276 | .unwrap(); 277 | pollster::block_on(super::question( 278 | &crate::message_dialog::MessageButtons::YesNo, 279 | "hi", 280 | "me", 281 | )) 282 | .unwrap(); 283 | } 284 | 285 | #[test] 286 | #[ignore] 287 | fn pick_file() { 288 | let path = pollster::block_on(super::pick_file(&FileDialog::default())).unwrap(); 289 | dbg!(path); 290 | } 291 | 292 | #[test] 293 | #[ignore] 294 | fn pick_files() { 295 | let path = pollster::block_on(super::pick_files(&FileDialog::default())).unwrap(); 296 | dbg!(path); 297 | } 298 | 299 | #[test] 300 | #[ignore] 301 | fn pick_folder() { 302 | let path = pollster::block_on(super::pick_folder(&FileDialog::default())).unwrap(); 303 | dbg!(path); 304 | } 305 | 306 | #[test] 307 | #[ignore] 308 | fn save_file() { 309 | let path = pollster::block_on(super::save_file(&FileDialog::default())).unwrap(); 310 | dbg!(path); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/backend/macos.rs: -------------------------------------------------------------------------------- 1 | mod file_dialog; 2 | mod message_dialog; 3 | 4 | mod modal_future; 5 | 6 | mod utils; 7 | -------------------------------------------------------------------------------- /src/backend/macos/file_dialog.rs: -------------------------------------------------------------------------------- 1 | use objc2::rc::autoreleasepool; 2 | use objc2_app_kit::NSModalResponseOK; 3 | use std::path::PathBuf; 4 | 5 | mod panel_ffi; 6 | 7 | use self::panel_ffi::Panel; 8 | use super::modal_future::ModalFuture; 9 | use super::utils::{run_on_main, window_from_raw_window_handle}; 10 | use crate::backend::DialogFutureType; 11 | use crate::{FileDialog, FileHandle}; 12 | 13 | // 14 | // File Picker 15 | // 16 | 17 | use crate::backend::FilePickerDialogImpl; 18 | impl FilePickerDialogImpl for FileDialog { 19 | fn pick_file(self) -> Option { 20 | autoreleasepool(move |_| { 21 | run_on_main(move |mtm| { 22 | let panel = Panel::build_pick_file(&self, mtm); 23 | 24 | if panel.run_modal() == NSModalResponseOK { 25 | Some(panel.get_result()) 26 | } else { 27 | None 28 | } 29 | }) 30 | }) 31 | } 32 | 33 | fn pick_files(self) -> Option> { 34 | autoreleasepool(move |_| { 35 | run_on_main(move |mtm| { 36 | let panel = Panel::build_pick_files(&self, mtm); 37 | 38 | if panel.run_modal() == NSModalResponseOK { 39 | Some(panel.get_results()) 40 | } else { 41 | None 42 | } 43 | }) 44 | }) 45 | } 46 | } 47 | 48 | use crate::backend::AsyncFilePickerDialogImpl; 49 | impl AsyncFilePickerDialogImpl for FileDialog { 50 | fn pick_file_async(self) -> DialogFutureType> { 51 | let win = self.parent.as_ref().map(window_from_raw_window_handle); 52 | 53 | let future = ModalFuture::new( 54 | win, 55 | move |mtm| Panel::build_pick_file(&self, mtm), 56 | |panel, res_id| { 57 | if res_id == NSModalResponseOK { 58 | Some(panel.get_result().into()) 59 | } else { 60 | None 61 | } 62 | }, 63 | ); 64 | 65 | Box::pin(future) 66 | } 67 | 68 | fn pick_files_async(self) -> DialogFutureType>> { 69 | let win = self.parent.as_ref().map(window_from_raw_window_handle); 70 | 71 | let future = ModalFuture::new( 72 | win, 73 | move |mtm| Panel::build_pick_files(&self, mtm), 74 | |panel, res_id| { 75 | if res_id == NSModalResponseOK { 76 | Some( 77 | panel 78 | .get_results() 79 | .into_iter() 80 | .map(FileHandle::wrap) 81 | .collect(), 82 | ) 83 | } else { 84 | None 85 | } 86 | }, 87 | ); 88 | 89 | Box::pin(future) 90 | } 91 | } 92 | 93 | // 94 | // Folder Picker 95 | // 96 | 97 | use crate::backend::FolderPickerDialogImpl; 98 | impl FolderPickerDialogImpl for FileDialog { 99 | fn pick_folder(self) -> Option { 100 | autoreleasepool(move |_| { 101 | run_on_main(move |mtm| { 102 | let panel = Panel::build_pick_folder(&self, mtm); 103 | if panel.run_modal() == NSModalResponseOK { 104 | Some(panel.get_result()) 105 | } else { 106 | None 107 | } 108 | }) 109 | }) 110 | } 111 | 112 | fn pick_folders(self) -> Option> { 113 | autoreleasepool(move |_| { 114 | run_on_main(move |mtm| { 115 | let panel = Panel::build_pick_folders(&self, mtm); 116 | if panel.run_modal() == NSModalResponseOK { 117 | Some(panel.get_results()) 118 | } else { 119 | None 120 | } 121 | }) 122 | }) 123 | } 124 | } 125 | 126 | use crate::backend::AsyncFolderPickerDialogImpl; 127 | impl AsyncFolderPickerDialogImpl for FileDialog { 128 | fn pick_folder_async(self) -> DialogFutureType> { 129 | let win = self.parent.as_ref().map(window_from_raw_window_handle); 130 | 131 | let future = ModalFuture::new( 132 | win, 133 | move |mtm| Panel::build_pick_folder(&self, mtm), 134 | |panel, res_id| { 135 | if res_id == NSModalResponseOK { 136 | Some(panel.get_result().into()) 137 | } else { 138 | None 139 | } 140 | }, 141 | ); 142 | 143 | Box::pin(future) 144 | } 145 | 146 | fn pick_folders_async(self) -> DialogFutureType>> { 147 | let win = self.parent.as_ref().map(window_from_raw_window_handle); 148 | 149 | let future = ModalFuture::new( 150 | win, 151 | move |mtm| Panel::build_pick_folders(&self, mtm), 152 | |panel, res_id| { 153 | if res_id == NSModalResponseOK { 154 | Some( 155 | panel 156 | .get_results() 157 | .into_iter() 158 | .map(FileHandle::wrap) 159 | .collect(), 160 | ) 161 | } else { 162 | None 163 | } 164 | }, 165 | ); 166 | 167 | Box::pin(future) 168 | } 169 | } 170 | 171 | // 172 | // File Save 173 | // 174 | 175 | use crate::backend::FileSaveDialogImpl; 176 | impl FileSaveDialogImpl for FileDialog { 177 | fn save_file(self) -> Option { 178 | autoreleasepool(move |_| { 179 | run_on_main(move |mtm| { 180 | let panel = Panel::build_save_file(&self, mtm); 181 | if panel.run_modal() == NSModalResponseOK { 182 | Some(panel.get_result()) 183 | } else { 184 | None 185 | } 186 | }) 187 | }) 188 | } 189 | } 190 | 191 | use crate::backend::AsyncFileSaveDialogImpl; 192 | impl AsyncFileSaveDialogImpl for FileDialog { 193 | fn save_file_async(self) -> DialogFutureType> { 194 | let win = self.parent.as_ref().map(window_from_raw_window_handle); 195 | 196 | let future = ModalFuture::new( 197 | win, 198 | move |mtm| Panel::build_save_file(&self, mtm), 199 | |panel, res_id| { 200 | if res_id == NSModalResponseOK { 201 | Some(panel.get_result().into()) 202 | } else { 203 | None 204 | } 205 | }, 206 | ); 207 | 208 | Box::pin(future) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/backend/macos/file_dialog/panel_ffi.rs: -------------------------------------------------------------------------------- 1 | use core::str; 2 | use std::path::Path; 3 | use std::path::PathBuf; 4 | 5 | use block2::Block; 6 | use objc2::rc::Retained; 7 | use objc2::MainThreadMarker; 8 | use objc2_app_kit::{NSModalResponse, NSOpenPanel, NSSavePanel, NSWindow, NSWindowLevel}; 9 | use objc2_foundation::{NSArray, NSString, NSURL}; 10 | use raw_window_handle::RawWindowHandle; 11 | 12 | use super::super::{ 13 | modal_future::{AsModal, InnerModal}, 14 | utils::{FocusManager, PolicyManager}, 15 | }; 16 | use crate::backend::macos::utils::window_from_raw_window_handle; 17 | use crate::FileDialog; 18 | 19 | extern "C" { 20 | pub fn CGShieldingWindowLevel() -> i32; 21 | } 22 | 23 | pub struct Panel { 24 | // Either `NSSavePanel` or the subclass `NSOpenPanel` 25 | pub(crate) panel: Retained, 26 | parent: Option>, 27 | _focus_manager: FocusManager, 28 | _policy_manager: PolicyManager, 29 | } 30 | 31 | impl AsModal for Panel { 32 | fn inner_modal(&self) -> &(impl InnerModal + 'static) { 33 | &*self.panel 34 | } 35 | } 36 | 37 | impl InnerModal for NSSavePanel { 38 | fn begin_modal(&self, window: &NSWindow, handler: &Block) { 39 | unsafe { self.beginSheetModalForWindow_completionHandler(window, handler) } 40 | } 41 | 42 | fn run_modal(&self) -> NSModalResponse { 43 | unsafe { self.runModal() } 44 | } 45 | } 46 | 47 | impl Panel { 48 | fn as_open_panel(&self) -> Option<&NSOpenPanel> { 49 | self.panel.downcast_ref::() 50 | } 51 | 52 | pub fn new(panel: Retained, parent: Option<&RawWindowHandle>) -> Self { 53 | let _policy_manager = PolicyManager::new(MainThreadMarker::from(&*panel)); 54 | 55 | let _focus_manager = FocusManager::new(MainThreadMarker::from(&*panel)); 56 | 57 | panel.setLevel(unsafe { CGShieldingWindowLevel() } as NSWindowLevel); 58 | Self { 59 | panel, 60 | parent: parent.map(window_from_raw_window_handle), 61 | _focus_manager, 62 | _policy_manager, 63 | } 64 | } 65 | 66 | pub fn run_modal(&self) -> NSModalResponse { 67 | if let Some(parent) = self.parent.clone() { 68 | let completion = block2::StackBlock::new(|_: isize| {}); 69 | 70 | unsafe { 71 | self.panel 72 | .beginSheetModalForWindow_completionHandler(&parent, &completion) 73 | } 74 | } 75 | 76 | unsafe { self.panel.runModal() } 77 | } 78 | 79 | pub fn get_result(&self) -> PathBuf { 80 | unsafe { 81 | let url = self.panel.URL().unwrap(); 82 | url.path().unwrap().to_string().into() 83 | } 84 | } 85 | 86 | pub fn get_results(&self) -> Vec { 87 | unsafe { 88 | let urls = self.as_open_panel().unwrap().URLs(); 89 | 90 | let mut res = Vec::new(); 91 | for url in urls { 92 | res.push(url.path().unwrap().to_string().into()); 93 | } 94 | 95 | res 96 | } 97 | } 98 | } 99 | 100 | trait PanelExt { 101 | fn panel(&self) -> &NSSavePanel; 102 | 103 | fn set_can_create_directories(&self, can: bool) { 104 | unsafe { self.panel().setCanCreateDirectories(can) } 105 | } 106 | 107 | fn add_filters(&self, opt: &FileDialog) { 108 | let mut exts: Vec = Vec::new(); 109 | 110 | for filter in opt.filters.iter() { 111 | exts.append(&mut filter.extensions.to_vec()); 112 | } 113 | 114 | let f_raw: Vec<_> = exts.iter().map(|ext| NSString::from_str(ext)).collect(); 115 | let array = NSArray::from_retained_slice(&f_raw); 116 | 117 | unsafe { 118 | #[allow(deprecated)] 119 | self.panel().setAllowedFileTypes(Some(&array)); 120 | } 121 | } 122 | 123 | fn set_path(&self, path: &Path, file_name: Option<&str>) { 124 | // if file_name is some, and path is a dir 125 | let path = if let (Some(name), true) = (file_name, path.is_dir()) { 126 | let mut path = path.to_owned(); 127 | // add a name to the end of path 128 | path.push(name); 129 | path 130 | } else { 131 | path.to_owned() 132 | }; 133 | 134 | if let Some(path) = path.to_str() { 135 | unsafe { 136 | let url = NSURL::fileURLWithPath_isDirectory(&NSString::from_str(path), true); 137 | self.panel().setDirectoryURL(Some(&url)); 138 | } 139 | } 140 | } 141 | 142 | fn set_file_name(&self, file_name: &str) { 143 | unsafe { 144 | self.panel() 145 | .setNameFieldStringValue(&NSString::from_str(file_name)) 146 | } 147 | } 148 | 149 | fn set_title(&self, title: &str) { 150 | unsafe { self.panel().setMessage(Some(&NSString::from_str(title))) } 151 | } 152 | } 153 | 154 | impl PanelExt for Retained { 155 | fn panel(&self) -> &NSSavePanel { 156 | &self 157 | } 158 | } 159 | 160 | impl PanelExt for Retained { 161 | fn panel(&self) -> &NSSavePanel { 162 | &self 163 | } 164 | } 165 | 166 | impl Panel { 167 | pub fn build_pick_file(opt: &FileDialog, mtm: MainThreadMarker) -> Self { 168 | let panel = unsafe { NSOpenPanel::openPanel(mtm) }; 169 | 170 | if !opt.filters.is_empty() { 171 | panel.add_filters(opt); 172 | } 173 | 174 | if let Some(path) = &opt.starting_directory { 175 | panel.set_path(path, opt.file_name.as_deref()); 176 | } 177 | 178 | if let Some(file_name) = &opt.file_name { 179 | panel.set_file_name(file_name); 180 | } 181 | 182 | if let Some(title) = &opt.title { 183 | panel.set_title(title); 184 | } 185 | 186 | if let Some(can) = opt.can_create_directories { 187 | panel.set_can_create_directories(can); 188 | } 189 | 190 | unsafe { panel.setCanChooseDirectories(false) }; 191 | unsafe { panel.setCanChooseFiles(true) }; 192 | 193 | Self::new(Retained::into_super(panel), opt.parent.as_ref()) 194 | } 195 | 196 | pub fn build_save_file(opt: &FileDialog, mtm: MainThreadMarker) -> Self { 197 | let panel = unsafe { NSSavePanel::savePanel(mtm) }; 198 | 199 | if !opt.filters.is_empty() { 200 | panel.add_filters(opt); 201 | } 202 | 203 | if let Some(path) = &opt.starting_directory { 204 | panel.set_path(path, opt.file_name.as_deref()); 205 | } 206 | 207 | if let Some(file_name) = &opt.file_name { 208 | panel.set_file_name(file_name); 209 | } 210 | 211 | if let Some(title) = &opt.title { 212 | panel.set_title(title); 213 | } 214 | 215 | if let Some(can) = opt.can_create_directories { 216 | panel.set_can_create_directories(can); 217 | } 218 | 219 | Self::new(panel, opt.parent.as_ref()) 220 | } 221 | 222 | pub fn build_pick_folder(opt: &FileDialog, mtm: MainThreadMarker) -> Self { 223 | let panel = unsafe { NSOpenPanel::openPanel(mtm) }; 224 | 225 | if let Some(path) = &opt.starting_directory { 226 | panel.set_path(path, opt.file_name.as_deref()); 227 | } 228 | 229 | if let Some(title) = &opt.title { 230 | panel.set_title(title); 231 | } 232 | 233 | let can = opt.can_create_directories.unwrap_or(true); 234 | panel.set_can_create_directories(can); 235 | 236 | unsafe { panel.setCanChooseDirectories(true) }; 237 | unsafe { panel.setCanChooseFiles(false) }; 238 | 239 | Self::new(Retained::into_super(panel), opt.parent.as_ref()) 240 | } 241 | 242 | pub fn build_pick_folders(opt: &FileDialog, mtm: MainThreadMarker) -> Self { 243 | let panel = unsafe { NSOpenPanel::openPanel(mtm) }; 244 | 245 | if let Some(path) = &opt.starting_directory { 246 | panel.set_path(path, opt.file_name.as_deref()); 247 | } 248 | 249 | if let Some(title) = &opt.title { 250 | panel.set_title(title); 251 | } 252 | 253 | let can = opt.can_create_directories.unwrap_or(true); 254 | panel.set_can_create_directories(can); 255 | 256 | unsafe { panel.setCanChooseDirectories(true) }; 257 | unsafe { panel.setCanChooseFiles(false) }; 258 | unsafe { panel.setAllowsMultipleSelection(true) }; 259 | 260 | Self::new(Retained::into_super(panel), opt.parent.as_ref()) 261 | } 262 | 263 | pub fn build_pick_files(opt: &FileDialog, mtm: MainThreadMarker) -> Self { 264 | let panel = unsafe { NSOpenPanel::openPanel(mtm) }; 265 | 266 | if !opt.filters.is_empty() { 267 | panel.add_filters(opt); 268 | } 269 | 270 | if let Some(path) = &opt.starting_directory { 271 | panel.set_path(path, opt.file_name.as_deref()); 272 | } 273 | 274 | if let Some(title) = &opt.title { 275 | panel.set_title(title); 276 | } 277 | 278 | if let Some(can) = opt.can_create_directories { 279 | panel.set_can_create_directories(can); 280 | } 281 | 282 | unsafe { panel.setCanChooseDirectories(false) }; 283 | unsafe { panel.setCanChooseFiles(true) }; 284 | unsafe { panel.setAllowsMultipleSelection(true) }; 285 | 286 | Self::new(Retained::into_super(panel), opt.parent.as_ref()) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/backend/macos/message_dialog.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::DialogFutureType; 2 | use crate::message_dialog::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}; 3 | 4 | use super::modal_future::AsModal; 5 | use super::{ 6 | modal_future::{InnerModal, ModalFuture}, 7 | utils::{self, run_on_main, FocusManager, PolicyManager}, 8 | }; 9 | 10 | use super::utils::window_from_raw_window_handle; 11 | use block2::Block; 12 | use objc2::rc::{autoreleasepool, Retained}; 13 | use objc2::MainThreadMarker; 14 | use objc2_app_kit::{ 15 | NSAlert, NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, NSAlertStyle, 16 | NSAlertThirdButtonReturn, NSApplication, NSModalResponse, NSWindow, 17 | }; 18 | use objc2_foundation::NSString; 19 | 20 | pub struct Alert { 21 | buttons: MessageButtons, 22 | alert: Retained, 23 | parent: Option>, 24 | _focus_manager: FocusManager, 25 | _policy_manager: PolicyManager, 26 | } 27 | 28 | impl Alert { 29 | pub fn new(opt: MessageDialog, mtm: MainThreadMarker) -> Self { 30 | let _policy_manager = PolicyManager::new(mtm); 31 | 32 | let alert = unsafe { NSAlert::new(mtm) }; 33 | 34 | let level = match opt.level { 35 | MessageLevel::Info => NSAlertStyle::Informational, 36 | MessageLevel::Warning => NSAlertStyle::Warning, 37 | MessageLevel::Error => NSAlertStyle::Critical, 38 | }; 39 | 40 | unsafe { alert.setAlertStyle(level) }; 41 | 42 | let buttons = match &opt.buttons { 43 | MessageButtons::Ok => vec!["OK".to_owned()], 44 | MessageButtons::OkCancel => vec!["OK".to_owned(), "Cancel".to_owned()], 45 | MessageButtons::YesNo => vec!["Yes".to_owned(), "No".to_owned()], 46 | MessageButtons::YesNoCancel => { 47 | vec!["Yes".to_owned(), "No".to_owned(), "Cancel".to_owned()] 48 | } 49 | MessageButtons::OkCustom(ok_text) => vec![ok_text.to_owned()], 50 | MessageButtons::OkCancelCustom(ok_text, cancel_text) => { 51 | vec![ok_text.to_owned(), cancel_text.to_owned()] 52 | } 53 | MessageButtons::YesNoCancelCustom(yes_text, no_text, cancel_text) => { 54 | vec![ 55 | yes_text.to_owned(), 56 | no_text.to_owned(), 57 | cancel_text.to_owned(), 58 | ] 59 | } 60 | }; 61 | 62 | for button in buttons { 63 | let label = NSString::from_str(&button); 64 | unsafe { alert.addButtonWithTitle(&label) }; 65 | } 66 | 67 | unsafe { 68 | let text = NSString::from_str(&opt.title); 69 | alert.setMessageText(&text); 70 | let text = NSString::from_str(&opt.description); 71 | alert.setInformativeText(&text); 72 | } 73 | 74 | let _focus_manager = FocusManager::new(mtm); 75 | 76 | Self { 77 | alert, 78 | parent: opt.parent.map(|x| window_from_raw_window_handle(&x)), 79 | buttons: opt.buttons, 80 | _focus_manager, 81 | _policy_manager, 82 | } 83 | } 84 | 85 | pub fn run(mut self) -> MessageDialogResult { 86 | let mtm = MainThreadMarker::from(&*self.alert); 87 | 88 | if let Some(parent) = self.parent.take() { 89 | let completion = { 90 | block2::StackBlock::new(move |result| unsafe { 91 | NSApplication::sharedApplication(mtm).stopModalWithCode(result); 92 | }) 93 | }; 94 | 95 | unsafe { 96 | self.alert 97 | .beginSheetModalForWindow_completionHandler(&parent, Some(&completion)) 98 | } 99 | } 100 | 101 | dialog_result(&self.buttons, unsafe { self.alert.runModal() }) 102 | } 103 | } 104 | 105 | fn dialog_result(buttons: &MessageButtons, ret: NSModalResponse) -> MessageDialogResult { 106 | match buttons { 107 | MessageButtons::Ok if ret == NSAlertFirstButtonReturn => MessageDialogResult::Ok, 108 | MessageButtons::OkCancel if ret == NSAlertFirstButtonReturn => MessageDialogResult::Ok, 109 | MessageButtons::OkCancel if ret == NSAlertSecondButtonReturn => MessageDialogResult::Cancel, 110 | MessageButtons::YesNo if ret == NSAlertFirstButtonReturn => MessageDialogResult::Yes, 111 | MessageButtons::YesNo if ret == NSAlertSecondButtonReturn => MessageDialogResult::No, 112 | MessageButtons::YesNoCancel if ret == NSAlertFirstButtonReturn => MessageDialogResult::Yes, 113 | MessageButtons::YesNoCancel if ret == NSAlertSecondButtonReturn => MessageDialogResult::No, 114 | MessageButtons::YesNoCancel if ret == NSAlertThirdButtonReturn => { 115 | MessageDialogResult::Cancel 116 | } 117 | MessageButtons::OkCustom(custom) if ret == NSAlertFirstButtonReturn => { 118 | MessageDialogResult::Custom(custom.to_owned()) 119 | } 120 | MessageButtons::OkCancelCustom(custom, _) if ret == NSAlertFirstButtonReturn => { 121 | MessageDialogResult::Custom(custom.to_owned()) 122 | } 123 | MessageButtons::OkCancelCustom(_, custom) if ret == NSAlertSecondButtonReturn => { 124 | MessageDialogResult::Custom(custom.to_owned()) 125 | } 126 | MessageButtons::YesNoCancelCustom(custom, _, _) if ret == NSAlertFirstButtonReturn => { 127 | MessageDialogResult::Custom(custom.to_owned()) 128 | } 129 | MessageButtons::YesNoCancelCustom(_, custom, _) if ret == NSAlertSecondButtonReturn => { 130 | MessageDialogResult::Custom(custom.to_owned()) 131 | } 132 | MessageButtons::YesNoCancelCustom(_, _, custom) if ret == NSAlertThirdButtonReturn => { 133 | MessageDialogResult::Custom(custom.to_owned()) 134 | } 135 | _ => MessageDialogResult::Cancel, 136 | } 137 | } 138 | 139 | impl AsModal for Alert { 140 | fn inner_modal(&self) -> &(impl InnerModal + 'static) { 141 | &*self.alert 142 | } 143 | } 144 | 145 | impl InnerModal for NSAlert { 146 | fn begin_modal(&self, window: &NSWindow, handler: &Block) { 147 | unsafe { self.beginSheetModalForWindow_completionHandler(window, Some(handler)) } 148 | } 149 | 150 | fn run_modal(&self) -> NSModalResponse { 151 | unsafe { self.runModal() } 152 | } 153 | } 154 | 155 | use crate::backend::MessageDialogImpl; 156 | impl MessageDialogImpl for MessageDialog { 157 | fn show(self) -> MessageDialogResult { 158 | autoreleasepool(move |_| { 159 | run_on_main(move |mtm| { 160 | if self.parent.is_none() { 161 | utils::sync_pop_dialog(self, mtm) 162 | } else { 163 | Alert::new(self, mtm).run() 164 | } 165 | }) 166 | }) 167 | } 168 | } 169 | 170 | use crate::backend::AsyncMessageDialogImpl; 171 | 172 | impl AsyncMessageDialogImpl for MessageDialog { 173 | fn show_async(self) -> DialogFutureType { 174 | if self.parent.is_none() { 175 | utils::async_pop_dialog(self) 176 | } else { 177 | Box::pin(ModalFuture::new( 178 | self.parent.as_ref().map(window_from_raw_window_handle), 179 | move |mtm| Alert::new(self, mtm), 180 | |dialog, ret| dialog_result(&dialog.buttons, ret), 181 | )) 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/backend/macos/modal_future.rs: -------------------------------------------------------------------------------- 1 | use block2::Block; 2 | use dispatch2::run_on_main; 3 | use objc2::rc::Retained; 4 | use objc2::{ClassType, MainThreadMarker, MainThreadOnly, Message}; 5 | use objc2_app_kit::{NSApplication, NSModalResponse, NSWindow}; 6 | 7 | use std::pin::Pin; 8 | use std::sync::{Arc, Mutex}; 9 | 10 | use std::task::{Context, Poll, Waker}; 11 | 12 | use super::utils::activate_cocoa_multithreading; 13 | 14 | pub(super) trait AsModal { 15 | fn inner_modal(&self) -> &(impl InnerModal + 'static); 16 | } 17 | 18 | pub(super) trait InnerModal: ClassType + MainThreadOnly { 19 | fn begin_modal(&self, window: &NSWindow, handler: &Block); 20 | fn run_modal(&self) -> NSModalResponse; 21 | } 22 | 23 | struct FutureState { 24 | waker: Option, 25 | data: Option, 26 | modal: Option, 27 | } 28 | 29 | unsafe impl Send for FutureState {} 30 | 31 | pub(super) struct ModalFuture { 32 | state: Arc>>, 33 | } 34 | 35 | unsafe impl Send for ModalFuture {} 36 | 37 | impl ModalFuture { 38 | pub fn new D + Send>( 39 | win: Option>, 40 | build_modal: DBULD, 41 | cb: F, 42 | ) -> Self 43 | where 44 | F: Fn(&mut D, isize) -> R + Send + 'static, 45 | { 46 | activate_cocoa_multithreading(); 47 | 48 | let state = Arc::new(Mutex::new(FutureState { 49 | waker: None, 50 | data: None, 51 | modal: None, 52 | })); 53 | 54 | let dialog_callback = move |state: Arc>>, 55 | result: NSModalResponse| { 56 | let mut state = state.lock().unwrap(); 57 | // take() to drop it when it's safe to do so 58 | state.data = if let Some(mut modal) = state.modal.take() { 59 | Some((cb)(&mut modal, result)) 60 | } else { 61 | Some(Default::default()) 62 | }; 63 | if let Some(waker) = state.waker.take() { 64 | waker.wake(); 65 | } 66 | }; 67 | 68 | let mtm = unsafe { MainThreadMarker::new_unchecked() }; 69 | let app = NSApplication::sharedApplication(mtm); 70 | 71 | let win = if let Some(win) = win { 72 | Some(win) 73 | } else { 74 | unsafe { app.mainWindow() }.or_else(|| app.windows().firstObject()) 75 | }; 76 | 77 | // if async exec is possible start sheet modal 78 | // otherwise fallback to sync 79 | if unsafe { app.isRunning() } && win.is_some() { 80 | let state = state.clone(); 81 | 82 | // Hack to work around us getting the window above 83 | struct WindowWrapper(Retained); 84 | unsafe impl Send for WindowWrapper {} 85 | let window = WindowWrapper(win.unwrap()); 86 | 87 | run_on_main(move |mtm| { 88 | let window = window; 89 | 90 | let completion = { 91 | let state = state.clone(); 92 | block2::RcBlock::new(move |result| { 93 | dialog_callback(state.clone(), result); 94 | }) 95 | }; 96 | 97 | let modal = build_modal(mtm); 98 | let inner = modal.inner_modal().retain(); 99 | 100 | state.lock().unwrap().modal = Some(modal); 101 | 102 | inner.begin_modal(&window.0, &completion); 103 | }); 104 | } else { 105 | eprintln!("\n Hi! It looks like you are running async dialog in unsupported environment, I will fallback to sync dialog for you. \n"); 106 | 107 | if let Some(mtm) = MainThreadMarker::new() { 108 | let modal = build_modal(mtm); 109 | let inner = modal.inner_modal().retain(); 110 | 111 | state.lock().unwrap().modal = Some(modal); 112 | 113 | let ret = inner.run_modal(); 114 | 115 | dialog_callback(state.clone(), ret); 116 | } else { 117 | panic!("Fallback Sync Dialog Must Be Spawned On Main Thread (Why? If async dialog is unsupported in this env, it also means that spawning dialogs outside of main thread is also inpossible"); 118 | } 119 | } 120 | 121 | Self { state } 122 | } 123 | } 124 | 125 | impl std::future::Future for ModalFuture { 126 | type Output = R; 127 | 128 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 129 | let mut state = self.state.lock().unwrap(); 130 | 131 | if state.data.is_some() { 132 | Poll::Ready(state.data.take().unwrap()) 133 | } else { 134 | state.waker = Some(cx.waker().clone()); 135 | Poll::Pending 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/backend/macos/utils.rs: -------------------------------------------------------------------------------- 1 | mod focus_manager; 2 | mod policy_manager; 3 | mod user_alert; 4 | 5 | pub use focus_manager::FocusManager; 6 | pub use policy_manager::PolicyManager; 7 | pub use user_alert::{async_pop_dialog, sync_pop_dialog}; 8 | 9 | use objc2::rc::Retained; 10 | use objc2::MainThreadMarker; 11 | use objc2_app_kit::{NSApplication, NSView, NSWindow}; 12 | use objc2_foundation::NSThread; 13 | use raw_window_handle::RawWindowHandle; 14 | 15 | pub fn activate_cocoa_multithreading() { 16 | let thread = NSThread::new(); 17 | unsafe { thread.start() }; 18 | } 19 | 20 | pub fn run_on_main R + Send>(run: F) -> R { 21 | if let Some(mtm) = MainThreadMarker::new() { 22 | run(mtm) 23 | } else { 24 | let mtm = unsafe { MainThreadMarker::new_unchecked() }; 25 | let app = NSApplication::sharedApplication(mtm); 26 | if unsafe { app.isRunning() } { 27 | dispatch2::run_on_main(run) 28 | } else { 29 | panic!("You are running RFD in NonWindowed environment, it is impossible to spawn dialog from thread different than main in this env."); 30 | } 31 | } 32 | } 33 | 34 | pub fn window_from_raw_window_handle(h: &RawWindowHandle) -> Retained { 35 | // TODO: Move this requirement up 36 | let _mtm = unsafe { MainThreadMarker::new_unchecked() }; 37 | match h { 38 | RawWindowHandle::AppKit(h) => { 39 | let view = h.ns_view.as_ptr() as *mut NSView; 40 | let view = unsafe { Retained::retain(view).unwrap() }; 41 | view.window().expect("NSView to be inside a NSWindow") 42 | } 43 | _ => unreachable!("unsupported window handle, expected: MacOS"), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/backend/macos/utils/focus_manager.rs: -------------------------------------------------------------------------------- 1 | use objc2::rc::Retained; 2 | use objc2::MainThreadMarker; 3 | use objc2_app_kit::{NSApplication, NSWindow}; 4 | 5 | pub struct FocusManager { 6 | key_window: Option>, 7 | } 8 | 9 | impl FocusManager { 10 | pub fn new(mtm: MainThreadMarker) -> Self { 11 | let app = NSApplication::sharedApplication(mtm); 12 | let key_window = app.keyWindow(); 13 | 14 | Self { key_window } 15 | } 16 | } 17 | 18 | impl Drop for FocusManager { 19 | fn drop(&mut self) { 20 | if let Some(win) = &self.key_window { 21 | win.makeKeyAndOrderFront(None); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/backend/macos/utils/policy_manager.rs: -------------------------------------------------------------------------------- 1 | use objc2::rc::Retained; 2 | use objc2::MainThreadMarker; 3 | use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy}; 4 | 5 | pub struct PolicyManager { 6 | app: Retained, 7 | initial_policy: NSApplicationActivationPolicy, 8 | } 9 | 10 | impl PolicyManager { 11 | pub fn new(mtm: MainThreadMarker) -> Self { 12 | let app = NSApplication::sharedApplication(mtm); 13 | let initial_policy = unsafe { app.activationPolicy() }; 14 | 15 | if initial_policy == NSApplicationActivationPolicy::Prohibited { 16 | let new_pol = NSApplicationActivationPolicy::Accessory; 17 | app.setActivationPolicy(new_pol); 18 | } 19 | 20 | Self { 21 | app, 22 | initial_policy, 23 | } 24 | } 25 | } 26 | 27 | impl Drop for PolicyManager { 28 | fn drop(&mut self) { 29 | // Restore initial policy 30 | self.app.setActivationPolicy(self.initial_policy); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/backend/macos/utils/user_alert.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | backend::{ 3 | macos::utils::{FocusManager, PolicyManager}, 4 | DialogFutureType, 5 | }, 6 | message_dialog::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}, 7 | }; 8 | 9 | use objc2::MainThreadMarker; 10 | use objc2_core_foundation::{ 11 | kCFUserNotificationAlternateResponse, kCFUserNotificationCancelResponse, 12 | kCFUserNotificationCautionAlertLevel, kCFUserNotificationDefaultResponse, 13 | kCFUserNotificationNoteAlertLevel, kCFUserNotificationOtherResponse, 14 | kCFUserNotificationStopAlertLevel, CFOptionFlags, CFRetained, CFString, CFTimeInterval, 15 | CFUserNotificationDisplayAlert, CFURL, 16 | }; 17 | 18 | use std::{mem::MaybeUninit, thread}; 19 | 20 | struct UserAlert { 21 | timeout: CFTimeInterval, 22 | flags: CFOptionFlags, 23 | icon_url: Option>, 24 | sound_url: Option>, 25 | localization_url: Option>, 26 | alert_header: String, 27 | alert_message: String, 28 | default_button_title: Option, 29 | alternate_button_title: Option, 30 | other_button_title: Option, 31 | buttons: MessageButtons, 32 | _focus_manager: Option, 33 | _policy_manager: Option, 34 | } 35 | 36 | impl UserAlert { 37 | fn new(opt: MessageDialog, mtm: Option) -> Self { 38 | let mut buttons: [Option; 3] = match &opt.buttons { 39 | MessageButtons::Ok => [None, None, None], 40 | MessageButtons::OkCancel => [None, Some("Cancel".to_string()), None], 41 | MessageButtons::YesNo => [Some("Yes".to_string()), Some("No".to_string()), None], 42 | MessageButtons::YesNoCancel => [ 43 | Some("Yes".to_string()), 44 | Some("No".to_string()), 45 | Some("Cancel".to_string()), 46 | ], 47 | MessageButtons::OkCustom(ok_text) => [Some(ok_text.to_string()), None, None], 48 | MessageButtons::OkCancelCustom(ok_text, cancel_text) => [ 49 | Some(ok_text.to_string()), 50 | Some(cancel_text.to_string()), 51 | None, 52 | ], 53 | MessageButtons::YesNoCancelCustom(yes_text, no_text, cancel_text) => [ 54 | Some(yes_text.to_string()), 55 | Some(no_text.to_string()), 56 | Some(cancel_text.to_string()), 57 | ], 58 | }; 59 | UserAlert { 60 | timeout: 0_f64, 61 | icon_url: None, 62 | sound_url: None, 63 | localization_url: None, 64 | flags: match opt.level { 65 | MessageLevel::Info => kCFUserNotificationNoteAlertLevel, 66 | MessageLevel::Warning => kCFUserNotificationCautionAlertLevel, 67 | MessageLevel::Error => kCFUserNotificationStopAlertLevel, 68 | }, 69 | alert_header: opt.title, 70 | alert_message: opt.description, 71 | default_button_title: buttons[0].take(), 72 | alternate_button_title: buttons[1].take(), 73 | other_button_title: buttons[2].take(), 74 | buttons: opt.buttons, 75 | _policy_manager: mtm.map(PolicyManager::new), 76 | _focus_manager: mtm.map(FocusManager::new), 77 | } 78 | } 79 | 80 | fn run(self) -> MessageDialogResult { 81 | let alert_header = CFString::from_str(&self.alert_header[..]); 82 | let alert_message = CFString::from_str(&self.alert_message[..]); 83 | let default_button_title = self 84 | .default_button_title 85 | .map(|string| CFString::from_str(&string[..])); 86 | let alternate_button_title = self 87 | .alternate_button_title 88 | .map(|value| CFString::from_str(&value[..])); 89 | let other_button_title = self 90 | .other_button_title 91 | .map(|value| CFString::from_str(&value[..])); 92 | let mut response_flags = MaybeUninit::::uninit(); 93 | let is_cancel = unsafe { 94 | CFUserNotificationDisplayAlert( 95 | self.timeout, 96 | self.flags, 97 | self.icon_url.as_deref(), 98 | self.sound_url.as_deref(), 99 | self.localization_url.as_deref(), 100 | Some(&alert_header), 101 | Some(&alert_message), 102 | default_button_title.as_deref(), 103 | alternate_button_title.as_deref(), 104 | other_button_title.as_deref(), 105 | response_flags.as_mut_ptr(), 106 | ) 107 | }; 108 | if is_cancel != 0 { 109 | return MessageDialogResult::Cancel; 110 | } 111 | let response = unsafe { response_flags.assume_init() }; 112 | if response == kCFUserNotificationCancelResponse { 113 | return MessageDialogResult::Cancel; 114 | } 115 | match self.buttons { 116 | MessageButtons::Ok if response == kCFUserNotificationDefaultResponse => { 117 | MessageDialogResult::Ok 118 | } 119 | MessageButtons::OkCancel if response == kCFUserNotificationDefaultResponse => { 120 | MessageDialogResult::Ok 121 | } 122 | MessageButtons::OkCancel if response == kCFUserNotificationAlternateResponse => { 123 | MessageDialogResult::Cancel 124 | } 125 | MessageButtons::YesNo if response == kCFUserNotificationDefaultResponse => { 126 | MessageDialogResult::Yes 127 | } 128 | MessageButtons::YesNo if response == kCFUserNotificationAlternateResponse => { 129 | MessageDialogResult::No 130 | } 131 | MessageButtons::YesNoCancel if response == kCFUserNotificationDefaultResponse => { 132 | MessageDialogResult::Yes 133 | } 134 | MessageButtons::YesNoCancel if response == kCFUserNotificationAlternateResponse => { 135 | MessageDialogResult::No 136 | } 137 | MessageButtons::YesNoCancel if response == kCFUserNotificationOtherResponse => { 138 | MessageDialogResult::Cancel 139 | } 140 | MessageButtons::OkCustom(custom) if response == kCFUserNotificationDefaultResponse => { 141 | MessageDialogResult::Custom(custom.to_owned()) 142 | } 143 | MessageButtons::OkCancelCustom(custom, _) 144 | if response == kCFUserNotificationDefaultResponse => 145 | { 146 | MessageDialogResult::Custom(custom.to_owned()) 147 | } 148 | MessageButtons::OkCancelCustom(_, custom) 149 | if response == kCFUserNotificationAlternateResponse => 150 | { 151 | MessageDialogResult::Custom(custom.to_owned()) 152 | } 153 | MessageButtons::YesNoCancelCustom(custom, _, _) 154 | if response == kCFUserNotificationDefaultResponse => 155 | { 156 | MessageDialogResult::Custom(custom.to_owned()) 157 | } 158 | MessageButtons::YesNoCancelCustom(_, custom, _) 159 | if response == kCFUserNotificationAlternateResponse => 160 | { 161 | MessageDialogResult::Custom(custom.to_owned()) 162 | } 163 | MessageButtons::YesNoCancelCustom(_, _, custom) 164 | if response == kCFUserNotificationOtherResponse => 165 | { 166 | MessageDialogResult::Custom(custom.to_owned()) 167 | } 168 | _ => MessageDialogResult::Cancel, 169 | } 170 | } 171 | } 172 | 173 | pub fn sync_pop_dialog(opt: MessageDialog, mtm: MainThreadMarker) -> MessageDialogResult { 174 | UserAlert::new(opt, Some(mtm)).run() 175 | } 176 | 177 | pub fn async_pop_dialog(opt: MessageDialog) -> DialogFutureType { 178 | let (tx, rx) = crate::oneshot::channel(); 179 | 180 | thread::spawn(move || { 181 | let message_dialog_result = UserAlert::new(opt.clone(), None).run(); 182 | if let Err(err) = tx.send(message_dialog_result) { 183 | log::error!("UserAler result send error: {err}"); 184 | } 185 | }); 186 | 187 | Box::pin(async { 188 | match rx.await { 189 | Ok(res) => res, 190 | Err(err) => { 191 | log::error!("UserAler error: {err}"); 192 | MessageDialogResult::Cancel 193 | } 194 | } 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /src/backend/wasm/file_dialog.rs: -------------------------------------------------------------------------------- 1 | // 2 | // File Save 3 | // 4 | 5 | use crate::{ 6 | backend::{AsyncFileSaveDialogImpl, DialogFutureType}, 7 | file_dialog::FileDialog, 8 | FileHandle, 9 | }; 10 | use std::future::ready; 11 | impl AsyncFileSaveDialogImpl for FileDialog { 12 | fn save_file_async(self) -> DialogFutureType> { 13 | let file = FileHandle::writable(self); 14 | Box::pin(ready(Some(file))) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/backend/wasm/style.css: -------------------------------------------------------------------------------- 1 | #rfd-overlay { 2 | z-index: 1000; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background-color: rgba(0, 0, 0, 0.5); 9 | 10 | animation: init 0.5s; 11 | 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | #rfd-card { 17 | padding: 20px; 18 | border-radius: 5px; 19 | 20 | box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 21 | 0 9px 46px 8px rgba(0, 0, 0, 0.12), 0 11px 15px -7px rgba(0, 0, 0, 0.2); 22 | 23 | color-scheme: light dark; 24 | background-color: canvas; 25 | color: canvastext; 26 | } 27 | #rfd-title { 28 | line-height: 1.6; 29 | } 30 | #rfd-input,#rfd-output { 31 | text-align: center; 32 | } 33 | .rfd-button { 34 | display: block; 35 | margin-top: 5px; 36 | width: 100%; 37 | } 38 | 39 | @keyframes init { 40 | 0% { 41 | opacity: 0; 42 | } 43 | 100% { 44 | opacity: 1; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/backend/win_cid.rs: -------------------------------------------------------------------------------- 1 | //! Windows Common Item Dialog 2 | //! Win32 Vista 3 | 4 | mod utils; 5 | 6 | mod file_dialog; 7 | mod message_dialog; 8 | 9 | mod thread_future; 10 | -------------------------------------------------------------------------------- /src/backend/win_cid/file_dialog.rs: -------------------------------------------------------------------------------- 1 | mod com; 2 | pub mod dialog_ffi; 3 | mod dialog_future; 4 | 5 | use dialog_ffi::{IDialog, Result}; 6 | use dialog_future::{multiple_return_future, single_return_future}; 7 | 8 | use crate::backend::DialogFutureType; 9 | use crate::FileDialog; 10 | use crate::FileHandle; 11 | 12 | use std::path::PathBuf; 13 | 14 | use super::utils::init_com; 15 | 16 | // 17 | // File Picker 18 | // 19 | 20 | use crate::backend::FilePickerDialogImpl; 21 | impl FilePickerDialogImpl for FileDialog { 22 | fn pick_file(self) -> Option { 23 | fn run(opt: FileDialog) -> Result { 24 | init_com(|| { 25 | let dialog = IDialog::build_pick_file(&opt)?; 26 | dialog.show()?; 27 | dialog.get_result() 28 | })? 29 | } 30 | run(self).ok() 31 | } 32 | 33 | fn pick_files(self) -> Option> { 34 | fn run(opt: FileDialog) -> Result> { 35 | init_com(|| { 36 | let dialog = IDialog::build_pick_files(&opt)?; 37 | dialog.show()?; 38 | dialog.get_results() 39 | })? 40 | } 41 | run(self).ok() 42 | } 43 | } 44 | 45 | use crate::backend::AsyncFilePickerDialogImpl; 46 | impl AsyncFilePickerDialogImpl for FileDialog { 47 | fn pick_file_async(self) -> DialogFutureType> { 48 | let ret = single_return_future(move || IDialog::build_pick_file(&self)); 49 | Box::pin(ret) 50 | } 51 | 52 | fn pick_files_async(self) -> DialogFutureType>> { 53 | let ret = multiple_return_future(move || IDialog::build_pick_files(&self)); 54 | Box::pin(ret) 55 | } 56 | } 57 | 58 | // 59 | // Folder Picker 60 | // 61 | 62 | use crate::backend::FolderPickerDialogImpl; 63 | impl FolderPickerDialogImpl for FileDialog { 64 | fn pick_folder(self) -> Option { 65 | fn run(opt: FileDialog) -> Result { 66 | init_com(|| { 67 | let dialog = IDialog::build_pick_folder(&opt)?; 68 | dialog.show()?; 69 | dialog.get_result() 70 | })? 71 | } 72 | 73 | run(self).ok() 74 | } 75 | 76 | fn pick_folders(self) -> Option> { 77 | fn run(opt: FileDialog) -> Result> { 78 | init_com(|| { 79 | let dialog = IDialog::build_pick_folders(&opt)?; 80 | dialog.show()?; 81 | dialog.get_results() 82 | })? 83 | } 84 | run(self).ok() 85 | } 86 | } 87 | 88 | use crate::backend::AsyncFolderPickerDialogImpl; 89 | impl AsyncFolderPickerDialogImpl for FileDialog { 90 | fn pick_folder_async(self) -> DialogFutureType> { 91 | let ret = single_return_future(move || IDialog::build_pick_folder(&self)); 92 | Box::pin(ret) 93 | } 94 | 95 | fn pick_folders_async(self) -> DialogFutureType>> { 96 | let ret = multiple_return_future(move || IDialog::build_pick_folders(&self)); 97 | Box::pin(ret) 98 | } 99 | } 100 | 101 | // 102 | // File Save 103 | // 104 | 105 | use crate::backend::FileSaveDialogImpl; 106 | impl FileSaveDialogImpl for FileDialog { 107 | fn save_file(self) -> Option { 108 | fn run(opt: FileDialog) -> Result { 109 | init_com(|| { 110 | let dialog = IDialog::build_save_file(&opt)?; 111 | dialog.show()?; 112 | dialog.get_result() 113 | })? 114 | } 115 | 116 | run(self).ok() 117 | } 118 | } 119 | 120 | use crate::backend::AsyncFileSaveDialogImpl; 121 | impl AsyncFileSaveDialogImpl for FileDialog { 122 | fn save_file_async(self) -> DialogFutureType> { 123 | let ret = single_return_future(move || IDialog::build_save_file(&self)); 124 | Box::pin(ret) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/backend/win_cid/file_dialog/com.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use std::ffi::c_void; 4 | use windows_sys::core::{HRESULT, PCWSTR, PWSTR}; 5 | pub use windows_sys::{ 6 | core::GUID, 7 | Win32::{ 8 | Foundation::HWND, 9 | UI::Shell::{Common::COMDLG_FILTERSPEC, FILEOPENDIALOGOPTIONS, SIGDN, SIGDN_FILESYSPATH}, 10 | }, 11 | }; 12 | 13 | pub(crate) type Result = std::result::Result; 14 | 15 | #[inline] 16 | pub(super) fn wrap_err(hresult: HRESULT) -> Result<()> { 17 | if hresult >= 0 { 18 | Ok(()) 19 | } else { 20 | Err(hresult) 21 | } 22 | } 23 | 24 | #[inline] 25 | unsafe fn read_to_string(ptr: *const u16) -> String { 26 | let mut cursor = ptr; 27 | 28 | while *cursor != 0 { 29 | cursor = cursor.add(1); 30 | } 31 | 32 | let slice = std::slice::from_raw_parts(ptr, cursor.offset_from(ptr) as usize); 33 | String::from_utf16(slice).unwrap() 34 | } 35 | 36 | #[repr(C)] 37 | pub(super) struct Interface { 38 | vtable: *mut T, 39 | } 40 | 41 | impl Interface { 42 | #[inline] 43 | pub(super) fn vtbl(&self) -> &T { 44 | unsafe { &*self.vtable } 45 | } 46 | } 47 | 48 | #[repr(C)] 49 | pub(super) struct IUnknownV { 50 | __query_interface: usize, 51 | __add_ref: usize, 52 | pub(super) release: unsafe extern "system" fn(this: *mut c_void) -> u32, 53 | } 54 | 55 | pub(super) type IUnknown = Interface; 56 | 57 | #[inline] 58 | fn drop_impl(ptr: *mut c_void) { 59 | unsafe { 60 | ((*ptr.cast::()).vtbl().release)(ptr); 61 | } 62 | } 63 | 64 | #[repr(C)] 65 | pub(super) struct IShellItemV { 66 | base: IUnknownV, 67 | BindToHandler: unsafe extern "system" fn( 68 | this: *mut c_void, 69 | pbc: *mut c_void, 70 | bhid: *const GUID, 71 | riid: *const GUID, 72 | ppv: *mut *mut c_void, 73 | ) -> HRESULT, 74 | GetParent: unsafe extern "system" fn(this: *mut c_void, ppsi: *mut *mut c_void) -> HRESULT, 75 | GetDisplayName: unsafe extern "system" fn( 76 | this: *mut c_void, 77 | sigdnname: SIGDN, 78 | ppszname: *mut PWSTR, 79 | ) -> HRESULT, 80 | GetAttributes: usize, 81 | Compare: unsafe extern "system" fn( 82 | this: *mut c_void, 83 | psi: *mut c_void, 84 | hint: u32, 85 | piorder: *mut i32, 86 | ) -> HRESULT, 87 | } 88 | 89 | #[repr(transparent)] 90 | pub(super) struct IShellItem(pub(super) *mut Interface); 91 | 92 | impl IShellItem { 93 | pub(super) fn get_path(&self) -> Result { 94 | let filename = unsafe { 95 | let mut dname = std::mem::MaybeUninit::uninit(); 96 | wrap_err(((*self.0).vtbl().GetDisplayName)( 97 | self.0.cast(), 98 | SIGDN_FILESYSPATH, 99 | dname.as_mut_ptr(), 100 | ))?; 101 | 102 | let dname = dname.assume_init(); 103 | let fname = read_to_string(dname); 104 | windows_sys::Win32::System::Com::CoTaskMemFree(dname.cast()); 105 | fname 106 | }; 107 | 108 | Ok(filename.into()) 109 | } 110 | } 111 | 112 | impl Drop for IShellItem { 113 | fn drop(&mut self) { 114 | drop_impl(self.0.cast()); 115 | } 116 | } 117 | 118 | #[repr(C)] 119 | struct IShellItemArrayV { 120 | base: IUnknownV, 121 | BindToHandler: unsafe extern "system" fn( 122 | this: *mut c_void, 123 | pbc: *mut c_void, 124 | bhid: *const GUID, 125 | riid: *const GUID, 126 | ppvout: *mut *mut c_void, 127 | ) -> HRESULT, 128 | GetPropertyStore: usize, 129 | GetPropertyDescriptionList: usize, 130 | GetAttributes: usize, 131 | GetCount: unsafe extern "system" fn(this: *mut c_void, pdwnumitems: *mut u32) -> HRESULT, 132 | GetItemAt: unsafe extern "system" fn( 133 | this: *mut c_void, 134 | dwindex: u32, 135 | ppsi: *mut IShellItem, 136 | ) -> HRESULT, 137 | EnumItems: 138 | unsafe extern "system" fn(this: *mut c_void, ppenumshellitems: *mut *mut c_void) -> HRESULT, 139 | } 140 | 141 | #[repr(transparent)] 142 | pub(super) struct IShellItemArray(*mut Interface); 143 | 144 | impl IShellItemArray { 145 | #[inline] 146 | pub(super) fn get_count(&self) -> Result { 147 | let mut count = 0; 148 | unsafe { 149 | wrap_err(((*self.0).vtbl().GetCount)(self.0.cast(), &mut count))?; 150 | } 151 | Ok(count) 152 | } 153 | 154 | #[inline] 155 | pub(super) fn get_item_at(&self, index: u32) -> Result { 156 | let mut item = std::mem::MaybeUninit::uninit(); 157 | unsafe { 158 | wrap_err(((*self.0).vtbl().GetItemAt)( 159 | self.0.cast(), 160 | index, 161 | item.as_mut_ptr(), 162 | ))?; 163 | Ok(item.assume_init()) 164 | } 165 | } 166 | } 167 | 168 | impl Drop for IShellItemArray { 169 | fn drop(&mut self) { 170 | drop_impl(self.0.cast()); 171 | } 172 | } 173 | 174 | #[repr(C)] 175 | pub(super) struct IModalWindowV { 176 | base: IUnknownV, 177 | pub(super) Show: unsafe extern "system" fn(this: *mut c_void, owner: HWND) -> HRESULT, 178 | } 179 | 180 | /// 181 | #[repr(C)] 182 | pub(super) struct IFileDialogV { 183 | pub(super) base: IModalWindowV, 184 | pub(super) SetFileTypes: unsafe extern "system" fn( 185 | this: *mut c_void, 186 | cfiletypes: u32, 187 | rgfilterspec: *const COMDLG_FILTERSPEC, 188 | ) -> HRESULT, 189 | SetFileTypeIndex: unsafe extern "system" fn(this: *mut c_void, ifiletype: u32) -> HRESULT, 190 | GetFileTypeIndex: unsafe extern "system" fn(this: *mut c_void, pifiletype: *mut u32) -> HRESULT, 191 | Advise: unsafe extern "system" fn( 192 | this: *mut c_void, 193 | pfde: *mut c_void, 194 | pdwcookie: *mut u32, 195 | ) -> HRESULT, 196 | Unadvise: unsafe extern "system" fn(this: *mut c_void, dwcookie: u32) -> HRESULT, 197 | pub(super) SetOptions: 198 | unsafe extern "system" fn(this: *mut c_void, fos: FILEOPENDIALOGOPTIONS) -> HRESULT, 199 | GetOptions: 200 | unsafe extern "system" fn(this: *mut c_void, pfos: *mut FILEOPENDIALOGOPTIONS) -> HRESULT, 201 | SetDefaultFolder: unsafe extern "system" fn(this: *mut c_void, psi: *mut c_void) -> HRESULT, 202 | pub(super) SetFolder: unsafe extern "system" fn(this: *mut c_void, psi: *mut c_void) -> HRESULT, 203 | GetFolder: unsafe extern "system" fn(this: *mut c_void, ppsi: *mut *mut c_void) -> HRESULT, 204 | GetCurrentSelection: 205 | unsafe extern "system" fn(this: *mut c_void, ppsi: *mut *mut c_void) -> HRESULT, 206 | pub(super) SetFileName: 207 | unsafe extern "system" fn(this: *mut c_void, pszname: PCWSTR) -> HRESULT, 208 | GetFileName: unsafe extern "system" fn(this: *mut c_void, pszname: *mut PWSTR) -> HRESULT, 209 | pub(super) SetTitle: unsafe extern "system" fn(this: *mut c_void, psztitle: PCWSTR) -> HRESULT, 210 | SetOkButtonLabel: unsafe extern "system" fn(this: *mut c_void, psztext: PCWSTR) -> HRESULT, 211 | SetFileNameLabel: unsafe extern "system" fn(this: *mut c_void, pszlabel: PCWSTR) -> HRESULT, 212 | pub(super) GetResult: 213 | unsafe extern "system" fn(this: *mut c_void, ppsi: *mut IShellItem) -> HRESULT, 214 | AddPlace: unsafe extern "system" fn( 215 | this: *mut c_void, 216 | psi: *mut c_void, 217 | fdap: windows_sys::Win32::UI::Shell::FDAP, 218 | ) -> HRESULT, 219 | pub(super) SetDefaultExtension: 220 | unsafe extern "system" fn(this: *mut c_void, pszdefaultextension: PCWSTR) -> HRESULT, 221 | Close: unsafe extern "system" fn(this: *mut c_void, hr: HRESULT) -> HRESULT, 222 | SetClientGuid: unsafe extern "system" fn(this: *mut c_void, guid: *const GUID) -> HRESULT, 223 | ClearClientData: unsafe extern "system" fn(this: *mut c_void) -> HRESULT, 224 | SetFilter: unsafe extern "system" fn(this: *mut c_void, pfilter: *mut c_void) -> HRESULT, 225 | } 226 | 227 | #[repr(transparent)] 228 | pub(super) struct IFileDialog(pub(super) *mut Interface); 229 | 230 | impl Drop for IFileDialog { 231 | fn drop(&mut self) { 232 | drop_impl(self.0.cast()); 233 | } 234 | } 235 | 236 | /// 237 | #[repr(C)] 238 | pub(super) struct IFileOpenDialogV { 239 | pub(super) base: IFileDialogV, 240 | /// 241 | GetResults: 242 | unsafe extern "system" fn(this: *mut c_void, ppenum: *mut IShellItemArray) -> HRESULT, 243 | GetSelectedItems: 244 | unsafe extern "system" fn(this: *mut c_void, ppsai: *mut *mut c_void) -> HRESULT, 245 | } 246 | 247 | #[repr(transparent)] 248 | pub(super) struct IFileOpenDialog(pub(super) *mut Interface); 249 | 250 | impl IFileOpenDialog { 251 | #[inline] 252 | pub(super) fn get_results(&self) -> Result { 253 | let mut res = std::mem::MaybeUninit::uninit(); 254 | unsafe { 255 | wrap_err((((*self.0).vtbl()).GetResults)( 256 | self.0.cast(), 257 | res.as_mut_ptr(), 258 | ))?; 259 | Ok(res.assume_init()) 260 | } 261 | } 262 | } 263 | 264 | impl Drop for IFileOpenDialog { 265 | fn drop(&mut self) { 266 | drop_impl(self.0.cast()); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/backend/win_cid/file_dialog/dialog_ffi.rs: -------------------------------------------------------------------------------- 1 | use super::super::utils::str_to_vec_u16; 2 | pub(crate) use super::com::Result; 3 | use super::com::{ 4 | wrap_err, IFileDialog, IFileDialogV, IFileOpenDialog, IShellItem, COMDLG_FILTERSPEC, 5 | FILEOPENDIALOGOPTIONS, HWND, 6 | }; 7 | use crate::FileDialog; 8 | 9 | use windows_sys::{ 10 | core::GUID, 11 | Win32::{ 12 | System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, 13 | UI::Shell::{ 14 | FileOpenDialog, FileSaveDialog, SHCreateItemFromParsingName, FOS_ALLOWMULTISELECT, 15 | FOS_PICKFOLDERS, 16 | }, 17 | }, 18 | }; 19 | 20 | use std::{ffi::c_void, path::PathBuf}; 21 | 22 | use raw_window_handle::RawWindowHandle; 23 | 24 | enum DialogInner { 25 | Open(IFileOpenDialog), 26 | Save(IFileDialog), 27 | } 28 | 29 | impl DialogInner { 30 | unsafe fn new(open: bool) -> Result { 31 | const FILE_OPEN_DIALOG_IID: GUID = GUID::from_u128(0xd57c7288_d4ad_4768_be02_9d969532d960); 32 | const FILE_SAVE_DIALOG_IID: GUID = GUID::from_u128(0x84bccd23_5fde_4cdb_aea4_af64b83d78ab); 33 | 34 | unsafe { 35 | let (cls_id, iid) = if open { 36 | (&FileOpenDialog, &FILE_OPEN_DIALOG_IID) 37 | } else { 38 | (&FileSaveDialog, &FILE_SAVE_DIALOG_IID) 39 | }; 40 | 41 | let mut iptr = std::mem::MaybeUninit::uninit(); 42 | wrap_err(CoCreateInstance( 43 | cls_id, 44 | std::ptr::null_mut(), 45 | CLSCTX_INPROC_SERVER, 46 | iid, 47 | iptr.as_mut_ptr(), 48 | ))?; 49 | 50 | let iptr = iptr.assume_init(); 51 | 52 | Ok(if open { 53 | Self::Open(IFileOpenDialog(iptr.cast())) 54 | } else { 55 | Self::Save(IFileDialog(iptr.cast())) 56 | }) 57 | } 58 | } 59 | 60 | #[inline] 61 | unsafe fn open() -> Result { 62 | unsafe { Self::new(true) } 63 | } 64 | 65 | #[inline] 66 | unsafe fn save() -> Result { 67 | unsafe { Self::new(false) } 68 | } 69 | 70 | #[inline] 71 | unsafe fn fd(&self) -> (*mut c_void, &IFileDialogV) { 72 | match self { 73 | Self::Save(s) => unsafe { (s.0.cast(), (*s.0).vtbl()) }, 74 | Self::Open(o) => unsafe { (o.0.cast(), &(*o.0).vtbl().base) }, 75 | } 76 | } 77 | 78 | #[inline] 79 | unsafe fn set_options(&self, opts: FILEOPENDIALOGOPTIONS) -> Result<()> { 80 | let (d, v) = self.fd(); 81 | wrap_err((v.SetOptions)(d, opts)) 82 | } 83 | 84 | #[inline] 85 | unsafe fn set_title(&self, title: &[u16]) -> Result<()> { 86 | let (d, v) = self.fd(); 87 | wrap_err((v.SetTitle)(d, title.as_ptr())) 88 | } 89 | 90 | #[inline] 91 | unsafe fn set_default_extension(&self, extension: &[u16]) -> Result<()> { 92 | let (d, v) = self.fd(); 93 | wrap_err((v.SetDefaultExtension)(d, extension.as_ptr())) 94 | } 95 | 96 | #[inline] 97 | unsafe fn set_file_types(&self, specs: &[COMDLG_FILTERSPEC]) -> Result<()> { 98 | let (d, v) = self.fd(); 99 | wrap_err((v.SetFileTypes)(d, specs.len() as _, specs.as_ptr())) 100 | } 101 | 102 | #[inline] 103 | unsafe fn set_filename(&self, fname: &[u16]) -> Result<()> { 104 | let (d, v) = self.fd(); 105 | wrap_err((v.SetFileName)(d, fname.as_ptr())) 106 | } 107 | 108 | #[inline] 109 | unsafe fn set_folder(&self, folder: &IShellItem) -> Result<()> { 110 | let (d, v) = self.fd(); 111 | wrap_err((v.SetFolder)(d, folder.0.cast())) 112 | } 113 | 114 | #[inline] 115 | unsafe fn show(&self, parent: Option) -> Result<()> { 116 | let (d, v) = self.fd(); 117 | wrap_err((v.base.Show)(d, parent.unwrap_or(std::ptr::null_mut()))) 118 | } 119 | 120 | #[inline] 121 | unsafe fn get_result(&self) -> Result { 122 | let (d, v) = self.fd(); 123 | let mut res = std::mem::MaybeUninit::uninit(); 124 | wrap_err((v.GetResult)(d, res.as_mut_ptr()))?; 125 | let res = res.assume_init(); 126 | res.get_path() 127 | } 128 | 129 | #[inline] 130 | unsafe fn get_results(&self) -> Result> { 131 | let Self::Open(od) = self else { unreachable!() }; 132 | 133 | let items = od.get_results()?; 134 | let count = items.get_count()?; 135 | 136 | let mut paths = Vec::with_capacity(count as usize); 137 | for index in 0..count { 138 | let item = items.get_item_at(index)?; 139 | 140 | let path = item.get_path()?; 141 | paths.push(path); 142 | } 143 | 144 | Ok(paths) 145 | } 146 | } 147 | 148 | pub struct IDialog(DialogInner, Option); 149 | 150 | impl IDialog { 151 | fn new_open_dialog(opt: &FileDialog) -> Result { 152 | let dialog = unsafe { DialogInner::open()? }; 153 | 154 | let parent = match opt.parent { 155 | Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd.get() as _), 156 | None => None, 157 | _ => unreachable!("unsupported window handle, expected: Windows"), 158 | }; 159 | 160 | Ok(Self(dialog, parent)) 161 | } 162 | 163 | fn new_save_dialog(opt: &FileDialog) -> Result { 164 | let dialog = unsafe { DialogInner::save()? }; 165 | 166 | let parent = match opt.parent { 167 | Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd.get() as _), 168 | None => None, 169 | _ => unreachable!("unsupported window handle, expected: Windows"), 170 | }; 171 | 172 | Ok(Self(dialog, parent)) 173 | } 174 | 175 | fn add_filters(&self, filters: &[crate::file_dialog::Filter]) -> Result<()> { 176 | if let Some(first_filter) = filters.first() { 177 | if let Some(first_extension) = first_filter.extensions.first() { 178 | let extension = str_to_vec_u16(first_extension); 179 | unsafe { self.0.set_default_extension(&extension)? } 180 | } 181 | } 182 | 183 | let mut f_list = { 184 | let mut f_list = Vec::new(); 185 | let mut ext_string = String::new(); 186 | 187 | for f in filters.iter() { 188 | let name = str_to_vec_u16(&f.name); 189 | ext_string.clear(); 190 | 191 | for ext in &f.extensions { 192 | use std::fmt::Write; 193 | // This is infallible for String (barring OOM) 194 | let _ = write!(&mut ext_string, "*.{ext};"); 195 | } 196 | 197 | // pop trailing ; 198 | ext_string.pop(); 199 | 200 | f_list.push((name, str_to_vec_u16(&ext_string))); 201 | } 202 | f_list 203 | }; 204 | 205 | if f_list.is_empty() { 206 | f_list.push((str_to_vec_u16("All Files"), str_to_vec_u16("*.*"))); 207 | } 208 | 209 | let spec: Vec<_> = f_list 210 | .iter() 211 | .map(|(name, ext)| COMDLG_FILTERSPEC { 212 | pszName: name.as_ptr(), 213 | pszSpec: ext.as_ptr(), 214 | }) 215 | .collect(); 216 | 217 | unsafe { 218 | self.0.set_file_types(&spec)?; 219 | } 220 | Ok(()) 221 | } 222 | 223 | fn set_path(&self, path: &Option) -> Result<()> { 224 | const SHELL_ITEM_IID: GUID = GUID::from_u128(0x43826d1e_e718_42ee_bc55_a1e261c37bfe); 225 | 226 | let Some(path) = path.as_ref().and_then(|p| p.to_str()) else { 227 | return Ok(()); 228 | }; 229 | 230 | // Strip Win32 namespace prefix from the path 231 | let path = path.strip_prefix(r"\\?\").unwrap_or(path); 232 | 233 | let wide_path = str_to_vec_u16(path); 234 | 235 | unsafe { 236 | let mut item = std::mem::MaybeUninit::uninit(); 237 | if wrap_err(SHCreateItemFromParsingName( 238 | wide_path.as_ptr(), 239 | std::ptr::null_mut(), 240 | &SHELL_ITEM_IID, 241 | item.as_mut_ptr(), 242 | )) 243 | .is_ok() 244 | { 245 | let item = IShellItem(item.assume_init().cast()); 246 | // For some reason SetDefaultFolder(), does not guarantees default path, so we use SetFolder 247 | self.0.set_folder(&item)?; 248 | } 249 | } 250 | 251 | Ok(()) 252 | } 253 | 254 | fn set_file_name(&self, file_name: &Option) -> Result<()> { 255 | if let Some(path) = file_name { 256 | let wide_path = str_to_vec_u16(path); 257 | 258 | unsafe { 259 | self.0.set_filename(&wide_path)?; 260 | } 261 | } 262 | Ok(()) 263 | } 264 | 265 | fn set_title(&self, title: &Option) -> Result<()> { 266 | if let Some(title) = title { 267 | let wide_title = str_to_vec_u16(title); 268 | 269 | unsafe { 270 | self.0.set_title(&wide_title)?; 271 | } 272 | } 273 | Ok(()) 274 | } 275 | 276 | pub fn get_results(&self) -> Result> { 277 | unsafe { self.0.get_results() } 278 | } 279 | 280 | pub fn get_result(&self) -> Result { 281 | unsafe { self.0.get_result() } 282 | } 283 | 284 | pub fn show(&self) -> Result<()> { 285 | unsafe { self.0.show(self.1) } 286 | } 287 | } 288 | 289 | impl IDialog { 290 | pub fn build_pick_file(opt: &FileDialog) -> Result { 291 | let dialog = IDialog::new_open_dialog(opt)?; 292 | 293 | dialog.add_filters(&opt.filters)?; 294 | dialog.set_path(&opt.starting_directory)?; 295 | dialog.set_file_name(&opt.file_name)?; 296 | dialog.set_title(&opt.title)?; 297 | 298 | Ok(dialog) 299 | } 300 | 301 | pub fn build_save_file(opt: &FileDialog) -> Result { 302 | let dialog = IDialog::new_save_dialog(opt)?; 303 | 304 | dialog.add_filters(&opt.filters)?; 305 | dialog.set_path(&opt.starting_directory)?; 306 | dialog.set_file_name(&opt.file_name)?; 307 | dialog.set_title(&opt.title)?; 308 | 309 | Ok(dialog) 310 | } 311 | 312 | pub fn build_pick_folder(opt: &FileDialog) -> Result { 313 | let dialog = IDialog::new_open_dialog(opt)?; 314 | 315 | dialog.set_path(&opt.starting_directory)?; 316 | dialog.set_title(&opt.title)?; 317 | 318 | unsafe { 319 | dialog.0.set_options(FOS_PICKFOLDERS)?; 320 | } 321 | 322 | Ok(dialog) 323 | } 324 | 325 | pub fn build_pick_folders(opt: &FileDialog) -> Result { 326 | let dialog = IDialog::new_open_dialog(opt)?; 327 | 328 | dialog.set_path(&opt.starting_directory)?; 329 | dialog.set_title(&opt.title)?; 330 | let opts = FOS_PICKFOLDERS | FOS_ALLOWMULTISELECT; 331 | 332 | unsafe { 333 | dialog.0.set_options(opts)?; 334 | } 335 | 336 | Ok(dialog) 337 | } 338 | 339 | pub fn build_pick_files(opt: &FileDialog) -> Result { 340 | let dialog = IDialog::new_open_dialog(opt)?; 341 | 342 | dialog.add_filters(&opt.filters)?; 343 | dialog.set_path(&opt.starting_directory)?; 344 | dialog.set_file_name(&opt.file_name)?; 345 | dialog.set_title(&opt.title)?; 346 | 347 | unsafe { 348 | dialog.0.set_options(FOS_ALLOWMULTISELECT)?; 349 | } 350 | 351 | Ok(dialog) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/backend/win_cid/file_dialog/dialog_future.rs: -------------------------------------------------------------------------------- 1 | use super::super::thread_future::ThreadFuture; 2 | use super::super::utils::init_com; 3 | use super::dialog_ffi::IDialog; 4 | 5 | use crate::file_handle::FileHandle; 6 | 7 | pub fn single_return_future Result + Send + 'static>( 8 | build: F, 9 | ) -> ThreadFuture> { 10 | ThreadFuture::new(move |data| { 11 | let ret: Result<(), i32> = (|| { 12 | init_com(|| { 13 | let dialog = build()?; 14 | dialog.show()?; 15 | 16 | let path = dialog.get_result().ok().map(FileHandle::wrap); 17 | *data = Some(path); 18 | 19 | Ok(()) 20 | })? 21 | })(); 22 | 23 | if ret.is_err() { 24 | *data = Some(None); 25 | } 26 | }) 27 | } 28 | 29 | pub fn multiple_return_future Result + Send + 'static>( 30 | build: F, 31 | ) -> ThreadFuture>> { 32 | ThreadFuture::new(move |data| { 33 | let ret: Result<(), i32> = (|| { 34 | init_com(|| { 35 | let dialog = build()?; 36 | dialog.show()?; 37 | 38 | let list = dialog 39 | .get_results() 40 | .ok() 41 | .map(|r| r.into_iter().map(FileHandle::wrap).collect()); 42 | *data = Some(list); 43 | 44 | Ok(()) 45 | })? 46 | })(); 47 | 48 | if ret.is_err() { 49 | *data = Some(None); 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/backend/win_cid/message_dialog.rs: -------------------------------------------------------------------------------- 1 | use super::thread_future::ThreadFuture; 2 | use super::utils::str_to_vec_u16; 3 | use crate::message_dialog::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}; 4 | 5 | use windows_sys::Win32::{ 6 | Foundation::HWND, 7 | UI::WindowsAndMessaging::{IDCANCEL, IDNO, IDOK, IDYES}, 8 | }; 9 | 10 | #[cfg(not(feature = "common-controls-v6"))] 11 | use windows_sys::Win32::UI::WindowsAndMessaging::{ 12 | MessageBoxW, MB_ICONERROR, MB_ICONINFORMATION, MB_ICONWARNING, MB_OK, MB_OKCANCEL, MB_YESNO, 13 | MB_YESNOCANCEL, MESSAGEBOX_STYLE, 14 | }; 15 | 16 | use raw_window_handle::RawWindowHandle; 17 | 18 | pub struct WinMessageDialog { 19 | parent: Option, 20 | text: Vec, 21 | caption: Vec, 22 | #[cfg(not(feature = "common-controls-v6"))] 23 | flags: MESSAGEBOX_STYLE, 24 | #[cfg(feature = "common-controls-v6")] 25 | opt: MessageDialog, 26 | } 27 | 28 | // Oh god, I don't like sending RawWindowHandle between threads but here we go anyways... 29 | // fingers crossed 30 | unsafe impl Send for WinMessageDialog {} 31 | 32 | impl WinMessageDialog { 33 | pub fn new(opt: MessageDialog) -> Self { 34 | let text: Vec = str_to_vec_u16(&opt.description); 35 | let caption: Vec = str_to_vec_u16(&opt.title); 36 | 37 | #[cfg(not(feature = "common-controls-v6"))] 38 | let level = match opt.level { 39 | MessageLevel::Info => MB_ICONINFORMATION, 40 | MessageLevel::Warning => MB_ICONWARNING, 41 | MessageLevel::Error => MB_ICONERROR, 42 | }; 43 | 44 | #[cfg(not(feature = "common-controls-v6"))] 45 | let buttons = match opt.buttons { 46 | MessageButtons::Ok | MessageButtons::OkCustom(_) => MB_OK, 47 | MessageButtons::OkCancel | MessageButtons::OkCancelCustom(_, _) => MB_OKCANCEL, 48 | MessageButtons::YesNo => MB_YESNO, 49 | MessageButtons::YesNoCancel | MessageButtons::YesNoCancelCustom(_, _, _) => { 50 | MB_YESNOCANCEL 51 | } 52 | }; 53 | 54 | let parent = match opt.parent { 55 | Some(RawWindowHandle::Win32(handle)) => Some(handle.hwnd.get() as _), 56 | None => None, 57 | _ => unreachable!("unsupported window handle, expected: Windows"), 58 | }; 59 | 60 | Self { 61 | parent, 62 | text, 63 | caption, 64 | #[cfg(not(feature = "common-controls-v6"))] 65 | flags: level | buttons, 66 | #[cfg(feature = "common-controls-v6")] 67 | opt, 68 | } 69 | } 70 | 71 | #[cfg(feature = "common-controls-v6")] 72 | pub fn run(self) -> MessageDialogResult { 73 | use windows_sys::Win32::{ 74 | Foundation::BOOL, 75 | UI::Controls::{ 76 | TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOGCONFIG_0, TASKDIALOGCONFIG_1, 77 | TASKDIALOG_BUTTON, TDCBF_CANCEL_BUTTON, TDCBF_NO_BUTTON, TDCBF_OK_BUTTON, 78 | TDCBF_YES_BUTTON, TDF_ALLOW_DIALOG_CANCELLATION, TDF_SIZE_TO_CONTENT, 79 | TD_ERROR_ICON, TD_INFORMATION_ICON, TD_WARNING_ICON, 80 | }, 81 | }; 82 | 83 | let mut pf_verification_flag_checked = 0; 84 | let mut pn_button = 0; 85 | let mut pn_radio_button = 0; 86 | 87 | const ID_CUSTOM_OK: i32 = 1000; 88 | const ID_CUSTOM_CANCEL: i32 = 1001; 89 | const ID_CUSTOM_YES: i32 = 1004; 90 | const ID_CUSTOM_NO: i32 = 1008; 91 | 92 | let main_icon_ptr = match self.opt.level { 93 | MessageLevel::Warning => TD_WARNING_ICON, 94 | MessageLevel::Error => TD_ERROR_ICON, 95 | MessageLevel::Info => TD_INFORMATION_ICON, 96 | }; 97 | 98 | let (system_buttons, custom_buttons) = match &self.opt.buttons { 99 | MessageButtons::Ok => (TDCBF_OK_BUTTON, vec![]), 100 | MessageButtons::OkCancel => (TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON, vec![]), 101 | MessageButtons::YesNo => (TDCBF_YES_BUTTON | TDCBF_NO_BUTTON, vec![]), 102 | MessageButtons::YesNoCancel => ( 103 | TDCBF_YES_BUTTON | TDCBF_NO_BUTTON | TDCBF_CANCEL_BUTTON, 104 | vec![], 105 | ), 106 | MessageButtons::OkCustom(ok_text) => ( 107 | Default::default(), 108 | vec![(ID_CUSTOM_OK, str_to_vec_u16(ok_text))], 109 | ), 110 | MessageButtons::OkCancelCustom(ok_text, cancel_text) => ( 111 | Default::default(), 112 | vec![ 113 | (ID_CUSTOM_OK, str_to_vec_u16(ok_text)), 114 | (ID_CUSTOM_CANCEL, str_to_vec_u16(cancel_text)), 115 | ], 116 | ), 117 | MessageButtons::YesNoCancelCustom(yes_text, no_text, cancel_text) => ( 118 | Default::default(), 119 | vec![ 120 | (ID_CUSTOM_YES, str_to_vec_u16(yes_text)), 121 | (ID_CUSTOM_NO, str_to_vec_u16(no_text)), 122 | (ID_CUSTOM_CANCEL, str_to_vec_u16(cancel_text)), 123 | ], 124 | ), 125 | }; 126 | 127 | let p_buttons = custom_buttons 128 | .iter() 129 | .map(|(id, text)| TASKDIALOG_BUTTON { 130 | nButtonID: *id, 131 | pszButtonText: text.as_ptr(), 132 | }) 133 | .collect::>(); 134 | 135 | let task_dialog_config = TASKDIALOGCONFIG { 136 | cbSize: core::mem::size_of::() as u32, 137 | hwndParent: self.parent.unwrap_or(std::ptr::null_mut()), 138 | dwFlags: TDF_ALLOW_DIALOG_CANCELLATION | TDF_SIZE_TO_CONTENT, 139 | pszWindowTitle: self.caption.as_ptr(), 140 | pszContent: self.text.as_ptr(), 141 | Anonymous1: TASKDIALOGCONFIG_0 { 142 | pszMainIcon: main_icon_ptr, 143 | }, 144 | Anonymous2: TASKDIALOGCONFIG_1 { 145 | pszFooterIcon: std::ptr::null(), 146 | }, 147 | dwCommonButtons: system_buttons, 148 | pButtons: p_buttons.as_ptr(), 149 | cButtons: custom_buttons.len() as u32, 150 | pRadioButtons: std::ptr::null(), 151 | cRadioButtons: 0, 152 | cxWidth: 0, 153 | hInstance: std::ptr::null_mut(), 154 | pfCallback: None, 155 | lpCallbackData: 0, 156 | nDefaultButton: 0, 157 | nDefaultRadioButton: 0, 158 | pszCollapsedControlText: std::ptr::null(), 159 | pszExpandedControlText: std::ptr::null(), 160 | pszExpandedInformation: std::ptr::null(), 161 | pszMainInstruction: std::ptr::null(), 162 | pszVerificationText: std::ptr::null(), 163 | pszFooter: std::ptr::null(), 164 | }; 165 | 166 | let ret = unsafe { 167 | TaskDialogIndirect( 168 | &task_dialog_config, 169 | &mut pn_button as *mut i32, 170 | &mut pn_radio_button as *mut i32, 171 | &mut pf_verification_flag_checked as *mut BOOL, 172 | ) 173 | }; 174 | 175 | if ret != 0 { 176 | return MessageDialogResult::Cancel; 177 | } 178 | 179 | match pn_button { 180 | IDOK => MessageDialogResult::Ok, 181 | IDYES => MessageDialogResult::Yes, 182 | IDCANCEL => MessageDialogResult::Cancel, 183 | IDNO => MessageDialogResult::No, 184 | custom => match self.opt.buttons { 185 | MessageButtons::OkCustom(ok_text) => match custom { 186 | ID_CUSTOM_OK => MessageDialogResult::Custom(ok_text), 187 | _ => MessageDialogResult::Cancel, 188 | }, 189 | MessageButtons::OkCancelCustom(ok_text, cancel_text) => match custom { 190 | ID_CUSTOM_OK => MessageDialogResult::Custom(ok_text), 191 | ID_CUSTOM_CANCEL => MessageDialogResult::Custom(cancel_text), 192 | _ => MessageDialogResult::Cancel, 193 | }, 194 | MessageButtons::YesNoCancelCustom(yes_text, no_text, cancel_text) => match custom { 195 | ID_CUSTOM_YES => MessageDialogResult::Custom(yes_text), 196 | ID_CUSTOM_NO => MessageDialogResult::Custom(no_text), 197 | ID_CUSTOM_CANCEL => MessageDialogResult::Custom(cancel_text), 198 | _ => MessageDialogResult::Cancel, 199 | }, 200 | _ => MessageDialogResult::Cancel, 201 | }, 202 | } 203 | } 204 | 205 | #[cfg(not(feature = "common-controls-v6"))] 206 | pub fn run(self) -> MessageDialogResult { 207 | let ret = unsafe { 208 | MessageBoxW( 209 | self.parent.unwrap_or(std::ptr::null_mut()), 210 | self.text.as_ptr(), 211 | self.caption.as_ptr(), 212 | self.flags, 213 | ) 214 | }; 215 | 216 | match ret { 217 | IDOK => MessageDialogResult::Ok, 218 | IDYES => MessageDialogResult::Yes, 219 | IDCANCEL => MessageDialogResult::Cancel, 220 | IDNO => MessageDialogResult::No, 221 | _ => MessageDialogResult::Cancel, 222 | } 223 | } 224 | 225 | pub fn run_async(self) -> ThreadFuture { 226 | ThreadFuture::new(move |data| *data = Some(self.run())) 227 | } 228 | } 229 | 230 | use crate::backend::MessageDialogImpl; 231 | 232 | impl MessageDialogImpl for MessageDialog { 233 | fn show(self) -> MessageDialogResult { 234 | let dialog = WinMessageDialog::new(self); 235 | dialog.run() 236 | } 237 | } 238 | 239 | use crate::backend::AsyncMessageDialogImpl; 240 | use crate::backend::DialogFutureType; 241 | 242 | impl AsyncMessageDialogImpl for MessageDialog { 243 | fn show_async(self) -> DialogFutureType { 244 | let dialog = WinMessageDialog::new(self); 245 | Box::pin(dialog.run_async()) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/backend/win_cid/thread_future.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::sync::{Arc, Mutex, TryLockError}; 3 | 4 | use std::task::{Context, Poll, Waker}; 5 | 6 | struct FutureState { 7 | waker: Mutex>, 8 | data: Mutex>, 9 | } 10 | 11 | pub struct ThreadFuture { 12 | state: Arc>, 13 | } 14 | 15 | unsafe impl Send for ThreadFuture {} 16 | 17 | impl ThreadFuture { 18 | pub fn new) + Send + 'static>(f: F) -> Self { 19 | let state = Arc::new(FutureState { 20 | waker: Mutex::new(None), 21 | data: Mutex::new(None), 22 | }); 23 | 24 | { 25 | let state = state.clone(); 26 | std::thread::spawn(move || { 27 | f(&mut state.data.lock().unwrap()); 28 | 29 | if let Some(waker) = state.waker.lock().unwrap().take() { 30 | waker.wake(); 31 | } 32 | }); 33 | } 34 | 35 | Self { state } 36 | } 37 | } 38 | 39 | impl std::future::Future for ThreadFuture { 40 | type Output = R; 41 | 42 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 43 | let state = &self.state; 44 | let data = state.data.try_lock(); 45 | 46 | match data { 47 | Ok(mut data) => match data.take() { 48 | Some(data) => Poll::Ready(data), 49 | None => { 50 | *state.waker.lock().unwrap() = Some(cx.waker().clone()); 51 | Poll::Pending 52 | } 53 | }, 54 | Err(TryLockError::Poisoned(err)) => { 55 | panic!("{}", err); 56 | } 57 | Err(TryLockError::WouldBlock) => { 58 | *state.waker.lock().unwrap() = Some(cx.waker().clone()); 59 | Poll::Pending 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/backend/win_cid/utils.rs: -------------------------------------------------------------------------------- 1 | use windows_sys::{ 2 | core::HRESULT, 3 | Win32::System::Com::{ 4 | CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, 5 | }, 6 | }; 7 | 8 | #[inline] 9 | pub(crate) fn str_to_vec_u16(s: &str) -> Vec { 10 | let mut v: Vec<_> = s.encode_utf16().collect(); 11 | v.push(0); 12 | v 13 | } 14 | 15 | /// Makes sure that COM lib is initialized long enough 16 | pub fn init_com T>(f: F) -> Result { 17 | let res = unsafe { 18 | CoInitializeEx( 19 | std::ptr::null(), 20 | (COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) as u32, 21 | ) 22 | }; 23 | 24 | if res < 0 { 25 | return Err(res); 26 | } 27 | 28 | let out = f(); 29 | 30 | unsafe { 31 | CoUninitialize(); 32 | } 33 | 34 | Ok(out) 35 | } 36 | -------------------------------------------------------------------------------- /src/backend/win_xp.rs.old: -------------------------------------------------------------------------------- 1 | //! Windows commdlg.h dialogs 2 | //! Win32 XP 3 | use crate::DialogOptions; 4 | 5 | use std::path::PathBuf; 6 | 7 | use winapi::um::commdlg::{ 8 | GetOpenFileNameW, GetSaveFileNameW, OFN_ALLOWMULTISELECT, OFN_EXPLORER, OFN_FILEMUSTEXIST, 9 | OFN_NOCHANGEDIR, OFN_OVERWRITEPROMPT, OFN_PATHMUSTEXIST, 10 | }; 11 | 12 | extern "C" { 13 | fn wcslen(buf: *const u16) -> usize; 14 | } 15 | 16 | mod utils { 17 | use crate::DialogOptions; 18 | 19 | use std::{ffi::OsStr, iter::once, mem, os::windows::ffi::OsStrExt}; 20 | 21 | use winapi::um::commdlg::OPENFILENAMEW; 22 | 23 | pub unsafe fn build_ofn( 24 | path: &mut Vec, 25 | filters: Option<&Vec>, 26 | flags: u32, 27 | ) -> OPENFILENAMEW { 28 | let mut ofn: OPENFILENAMEW = std::mem::zeroed(); 29 | ofn.lStructSize = mem::size_of::() as u32; 30 | ofn.hwndOwner = std::mem::zeroed(); 31 | 32 | ofn.lpstrFile = path.as_mut_ptr(); 33 | ofn.nMaxFile = path.len() as _; 34 | 35 | if let Some(filters) = filters { 36 | ofn.lpstrFilter = filters.as_ptr(); 37 | ofn.nFilterIndex = 1; 38 | } 39 | 40 | ofn.Flags = flags; 41 | 42 | ofn 43 | } 44 | 45 | pub fn build_filters(params: &DialogParams) -> Option> { 46 | let mut filters = String::new(); 47 | 48 | for f in params.filters.iter() { 49 | filters += &format!("{}\0{}\0", f.0, f.1); 50 | } 51 | 52 | let filter: Option> = if !params.filters.is_empty() { 53 | Some(OsStr::new(&filters).encode_wide().chain(once(0)).collect()) 54 | } else { 55 | None 56 | }; 57 | 58 | filter 59 | } 60 | } 61 | 62 | use utils::*; 63 | 64 | pub fn open_file_with_params(params: DialogOptions) -> Option { 65 | let filters = build_filters(¶ms); 66 | 67 | unsafe { 68 | // This vec needs to be initialized with zeros, so we do not use `Vec::with_capacity` here 69 | let mut path: Vec = vec![0; 260]; 70 | 71 | let flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; 72 | 73 | let mut ofn = build_ofn(&mut path, filters.as_ref(), flags); 74 | let out = GetOpenFileNameW(&mut ofn); 75 | 76 | if out == 1 { 77 | let l = wcslen(ofn.lpstrFile); 78 | 79 | // Trim string 80 | path.set_len(l); 81 | 82 | String::from_utf16(&path).ok().map(PathBuf::from) 83 | } else { 84 | None 85 | } 86 | } 87 | } 88 | 89 | pub fn save_file_with_params(params: DialogOptions) -> Option { 90 | let filters = build_filters(¶ms); 91 | 92 | unsafe { 93 | // This vec needs to be initialized with zeros, so we do not use `Vec::with_capacity` here 94 | let mut path: Vec = vec![0; 260]; 95 | 96 | let flags = OFN_EXPLORER 97 | | OFN_OVERWRITEPROMPT 98 | | OFN_PATHMUSTEXIST 99 | | OFN_FILEMUSTEXIST 100 | | OFN_NOCHANGEDIR; 101 | 102 | let mut ofn = build_ofn(&mut path, filters.as_ref(), flags); 103 | let out = GetSaveFileNameW(&mut ofn); 104 | 105 | if out == 1 { 106 | let l = wcslen(ofn.lpstrFile); 107 | // Trim string 108 | path.set_len(l); 109 | 110 | String::from_utf16(&path).ok().map(PathBuf::from) 111 | } else { 112 | None 113 | } 114 | } 115 | } 116 | 117 | pub fn pick_folder_with_params(params: DialogOptions) -> Option { 118 | unimplemented!("pick_folder"); 119 | } 120 | 121 | pub fn open_multiple_files_with_params(params: DialogOptions) -> Option> { 122 | let filters = build_filters(¶ms); 123 | 124 | unsafe { 125 | // This vec needs to be initialized with zeros, so we do not use `Vec::with_capacity` here 126 | let mut path: Vec = vec![0; 260]; 127 | 128 | let flags = OFN_EXPLORER 129 | | OFN_ALLOWMULTISELECT 130 | | OFN_PATHMUSTEXIST 131 | | OFN_FILEMUSTEXIST 132 | | OFN_NOCHANGEDIR; 133 | 134 | let mut ofn = build_ofn(&mut path, filters.as_ref(), flags); 135 | let out = GetOpenFileNameW(&mut ofn); 136 | 137 | if out == 1 { 138 | String::from_utf16(&path).ok().map(|s| { 139 | let mut res = Vec::new(); 140 | 141 | let split = s.split("\u{0}"); 142 | for elm in split { 143 | if !elm.is_empty() { 144 | res.push(PathBuf::from(elm)); 145 | } else { 146 | break; 147 | } 148 | } 149 | 150 | if res.len() == 1 { 151 | // 0th element is path to a files 152 | res 153 | } else { 154 | // 0th element is base path of all files 155 | let dir = res.remove(0); 156 | // Add base path to all files 157 | res.into_iter().map(|i| dir.clone().join(i)).collect() 158 | } 159 | }) 160 | } else { 161 | None 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/backend/xdg_desktop_portal.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::linux::zenity; 4 | use crate::backend::DialogFutureType; 5 | use crate::file_dialog::Filter; 6 | use crate::message_dialog::MessageDialog; 7 | use crate::{FileDialog, FileHandle, MessageButtons, MessageDialogResult}; 8 | 9 | use ashpd::desktop::file_chooser::{FileFilter, OpenFileRequest, SaveFileRequest}; 10 | use ashpd::WindowIdentifier; 11 | 12 | use log::error; 13 | use pollster::block_on; 14 | use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; 15 | 16 | fn to_window_identifier( 17 | window: Option, 18 | display: Option, 19 | ) -> Option { 20 | window.map(|window| { 21 | block_on(Box::pin(async move { 22 | WindowIdentifier::from_raw_handle(&window, display.as_ref()).await 23 | })) 24 | })? 25 | } 26 | 27 | impl From<&Filter> for FileFilter { 28 | fn from(filter: &Filter) -> Self { 29 | let mut ashpd_filter = FileFilter::new(&filter.name); 30 | for file_extension in &filter.extensions { 31 | if file_extension == "*" || file_extension.is_empty() { 32 | ashpd_filter = ashpd_filter.glob("*"); 33 | } else { 34 | ashpd_filter = ashpd_filter.glob(&format!("*.{file_extension}")); 35 | } 36 | } 37 | ashpd_filter 38 | } 39 | } 40 | 41 | // 42 | // File Picker 43 | // 44 | 45 | use crate::backend::FilePickerDialogImpl; 46 | impl FilePickerDialogImpl for FileDialog { 47 | fn pick_file(self) -> Option { 48 | block_on(self.pick_file_async()).map(PathBuf::from) 49 | } 50 | 51 | fn pick_files(self) -> Option> { 52 | block_on(self.pick_files_async()) 53 | .map(|vec_file_handle| vec_file_handle.iter().map(PathBuf::from).collect()) 54 | } 55 | } 56 | 57 | use crate::backend::AsyncFilePickerDialogImpl; 58 | impl AsyncFilePickerDialogImpl for FileDialog { 59 | fn pick_file_async(self) -> DialogFutureType> { 60 | Box::pin(async move { 61 | let res = OpenFileRequest::default() 62 | .identifier(to_window_identifier(self.parent, self.parent_display)) 63 | .multiple(false) 64 | .title(self.title.as_deref().or(None)) 65 | .filters(self.filters.iter().map(From::from)) 66 | .current_folder::<&PathBuf>(&self.starting_directory) 67 | .expect("File path should not be nul-terminated") 68 | .send() 69 | .await; 70 | 71 | if let Err(err) = res { 72 | error!("Failed to pick file: {err}"); 73 | match zenity::pick_file(&self).await { 74 | Ok(res) => res, 75 | Err(err) => { 76 | error!("Failed to pick file with zenity: {err}"); 77 | return None; 78 | } 79 | } 80 | } else { 81 | res.ok() 82 | .and_then(|request| request.response().ok()) 83 | .and_then(|response| { 84 | response 85 | .uris() 86 | .first() 87 | .and_then(|uri| uri.to_file_path().ok()) 88 | }) 89 | } 90 | .map(FileHandle::from) 91 | }) 92 | } 93 | 94 | fn pick_files_async(self) -> DialogFutureType>> { 95 | Box::pin(async move { 96 | let res = OpenFileRequest::default() 97 | .identifier(to_window_identifier(self.parent, self.parent_display)) 98 | .multiple(true) 99 | .title(self.title.as_deref().or(None)) 100 | .filters(self.filters.iter().map(From::from)) 101 | .current_folder::<&PathBuf>(&self.starting_directory) 102 | .expect("File path should not be nul-terminated") 103 | .send() 104 | .await; 105 | 106 | if let Err(err) = res { 107 | error!("Failed to pick files: {err}"); 108 | match zenity::pick_files(&self).await { 109 | Ok(res) => Some(res.into_iter().map(FileHandle::from).collect::>()), 110 | Err(err) => { 111 | error!("Failed to pick files with zenity: {err}"); 112 | None 113 | } 114 | } 115 | } else { 116 | res.ok() 117 | .and_then(|request| request.response().ok()) 118 | .map(|response| { 119 | response 120 | .uris() 121 | .iter() 122 | .filter_map(|uri| uri.to_file_path().ok()) 123 | .map(FileHandle::from) 124 | .collect::>() 125 | }) 126 | } 127 | }) 128 | } 129 | } 130 | 131 | // 132 | // Folder Picker 133 | // 134 | 135 | use crate::backend::FolderPickerDialogImpl; 136 | impl FolderPickerDialogImpl for FileDialog { 137 | fn pick_folder(self) -> Option { 138 | block_on(self.pick_folder_async()).map(PathBuf::from) 139 | } 140 | 141 | fn pick_folders(self) -> Option> { 142 | block_on(self.pick_folders_async()) 143 | .map(|vec_file_handle| vec_file_handle.iter().map(PathBuf::from).collect()) 144 | } 145 | } 146 | 147 | use crate::backend::AsyncFolderPickerDialogImpl; 148 | impl AsyncFolderPickerDialogImpl for FileDialog { 149 | fn pick_folder_async(self) -> DialogFutureType> { 150 | Box::pin(async move { 151 | let res = OpenFileRequest::default() 152 | .identifier(to_window_identifier(self.parent, self.parent_display)) 153 | .multiple(false) 154 | .directory(true) 155 | .title(self.title.as_deref().or(None)) 156 | .filters(self.filters.iter().map(From::from)) 157 | .current_folder::<&PathBuf>(&self.starting_directory) 158 | .expect("File path should not be nul-terminated") 159 | .send() 160 | .await; 161 | 162 | if let Err(err) = res { 163 | error!("Failed to pick folder: {err}"); 164 | match zenity::pick_folder(&self).await { 165 | Ok(res) => res, 166 | Err(err) => { 167 | error!("Failed to pick folder with zenity: {err}"); 168 | return None; 169 | } 170 | } 171 | } else { 172 | res.ok() 173 | .and_then(|request| request.response().ok()) 174 | .and_then(|response| { 175 | response 176 | .uris() 177 | .first() 178 | .and_then(|uri| uri.to_file_path().ok()) 179 | }) 180 | } 181 | .map(FileHandle::from) 182 | }) 183 | } 184 | 185 | fn pick_folders_async(self) -> DialogFutureType>> { 186 | Box::pin(async move { 187 | let res = OpenFileRequest::default() 188 | .identifier(to_window_identifier(self.parent, self.parent_display)) 189 | .multiple(true) 190 | .directory(true) 191 | .title(self.title.as_deref().or(None)) 192 | .filters(self.filters.iter().map(From::from)) 193 | .current_folder::<&PathBuf>(&self.starting_directory) 194 | .expect("File path should not be nul-terminated") 195 | .send() 196 | .await; 197 | 198 | if let Err(err) = res { 199 | error!("Failed to pick folders: {err}"); 200 | match zenity::pick_folders(&self).await { 201 | Ok(res) => Some(res.into_iter().map(FileHandle::from).collect::>()), 202 | Err(err) => { 203 | error!("Failed to pick folders with zenity: {err}"); 204 | None 205 | } 206 | } 207 | } else { 208 | res.ok() 209 | .and_then(|request| request.response().ok()) 210 | .map(|response| { 211 | response 212 | .uris() 213 | .iter() 214 | .filter_map(|uri| uri.to_file_path().ok()) 215 | .map(FileHandle::from) 216 | .collect::>() 217 | }) 218 | } 219 | }) 220 | } 221 | } 222 | 223 | // 224 | // File Save 225 | // 226 | 227 | use crate::backend::FileSaveDialogImpl; 228 | impl FileSaveDialogImpl for FileDialog { 229 | fn save_file(self) -> Option { 230 | block_on(self.save_file_async()).map(PathBuf::from) 231 | } 232 | } 233 | 234 | use crate::backend::AsyncFileSaveDialogImpl; 235 | impl AsyncFileSaveDialogImpl for FileDialog { 236 | fn save_file_async(self) -> DialogFutureType> { 237 | Box::pin(async move { 238 | let res = SaveFileRequest::default() 239 | .identifier(to_window_identifier(self.parent, self.parent_display)) 240 | .title(self.title.as_deref().or(None)) 241 | .current_name(self.file_name.as_deref()) 242 | .filters(self.filters.iter().map(From::from)) 243 | .current_folder::<&PathBuf>(&self.starting_directory) 244 | .expect("File path should not be nul-terminated") 245 | .send() 246 | .await; 247 | 248 | if let Err(err) = res { 249 | error!("Failed to save file: {err}"); 250 | match zenity::save_file(&self).await { 251 | Ok(res) => res, 252 | Err(err) => { 253 | error!("Failed to save file with zenity: {err}"); 254 | return None; 255 | } 256 | } 257 | } else { 258 | res.ok() 259 | .and_then(|request| request.response().ok()) 260 | .and_then(|response| { 261 | response 262 | .uris() 263 | .first() 264 | .and_then(|uri| uri.to_file_path().ok()) 265 | }) 266 | } 267 | .map(FileHandle::from) 268 | }) 269 | } 270 | } 271 | 272 | use crate::backend::MessageDialogImpl; 273 | impl MessageDialogImpl for MessageDialog { 274 | fn show(self) -> MessageDialogResult { 275 | block_on(self.show_async()) 276 | } 277 | } 278 | 279 | use crate::backend::AsyncMessageDialogImpl; 280 | impl AsyncMessageDialogImpl for MessageDialog { 281 | fn show_async(self) -> DialogFutureType { 282 | Box::pin(async move { 283 | match &self.buttons { 284 | MessageButtons::Ok | MessageButtons::OkCustom(_) => { 285 | let res = crate::backend::linux::zenity::message( 286 | &self.level, 287 | &self.buttons, 288 | &self.title, 289 | &self.description, 290 | ) 291 | .await; 292 | 293 | match res { 294 | Ok(res) => res, 295 | Err(err) => { 296 | error!("Failed to open zenity dialog: {err}"); 297 | MessageDialogResult::Cancel 298 | } 299 | } 300 | } 301 | MessageButtons::OkCancel 302 | | MessageButtons::YesNo 303 | | MessageButtons::OkCancelCustom(..) 304 | | MessageButtons::YesNoCancel 305 | | MessageButtons::YesNoCancelCustom(..) => { 306 | let res = crate::backend::linux::zenity::question( 307 | &self.buttons, 308 | &self.title, 309 | &self.description, 310 | ) 311 | .await; 312 | 313 | match res { 314 | Ok(res) => res, 315 | Err(err) => { 316 | error!("Failed to open zenity dialog: {err}"); 317 | MessageDialogResult::Cancel 318 | } 319 | } 320 | } 321 | } 322 | }) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/file_dialog.rs: -------------------------------------------------------------------------------- 1 | use crate::FileHandle; 2 | 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | 6 | use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub(crate) struct Filter { 10 | #[allow(dead_code)] 11 | pub name: String, 12 | pub extensions: Vec, 13 | } 14 | 15 | /// Synchronous File Dialog. Supported platforms: 16 | /// * Linux 17 | /// * Windows 18 | /// * Mac 19 | #[derive(Default, Debug, Clone)] 20 | pub struct FileDialog { 21 | pub(crate) filters: Vec, 22 | pub(crate) starting_directory: Option, 23 | pub(crate) file_name: Option, 24 | pub(crate) title: Option, 25 | pub(crate) parent: Option, 26 | pub(crate) parent_display: Option, 27 | pub(crate) can_create_directories: Option, 28 | } 29 | 30 | // Oh god, I don't like sending RawWindowHandle between threads but here we go anyways... 31 | // fingers crossed 32 | unsafe impl Send for FileDialog {} 33 | unsafe impl Sync for FileDialog {} 34 | 35 | impl FileDialog { 36 | /// New file dialog builder 37 | #[cfg(not(target_arch = "wasm32"))] 38 | pub fn new() -> Self { 39 | Default::default() 40 | } 41 | 42 | /// Add file extension filter. 43 | /// 44 | /// Takes in the name of the filter, and list of extensions 45 | /// 46 | /// The name of the filter will be displayed on supported platforms: 47 | /// * Windows 48 | /// * Linux 49 | /// 50 | /// On platforms that don't support filter names, all filters will be merged into one filter 51 | pub fn add_filter(mut self, name: impl Into, extensions: &[impl ToString]) -> Self { 52 | self.filters.push(Filter { 53 | name: name.into(), 54 | extensions: extensions.iter().map(|e| e.to_string()).collect(), 55 | }); 56 | self 57 | } 58 | 59 | /// Set starting directory of the dialog. Supported platforms: 60 | /// * Linux ([GTK only](https://github.com/PolyMeilex/rfd/issues/42)) 61 | /// * Windows 62 | /// * Mac 63 | pub fn set_directory>(mut self, path: P) -> Self { 64 | let path = path.as_ref(); 65 | if path.to_str().map(|p| p.is_empty()).unwrap_or(false) { 66 | self.starting_directory = None; 67 | } else { 68 | self.starting_directory = Some(path.into()); 69 | } 70 | self 71 | } 72 | 73 | /// Set starting file name of the dialog. Supported platforms: 74 | /// * Windows 75 | /// * Linux 76 | /// * Mac 77 | pub fn set_file_name(mut self, file_name: impl Into) -> Self { 78 | self.file_name = Some(file_name.into()); 79 | self 80 | } 81 | 82 | /// Set the title of the dialog. Supported platforms: 83 | /// * Windows 84 | /// * Linux 85 | /// * Mac 86 | pub fn set_title(mut self, title: impl Into) -> Self { 87 | self.title = Some(title.into()); 88 | self 89 | } 90 | 91 | /// Set parent windows explicitly (optional). 92 | /// Supported platforms: 93 | /// * Windows 94 | /// * Mac 95 | /// * Linux (XDG only) 96 | pub fn set_parent(mut self, parent: &W) -> Self { 97 | self.parent = parent.window_handle().ok().map(|x| x.as_raw()); 98 | self.parent_display = parent.display_handle().ok().map(|x| x.as_raw()); 99 | self 100 | } 101 | 102 | /// Set can create directories in the dialog. 103 | /// Suported in: `macos`. 104 | pub fn set_can_create_directories(mut self, can: bool) -> Self { 105 | self.can_create_directories.replace(can); 106 | self 107 | } 108 | } 109 | 110 | #[cfg(not(target_arch = "wasm32"))] 111 | use crate::backend::{FilePickerDialogImpl, FileSaveDialogImpl, FolderPickerDialogImpl}; 112 | 113 | #[cfg(not(target_arch = "wasm32"))] 114 | impl FileDialog { 115 | /// Pick one file 116 | pub fn pick_file(self) -> Option { 117 | FilePickerDialogImpl::pick_file(self) 118 | } 119 | 120 | /// Pick multiple files 121 | pub fn pick_files(self) -> Option> { 122 | FilePickerDialogImpl::pick_files(self) 123 | } 124 | 125 | /// Pick one folder 126 | pub fn pick_folder(self) -> Option { 127 | FolderPickerDialogImpl::pick_folder(self) 128 | } 129 | 130 | /// Pick multiple folders 131 | pub fn pick_folders(self) -> Option> { 132 | FolderPickerDialogImpl::pick_folders(self) 133 | } 134 | 135 | /// Opens save file dialog 136 | /// 137 | /// #### Platform specific notes regarding save dialog filters: 138 | /// - On macOS 139 | /// - If filter is set, all files will be grayed out (no matter the extension sadly) 140 | /// - If user does not type an extension MacOs will append first available extension from filters list 141 | /// - If user types in filename with extension MacOs will check if it exists in filters list, if not it will display appropriate message 142 | /// - On GTK 143 | /// - It only filters which already existing files get shown to the user 144 | /// - It does not append extensions automatically 145 | /// - It does not prevent users from adding any unsupported extension 146 | /// - On Win: 147 | /// - If no extension was provided it will just add currently selected one 148 | /// - If selected extension was typed in by the user it will just return 149 | /// - If unselected extension was provided it will append selected one at the end, example: `test.png.txt` 150 | pub fn save_file(self) -> Option { 151 | FileSaveDialogImpl::save_file(self) 152 | } 153 | } 154 | 155 | /// Asynchronous File Dialog. Supported platforms: 156 | /// * Linux 157 | /// * Windows 158 | /// * Mac 159 | /// * WASM32 160 | #[derive(Default, Debug, Clone)] 161 | pub struct AsyncFileDialog { 162 | file_dialog: FileDialog, 163 | } 164 | 165 | impl AsyncFileDialog { 166 | /// New file dialog builder 167 | pub fn new() -> Self { 168 | Default::default() 169 | } 170 | 171 | /// Add file extension filter. 172 | /// 173 | /// Takes in the name of the filter, and list of extensions 174 | /// 175 | /// The name of the filter will be displayed on supported platforms: 176 | /// * Windows 177 | /// * Linux 178 | /// 179 | /// On platforms that don't support filter names, all filters will be merged into one filter 180 | pub fn add_filter(mut self, name: impl Into, extensions: &[impl ToString]) -> Self { 181 | self.file_dialog = self.file_dialog.add_filter(name, extensions); 182 | self 183 | } 184 | 185 | /// Set starting directory of the dialog. Supported platforms: 186 | /// * Linux ([GTK only](https://github.com/PolyMeilex/rfd/issues/42)) 187 | /// * Windows 188 | /// * Mac 189 | pub fn set_directory>(mut self, path: P) -> Self { 190 | self.file_dialog = self.file_dialog.set_directory(path); 191 | self 192 | } 193 | 194 | /// Set starting file name of the dialog. Supported platforms: 195 | /// * Windows 196 | /// * Linux 197 | /// * Mac 198 | pub fn set_file_name(mut self, file_name: impl Into) -> Self { 199 | self.file_dialog = self.file_dialog.set_file_name(file_name); 200 | self 201 | } 202 | 203 | /// Set the title of the dialog. Supported platforms: 204 | /// * Windows 205 | /// * Linux 206 | /// * Mac 207 | /// * WASM32 208 | pub fn set_title(mut self, title: impl Into) -> Self { 209 | self.file_dialog = self.file_dialog.set_title(title); 210 | self 211 | } 212 | 213 | /// Set parent windows explicitly (optional). 214 | /// Supported platforms: 215 | /// * Windows 216 | /// * Mac 217 | /// * Linux (XDG only) 218 | pub fn set_parent(mut self, parent: &W) -> Self { 219 | self.file_dialog = self.file_dialog.set_parent(parent); 220 | self 221 | } 222 | 223 | /// Set can create directories in the dialog. 224 | /// Suported in: `macos`. 225 | pub fn set_can_create_directories(mut self, can: bool) -> Self { 226 | self.file_dialog = self.file_dialog.set_can_create_directories(can); 227 | self 228 | } 229 | } 230 | 231 | use crate::backend::AsyncFilePickerDialogImpl; 232 | use crate::backend::AsyncFileSaveDialogImpl; 233 | #[cfg(not(target_arch = "wasm32"))] 234 | use crate::backend::AsyncFolderPickerDialogImpl; 235 | 236 | use std::future::Future; 237 | 238 | impl AsyncFileDialog { 239 | /// Pick one file 240 | pub fn pick_file(self) -> impl Future> { 241 | AsyncFilePickerDialogImpl::pick_file_async(self.file_dialog) 242 | } 243 | 244 | /// Pick multiple files 245 | pub fn pick_files(self) -> impl Future>> { 246 | AsyncFilePickerDialogImpl::pick_files_async(self.file_dialog) 247 | } 248 | 249 | #[cfg(not(target_arch = "wasm32"))] 250 | /// Pick one folder 251 | /// 252 | /// Does not exist in `WASM32` 253 | pub fn pick_folder(self) -> impl Future> { 254 | AsyncFolderPickerDialogImpl::pick_folder_async(self.file_dialog) 255 | } 256 | 257 | #[cfg(not(target_arch = "wasm32"))] 258 | /// Pick multiple folders 259 | /// 260 | /// Does not exist in `WASM32` 261 | pub fn pick_folders(self) -> impl Future>> { 262 | AsyncFolderPickerDialogImpl::pick_folders_async(self.file_dialog) 263 | } 264 | 265 | /// Opens save file dialog 266 | /// 267 | /// #### Platform specific notes regarding save dialog filters: 268 | /// - On MacOs 269 | /// - If filter is set, all files will be grayed out (no matter the extension sadly) 270 | /// - If user does not type an extension MacOs will append first available extension from filters list 271 | /// - If user types in filename with extension MacOs will check if it exists in filters list, if not it will display appropriate message 272 | /// - On GTK 273 | /// - It only filters which already existing files get shown to the user 274 | /// - It does not append extensions automatically 275 | /// - It does not prevent users from adding any unsupported extension 276 | /// - On Win: 277 | /// - If no extension was provided it will just add currently selected one 278 | /// - If selected extension was typed in by the user it will just return 279 | /// - If unselected extension was provided it will append selected one at the end, example: `test.png.txt` 280 | /// - On Wasm32: 281 | /// - No filtering is applied. 282 | /// - `save_file` returns immediately without a dialog prompt. 283 | /// Instead the user is prompted by their browser on where to save the file when [`FileHandle::write`] is used. 284 | pub fn save_file(self) -> impl Future> { 285 | AsyncFileSaveDialogImpl::save_file_async(self.file_dialog) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/file_handle/mod.rs: -------------------------------------------------------------------------------- 1 | //! FileHandle is a way of abstracting over a file returned by a dialog 2 | //! 3 | //! On native targets it just wraps a path of a file. 4 | //! In web browsers it wraps `File` js object 5 | //! 6 | //! It should allow a user to treat web browser files same way as native files 7 | 8 | #[cfg(not(target_arch = "wasm32"))] 9 | mod native; 10 | #[cfg(not(target_arch = "wasm32"))] 11 | pub use native::FileHandle; 12 | 13 | #[cfg(target_arch = "wasm32")] 14 | mod web; 15 | #[cfg(target_arch = "wasm32")] 16 | pub use web::FileHandle; 17 | #[cfg(target_arch = "wasm32")] 18 | pub(crate) use web::WasmFileHandleKind; 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::FileHandle; 23 | 24 | #[test] 25 | fn fn_def_check() { 26 | let _ = FileHandle::wrap; 27 | let _ = FileHandle::read; 28 | #[cfg(feature = "file-handle-inner")] 29 | let _ = FileHandle::inner; 30 | #[cfg(not(target_arch = "wasm32"))] 31 | let _ = FileHandle::path; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/file_handle/native.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | path::{Path, PathBuf}, 4 | pin::Pin, 5 | sync::{Arc, Mutex}, 6 | task::{Context, Poll, Waker}, 7 | }; 8 | 9 | #[derive(Default)] 10 | struct ReaderState { 11 | res: Option>>, 12 | waker: Option, 13 | } 14 | 15 | struct Reader { 16 | state: Arc>, 17 | } 18 | 19 | impl Reader { 20 | fn new(path: &Path) -> Self { 21 | let state: Arc> = Arc::new(Mutex::new(Default::default())); 22 | 23 | { 24 | let path = path.to_owned(); 25 | let state = state.clone(); 26 | std::thread::Builder::new() 27 | .name("rfd_file_read".into()) 28 | .spawn(move || { 29 | let res = std::fs::read(path); 30 | 31 | let mut state = state.lock().unwrap(); 32 | state.res.replace(res); 33 | 34 | if let Some(waker) = state.waker.take() { 35 | waker.wake(); 36 | } 37 | }) 38 | .unwrap(); 39 | } 40 | 41 | Self { state } 42 | } 43 | } 44 | 45 | impl Future for Reader { 46 | type Output = Vec; 47 | 48 | fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { 49 | let mut state = self.state.lock().unwrap(); 50 | if let Some(res) = state.res.take() { 51 | Poll::Ready(res.unwrap()) 52 | } else { 53 | state.waker.replace(ctx.waker().clone()); 54 | Poll::Pending 55 | } 56 | } 57 | } 58 | 59 | struct WriterState { 60 | waker: Option, 61 | res: Option>, 62 | } 63 | 64 | struct Writer { 65 | state: Arc>, 66 | } 67 | 68 | impl Writer { 69 | fn new(path: &Path, bytes: &[u8]) -> Self { 70 | let state = Arc::new(Mutex::new(WriterState { 71 | waker: None, 72 | res: None, 73 | })); 74 | 75 | { 76 | let path = path.to_owned(); 77 | let bytes = bytes.to_owned(); 78 | let state = state.clone(); 79 | std::thread::Builder::new() 80 | .name("rfd_file_write".into()) 81 | .spawn(move || { 82 | let res = std::fs::write(path, bytes); 83 | 84 | let mut state = state.lock().unwrap(); 85 | state.res.replace(res); 86 | 87 | if let Some(waker) = state.waker.take() { 88 | waker.wake(); 89 | } 90 | }) 91 | .unwrap(); 92 | } 93 | 94 | Self { state } 95 | } 96 | } 97 | 98 | impl Future for Writer { 99 | type Output = std::io::Result<()>; 100 | 101 | fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { 102 | let mut state = self.state.lock().unwrap(); 103 | if let Some(res) = state.res.take() { 104 | Poll::Ready(res) 105 | } else { 106 | state.waker.replace(ctx.waker().clone()); 107 | Poll::Pending 108 | } 109 | } 110 | } 111 | 112 | /// FileHandle is a way of abstracting over a file returned by a dialog 113 | #[derive(Clone)] 114 | pub struct FileHandle(PathBuf); 115 | 116 | impl FileHandle { 117 | /// On native platforms it wraps path. 118 | /// 119 | /// On `WASM32` it wraps JS `File` object. 120 | pub(crate) fn wrap(path_buf: PathBuf) -> Self { 121 | Self(path_buf) 122 | } 123 | 124 | /// Get name of a file 125 | pub fn file_name(&self) -> String { 126 | self.0 127 | .file_name() 128 | .and_then(|f| f.to_str()) 129 | .map(|f| f.to_string()) 130 | .unwrap_or_default() 131 | } 132 | 133 | /// Gets path to a file. 134 | /// 135 | /// Does not exist in `WASM32` 136 | pub fn path(&self) -> &Path { 137 | &self.0 138 | } 139 | 140 | /// Reads a file asynchronously. 141 | /// 142 | /// On native platforms it spawns a `std::thread` in the background. 143 | /// 144 | /// `This fn exists solely to keep native api in pair with async only web api.` 145 | pub async fn read(&self) -> Vec { 146 | Reader::new(&self.0).await 147 | } 148 | 149 | /// Writes a file asynchronously. 150 | /// 151 | /// On native platforms it spawns a `std::thread` in the background. 152 | /// 153 | /// `This fn exists solely to keep native api in pair with async only web api.` 154 | pub async fn write(&self, data: &[u8]) -> std::io::Result<()> { 155 | Writer::new(&self.0, data).await 156 | } 157 | 158 | /// Unwraps a `FileHandle` and returns inner type. 159 | /// 160 | /// It should be used, if user wants to handle file read themselves 161 | /// 162 | /// On native platforms returns path. 163 | /// 164 | /// On `WASM32` it returns JS `File` object. 165 | /// 166 | /// #### Behind a `file-handle-inner` feature flag 167 | #[cfg(feature = "file-handle-inner")] 168 | pub fn inner(&self) -> &Path { 169 | &self.0 170 | } 171 | } 172 | 173 | impl std::fmt::Debug for FileHandle { 174 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 175 | write!(f, "{:?}", self.path()) 176 | } 177 | } 178 | 179 | impl From for FileHandle { 180 | fn from(path: PathBuf) -> Self { 181 | Self::wrap(path) 182 | } 183 | } 184 | 185 | impl From for PathBuf { 186 | fn from(file_handle: FileHandle) -> Self { 187 | file_handle.0 188 | } 189 | } 190 | 191 | impl From<&FileHandle> for PathBuf { 192 | fn from(file_handle: &FileHandle) -> Self { 193 | PathBuf::from(file_handle.path()) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/file_handle/web.rs: -------------------------------------------------------------------------------- 1 | use crate::file_dialog::FileDialog; 2 | use wasm_bindgen::prelude::*; 3 | use wasm_bindgen::JsCast; 4 | 5 | #[derive(Clone, Debug)] 6 | pub(crate) enum WasmFileHandleKind { 7 | Readable(web_sys::File), 8 | Writable(FileDialog), 9 | } 10 | 11 | #[derive(Clone)] 12 | pub struct FileHandle(pub(crate) WasmFileHandleKind); 13 | 14 | impl FileHandle { 15 | /// Wrap a [`web_sys::File`] for reading. Use with [`FileHandle::read`] 16 | pub(crate) fn wrap(file: web_sys::File) -> Self { 17 | Self(WasmFileHandleKind::Readable(file)) 18 | } 19 | 20 | /// Create a dummy `FileHandle`. Use with [`FileHandle::write`]. 21 | pub(crate) fn writable(dialog: FileDialog) -> Self { 22 | FileHandle(WasmFileHandleKind::Writable(dialog)) 23 | } 24 | 25 | pub fn file_name(&self) -> String { 26 | match &self.0 { 27 | WasmFileHandleKind::Readable(x) => x.name(), 28 | WasmFileHandleKind::Writable(x) => x.file_name.clone().unwrap_or_default(), 29 | } 30 | } 31 | 32 | // Path is not supported in browsers. 33 | // Use read() instead. 34 | // pub fn path(&self) -> &Path { 35 | // // compile_error!(); 36 | // unimplemented!("Path is not supported in browsers"); 37 | // } 38 | 39 | pub async fn read(&self) -> Vec { 40 | let promise = js_sys::Promise::new(&mut move |res, _rej| { 41 | let file_reader = web_sys::FileReader::new().unwrap(); 42 | 43 | let fr = file_reader.clone(); 44 | let closure = Closure::wrap(Box::new(move || { 45 | res.call1(&JsValue::undefined(), &fr.result().unwrap()) 46 | .unwrap(); 47 | }) as Box); 48 | 49 | file_reader.set_onload(Some(closure.as_ref().unchecked_ref())); 50 | 51 | closure.forget(); 52 | 53 | if let WasmFileHandleKind::Readable(reader) = &self.0 { 54 | file_reader.read_as_array_buffer(reader).unwrap(); 55 | } else { 56 | panic!("This File Handle doesn't support reading. Use `pick_file` to get a readable FileHandle"); 57 | } 58 | }); 59 | 60 | let future = wasm_bindgen_futures::JsFuture::from(promise); 61 | 62 | let res = future.await.unwrap(); 63 | 64 | let buffer: js_sys::Uint8Array = js_sys::Uint8Array::new(&res); 65 | let mut vec = vec![0; buffer.length() as usize]; 66 | buffer.copy_to(&mut vec[..]); 67 | 68 | vec 69 | } 70 | 71 | #[cfg(feature = "file-handle-inner")] 72 | pub fn inner(&self) -> &web_sys::File { 73 | if let WasmFileHandleKind::Readable(reader) = &self.0 { 74 | reader 75 | } else { 76 | panic!("This File Handle doesn't support reading. Use `pick_file` to get a readable FileHandle"); 77 | } 78 | } 79 | } 80 | 81 | impl std::fmt::Debug for FileHandle { 82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 83 | write!(f, "{}", self.file_name()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Rusty File Dialogs is a cross platform library for using native file open/save dialogs. 2 | //! It provides both asynchronous and synchronous APIs. Supported platforms: 3 | //! 4 | //! * Windows 5 | //! * macOS 6 | //! * Linux & BSDs (GTK3 or XDG Desktop Portal) 7 | //! * WASM32 (async only) 8 | //! 9 | //! # Examples 10 | //! 11 | //! ## Synchronous 12 | //! ```no_run 13 | //! use rfd::FileDialog; 14 | //! 15 | //! let files = FileDialog::new() 16 | //! .add_filter("text", &["txt", "rs"]) 17 | //! .add_filter("rust", &["rs", "toml"]) 18 | //! .set_directory("/") 19 | //! .pick_file(); 20 | //! ``` 21 | //! 22 | //! ## Asynchronous 23 | //! ```no_run 24 | //! use rfd::AsyncFileDialog; 25 | //! 26 | //! let future = async { 27 | //! let file = AsyncFileDialog::new() 28 | //! .add_filter("text", &["txt", "rs"]) 29 | //! .add_filter("rust", &["rs", "toml"]) 30 | //! .set_directory("/") 31 | //! .pick_file() 32 | //! .await; 33 | //! 34 | //! let data = file.unwrap().read().await; 35 | //! }; 36 | //! ``` 37 | //! 38 | //! # Linux & BSD backends 39 | //! 40 | //! On Linux & BSDs, two backends are available, one using the [GTK3 Rust bindings](https://gtk-rs.org/) 41 | //! and the other using the [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal) 42 | //! D-Bus API through [ashpd](https://github.com/bilelmoussaoui/ashpd) & 43 | //! [zbus](https://gitlab.freedesktop.org/dbus/zbus/). 44 | //! 45 | //! ## GTK backend 46 | //! The GTK backend is used when the `xdg-portal` feature is disabled with the [`default-features = false`](https://doc.rust-lang.org/cargo/reference/features.html#dependency-features), and `gtk3` is enabled instead. The GTK3 47 | //! backend requires the C library and development headers to be installed to build RFD. The package 48 | //! names on various distributions are: 49 | //! 50 | //! | Distribution | Installation Command | 51 | //! | --------------- | ------------ | 52 | //! | Fedora | dnf install gtk3-devel | 53 | //! | Arch | pacman -S gtk3 | 54 | //! | Debian & Ubuntu | apt install libgtk-3-dev | 55 | //! 56 | //! ## XDG Desktop Portal backend 57 | //! The XDG Desktop Portal backend is used with the `xdg-portal` Cargo feature which is enabled by default. Either the `tokio` or `async-std` feature must be enabled. This backend will use either the GTK or KDE file dialog depending on the desktop environment 58 | //! in use at runtime. It does not have any non-Rust 59 | //! build dependencies, however it requires the user to have either the 60 | //! [GTK](https://github.com/flatpak/xdg-desktop-portal-gtk), 61 | //! [GNOME](https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome), or 62 | //! [KDE](https://invent.kde.org/plasma/xdg-desktop-portal-kde/) XDG Desktop Portal backend installed 63 | //! at runtime. These are typically installed by the distribution together with the desktop environment. 64 | //! If you are packaging an application that uses RFD, ensure either one of these is installed 65 | //! with the package. The 66 | //! [wlroots portal backend](https://github.com/emersion/xdg-desktop-portal-wlr) does not implement the 67 | //! D-Bus API that RFD requires (it does not interfere with the other portal implementations; 68 | //! they can all be installed simultaneously). 69 | //! 70 | //! The XDG Desktop Portal has no API for message dialogs, so the [MessageDialog] and 71 | //! [AsyncMessageDialog] structs will not build with this backend. 72 | //! 73 | //! # macOS non-windowed applications, async, and threading 74 | //! 75 | //! macOS async dialogs require an `NSApplication` instance, so the dialog is only truly async when 76 | //! opened in windowed environment like `winit` or `SDL2`. Otherwise, it will fallback to sync dialog. 77 | //! It is also recommended to spawn dialogs on your main thread. RFD can run dialogs from any thread 78 | //! but it is only possible in a windowed app and it adds a little bit of overhead. So it is recommended 79 | //! to [spawn on main and await in other thread](https://github.com/PolyMeilex/rfd/blob/master/examples/async.rs). 80 | //! Non-windowed apps will never be able to spawn async dialogs or from threads other than the main thread. 81 | //! 82 | //! # Customize button texts of message dialog in Windows 83 | //! 84 | //! `TaskDialogIndirect` API is used for showing message dialog which can have customized button texts. 85 | //! It is only provided by ComCtl32.dll v6 but Windows use v5 by default. 86 | //! If you want to customize button texts or just need a modern dialog style (aka *visual styles*), you will need to: 87 | //! 88 | //! 1. Enable cargo feature `common-controls-v6`. 89 | //! 2. Add an application manifest to use ComCtl32.dll v5. See [Windows Controls / Enabling Visual Styles](https://docs.microsoft.com/en-us/windows/win32/controls/cookbook-overview) 90 | //! 91 | //! 92 | //! Here is an [example](https://github.com/PolyMeilex/rfd/tree/master/examples/message-custom-buttons) using [embed-resource](https://docs.rs/embed-resource/latest/embed_resource/). 93 | //! 94 | //! # Cargo features 95 | //! * `gtk3`: Uses GTK for dialogs on Linux & BSDs; has no effect on Windows and macOS 96 | //! * `xdg-portal`: Uses XDG Desktop Portal instead of GTK on Linux & BSDs 97 | //! * `common-controls-v6`: Use `TaskDialogIndirect` API from ComCtl32.dll v6 for showing message dialog. This is necessary if you need to customize dialog button texts. 98 | //! 99 | //! # State 100 | //! 101 | //! | API Stability | 102 | //! | ------------- | 103 | //! | 🚧 | 104 | //! 105 | //! | Feature | Linux | Windows | MacOS | Wasm32 | 106 | //! | ------------ | ----- | ------- | --------- | ------ | 107 | //! | SingleFile | ✔ | ✔ | ✔ | ✔ | 108 | //! | MultipleFile | ✔ | ✔ | ✔ | ✔ | 109 | //! | PickFolder | ✔ | ✔ | ✔ | ✖ | 110 | //! | SaveFile | ✔ | ✔ | ✔ | ✖ | 111 | //! | | | | | | 112 | //! | Filters | ✔ ([GTK only](https://github.com/PolyMeilex/rfd/issues/42)) | ✔ | ✔ | ✔ | 113 | //! | StartingPath | ✔ | ✔ | ✔ | ✖ | 114 | //! | Async | ✔ | ✔ | ✔ | ✔ | 115 | //! 116 | //! # rfd-extras 117 | //! 118 | //! AKA features that are not file related 119 | //! 120 | //! | Feature | Linux | Windows | MacOS | Wasm32 | 121 | //! | ------------- | ----- | ------- | ----- | ------ | 122 | //! | MessageDialog | ✔ (GTK only) | ✔ | ✔ | ✔ | 123 | //! | PromptDialog | | | | | 124 | //! | ColorPicker | | | | | 125 | 126 | mod backend; 127 | 128 | mod file_handle; 129 | pub use file_handle::FileHandle; 130 | 131 | mod file_dialog; 132 | #[cfg(target_os = "macos")] 133 | mod oneshot; 134 | 135 | #[cfg(not(target_arch = "wasm32"))] 136 | pub use file_dialog::FileDialog; 137 | 138 | pub use file_dialog::AsyncFileDialog; 139 | 140 | mod message_dialog; 141 | pub use message_dialog::{ 142 | AsyncMessageDialog, MessageButtons, MessageDialog, MessageDialogResult, MessageLevel, 143 | }; 144 | -------------------------------------------------------------------------------- /src/message_dialog.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::AsyncMessageDialogImpl; 2 | use crate::backend::MessageDialogImpl; 3 | use std::fmt::{Display, Formatter}; 4 | 5 | use std::future::Future; 6 | 7 | use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; 8 | 9 | /// Synchronous Message Dialog. Supported platforms: 10 | /// * Windows 11 | /// * macOS 12 | /// * Linux (GTK only) 13 | /// * WASM 14 | #[derive(Default, Debug, Clone)] 15 | pub struct MessageDialog { 16 | pub(crate) title: String, 17 | pub(crate) description: String, 18 | pub(crate) level: MessageLevel, 19 | pub(crate) buttons: MessageButtons, 20 | pub(crate) parent: Option, 21 | pub(crate) parent_display: Option, 22 | } 23 | 24 | // Oh god, I don't like sending RawWindowHandle between threads but here we go anyways... 25 | // fingers crossed 26 | unsafe impl Send for MessageDialog {} 27 | 28 | impl MessageDialog { 29 | pub fn new() -> Self { 30 | Default::default() 31 | } 32 | 33 | /// Set level of a dialog 34 | /// 35 | /// Depending on the system it can result in level specific icon to show up, 36 | /// the will inform user it message is a error, warning or just information. 37 | pub fn set_level(mut self, level: MessageLevel) -> Self { 38 | self.level = level; 39 | self 40 | } 41 | 42 | /// Set title of a dialog 43 | pub fn set_title(mut self, text: impl Into) -> Self { 44 | self.title = text.into(); 45 | self 46 | } 47 | 48 | /// Set description of a dialog 49 | /// 50 | /// Description is a content of a dialog 51 | pub fn set_description(mut self, text: impl Into) -> Self { 52 | self.description = text.into(); 53 | self 54 | } 55 | 56 | /// Set the set of button that will be displayed on the dialog 57 | /// 58 | /// - `Ok` dialog is a single `Ok` button 59 | /// - `OkCancel` dialog, will display 2 buttons: ok and cancel. 60 | /// - `YesNo` dialog, will display 2 buttons: yes and no. 61 | /// - `YesNoCancel` dialog, will display 3 buttons: yes, no, and cancel. 62 | pub fn set_buttons(mut self, btn: MessageButtons) -> Self { 63 | self.buttons = btn; 64 | self 65 | } 66 | 67 | /// Set parent windows explicitly (optional). 68 | /// Supported platforms: 69 | /// * Windows 70 | /// * Mac 71 | /// * Linux (XDG only) 72 | pub fn set_parent(mut self, parent: &W) -> Self { 73 | self.parent = parent.window_handle().ok().map(|x| x.as_raw()); 74 | self.parent_display = parent.display_handle().ok().map(|x| x.as_raw()); 75 | self 76 | } 77 | 78 | /// Shows a message dialog and returns the button that was pressed. 79 | pub fn show(self) -> MessageDialogResult { 80 | MessageDialogImpl::show(self) 81 | } 82 | } 83 | 84 | /// Asynchronous Message Dialog. Supported platforms: 85 | /// * Windows 86 | /// * macOS 87 | /// * Linux (GTK only) 88 | /// * WASM 89 | #[derive(Default, Debug, Clone)] 90 | pub struct AsyncMessageDialog(MessageDialog); 91 | 92 | impl AsyncMessageDialog { 93 | pub fn new() -> Self { 94 | Default::default() 95 | } 96 | 97 | /// Set level of a dialog 98 | /// 99 | /// Depending on the system it can result in level specific icon to show up, 100 | /// the will inform user it message is a error, warning or just information. 101 | pub fn set_level(mut self, level: MessageLevel) -> Self { 102 | self.0 = self.0.set_level(level); 103 | self 104 | } 105 | 106 | /// Set title of a dialog 107 | pub fn set_title(mut self, text: impl Into) -> Self { 108 | self.0 = self.0.set_title(text); 109 | self 110 | } 111 | 112 | /// Set description of a dialog 113 | /// 114 | /// Description is a content of a dialog 115 | pub fn set_description(mut self, text: impl Into) -> Self { 116 | self.0 = self.0.set_description(text); 117 | self 118 | } 119 | 120 | /// Set the set of button that will be displayed on the dialog 121 | /// 122 | /// - `Ok` dialog is a single `Ok` button 123 | /// - `OkCancel` dialog, will display 2 buttons ok and cancel. 124 | /// - `YesNo` dialog, will display 2 buttons yes and no. 125 | /// - `YesNoCancel` dialog, will display 3 buttons: yes, no, and cancel. 126 | pub fn set_buttons(mut self, btn: MessageButtons) -> Self { 127 | self.0 = self.0.set_buttons(btn); 128 | self 129 | } 130 | 131 | /// Set parent windows explicitly (optional). 132 | /// Supported platforms: 133 | /// * Windows 134 | /// * Mac 135 | /// * Linux (XDG only) 136 | pub fn set_parent(mut self, parent: &W) -> Self { 137 | self.0 = self.0.set_parent(parent); 138 | self 139 | } 140 | 141 | /// Shows a message dialog and returns the button that was pressed. 142 | pub fn show(self) -> impl Future { 143 | AsyncMessageDialogImpl::show_async(self.0) 144 | } 145 | } 146 | 147 | #[derive(Debug, Clone, Copy)] 148 | pub enum MessageLevel { 149 | Info, 150 | Warning, 151 | Error, 152 | } 153 | 154 | impl Default for MessageLevel { 155 | fn default() -> Self { 156 | Self::Info 157 | } 158 | } 159 | 160 | #[derive(Debug, Clone)] 161 | pub enum MessageButtons { 162 | Ok, 163 | OkCancel, 164 | YesNo, 165 | YesNoCancel, 166 | /// One customizable button. 167 | /// Notice that in Windows, this only works with the feature *common-controls-v6* enabled 168 | OkCustom(String), 169 | /// Two customizable buttons. 170 | /// Notice that in Windows, this only works with the feature *common-controls-v6* enabled 171 | OkCancelCustom(String, String), 172 | /// Three customizable buttons. 173 | /// Notice that in Windows, this only works with the feature *common-controls-v6* enabled 174 | YesNoCancelCustom(String, String, String), 175 | } 176 | 177 | impl Default for MessageButtons { 178 | fn default() -> Self { 179 | Self::Ok 180 | } 181 | } 182 | 183 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 184 | pub enum MessageDialogResult { 185 | Yes, 186 | No, 187 | Ok, 188 | #[default] 189 | Cancel, 190 | Custom(String), 191 | } 192 | 193 | impl Display for MessageDialogResult { 194 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 195 | write!( 196 | f, 197 | "{}", 198 | match self { 199 | Self::Yes => "Yes".to_string(), 200 | Self::No => "No".to_string(), 201 | Self::Ok => "Ok".to_string(), 202 | Self::Cancel => "Cancel".to_string(), 203 | Self::Custom(custom) => format!("Custom({custom})"), 204 | } 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/oneshot.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::sync::{ 5 | atomic::{AtomicBool, Ordering::SeqCst}, 6 | Arc, Mutex, 7 | }; 8 | use std::task::{Context, Poll, Waker}; 9 | 10 | #[must_use = "futures do nothing unless you `.await` or poll them"] 11 | pub struct Receiver { 12 | inner: Arc>, 13 | } 14 | 15 | pub struct Sender { 16 | inner: Arc>, 17 | } 18 | 19 | // The channels do not ever project Pin to the inner T 20 | impl Unpin for Receiver {} 21 | impl Unpin for Sender {} 22 | 23 | /// Internal state of the `Receiver`/`Sender` pair above. This is all used as 24 | /// the internal synchronization between the two for send/recv operations. 25 | struct Inner { 26 | /// Indicates whether this oneshot is complete yet. This is filled in both 27 | /// by `Sender::drop` and by `Receiver::drop`, and both sides interpret it 28 | /// appropriately. 29 | /// 30 | /// For `Receiver`, if this is `true`, then it's guaranteed that `data` is 31 | /// unlocked and ready to be inspected. 32 | /// 33 | /// For `Sender` if this is `true` then the oneshot has gone away and it 34 | /// can return ready from `poll_canceled`. 35 | complete: AtomicBool, 36 | 37 | /// The actual data being transferred as part of this `Receiver`. This is 38 | /// filled in by `Sender::complete` and read by `Receiver::poll`. 39 | /// 40 | /// Note that this is protected by `Lock`, but it is in theory safe to 41 | /// replace with an `UnsafeCell` as it's actually protected by `complete` 42 | /// above. I wouldn't recommend doing this, however, unless someone is 43 | /// supremely confident in the various atomic orderings here and there. 44 | data: Mutex>, 45 | 46 | /// Field to store the task which is blocked in `Receiver::poll`. 47 | /// 48 | /// This is filled in when a oneshot is polled but not ready yet. Note that 49 | /// the `Lock` here, unlike in `data` above, is important to resolve races. 50 | /// Both the `Receiver` and the `Sender` halves understand that if they 51 | /// can't acquire the lock then some important interference is happening. 52 | rx_task: Mutex>, 53 | 54 | /// Like `rx_task` above, except for the task blocked in 55 | /// `Sender::poll_canceled`. Additionally, `Lock` cannot be `UnsafeCell`. 56 | tx_task: Mutex>, 57 | } 58 | 59 | pub fn channel() -> (Sender, Receiver) { 60 | let inner = Arc::new(Inner::new()); 61 | let receiver = Receiver { 62 | inner: inner.clone(), 63 | }; 64 | let sender = Sender { inner }; 65 | (sender, receiver) 66 | } 67 | 68 | impl Inner { 69 | fn new() -> Self { 70 | Self { 71 | complete: AtomicBool::new(false), 72 | data: Mutex::new(None), 73 | rx_task: Mutex::new(None), 74 | tx_task: Mutex::new(None), 75 | } 76 | } 77 | 78 | fn send(&self, t: T) -> Result<(), T> { 79 | if self.complete.load(SeqCst) { 80 | return Err(t); 81 | } 82 | 83 | // Note that this lock acquisition may fail if the receiver 84 | // is closed and sets the `complete` flag to `true`, whereupon 85 | // the receiver may call `poll()`. 86 | if let Ok(mut slot) = self.data.try_lock() { 87 | assert!(slot.is_none()); 88 | *slot = Some(t); 89 | drop(slot); 90 | 91 | // If the receiver called `close()` between the check at the 92 | // start of the function, and the lock being released, then 93 | // the receiver may not be around to receive it, so try to 94 | // pull it back out. 95 | if self.complete.load(SeqCst) { 96 | // If lock acquisition fails, then receiver is actually 97 | // receiving it, so we're good. 98 | if let Ok(mut slot) = self.data.try_lock() { 99 | if let Some(t) = slot.take() { 100 | return Err(t); 101 | } 102 | } 103 | } 104 | Ok(()) 105 | } else { 106 | // Must have been closed 107 | Err(t) 108 | } 109 | } 110 | 111 | fn drop_tx(&self) { 112 | // Flag that we're a completed `Sender` and try to wake up a receiver. 113 | // Whether or not we actually stored any data will get picked up and 114 | // translated to either an item or cancellation. 115 | // 116 | // Note that if we fail to acquire the `rx_task` lock then that means 117 | // we're in one of two situations: 118 | // 119 | // 1. The receiver is trying to block in `poll` 120 | // 2. The receiver is being dropped 121 | // 122 | // In the first case it'll check the `complete` flag after it's done 123 | // blocking to see if it succeeded. In the latter case we don't need to 124 | // wake up anyone anyway. So in both cases it's ok to ignore the `None` 125 | // case of `try_lock` and bail out. 126 | // 127 | // The first case crucially depends on `Lock` using `SeqCst` ordering 128 | // under the hood. If it instead used `Release` / `Acquire` ordering, 129 | // then it would not necessarily synchronize with `inner.complete` 130 | // and deadlock might be possible, as was observed in 131 | // https://github.com/rust-lang/futures-rs/pull/219. 132 | self.complete.store(true, SeqCst); 133 | 134 | if let Ok(mut slot) = self.rx_task.try_lock() { 135 | if let Some(task) = slot.take() { 136 | drop(slot); 137 | task.wake(); 138 | } 139 | } 140 | 141 | // If we registered a task for cancel notification drop it to reduce 142 | // spurious wakeups 143 | if let Ok(mut slot) = self.tx_task.try_lock() { 144 | drop(slot.take()); 145 | } 146 | } 147 | 148 | fn recv(&self, cx: &mut Context<'_>) -> Poll> { 149 | // Check to see if some data has arrived. If it hasn't then we need to 150 | // block our task. 151 | // 152 | // Note that the acquisition of the `rx_task` lock might fail below, but 153 | // the only situation where this can happen is during `Sender::drop` 154 | // when we are indeed completed already. If that's happening then we 155 | // know we're completed so keep going. 156 | let done = if self.complete.load(SeqCst) { 157 | true 158 | } else { 159 | let task = cx.waker().clone(); 160 | match self.rx_task.try_lock() { 161 | Ok(mut slot) => { 162 | *slot = Some(task); 163 | false 164 | } 165 | Err(_) => true, 166 | } 167 | }; 168 | 169 | // If we're `done` via one of the paths above, then look at the data and 170 | // figure out what the answer is. If, however, we stored `rx_task` 171 | // successfully above we need to check again if we're completed in case 172 | // a message was sent while `rx_task` was locked and couldn't notify us 173 | // otherwise. 174 | // 175 | // If we're not done, and we're not complete, though, then we've 176 | // successfully blocked our task and we return `Pending`. 177 | if done || self.complete.load(SeqCst) { 178 | // If taking the lock fails, the sender will realise that the we're 179 | // `done` when it checks the `complete` flag on the way out, and 180 | // will treat the send as a failure. 181 | if let Ok(mut slot) = self.data.try_lock() { 182 | if let Some(data) = slot.take() { 183 | return Poll::Ready(Ok(data)); 184 | } 185 | } 186 | Poll::Ready(Err(Canceled)) 187 | } else { 188 | Poll::Pending 189 | } 190 | } 191 | 192 | fn drop_rx(&self) { 193 | // Indicate to the `Sender` that we're done, so any future calls to 194 | // `poll_canceled` are weeded out. 195 | self.complete.store(true, SeqCst); 196 | 197 | // If we've blocked a task then there's no need for it to stick around, 198 | // so we need to drop it. If this lock acquisition fails, though, then 199 | // it's just because our `Sender` is trying to take the task, so we 200 | // let them take care of that. 201 | if let Ok(mut slot) = self.rx_task.try_lock() { 202 | let task = slot.take(); 203 | drop(slot); 204 | drop(task); 205 | } 206 | 207 | // Finally, if our `Sender` wants to get notified of us going away, it 208 | // would have stored something in `tx_task`. Here we try to peel that 209 | // out and unpark it. 210 | // 211 | // Note that the `try_lock` here may fail, but only if the `Sender` is 212 | // in the process of filling in the task. If that happens then we 213 | // already flagged `complete` and they'll pick that up above. 214 | if let Ok(mut handle) = self.tx_task.try_lock() { 215 | if let Some(task) = handle.take() { 216 | drop(handle); 217 | task.wake() 218 | } 219 | } 220 | } 221 | } 222 | 223 | impl Sender { 224 | pub fn send(self, t: T) -> Result<(), T> { 225 | self.inner.send(t) 226 | } 227 | } 228 | 229 | impl Drop for Sender { 230 | fn drop(&mut self) { 231 | self.inner.drop_tx() 232 | } 233 | } 234 | 235 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 236 | pub struct Canceled; 237 | 238 | impl fmt::Display for Canceled { 239 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 240 | write!(f, "oneshot canceled") 241 | } 242 | } 243 | 244 | impl std::error::Error for Canceled {} 245 | 246 | impl Future for Receiver { 247 | type Output = Result; 248 | 249 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 250 | self.inner.recv(cx) 251 | } 252 | } 253 | 254 | impl Drop for Receiver { 255 | fn drop(&mut self) { 256 | self.inner.drop_rx() 257 | } 258 | } 259 | --------------------------------------------------------------------------------