app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit cd17631851a68c8ed8867932e8458c7d7f7d262b
parent 468e1c1895b59a4a45d330a32afa08bd58384094
Author: triesap <tyson@radroots.org>
Date:   Fri, 17 Apr 2026 17:17:46 +0000

app: add settings shell menu wiring

- install a native app menu with cmd-, and cmd-q bindings
- open a reusable settings window shell with transparent titlebar chrome
- add home and settings navigation actions for app-owned panels
- extend theme and locale keys for settings navigation labels

Diffstat:
Mcrates/launchers/desktop/src/app.rs | 2++
Mcrates/launchers/desktop/src/lib.rs | 1+
Acrates/launchers/desktop/src/menus.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/launchers/desktop/src/window.rs | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/shared/i18n/src/keys.rs | 5+++++
Mcrates/shared/ui/src/theme.rs | 2++
Mi18n/locales/en/messages.json | 5+++++
7 files changed, 285 insertions(+), 3 deletions(-)

diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs @@ -3,6 +3,7 @@ use radroots_app_core::{APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeSnaps use radroots_app_i18n::select_locale_from_host; use radroots_app_ui::APP_UI_THEME; +use crate::menus::install_native_app_menu; use crate::window::{HomeView, home_titlebar_options}; pub fn launch() { @@ -11,6 +12,7 @@ pub fn launch() { app.run(move |cx| { select_locale_from_host(&snapshot.host.host_locale); + install_native_app_menu(cx); cx.on_window_closed(|cx| { if cx.windows().is_empty() { diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] mod app; +mod menus; mod window; pub fn run() { diff --git a/crates/launchers/desktop/src/menus.rs b/crates/launchers/desktop/src/menus.rs @@ -0,0 +1,54 @@ +use gpui::{ + App, AppContext, Bounds, KeyBinding, Menu, MenuItem, WindowBounds, WindowOptions, actions, px, + size, +}; +use radroots_app_i18n::{AppTextKey, app_text}; +use radroots_app_ui::APP_UI_THEME; + +use crate::window::{SettingsPanelViewKey, SettingsWindowView, settings_titlebar_options}; + +actions!(radroots_app, [OpenSettingsWindow, QuitApp]); + +pub fn install_native_app_menu(cx: &mut App) { + cx.on_action(|_: &OpenSettingsWindow, cx| open_settings_window(cx)); + cx.on_action(|_: &QuitApp, cx| cx.quit()); + cx.bind_keys([ + KeyBinding::new("cmd-,", OpenSettingsWindow, None), + KeyBinding::new("cmd-q", QuitApp, None), + ]); + + let app_name = app_text(AppTextKey::AppName); + cx.set_menus(vec![Menu { + name: app_name.into(), + items: vec![ + MenuItem::action(app_text(AppTextKey::MenuSettings), OpenSettingsWindow), + MenuItem::separator(), + MenuItem::action(app_text(AppTextKey::MenuQuit), QuitApp), + ], + }]); +} + +fn open_settings_window(cx: &mut App) { + let bounds = Bounds::centered( + None, + size( + px(APP_UI_THEME.windows.settings_width_px), + px(APP_UI_THEME.windows.settings_height_px), + ), + cx, + ); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + window_min_size: Some(size( + px(APP_UI_THEME.windows.settings_width_px), + px(APP_UI_THEME.windows.settings_height_px), + )), + titlebar: Some(settings_titlebar_options()), + ..Default::default() + }, + |_, cx| cx.new(|_| SettingsWindowView::new(SettingsPanelViewKey::default())), + ) + .expect("settings window should open"); +} diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -9,6 +9,8 @@ use radroots_app_ui::{ runtime_metadata_rows, section_divider, utility_title_row, }; +use crate::menus::OpenSettingsWindow; + pub fn home_titlebar_options() -> gpui::TitlebarOptions { gpui::TitlebarOptions { title: None, @@ -17,6 +19,32 @@ pub fn home_titlebar_options() -> gpui::TitlebarOptions { } } +pub fn settings_titlebar_options() -> gpui::TitlebarOptions { + gpui::TitlebarOptions { + title: None, + appears_transparent: true, + ..Default::default() + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum SettingsPanelViewKey { + #[default] + Accounts, + Settings, + About, +} + +impl SettingsPanelViewKey { + fn label_key(self) -> AppTextKey { + match self { + Self::Accounts => AppTextKey::SettingsNavAccounts, + Self::Settings => AppTextKey::SettingsNavSettings, + Self::About => AppTextKey::SettingsNavAbout, + } + } +} + pub struct HomeView { snapshot: AppRuntimeSnapshot, metadata_rows: Vec<LabelValueRow>, @@ -71,9 +99,38 @@ impl Render for HomeView { ) .child( div() - .text_size(px(APP_UI_THEME.typography.utility_title_text_px)) - .text_color(rgb(APP_UI_THEME.text.secondary)) - .child(format!("v{}", self.snapshot.host.app_version)), + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME + .typography + .utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(format!("v{}", self.snapshot.host.app_version)), + ) + .child( + div() + .id("home-open-settings") + .w_full() + .px(px(APP_UI_THEME + .layout + .settings_navigation_row_padding_px)) + .py(px(APP_UI_THEME + .layout + .settings_navigation_row_padding_px)) + .bg(rgb(APP_UI_THEME.surfaces.card_background)) + .rounded(px(8.0)) + .cursor_pointer() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(app_shared_text(AppTextKey::SettingsTitle)) + .on_click(|_, window, cx| { + window + .dispatch_action(Box::new(OpenSettingsWindow), cx); + }), + ), ), ) .child( @@ -111,3 +168,159 @@ impl Render for HomeView { ) } } + +pub struct SettingsWindowView { + selected_view: SettingsPanelViewKey, +} + +impl SettingsWindowView { + pub fn new(initial_view: SettingsPanelViewKey) -> Self { + Self { + selected_view: initial_view, + } + } + + fn select_view(&mut self, view: SettingsPanelViewKey, cx: &mut Context<Self>) { + if self.selected_view != view { + self.selected_view = view; + cx.notify(); + } + } + + fn navigation_button( + &mut self, + view: SettingsPanelViewKey, + cx: &mut Context<Self>, + ) -> impl IntoElement { + let is_selected = self.selected_view == view; + let background = if is_selected { + APP_UI_THEME.surfaces.card_background + } else { + APP_UI_THEME.surfaces.panel_background + }; + let foreground = if is_selected { + APP_UI_THEME.text.primary + } else { + APP_UI_THEME.text.secondary + }; + + div() + .w_full() + .px(px(APP_UI_THEME.layout.settings_navigation_row_padding_px)) + .py(px(APP_UI_THEME.layout.settings_navigation_row_padding_px)) + .bg(rgb(background)) + .rounded(px(8.0)) + .cursor_pointer() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .text_color(rgb(foreground)) + .child(app_shared_text(view.label_key())) + .id(match view { + SettingsPanelViewKey::Accounts => "settings-nav-accounts", + SettingsPanelViewKey::Settings => "settings-nav-settings", + SettingsPanelViewKey::About => "settings-nav-about", + }) + .on_click(cx.listener(move |this, _, _, cx| { + this.select_view(view, cx); + })) + } +} + +impl Render for SettingsWindowView { + fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + app_window_shell( + APP_UI_THEME.surfaces.window_background, + div() + .size_full() + .overflow_hidden() + .flex() + .flex_col() + .child( + div() + .w_full() + .h(px(APP_UI_THEME.layout.settings_chrome_height_px)) + .bg(rgb(APP_UI_THEME.surfaces.chrome_background)) + .p(px(APP_UI_THEME.layout.settings_content_padding_px)) + .flex() + .items_center() + .justify_between() + .child( + div() + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.settings_section_gap_px)) + .child( + div() + .text_size(px(APP_UI_THEME.typography.brand_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.text.primary)) + .child(app_shared_text(AppTextKey::SettingsTitle)), + ) + .child( + div() + .text_size(px(APP_UI_THEME + .typography + .utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_text(self.selected_view.label_key())), + ), + ) + .child( + div() + .text_size(px(APP_UI_THEME.typography.utility_title_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_text(AppTextKey::AppName)), + ), + ) + .child(section_divider()) + .child( + div() + .flex_1() + .overflow_hidden() + .flex() + .child( + div() + .h_full() + .w(px(APP_UI_THEME.layout.settings_navigation_width_px)) + .p(px(APP_UI_THEME.layout.settings_content_padding_px)) + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.settings_navigation_row_gap_px)) + .bg(rgb(APP_UI_THEME.surfaces.panel_background)) + .child(self.navigation_button(SettingsPanelViewKey::Accounts, cx)) + .child(self.navigation_button(SettingsPanelViewKey::Settings, cx)) + .child(self.navigation_button(SettingsPanelViewKey::About, cx)), + ) + .child( + div() + .h_full() + .w(px(APP_UI_THEME.layout.divider_thickness_px)) + .bg(rgb(APP_UI_THEME.surfaces.divider)), + ) + .child( + div() + .flex_1() + .h_full() + .p(px(APP_UI_THEME.layout.settings_content_padding_px)) + .child( + div() + .size_full() + .bg(rgb(APP_UI_THEME.surfaces.panel_background)) + .rounded(px(12.0)) + .child( + div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_size(px(APP_UI_THEME.typography.body_text_px)) + .text_color(rgb(APP_UI_THEME.text.secondary)) + .child(app_shared_text( + self.selected_view.label_key(), + )), + ), + ), + ), + ), + ) + } +} diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -24,7 +24,12 @@ define_app_text_keys! { HomeBrand => "home.brand", HomeTitle => "home.title", HomeMetadataTitle => "home.metadata_title", + MenuSettings => "menu.settings", + MenuQuit => "menu.quit", SettingsTitle => "settings.title", + SettingsNavAccounts => "settings.nav.accounts", + SettingsNavSettings => "settings.nav.settings", + SettingsNavAbout => "settings.nav.about", MetadataCorePackage => "metadata.core_package", MetadataCoreVersion => "metadata.core_version", MetadataCoreAuthors => "metadata.core_authors", diff --git a/crates/shared/ui/src/theme.rs b/crates/shared/ui/src/theme.rs @@ -50,6 +50,7 @@ pub struct AppLayoutTokens { pub metadata_row_gap_px: f32, pub utility_title_row_height_px: f32, pub settings_chrome_height_px: f32, + pub settings_navigation_width_px: f32, pub settings_section_gap_px: f32, pub settings_navigation_row_padding_px: f32, pub settings_navigation_row_gap_px: f32, @@ -91,6 +92,7 @@ pub const APP_UI_THEME: AppUiTheme = AppUiTheme { metadata_row_gap_px: 12.0, utility_title_row_height_px: 24.0, settings_chrome_height_px: 88.0, + settings_navigation_width_px: 216.0, settings_section_gap_px: 8.0, settings_navigation_row_padding_px: 8.0, settings_navigation_row_gap_px: 8.0, diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -3,7 +3,12 @@ "home.brand": "radroots", "home.title": "home", "home.metadata_title": "runtime metadata", + "menu.settings": "settings", + "menu.quit": "quit radroots", "settings.title": "settings", + "settings.nav.accounts": "accounts", + "settings.nav.settings": "settings", + "settings.nav.about": "about", "metadata.core_package": "core package", "metadata.core_version": "core version", "metadata.core_authors": "core authors",