From 59cd8951f6859d277b7c6c80099007623369d9d3 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Wed, 26 Aug 2026 15:59:45 +0200 Subject: [PATCH] Use standard desktop application menus --- Cargo.lock | 37 +++++++++++++++++++ Cargo.toml | 1 + DEPENDENCIES.md | 4 +-- apps/desktop/Cargo.toml | 1 + apps/desktop/src/main.rs | 77 +++++++++++++++++++--------------------- 5 files changed, 77 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79bcfb8..c8b438d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4933,6 +4933,19 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "iced_aw" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b7f923642b024c415150b70ef84ee5c11626e3b0af73b954d817d573bcd0a4" +dependencies = [ + "cfg-if", + "iced_core", + "iced_fonts", + "iced_widget", + "web-time", +] + [[package]] name = "iced_core" version = "0.14.0" @@ -4962,6 +4975,29 @@ dependencies = [ "log", ] +[[package]] +name = "iced_fonts" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214cff7c8499e328774216690e58e315a1a5f8f6fdd1035aed6298e62ffc4c1d" +dependencies = [ + "iced_core", + "iced_fonts_macros", + "iced_widget", +] + +[[package]] +name = "iced_fonts_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef5125e110cb19cd1910a28298661c98c5d9ab02eef43594968352940e8752e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "ttf-parser", +] + [[package]] name = "iced_futures" version = "0.14.0" @@ -5430,6 +5466,7 @@ version = "1.0.0" dependencies = [ "ashpd", "iced", + "iced_aw", "ironstorage", "muda", "rfd", diff --git a/Cargo.toml b/Cargo.toml index 9b8a054..e02c602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ gix = { version = "0.86", default-features = false, features = ["blocking-http-t gix-config = "0.59" hmac = "0.12" iced = { version = "0.14", features = ["canvas", "tokio"] } +iced_aw = { version = "0.14.1", default-features = false, features = ["menu"] } icns = "0.4" image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] } ironstorage = { path = "crates/storage" } diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 73bb860..897984f 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -18,7 +18,7 @@ The current direct dependencies are: | [clap 4.6](https://crates.io/crates/clap/4.6.4), [clap_complete 4.6](https://crates.io/crates/clap_complete/4.6.9) | CLI parsing and in-process shell completion generation | MIT OR Apache-2.0 | | [crossterm 0.29](https://crates.io/crates/crossterm/0.29.0) | Terminal I/O | MIT | | [Ratatui 0.30](https://crates.io/crates/ratatui/0.30.2) | TUI | MIT | -| [Iced 0.14](https://crates.io/crates/iced/0.14.0) | Desktop UI | MIT | +| [Iced 0.14](https://crates.io/crates/iced/0.14.0), [iced_aw 0.14](https://crates.io/crates/iced_aw/0.14.1) | Desktop UI and standard in-window application menus | MIT | | [muda 0.19](https://crates.io/crates/muda/0.19.3) | Safe native macOS application menus and standard roles | MIT OR Apache-2.0 | | [rfd 0.17](https://crates.io/crates/rfd/0.17.2), [ashpd 0.13](https://crates.io/crates/ashpd/0.13.13) | Native macOS/Windows folder picker and Linux XDG portal folder picker | MIT | | [gix 0.86](https://crates.io/crates/gix/0.86.0), [gix-config 0.59](https://crates.io/crates/gix-config/0.59.0) | Embedded Git objects, index, references, fetch, and merge | MIT OR Apache-2.0 | @@ -59,7 +59,7 @@ decision. | Password generation | [`rand`](https://crates.io/crates/rand) | MIT OR Apache-2.0 | Preferred using the operating-system CSPRNG. | | TOTP and HOTP | [`hmac` 0.12](https://crates.io/crates/hmac/0.12.1), [`sha1` 0.10](https://crates.io/crates/sha1/0.10.7), [`sha2` 0.10](https://crates.io/crates/sha2/0.10.9), [`data-encoding` 2.11](https://crates.io/crates/data-encoding/2.11.1) | MIT or MIT OR Apache-2.0 | Selected for a small storage-owned implementation with RFC 4226/6238 vectors. Handled URIs retain exact bytes while decoded secrets zeroize; `totp-rs` rejects HOTP URIs and cannot cover all of `pass-otp`. | | Native desktop clipboard | [`arboard` 3.6](https://crates.io/crates/arboard/3.6.1) | MIT OR Apache-2.0 | Selected with image support disabled and Wayland data-control enabled. Storage owns timeout, cleanup, and newer-content race policy; the safe adapter provides macOS, Windows, X11, and Wayland text access without helper processes. | -| Native macOS menu bar | [`muda` 0.19](https://crates.io/crates/muda/0.19.3) | MIT OR Apache-2.0 | Selected only on macOS. Its safe `NSApp` adapter supplies standard roles and accelerators; Linux and Windows use the same action registry through an in-window Iced menu bar, avoiding GTK and Win32 integration dependencies. | +| Desktop menu bar | [`muda` 0.19](https://crates.io/crates/muda/0.19.3), [`iced_aw` 0.14](https://crates.io/crates/iced_aw/0.14.1) | MIT OR Apache-2.0; MIT | `muda` supplies native macOS roles and accelerators. Linux and Windows use `iced_aw`'s standard Iced menu bar over the same action registry, avoiding a separate GTK or Win32 window integration. | | Native folder picker | [`rfd` 0.17](https://crates.io/crates/rfd/0.17.2), [`ashpd` 0.13](https://crates.io/crates/ashpd/0.13.13) | MIT | `rfd` is selected without default features for safe native macOS and Windows panels. Linux uses `ashpd` directly over the XDG Desktop Portal so the application never takes `rfd`'s `zenity` subprocess fallback. | | CLI cancellation | [`ctrlc` 3.5](https://crates.io/crates/ctrlc/3.5.2) | MIT OR Apache-2.0 | Selected for cross-platform interruption of the blocking clipboard lease. Ctrl-C requests storage cleanup before the CLI returns cancellation. | | Hidden CLI input | [`rpassword` 7.5](https://crates.io/crates/rpassword/7.5.4) | Apache-2.0 | Selected for portable terminal input with echo disabled. The CLI immediately moves returned strings into storage-owned zeroizing OTP input objects; it does not own validation or confirmation policy. | diff --git a/apps/desktop/Cargo.toml b/apps/desktop/Cargo.toml index 5ee9c8a..c0e987b 100644 --- a/apps/desktop/Cargo.toml +++ b/apps/desktop/Cargo.toml @@ -29,6 +29,7 @@ path = "src/main.rs" [dependencies] iced.workspace = true +iced_aw.workspace = true ironstorage = { workspace = true, features = ["ssh"] } tokio.workspace = true zeroize.workspace = true diff --git a/apps/desktop/src/main.rs b/apps/desktop/src/main.rs index e8a397f..a4b78df 100644 --- a/apps/desktop/src/main.rs +++ b/apps/desktop/src/main.rs @@ -31,6 +31,7 @@ use iced::{ }, window, }; +use iced_aw::menu::{Item, Menu, MenuBar}; use ironstorage::{ authentication::{ AuthenticationClock, AuthenticationError, AuthenticationHandle, AuthenticationSession, @@ -98,7 +99,6 @@ struct RecipientSuccess { enum Message { Action(UiAction), FieldAction(EntryFieldId, UiAction), - ToggleMenu(MenuGroup), PaletteQueryChanged(String), PaletteCancel, PaletteInvoke(UiAction), @@ -358,7 +358,6 @@ struct App { generation_form: Option, conflict: bool, status: String, - open_menu: Option, utility: Option, context_target: Option, palette: CommandPalette, @@ -1064,7 +1063,6 @@ impl App { generation_form: None, conflict: false, status: "Loading shared configuration…".to_owned(), - open_menu: None, utility: None, context_target: None, palette: CommandPalette::default(), @@ -1097,9 +1095,6 @@ impl App { } return self.invoke_action(action); } - Message::ToggleMenu(group) => { - self.open_menu = (self.open_menu != Some(group)).then_some(group); - } Message::PaletteQueryChanged(query) => { self.palette.update_query(query); return iced::widget::operation::snap_to( @@ -2680,7 +2675,6 @@ impl App { } fn invoke_action(&mut self, action: UiAction) -> Task { - self.open_menu = None; if !action::enabled(action, self.action_context()) { if self.content_mode == ContentMode::Editor && matches!( @@ -2870,7 +2864,6 @@ impl App { self.status = reason.to_owned(); return Task::none(); } - self.open_menu = None; self.utility = None; self.palette.open(); iced::widget::operation::focus(command_palette_input_id()) @@ -4340,41 +4333,44 @@ fn platform_menu_bar(app: &App) -> Element<'_, Message> { if cfg!(target_os = "macos") { return container(row![]).height(Length::Fixed(0.0)).into(); } - let mut headers = row![].spacing(2).padding([0, 8]); + let context = app.action_context(); + let mut menus = Vec::new(); for group in MenuGroup::ALL { - headers = headers.push( - button(group.label()) - .on_press(Message::ToggleMenu(group)) - .style(if app.open_menu == Some(group) { - button::primary + let width = match group { + MenuGroup::Window => 140.0, + MenuGroup::Help => 180.0, + _ => 240.0, + }; + let items = action::actions_in(group) + .map(|spec| { + let label = action::shortcut_label(spec.action).map_or_else( + || spec.label.to_owned(), + |key| format!("{} {key}", spec.label), + ); + let item = button( + text(label) + .width(Length::Fill) + .align_x(iced::alignment::Horizontal::Left), + ) + .width(width) + .padding([3, 8]) + .style(button::text); + Item::new(if action::enabled(spec.action, context) { + item.on_press(Message::Action(spec.action)) } else { - button::text - }), - ); + item + }) + }) + .collect(); + menus.push(Item::with_menu( + button(group.label()).padding([3, 8]).style(button::text), + Menu::new(items).width(width), + )); } - let mut menu = column![headers].spacing(4); - if let Some(group) = app.open_menu { - let context = app.action_context(); - let mut actions = row![].spacing(4).padding([6, 8]); - for spec in action::actions_in(group) { - let label = action::shortcut_label(spec.action).map_or_else( - || spec.label.to_owned(), - |key| format!("{} {key}", spec.label), - ); - let item = button(text(label)); - actions = actions.push(if action::enabled(spec.action, context) { - item.on_press(Message::Action(spec.action)) - } else { - item - }); - } - menu = menu.push( - scrollable(actions).direction(scrollable::Direction::Horizontal( - scrollable::Scrollbar::default(), - )), - ); - } - container(menu).width(Length::Fill).into() + MenuBar::new(menus) + .close_on_item_click(true) + .close_on_background_click(true) + .into() } fn utility_title(utility: &UtilityView) -> &'static str { @@ -6734,7 +6730,6 @@ mod tests { generation_form: None, conflict: false, status: String::new(), - open_menu: None, utility: None, context_target: None, palette: CommandPalette::default(),