app

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

commit 051109b54118cb92038da9220478098d8f9ed354
parent f2deead90580f749f5f5e0026559da92a5ed31c9
Author: triesap <tyson@radroots.org>
Date:   Sun,  7 Jun 2026 11:31:15 -0700

app: refine settings account selector

Diffstat:
Mcrates/desktop/src/source_guards.rs | 2++
Mcrates/desktop/src/window.rs | 34+++++++++++++++++++++++++++++-----
Mcrates/ui/src/lib.rs | 26+++++++++++++-------------
Mcrates/ui/src/primitives.rs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/ui/src/theme.rs | 15+++++++++++++++
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]