# Nativer Ordner-Dialog unter Dioxus 0.7 Desktop > Research zu Gitea-Issue #2. Stand: 2026-08-27. > Recherchiert gegen Primärquellen (docs.rs, crates.io-API, Quellcode auf GitHub, offizielle Dioxus-Doku). > **Nichts davon wurde kompiliert** — im Repo wurde bewusst keine Dependency hinzugefügt. Alles unter > „Belegt" ist aus Signaturen/Quellcode/Manifesten abgeleitet, nicht aus einem laufenden Build. ## Kurzantwort Die zugrundeliegende Bibliothek ist in beiden Fällen **`rfd` („Rusty File Dialogs"), `0.17.2`** — aber es gibt **zwei Wege**, und der bequemere war zunächst übersehen worden: **Weg A — ohne neue Dependency (offiziell dokumentiert).** `input { r#type: "file", directory: true, onchange: … }` und der Pfad kommt aus `evt.files()[0].path() -> PathBuf`. `dioxus-desktop` ruft dahinter intern `rfd::AsyncFileDialog::pick_folder()` auf. Kostet **null** zusätzliche Abhängigkeiten. Nachteil: braucht ein DOM-Element und einen echten Nutzer-Klick darauf — nicht aus beliebigem Rust-Code heraus auslösbar. **Weg B — `rfd` direkt als eigene Dependency.** `rfd::AsyncFileDialog::new().pick_folder().await` in einem `onclick`-Handler. Der einzige Weg für einen Dialog, den man **programmatisch** startet. Kostet praktisch nichts, weil `rfd 0.17.2` über `dioxus-desktop` ohnehin schon im Dependency-Graph steht (siehe Abschnitt 2). Abbruch liefert in beiden Fällen `None`. Unter Windows läuft beides **ohne Extra-Abhängigkeit und ohne Feature-Flag** — nur Linux bringt zusätzliche Anforderungen mit. > **Korrektur zur ersten Fassung dieses Dokuments:** Dort stand „Dioxus selbst bringt keine > Datei-/Ordner-Dialog-API mit". **Das war falsch** — der `directory`-Attributweg (Weg A) ist in der > offiziellen 0.7-Doku beschrieben. Richtig bleibt: `dioxus-desktop` **re-exportiert** `rfd` nicht, > und `dioxus-sdk` hat kein Dialog-Modul. --- ## 1. Welche Crate? ### Belegt - `rfd` ist eine plattformübergreifende Bibliothek für **native** Öffnen-/Speichern-Dialoge und bietet „both asynchronous and synchronous APIs". Unterstützte Plattformen laut Crate-Doku: Windows, macOS, Linux & BSDs (GTK3 oder XDG Desktop Portal), WASM32 (nur async). → - Aktuelle Version: **0.17.2**, veröffentlicht 2026-01-12, ~27 Mio. Downloads gesamt. → - Repository: - „PickFolder" ist in der offiziellen Feature-Matrix der Crate für **Linux ✔ / Windows ✔ / macOS ✔** ausgewiesen (nur Wasm32 ✖). → ### Was es *nicht* gibt - **Das offizielle `dioxus-sdk` hat kein Dialog-Modul.** Aktuell `dioxus-sdk 0.7.0` (2025-11-06), Repo . Die vollständige Feature-Liste der Crate lautet: `geolocation`, `notification`, `storage`, `sync`, `time`, `util`, `window` — kein `file`, kein `dialog`, kein `fs`. → - **`dioxus-desktop` re-exportiert `rfd` nicht.** Die Crate *benutzt* `rfd` intern (siehe Weg A in Abschnitt 3a), aber `mod file_upload;` ist privat und die Re-Export-Liste in `lib.rs` enthält nur `tao`, `wry`, `muda`, `trayicon`, `launch`, `AssetRequest`, `Config`, `DesktopContext`, `DesktopService`, `WryEventHandler`, `hooks::*`, Shortcut-Typen und `wry::RequestAsyncResponder`. Ein `dioxus_desktop::rfd` gibt es also **nicht** — wer `rfd` direkt aufrufen will, muss es selbst in die `Cargo.toml` schreiben. → ### Korrigiert gegenüber der ersten Fassung Ein reiner Webview-/HTML-Weg **existiert doch** und ist offiziell dokumentiert — er ist nur kein „Webview-eigener" Dialog, sondern eine Dioxus-Eigenheit, die auf den nativen `rfd`-Dialog abgebildet wird. Siehe Abschnitt 3a. --- ## 2. Kompatibilität — und: `rfd` ist bereits im Graph ### Belegt: die tatsächlich aufgelöste Version ist 0.7.10, nicht 0.7.1 `Cargo.toml` deklariert `dioxus = "0.7.1"`, das ist ein Caret-Requirement. Aufgelöst wird in `Cargo.lock` tatsächlich: | Crate | Version in `Cargo.lock` | |---|---| | `dioxus` | **0.7.10** | | `dioxus-desktop` | **0.7.10** | | `rfd` | **0.17.2** | | `tao` | 0.34.8 | | `wry` | 0.53.5 | | `raw-window-handle` | 0.5.2 **und** 0.6.2 | → `Cargo.lock` im Repo-Root (Zeilen 2337, 2493, 5847, 6521, 5716/5722). **Das ist der wichtigste praktische Befund:** `dioxus-desktop 0.7.10` hat `rfd` als **nicht-optionale** Dependency mit `req = "^0.17.2"`, `default-features = false`, `features = ["xdg-portal"]`, unter `cfg(any(windows, macos, linux, dragonfly, freebsd, netbsd, openbsd))`. → Ein `rfd = "0.17"` in unserer `Cargo.toml` fügt dem Graph also **keinen neuen Crate hinzu** und kostet **keinen zusätzlichen Compile-Unit** — es macht nur den bereits gebauten Knoten sichtbar. Cargo vereinigt die Features additiv; unsere Default-Features von `rfd` sind ohnehin `xdg-portal`, also genau das, was `dioxus-desktop` schon setzt. *(Historische Notiz: `dioxus-desktop 0.7.1` hatte noch `rfd ^0.15.3`. Der Sprung auf 0.17 kam zwischen 0.7.1 und 0.7.10. Für uns zählt 0.7.10.)* ### Belegt: keine Versionskonflikte - `dioxus-desktop 0.7.1` zieht **`tao ^0.34.0`** und **`wry ^0.53.5`**. → - `rfd 0.17.2` hat **keinerlei Abhängigkeit auf Dioxus, wry oder tao**. Die einzige gemeinsame Berührungsfläche ist `raw-window-handle ^0.6` (unbedingte Dependency von `rfd`). → - `tao 0.34.x` implementiert `HasWindowHandle` und `HasDisplayHandle` aus `raw-window-handle 0.6` für `tao::window::Window`, hinter dem Feature `rwh_06`. → - `rwh_06` ist ein **Default-Feature** von `tao` → , und `dioxus-desktop` schaltet Default-Features nicht ab (`tao = { workspace = true, features = ["rwh_05"] }` — additiv, kein `default-features = false`). → **Fazit:** Es gibt keinen Versionskonflikt. `rfd` und `dioxus-desktop` teilen sich `raw-window-handle 0.6` und passen semver-kompatibel zusammen. ### Vermutung (nicht verifiziert) - Daraus folgt *rechnerisch*, dass `AsyncFileDialog::set_parent(&window)` mit dem Dioxus-Fenster typprüfen sollte: `use_window()` liefert `DesktopContext = Rc` (, ), `DesktopService` implementiert `Deref` (), und `set_parent` nimmt `&W where W: HasWindowHandle + HasDisplayHandle + ?Sized` (). **Das ist nicht kompiliert worden.** Für einen ersten Wurf ist `set_parent` optional — ohne Parent erscheint der Dialog als eigenständiges Fenster statt als besitzer-modaler Dialog. --- ## 3a. Weg A — der offizielle Weg ohne neue Dependency ### Belegt Die offizielle Dioxus-0.7-Doku beschreibt Ordner-Auswahl über ein `directory`-Attribut am File-Input: > „Dioxus will map this attribute to browser specific attributes, because there is no standardized > way to allow a directory to be selected." ```rust input { type: "file", // Select a folder by setting the directory attribute directory: true, onchange: move |evt| { for file in evt.files() { println!("{}", file.name()); } } } ``` → Auf dem **Desktop** ist das kein Browser-Dialog, sondern feuert intern exakt den nativen `rfd`-Ordner-Dialog: ```rust // packages/desktop/src/file_upload.rs if self.directory { if self.multiple { dialog.pick_folders().await } else { dialog.pick_folder().await } } pub struct DesktopFileData(pub(crate) PathBuf); fn path(&self) -> PathBuf { self.0.clone() } ``` → Und der **absolute Pfad** ist öffentlich abgreifbar — `FileData::path()` ist Teil der `dioxus-html`-API: ```rust pub fn path(&self) -> PathBuf ``` → ### Snippet für Weg A ```rust use dioxus::prelude::*; use std::path::PathBuf; #[component] fn WorkspaceOrdnerWahlOhneDependency() -> Element { let mut ordner = use_signal(|| None::); let label = ordner() .map(|p| p.display().to_string()) .unwrap_or_else(|| "Noch kein Ordner gewählt".to_string()); rsx! { input { r#type: "file", directory: true, onchange: move |evt| { // Bei Abbruch ist die Liste leer -> kein Event-File if let Some(file) = evt.files().first() { ordner.set(Some(file.path())); } }, } p { "{label}" } } } ``` ### Grenzen von Weg A - Braucht ein **DOM-Element** und einen echten Nutzer-Klick darauf. Ein Ordner-Dialog, den man aus Rust heraus programmatisch öffnet (z. B. aus einem Menüeintrag, einem Shortcut oder nach einem anderen Ereignis), ist damit **nicht** möglich. - Das Styling ist ein `` — für einen hübschen Button muss man das Element verstecken und per Label/JS triggern. - **Vermutung, nicht verifiziert:** wie sich `evt.files()` bei Abbruch genau verhält (leere Liste vs. gar kein `onchange`-Event). Das obige `if let Some(...)` ist gegen beides robust. --- ## 3b. Blockierend oder async? — Für Weg B im `onclick` zwingend **async** ### Belegt: die Signaturen `rfd` hat zwei parallele Typen. Beide bauen per Builder, unterscheiden sich nur im Rückgabetyp: ```rust // Synchron — rfd::FileDialog pub fn pick_folder(self) -> Option; pub fn pick_folders(self) -> Option>; // Asynchron — rfd::AsyncFileDialog pub fn pick_folder(self) -> impl Future>; pub fn pick_folders(self) -> impl Future>>; ``` → , ### Belegt: warum die synchrone Variante in Dioxus schadet - Die synchrone Windows-Implementierung fährt die modale COM-Dialogschleife **auf dem aufrufenden Thread**: ```rust fn pick_folder(self) -> Option { fn run(opt: FileDialog) -> Result { init_com(|| { let dialog = IDialog::build_pick_folder(&opt)?; dialog.show()?; // blockiert hier bis der Nutzer fertig ist dialog.get_result() })? } run(self).ok() } ``` → - Dioxus-Event-Handler und via `spawn` gestartete Tasks laufen auf **demselben Thread** wie die Event-Loop: `pub fn spawn(fut: impl Future + 'static) -> Task` — **kein `Send`-Bound**, d. h. die Futures können den Thread gar nicht wechseln. → Ein `FileDialog::pick_folder()` direkt im `onclick` blockiert also die tao-Event-Loop → Fenster friert ein, solange der Dialog offen ist. - Die Crate-Doku selbst empfiehlt, Dialoge auf dem Main-Thread zu *starten* („It is also recommended to spawn dialogs on your main thread") — genau das tut man mit der async-Variante aus einem Dioxus-Handler heraus. → ### Belegt: warum die async-Variante unter Windows *nicht* blockiert Die Windows-async-Variante lagert den modalen Dialog auf einen eigenen OS-Thread aus und liefert das Ergebnis über einen Channel zurück: ```rust // async_thread(..) — Kern der Implementierung std::thread::spawn(move || { tx.send(f()).ok(); }); // pick_folder_async async_thread(move || Self::pick_folder(self).map(FileHandle::wrap)) ``` → Die Dioxus-Event-Loop läuft weiter; das `.await` gibt den Main-Thread frei, bis der Channel liefert. **Kein Tokio-/async-std-Feature nötig** — `rfd` hat keine Runtime-Features, die Implementierung nutzt `std::thread` plus Channel. → (keine Runtime-Dependency; nur `log`, `raw-window-handle` unbedingt) Nebenbedingung aus dem offiziellen `rfd`-Beispiel: der Dialog soll **auf dem Main-Thread gestartet** (`pick_*()` aufgerufen) und darf anderswo awaited werden — „Spawn dialog on main thread / Await somewhere else". → Im Dioxus-Fall passiert **beides** auf dem Main-Thread (`spawn` hat kein `Send`-Bound), die Bedingung ist damit erfüllt. --- ## 4. Was kommt bei Abbruch zurück? ### Belegt **`None`.** Sowohl `FileDialog::pick_folder() -> Option` als auch `AsyncFileDialog::pick_folder() -> impl Future>` liefern bei Abbruch `None`. Es gibt keinen `Result`-Typ und keine Fehlerunterscheidung „abgebrochen vs. fehlgeschlagen" — ein technisch fehlgeschlagener Dialog ist von einem abgebrochenen nicht unterscheidbar (die Windows-Impl macht am Ende `run(self).ok()`, verwirft den Fehler also). → , , Aus dem `FileHandle` kommt man so an den Pfad: ```rust pub fn path(&self) -> &Path; // existiert nicht unter WASM32 pub fn file_name(&self) -> String; // zusätzlich: impl From for PathBuf ``` → --- ## 5. Plattformen und Feature-Flags ### Belegt: Windows — out of the box, ja Einzige Windows-Dependency von `rfd 0.17.2`: | Target | Crate | Version | Features | |---|---|---|---| | `cfg(target_os = "windows")` | `windows-sys` | `^0.61` | `Win32_Foundation`, `Win32_System_Com`, `Win32_UI_Shell_Common`, `Win32_UI_Shell`, `Win32_UI_WindowsAndMessaging` | → `windows-sys` ist reines Rust-Binding auf die Systembibliotheken — **keine C-Header, kein vcpkg, kein Build-Tool, kein Feature-Flag**. Zusammen mit der Implementierung über `IFileOpenDialog`/COM (siehe `init_com` / `IDialog::build_pick_folder` oben) heißt das: **`rfd = "0.17"` in die `Cargo.toml`, fertig.** Optional unter Windows: das Feature `common-controls-v6` — betrifft ausschließlich **Message-Dialoge** (`TaskDialogIndirect` aus ComCtl32.dll v6, für eigene Button-Texte) und braucht zusätzlich ein Application-Manifest. **Für Ordner-Dialoge irrelevant.** → ### Belegt: macOS Reine Rust/objc2-Dependencies (`objc2`, `objc2-app-kit`, `objc2-foundation`, `block2`, `dispatch2`), keine Feature-Flags nötig. Einschränkung aus der Crate-Doku: > „macOS async dialogs require an `NSApplication` instance, so the dialog is only truly async when > opened in windowed environment like `winit` or `SDL2`. Otherwise, it will fallback to sync dialog." Eine Dioxus-Desktop-App **ist** eine windowed App (tao/`NSApplication`), die Bedingung ist also erfüllt. → , ### Belegt: Linux — hier steckt der einzige echte Stolperstein Zwei Backends, per Cargo-Feature wählbar: - **`xdg-portal` (Default)** — XDG Desktop Portal über D-Bus. Braucht zur **Laufzeit** `libdbus` (fällt sonst auf `zenity` zurück) **und** ein installiertes Portal-Backend (GTK, GNOME oder KDE). Das wlroots-Portal-Backend implementiert die benötigte D-Bus-API **nicht**. - **`gtk3`** — `default-features = false, features = ["gtk3"]`. Braucht die GTK3-**Entwicklungs**- Header schon zur **Build**-Zeit (`libgtk-3-dev` / `gtk3-devel` / `pacman -S gtk3`). → **Also: nein, „derselbe Aufruf überall" gilt für den *Rust-Code*, aber nicht für das *Packaging*.** Der Aufruf im Code ist auf allen drei Plattformen identisch; Linux-Builds brauchen zusätzlich eine Aussage darüber, welches Backend gewählt wird und was beim Nutzer installiert sein muss. Für DioNotes auf Windows 11 ist das derzeit kein Thema. --- ## 6. Lauffähiges Beispiel-Snippet für Weg B (Dioxus 0.7 API) `Cargo.toml` — **nicht angewendet**, gehört in ein Implementierungs-Ticket: ```toml [dependencies] rfd = "0.17" # bereits transitiv via dioxus-desktop 0.7.10 im Lock-File ``` Komponente: ```rust use dioxus::prelude::*; use std::path::PathBuf; #[component] fn WorkspaceOrdnerWahl() -> Element { let mut ordner = use_signal(|| None::); // Anzeige-Text vorberechnen, damit im rsx! kein Ausdruck nötig ist let label = ordner() .map(|p| p.display().to_string()) .unwrap_or_else(|| "Noch kein Ordner gewählt".to_string()); rsx! { button { // Async-Closures in Event-Handlern werden von Dioxus automatisch gespawnt. onclick: move |_| async move { let auswahl = rfd::AsyncFileDialog::new() .set_title("Workspace-Ordner wählen") .pick_folder() .await; match auswahl { // FileHandle::path() -> &Path Some(handle) => ordner.set(Some(handle.path().to_path_buf())), // Nutzer hat abgebrochen -> None; Zustand bewusst unverändert lassen None => {} } }, "Ordner wählen…" } p { "{label}" } } } ``` Belege für die Dioxus-Seite des Snippets: - Async-Closure direkt als Event-Handler: „Async closures passed to event handlers are automatically spawned" — offizielles Beispiel in der 0.7-Doku. → (Abschnitt „Use async closures in event handlers") - Alternative, falls die Async-Closure irgendwo Ärger macht — `spawn` explizit: ```rust onclick: move |_| { spawn(async move { if let Some(h) = rfd::AsyncFileDialog::new().pick_folder().await { ordner.set(Some(h.path().to_path_buf())); } }); }, ``` → , (Abschnitt „Spawn background tasks in event handlers") - `use_signal` / `.set()` statt `use_state`: entspricht der 0.7-API in `AGENTS.md`. ### Optionale Erweiterung (Vermutung, nicht kompiliert) Damit der Dialog besitzer-modal am App-Fenster hängt: ```rust let window = use_window(); // DesktopContext = Rc, Deref -> tao::Window // ... rfd::AsyncFileDialog::new() .set_parent(&*window) // &DesktopService --deref--> &tao::window::Window .set_title("Workspace-Ordner wählen") .pick_folder() .await ``` Typtheoretisch geht das auf (siehe Abschnitt 2), **aber ungetestet**. Zwei offene Punkte: 1. Ob die Deref-Kette bis `tao::window::Window` vom Compiler an `set_parent`s Bound `W: HasWindowHandle + HasDisplayHandle + ?Sized` angepasst wird, oder ob ein expliziter `&*(*window)` / Zwischenschritt nötig ist. 2. Windows führt den async-Dialog auf einem **anderen** Thread aus (Abschnitt 3). Ein Parent-HWND über Thread-Grenzen hinweg ist unter Win32 grundsätzlich zulässig, das Modalitäts-Verhalten (Deaktivierung des Owner-Fensters) sollte man aber praktisch prüfen. Empfehlung: **erst ohne `set_parent` implementieren**, das ist die belegte Variante. --- ## 7. Empfehlung für DioNotes Für „Ordner beim Anlegen eines Workspace wählen" ist die Auswahl eine reine UX-Frage: - Wenn der Dialog aus einem **sichtbaren Button/Input im Formular** kommt → **Weg A**, null neue Dependencies, offiziell dokumentiert. - Wenn der Dialog **programmatisch** aufgehen soll (Menü, Shortcut, Folgeschritt eines Wizards) → **Weg B** mit `rfd = "0.17"`. Kostet dank des schon vorhandenen Lock-Eintrags praktisch nichts. Beide landen unter Windows im selben nativen `IFileOpenDialog`. ## 8. Zusammenfassung der offenen Punkte | Punkt | Status | |---|---| | Crate + Version | **belegt** — `rfd 0.17.2` | | `rfd` bereits transitiv im Lock-File (via dioxus-desktop 0.7.10) | **belegt** (`Cargo.lock` + crates.io) | | Projekt löst `dioxus 0.7.1` real auf **0.7.10** auf | **belegt** (`Cargo.lock`) | | Weg A: `directory: true` + `FileData::path()` | **belegt** (offizielle Doku + docs.rs + Quellcode) | | Weg A nutzt intern `rfd::…::pick_folder()` | **belegt** (`packages/desktop/src/file_upload.rs`) | | Kein Konflikt mit tao 0.34 / wry 0.53 | **belegt** (Manifeste) | | async statt sync in `onclick` (Weg B) | **belegt** (Signaturen + Quellcode + `spawn`-Signatur ohne `Send`) | | Abbruch → `None` | **belegt** | | Windows out of the box, kein Feature-Flag | **belegt** (nur `windows-sys ^0.61`) | | Linux braucht Portal-Backend bzw. `gtk3`-Header | **belegt** | | `dioxus-sdk` hat kein Dialog-Modul | **belegt** (Feature-Liste 0.7.0) | | `dioxus_desktop::rfd` existiert nicht (kein Re-Export) | **belegt** (`lib.rs`) | | `set_parent` mit dem Dioxus-Fenster | **Vermutung** — plausibel, nicht kompiliert | | `evt.files()`-Verhalten bei Abbruch in Weg A | **Vermutung** — Snippet ist gegen beide Varianten robust | | Verhalten bei Netzlaufwerken / UNC-Pfaden | **nicht recherchiert** | | Verhalten unter `dx serve` / Hot-Reload | **nicht recherchiert** | ## Quellenliste - rfd Crate-Doku (Quellcode-Ansicht der `//!`-Doku): - `rfd::FileDialog`: - `rfd::AsyncFileDialog`: - `rfd::FileHandle`: - rfd Windows-Backend (Quellcode): - rfd Crate-Metadaten: - rfd async-Beispiel: - dioxus-sdk Crate-Metadaten (Feature-Liste): - rfd 0.17.2 Dependencies: - dioxus-desktop 0.7.1 Dependencies: - dioxus-desktop **0.7.10** Dependencies (die real aufgelöste Version): - dioxus-desktop `file_upload.rs` (interner rfd-Aufruf) @ v0.7.10: - dioxus-desktop `lib.rs` (Re-Export-Liste) @ v0.7.1: - `dioxus_html::FileData` (`path()`): - Dioxus 0.7 Doku, `directory`-Attribut: - dioxus-desktop `Cargo.toml` @ v0.7.1: - `DesktopService`: - `DesktopContext`: - `use_window`: - `dioxus::prelude::spawn`: - tao 0.34.4 `Window` (rwh-Impls): - tao 0.34.4 Feature-Liste: - Dioxus 0.7 Doku (Async, Desktop): /