From 7271e5e2ef93dc94d6591fbfb540cecb53679be4 Mon Sep 17 00:00:00 2001 From: landon Date: Sun, 26 Jul 2026 14:17:24 -0500 Subject: [PATCH] Add native file dialogs for workspace and export paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 📂 button beside each path field opens a native picker: a folder chooser for the workspace and an .odt save dialog for the export (which appends the extension if omitted). Uses rfd with the XDG Desktop Portal backend, so there is no GTK build dependency and no new shared library at runtime. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 308 +++++++++++++++++++++++++++++++----- Cargo.toml | 2 + INSTALL-debian12-surface.md | 9 ++ README.md | 8 +- src/app.rs | 54 ++++++- 5 files changed, 339 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3bf92a..ffa8678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ dependencies = [ "atspi-common", "serde", "thiserror 1.0.69", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -77,7 +77,7 @@ dependencies = [ "futures-lite", "futures-util", "serde", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -214,6 +214,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.5", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.18.0", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -292,6 +314,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" @@ -382,11 +415,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", + "zbus 4.4.0", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -398,7 +431,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -409,8 +442,8 @@ checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" dependencies = [ "atspi-common", "serde", - "zbus", - "zvariant", + "zbus 4.4.0", + "zvariant 4.2.0", ] [[package]] @@ -470,6 +503,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + [[package]] name = "blocking" version = "1.6.2" @@ -825,6 +867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", ] @@ -1157,6 +1201,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.33" @@ -1796,6 +1849,7 @@ dependencies = [ "eframe", "egui", "pulldown-cmark", + "rfd", "serde", "serde_json", "zip", @@ -2001,7 +2055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -2017,6 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -2030,7 +2085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2042,7 +2097,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2054,7 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2089,7 +2144,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2101,7 +2156,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -2120,7 +2175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -2154,7 +2209,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -2167,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2179,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2202,7 +2257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -2222,7 +2277,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2234,7 +2289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2398,6 +2453,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2504,8 +2565,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2515,7 +2586,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2527,6 +2608,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2577,6 +2667,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3168,14 +3282,32 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3901,7 +4033,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.13.1", - "block2", + "block2 0.5.1", "bytemuck", "calloop 0.13.0", "cfg_aliases 0.2.2", @@ -4084,7 +4216,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand", + "rand 0.8.7", "serde", "serde_repr", "sha1", @@ -4093,9 +4225,44 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros 5.18.0", + "zbus_names 4.3.4", + "zvariant 5.13.1", ] [[package]] @@ -4105,7 +4272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" dependencies = [ "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -4119,7 +4286,7 @@ dependencies = [ "syn 2.0.119", "zbus-lockstep", "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -4132,7 +4299,22 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names 4.3.4", + "zvariant 5.13.1", + "zvariant_utils 3.5.0", ] [[package]] @@ -4143,7 +4325,18 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow", + "zvariant 5.13.1", ] [[package]] @@ -4155,8 +4348,8 @@ dependencies = [ "quick-xml 0.30.0", "serde", "static_assertions", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -4278,7 +4471,22 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow", + "zvariant_derive 5.13.1", + "zvariant_utils 3.5.0", ] [[package]] @@ -4291,7 +4499,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils 3.5.0", ] [[package]] @@ -4304,3 +4525,16 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 51828c1..e7c5164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ serde_json = "1" pulldown-cmark = { version = "0.12", default-features = false } zip = { version = "2", default-features = false, features = ["deflate"] } dirs = "5" +# Native open/save dialogs via the XDG Desktop Portal (no GTK build dependency). +rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] } [profile.release] opt-level = 2 diff --git a/INSTALL-debian12-surface.md b/INSTALL-debian12-surface.md index 25b7206..263d2cc 100644 --- a/INSTALL-debian12-surface.md +++ b/INSTALL-debian12-surface.md @@ -44,6 +44,14 @@ sudo apt install -y \ libx11-6 libx11-xcb1 libxcursor1 libxi6 libxrandr2 libxinerama1 libxrender1 ``` +The **📂 Browse** buttons open native file dialogs through the XDG Desktop +Portal. On GNOME this is already installed, but if the dialogs don't appear, +ensure the portal and a file-chooser backend are present: + +```sh +sudo apt install -y xdg-desktop-portal xdg-desktop-portal-gtk +``` + --- ## 2. Get the source onto the tablet @@ -188,6 +196,7 @@ rm -f ~/.local/share/applications/md-manuscript.desktop |---|---| | `error while loading shared libraries: libGL.so.1` (or similar) | Install the runtime libraries in step 1b. | | Window fails with *"neither WAYLAND_DISPLAY nor DISPLAY is set"* | You're in a text-only session/SSH; run it from the tablet's graphical desktop. | +| **📂 Browse** buttons do nothing / no dialog appears | Install `xdg-desktop-portal` and `xdg-desktop-portal-gtk` (step 1b); you can still type paths by hand. | | Build error mentioning `rustc … is not supported` | `rustup update`, then `cargo build --release` again. | | `cargo: command not found` | Run `. "$HOME/.cargo/env"` or open a new terminal after installing rustup. | | UI text far too small/large | Adjust GNOME *Displays → Scale*, or set `WINIT_X11_SCALE_FACTOR` (X11). | diff --git a/README.md b/README.md index 343c099..bf56adc 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ Terminal=false ## Use -1. Set the **Workspace** path in the top bar (default: `~/Manuscript`) and press - **Open**. The directory is created if it does not exist. +1. Set the **Workspace** path in the top bar (default: `~/Manuscript`) — type it, + or click **📂** to pick a folder in a native file browser — and press **Open**. + The directory is created if it does not exist. 2. Press **Init git** once to make the workspace a git repository. To sync with another machine, add a remote yourself: ```sh @@ -69,7 +70,8 @@ Terminal=false blank falls back to a `# Title:` line, then the file's first level-1 heading (`# ...`), then the file name. Titles are saved to `titles.json` and sync with git. -5. Set the **Export** path and press **Export ODT** to produce the concatenated +5. Set the **Export** path (type it, or click **📂** to choose the `.odt` file in a + native save dialog) and press **Export ODT** to produce the concatenated document. Each file becomes a chapter headed by its title, optionally followed by a caption from its `# Slug:` line. diff --git a/src/app.rs b/src/app.rs index 7279917..30f658b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -340,6 +340,50 @@ impl App { } } + /// Open a native folder picker to choose the workspace directory. + fn browse_workspace(&mut self) { + let start = PathBuf::from(self.workspace_input.trim()); + let mut dialog = rfd::FileDialog::new().set_title("Choose workspace folder"); + if start.is_dir() { + dialog = dialog.set_directory(&start); + } + if let Some(path) = dialog.pick_folder() { + self.save_current(); + self.workspace_input = path.display().to_string(); + self.config.workspace = path; + self.config.save(); + self.open_workspace(); + } + } + + /// Open a native save dialog to choose the export `.odt` path. + fn browse_export(&mut self) { + let current = PathBuf::from(self.export_input.trim()); + let mut dialog = rfd::FileDialog::new() + .set_title("Choose export file") + .add_filter("OpenDocument Text", &["odt"]); + if let Some(parent) = current.parent().filter(|p| p.is_dir()) { + dialog = dialog.set_directory(parent); + } + let name = current + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("manuscript.odt"); + if let Some(mut path) = dialog.set_file_name(name).save_file() { + // Ensure the chosen path ends in .odt even if the user omitted it. + let has_odt = path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("odt")); + if !has_odt { + path.set_extension("odt"); + } + self.export_input = path.display().to_string(); + self.config.export_path = path; + self.config.save(); + } + } + fn reorder(&mut self, from: usize, mut to: usize) { if from >= self.files.len() || from == to { return; @@ -370,8 +414,11 @@ impl App { ui.label("Workspace:"); let resp = ui.add( egui::TextEdit::singleline(&mut self.workspace_input) - .desired_width(360.0), + .desired_width(300.0), ); + if ui.button("📂").on_hover_text("Browse for workspace folder").clicked() { + self.browse_workspace(); + } if ui.button("Open").clicked() || (resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { @@ -396,8 +443,11 @@ impl App { ui.horizontal(|ui| { ui.label("Export:"); ui.add( - egui::TextEdit::singleline(&mut self.export_input).desired_width(360.0), + egui::TextEdit::singleline(&mut self.export_input).desired_width(300.0), ); + if ui.button("📂").on_hover_text("Browse for export .odt file").clicked() { + self.browse_export(); + } if ui.button("Export ODT").clicked() { self.export_odt(); }