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:
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",