commit 051109b54118cb92038da9220478098d8f9ed354
parent f2deead90580f749f5f5e0026559da92a5ed31c9
Author: triesap <tyson@radroots.org>
Date: Sun, 7 Jun 2026 11:31:15 -0700
app: refine settings account selector
Diffstat:
5 files changed, 149 insertions(+), 18 deletions(-)
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -322,8 +322,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"settings.farm.save_failed",
"settings.about.sync_refresh_failed",
"settings.about.conflict_resolution_failed",
+ "settings.account.select_failed",
"failed to refresh sync from the about panel",
"failed to resolve sync conflict from the about panel",
+ "failed to select account from settings panel",
"switch_relays",
"startup-title-radroots",
"startup-title-starting",
diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs
@@ -26,7 +26,8 @@ use radroots_app_sync::{
use radroots_app_ui::{
APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec,
AppSegmentButtonIconSpec as IconSegmentButtonSpec, LabelValueRow,
- SettingsPreferencesGeneralRowState, app_button_card, app_button_choice as choice_button,
+ SettingsPreferencesGeneralRowState, app_button_account_selector_row as account_selector_row,
+ app_button_card, app_button_choice as choice_button,
app_button_compact as action_button_compact, app_button_list_row as list_row_button,
app_button_primary as action_button_primary,
app_button_primary_disabled as action_button_primary_disabled,
@@ -6369,6 +6370,25 @@ impl SettingsWindowView {
self.runtime.selected_settings_section()
}
+ fn select_account(&mut self, account_id: String, cx: &mut Context<Self>) {
+ match self.runtime.select_local_account(account_id.as_str()) {
+ Ok(changed) => {
+ if changed {
+ cx.refresh_windows();
+ }
+ cx.notify();
+ }
+ Err(runtime_error) => {
+ error!(
+ target: "settings",
+ event = "settings.account.select_failed",
+ error = %runtime_error,
+ "failed to select account from settings panel"
+ );
+ }
+ }
+ }
+
fn handle_farm_rules_input_event(
&mut self,
_: &Entity<InputState>,
@@ -6710,12 +6730,16 @@ impl SettingsWindowView {
.iter()
.enumerate()
.map(|(index, account)| {
- list_row_button(
+ let account_id = account.account_id.clone();
+ let is_selected = selected_account_id
+ .is_some_and(|selected_account_id| selected_account_id == account.account_id);
+
+ account_selector_row(
("settings-account-row", index),
account_display_name(account),
- Some(SharedString::from(abbreviated_npub(account.npub.as_str()))),
- false,
- cx.listener(|_, _, _, _| {}),
+ SharedString::from(abbreviated_npub(account.npub.as_str())),
+ is_selected,
+ cx.listener(move |this, _, _, cx| this.select_account(account_id.clone(), cx)),
cx,
)
.into_any_element()
diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs
@@ -6,25 +6,25 @@ mod theme;
pub use primitives::{
AppCheckboxFieldSpec, AppFormFieldSpec, AppIconButtonSpec, AppSegmentButtonIconSpec,
- LabelValueRow, app_button_card, app_button_choice, app_button_compact, app_button_icon,
- app_button_list_row, app_button_primary, app_button_primary_disabled, app_button_secondary,
- app_button_secondary_disabled, app_button_square_dropdown_secondary, app_button_text,
- app_checkbox_field, app_cluster, app_detail_row, app_divider, app_focused_detail_view,
- app_focused_task_view, app_form_field, app_form_input_text, app_form_section,
- app_heading_section, app_heading_view, app_input_text, app_scroll_panel,
- app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v, app_status_indicator,
- app_surface_card, app_surface_card_section, app_surface_panel, app_surface_sidebar,
- app_surface_window, app_text_badge, app_text_body, app_text_body_subtle, app_text_label,
- app_text_value, label_value_list, utility_title_row,
+ LabelValueRow, app_button_account_selector_row, app_button_card, app_button_choice,
+ app_button_compact, app_button_icon, app_button_list_row, app_button_primary,
+ app_button_primary_disabled, app_button_secondary, app_button_secondary_disabled,
+ app_button_square_dropdown_secondary, app_button_text, app_checkbox_field, app_cluster,
+ app_detail_row, app_divider, app_focused_detail_view, app_focused_task_view, app_form_field,
+ app_form_input_text, app_form_section, app_heading_section, app_heading_view, app_input_text,
+ app_scroll_panel, app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v,
+ app_status_indicator, app_surface_card, app_surface_card_section, app_surface_panel,
+ app_surface_sidebar, app_surface_window, app_text_badge, app_text_body, app_text_body_subtle,
+ app_text_label, app_text_value, label_value_list, utility_title_row,
};
pub use text::{
SettingsPreferencesGeneralRowState, app_shared_label_text, app_shared_text,
runtime_metadata_rows, settings_preferences_general_rows,
};
pub use theme::{
- APP_UI_THEME, AppBorderTokens, AppButtonColors, AppButtonSizing, AppButtonTokens,
- AppCheckboxFieldTokens, AppComponentTokens, AppFoundationTokens, AppInputTextTokens,
- AppRadiusTokens, AppSegmentButtonIconColors, AppSegmentButtonIconSizing,
+ APP_UI_THEME, AppAccountSelectorRowTokens, AppBorderTokens, AppButtonColors, AppButtonSizing,
+ AppButtonTokens, AppCheckboxFieldTokens, AppComponentTokens, AppFoundationTokens,
+ AppInputTextTokens, AppRadiusTokens, AppSegmentButtonIconColors, AppSegmentButtonIconSizing,
AppSegmentButtonIconTokens, AppShellTokens, AppSpacingTokens, AppStatusIndicatorTokens,
AppSurfaceTokens, AppTextTokens, AppTypographyTokens, AppUiTheme,
};
diff --git a/crates/ui/src/primitives.rs b/crates/ui/src/primitives.rs
@@ -890,6 +890,96 @@ pub fn app_button_list_row(
)
}
+pub fn app_button_account_selector_row(
+ id: impl Into<ElementId>,
+ title: impl Into<SharedString>,
+ subtitle: impl Into<SharedString>,
+ is_selected: bool,
+ on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> impl IntoElement {
+ let tokens = APP_UI_THEME.components.app_account_selector_row;
+ let background = if is_selected {
+ tokens.active_background
+ } else {
+ tokens.inactive_background
+ };
+
+ Button::new(id)
+ .custom(
+ ButtonCustomVariant::new(cx)
+ .color(rgb(background).into())
+ .foreground(rgb(APP_UI_THEME.foundation.text.primary).into())
+ .border(transparent_black())
+ .hover(rgb(background).into())
+ .active(rgb(background).into()),
+ )
+ .rounded(ButtonRounded::Size(px(APP_UI_THEME
+ .shells
+ .settings_account_sidebar_button_corner_radius_px)))
+ .w_full()
+ .min_w_0()
+ .on_click(on_click)
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .items_center()
+ .gap(px(APP_UI_THEME
+ .shells
+ .settings_account_sidebar_button_gap_px))
+ .px(px(APP_UI_THEME
+ .shells
+ .settings_account_sidebar_button_padding_px))
+ .py(px(APP_UI_THEME
+ .shells
+ .settings_account_sidebar_button_padding_px))
+ .child(
+ div()
+ .size(px(APP_UI_THEME
+ .shells
+ .settings_account_sidebar_avatar_size_px))
+ .rounded_full()
+ .bg(rgb(APP_UI_THEME.foundation.surfaces.divider))
+ .flex_shrink_0(),
+ )
+ .child(
+ div()
+ .min_w_0()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(APP_UI_THEME
+ .shells
+ .settings_account_identity_text_gap_px))
+ .child(
+ div()
+ .max_w_full()
+ .overflow_hidden()
+ .text_ellipsis()
+ .whitespace_nowrap()
+ .text_size(px(APP_UI_THEME
+ .foundation
+ .typography
+ .settings_account_identity_text_px))
+ .font_weight(gpui::FontWeight::MEDIUM)
+ .text_color(rgb(APP_UI_THEME.foundation.text.primary))
+ .child(title.into()),
+ )
+ .child(
+ div()
+ .max_w_full()
+ .overflow_hidden()
+ .text_ellipsis()
+ .whitespace_nowrap()
+ .text_size(px(APP_UI_THEME.foundation.typography.body_text_px))
+ .text_color(rgb(APP_UI_THEME.foundation.text.secondary))
+ .child(subtitle.into()),
+ ),
+ ),
+ )
+}
+
pub fn app_button_card(
id: impl Into<ElementId>,
is_selected: bool,
diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs
@@ -72,6 +72,13 @@ pub struct AppComponentTokens {
pub app_input_text: AppInputTextTokens,
pub app_checkbox_field: AppCheckboxFieldTokens,
pub app_status_indicator: AppStatusIndicatorTokens,
+ pub app_account_selector_row: AppAccountSelectorRowTokens,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct AppAccountSelectorRowTokens {
+ pub inactive_background: u32,
+ pub active_background: u32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -202,6 +209,7 @@ const APP_SURFACE_CHROME_BACKGROUND: u32 = 0xF5F5F7;
const APP_SURFACE_PANEL_BACKGROUND: u32 = 0xFFFFFF;
const APP_SURFACE_CARD_BACKGROUND: u32 = 0xF2F2F7;
const APP_SURFACE_DIVIDER: u32 = 0xD2D2D7;
+const APP_SURFACE_ACCOUNT_SELECTOR_ACTIVE_BACKGROUND: u32 = 0xE5E5EA;
const APP_TEXT_PRIMARY: u32 = 0x1D1D1F;
const APP_TEXT_SECONDARY: u32 = 0x6E6E73;
const APP_TEXT_ACCENT: u32 = 0x0A84FF;
@@ -328,6 +336,10 @@ pub const APP_UI_THEME: AppUiTheme = AppUiTheme {
offline: APP_STATUS_OFFLINE,
attention: APP_STATUS_ATTENTION,
},
+ app_account_selector_row: AppAccountSelectorRowTokens {
+ inactive_background: APP_SURFACE_CARD_BACKGROUND,
+ active_background: APP_SURFACE_ACCOUNT_SELECTOR_ACTIVE_BACKGROUND,
+ },
},
shells: AppShellTokens {
home_min_width_px: 1284.0,
@@ -437,6 +449,7 @@ mod tests {
let text_input = APP_UI_THEME.components.app_input_text;
let checkbox = APP_UI_THEME.components.app_checkbox_field;
let status = APP_UI_THEME.components.app_status_indicator;
+ let account_selector = APP_UI_THEME.components.app_account_selector_row;
assert_eq!(segmented.height_px, 44.0);
assert_eq!(segmented.corner_radius_px, 8.0);
@@ -450,6 +463,8 @@ mod tests {
assert_eq!(checkbox.size_px, 16.0);
assert_eq!(checkbox.corner_radius_px, 5.0);
assert_eq!(status.size_px, 12.0);
+ assert_eq!(account_selector.inactive_background, 0xF2F2F7);
+ assert_eq!(account_selector.active_background, 0xE5E5EA);
}
#[test]