commit a8cca05d2e7782780dea9c85f4add0424debbf97
parent 68132c54581d5fab068714f91d206a40a4635fe8
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Apr 2026 03:51:56 +0000
app: route startup through account surfaces
Diffstat:
5 files changed, 651 insertions(+), 193 deletions(-)
diff --git a/crates/launchers/desktop/src/accounts.rs b/crates/launchers/desktop/src/accounts.rs
@@ -5,8 +5,9 @@ use std::{
use radroots_app_core::AppSharedAccountsPaths;
use radroots_app_models::{
- AccountSummary, AppIdentityProjection, FarmerActivationProjection, IdentityBlockedReason,
- SelectedAccountProjection, SelectedSurfaceProjection,
+ AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, AppIdentityProjection,
+ FarmerActivationProjection, IdentityBlockedReason, SelectedAccountProjection,
+ SelectedSurfaceProjection,
};
use radroots_app_sqlite::{AppSqliteError, AppSqliteStore};
use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityId};
@@ -117,6 +118,26 @@ pub fn select_local_account(
Ok(identity_projection_from_manager(manager, sqlite_store)?)
}
+pub fn select_active_surface(
+ manager: &RadrootsNostrAccountsManager,
+ sqlite_store: &AppSqliteStore,
+ active_surface: ActiveSurface,
+) -> Result<AppIdentityProjection, DesktopAccountsCommandError> {
+ let Some(selected_account) = manager.selected_account()? else {
+ return Ok(identity_projection_from_manager(manager, sqlite_store)?);
+ };
+ let selected_projection =
+ selected_account_projection_from_record(&selected_account, sqlite_store)?;
+ let activation = AccountSurfaceActivationProjection::new(
+ selected_projection.account.account_id.clone(),
+ SelectedSurfaceProjection::new(active_surface),
+ selected_projection.farmer_activation.clone(),
+ );
+
+ sqlite_store.save_surface_activation(&activation)?;
+ Ok(identity_projection_from_manager(manager, sqlite_store)?)
+}
+
pub fn remove_selected_local_key(
manager: &RadrootsNostrAccountsManager,
sqlite_store: &AppSqliteStore,
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -1,16 +1,18 @@
-use gpui::{Application, Bounds, WindowBounds, WindowOptions, px, size};
+use gpui::Application;
use radroots_app_core::{
APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeConfig, AppRuntimeConfigError,
AppRuntimeSnapshot, bootstrap_logging, install_panic_hook, launch_startup_event,
};
use radroots_app_i18n::select_locale_from_host;
-use radroots_app_ui::APP_UI_THEME;
use thiserror::Error;
use tracing::{error, info};
use crate::menus::install_native_app_menu;
use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary};
-use crate::window::{home_titlebar_options, open_home_window};
+use crate::window::{
+ PrimaryWindowTarget, SettingsPanelViewKey, home_window_options, open_home_window,
+ open_settings_window, primary_window_target, settings_window_options,
+};
#[derive(Debug, Error)]
pub enum AppLaunchError {
@@ -28,7 +30,9 @@ pub fn launch() -> Result<(), AppLaunchError> {
install_panic_hook();
let runtime = DesktopAppRuntime::bootstrap();
- emit_runtime_events(&snapshot, &runtime.summary());
+ let runtime_summary = runtime.summary();
+ emit_runtime_events(&snapshot, &runtime_summary);
+ let launch_target = primary_window_target(&runtime_summary);
let app = Application::new().with_assets(gpui_component_assets::Assets);
@@ -46,36 +50,39 @@ pub fn launch() -> Result<(), AppLaunchError> {
let snapshot = snapshot.clone();
let runtime = runtime.clone();
- let home_bounds = Bounds::centered(
- None,
- size(
- px(APP_UI_THEME.windows.home_min_width_px),
- px(APP_UI_THEME.windows.home_min_height_px),
- ),
- cx,
- );
+ let launch_target = launch_target;
+ let mut primary_window_options = match launch_target {
+ PrimaryWindowTarget::Home => home_window_options(cx),
+ PrimaryWindowTarget::SettingsAccount => settings_window_options(cx),
+ };
+ primary_window_options.app_id = Some(snapshot.host.app_identifier.clone());
cx.spawn(async move |cx| {
- if let Err(error) = cx.open_window(
- WindowOptions {
- app_id: Some(snapshot.host.app_identifier.clone()),
- window_bounds: Some(WindowBounds::Windowed(home_bounds)),
- window_min_size: Some(size(
- px(APP_UI_THEME.windows.home_min_width_px),
- px(APP_UI_THEME.windows.home_min_height_px),
- )),
- titlebar: Some(home_titlebar_options()),
- ..Default::default()
- },
- |window, cx| {
- window.activate_window();
- open_home_window(window, cx, runtime.clone())
- },
- ) {
+ let open_result = match launch_target {
+ PrimaryWindowTarget::Home => {
+ cx.open_window(primary_window_options, |window, cx| {
+ window.activate_window();
+ open_home_window(window, cx, runtime.clone())
+ })
+ }
+ PrimaryWindowTarget::SettingsAccount => {
+ cx.open_window(primary_window_options, |window, cx| {
+ window.activate_window();
+ open_settings_window(
+ window,
+ cx,
+ runtime.clone(),
+ SettingsPanelViewKey::Account,
+ )
+ })
+ }
+ };
+
+ if let Err(error) = open_result {
error!(
target: "window",
- event = "window.home_open_failed",
+ event = "window.primary_open_failed",
error = %error,
- "failed to open home window"
+ "failed to open primary window"
);
let _ = cx.update(|cx| cx.quit());
return;
@@ -83,9 +90,9 @@ pub fn launch() -> Result<(), AppLaunchError> {
info!(
target: "window",
- event = "window.home_opened",
+ event = "window.primary_opened",
app_id = %snapshot.host.app_identifier,
- "home window opened"
+ "primary window opened"
);
if let Err(error) = cx.update(|cx| cx.activate(true)) {
@@ -164,6 +171,7 @@ mod tests {
use crate::runtime::DesktopAppRuntimeSummary;
use super::emit_runtime_events;
+ use crate::window::{HomeStage, PrimaryWindowTarget, home_stage, primary_window_target};
#[derive(Clone, Debug, Eq, PartialEq)]
struct CapturedEvent {
@@ -279,4 +287,105 @@ mod tests {
Some("desktop runtime degraded")
);
}
+
+ #[test]
+ fn blocked_and_setup_runtime_target_the_settings_account_window() {
+ let blocked = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Blocked,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+ let setup = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::SetupRequired,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+
+ assert_eq!(
+ primary_window_target(&blocked),
+ PrimaryWindowTarget::SettingsAccount
+ );
+ assert_eq!(
+ primary_window_target(&setup),
+ PrimaryWindowTarget::SettingsAccount
+ );
+ }
+
+ #[test]
+ fn ready_runtime_targets_the_home_window() {
+ let personal = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Personal,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+ let farmer = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Farmer,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+
+ assert_eq!(primary_window_target(&personal), PrimaryWindowTarget::Home);
+ assert_eq!(primary_window_target(&farmer), PrimaryWindowTarget::Home);
+ }
+
+ #[test]
+ fn degraded_runtime_targets_the_settings_account_window() {
+ let degraded = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Personal,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: Some("runtime unavailable".to_owned()),
+ };
+
+ assert_eq!(
+ primary_window_target(°raded),
+ PrimaryWindowTarget::SettingsAccount
+ );
+ }
+
+ #[test]
+ fn home_stage_tracks_setup_personal_and_farmer_states() {
+ let setup = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::SetupRequired,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+ let personal = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Personal,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+ let farmer = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Farmer,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: None,
+ };
+ let blocked = DesktopAppRuntimeSummary {
+ shell_projection: AppShellProjection::default(),
+ settings_account_projection: SettingsAccountProjection::default(),
+ startup_gate: AppStartupGate::Farmer,
+ today_projection: TodayAgendaProjection::default(),
+ startup_issue: Some("runtime unavailable".to_owned()),
+ };
+
+ assert_eq!(home_stage(&setup), HomeStage::Setup);
+ assert_eq!(home_stage(&personal), HomeStage::PersonalHolding);
+ assert_eq!(home_stage(&farmer), HomeStage::FarmerWorkspace);
+ assert_eq!(home_stage(&blocked), HomeStage::Setup);
+ }
}
diff --git a/crates/launchers/desktop/src/menus.rs b/crates/launchers/desktop/src/menus.rs
@@ -1,13 +1,9 @@
-use gpui::{
- App, Bounds, KeyBinding, Menu, MenuItem, SystemMenuType, WindowBackgroundAppearance,
- WindowBounds, WindowOptions, actions, px, size,
-};
+use gpui::{App, KeyBinding, Menu, MenuItem, SystemMenuType, actions};
use radroots_app_i18n::{AppTextKey, app_text};
-use radroots_app_ui::APP_UI_THEME;
use crate::{
runtime::DesktopAppRuntime,
- window::{SettingsPanelViewKey, open_settings_window, settings_titlebar_options},
+ window::{SettingsPanelViewKey, open_settings_window, settings_window_options},
};
actions!(radroots_app, [OpenAboutWindow, QuitApp]);
@@ -18,30 +14,10 @@ const fn about_menu_settings_view() -> SettingsPanelViewKey {
pub fn install_native_app_menu(runtime: DesktopAppRuntime, cx: &mut App) {
cx.on_action(move |_: &OpenAboutWindow, cx| {
- 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()),
- window_background: WindowBackgroundAppearance::Transparent,
- ..Default::default()
- },
- |window, cx| {
- open_settings_window(window, cx, runtime.clone(), about_menu_settings_view())
- },
- )
+ let options = settings_window_options(cx);
+ cx.open_window(options, |window, cx| {
+ open_settings_window(window, cx, runtime.clone(), about_menu_settings_view())
+ })
.expect("settings window should open");
});
cx.on_action(|_: &QuitApp, cx| cx.quit());
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use radroots_app_core::{AppDesktopRuntimePaths, AppRuntimePathsError, AppSharedAccountsPaths};
use radroots_app_models::{
- AppActivityContext, AppActivityKind, AppStartupGate, SettingsAccountProjection,
+ ActiveSurface, AppActivityContext, AppActivityKind, AppStartupGate, SettingsAccountProjection,
SettingsPreference, SettingsSection, TodayAgendaProjection,
};
use radroots_app_sqlite::{
@@ -20,7 +20,8 @@ use tracing::error;
use crate::accounts::{
DesktopAccountsBootstrapError, DesktopAccountsCommandError, DesktopLocalIdentityImportRequest,
bootstrap_desktop_accounts, generate_local_account, import_local_account,
- remove_selected_local_key, reset_local_device_state, select_local_account,
+ remove_selected_local_key, reset_local_device_state, select_active_surface,
+ select_local_account,
};
const APP_DATABASE_FILE_NAME: &str = "app.sqlite3";
@@ -112,6 +113,13 @@ impl DesktopAppRuntime {
self.lock_state_mut().select_local_account(account_id)
}
+ pub fn select_active_surface(
+ &self,
+ active_surface: ActiveSurface,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ self.lock_state_mut().select_active_surface(active_surface)
+ }
+
pub fn remove_selected_local_key(&self) -> Result<bool, DesktopAppRuntimeCommandError> {
self.lock_state_mut().remove_selected_local_key()
}
@@ -286,6 +294,19 @@ impl DesktopAppRuntimeState {
Ok(self.replace_identity_projection(projection))
}
+ fn select_active_surface(
+ &mut self,
+ active_surface: ActiveSurface,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ let projection = {
+ let accounts_manager = self.accounts_manager()?;
+ let sqlite_store = self.sqlite_store()?;
+ select_active_surface(accounts_manager, sqlite_store, active_surface)?
+ };
+
+ Ok(self.replace_identity_projection(projection))
+ }
+
fn remove_selected_local_key(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> {
let projection = {
let accounts_manager = self.accounts_manager()?;
@@ -740,6 +761,89 @@ mod tests {
}
#[test]
+ fn runtime_select_active_surface_persists_selected_surface() {
+ let runtime = memory_runtime();
+
+ assert!(
+ runtime
+ .generate_local_account(Some("Farmer".to_owned()))
+ .expect("account should generate")
+ );
+ let account_id = runtime
+ .summary()
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .expect("selected account")
+ .account
+ .account_id
+ .clone();
+ save_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer, true);
+ assert!(
+ runtime
+ .select_local_account(account_id.as_str())
+ .expect("account should select")
+ );
+ assert_eq!(runtime.summary().startup_gate, AppStartupGate::Farmer);
+
+ assert!(
+ runtime
+ .select_active_surface(ActiveSurface::Personal)
+ .expect("surface should select")
+ );
+ let personal_summary = runtime.summary();
+ assert_eq!(personal_summary.startup_gate, AppStartupGate::Personal);
+ assert_eq!(
+ personal_summary
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .map(|account| account.active_surface()),
+ Some(ActiveSurface::Personal)
+ );
+ assert_eq!(
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .load_surface_activation(account_id.as_str())
+ .expect("surface activation should load")
+ .expect("surface activation should exist")
+ .active_surface(),
+ ActiveSurface::Personal
+ );
+
+ assert!(
+ runtime
+ .select_active_surface(ActiveSurface::Farmer)
+ .expect("surface should reselect")
+ );
+ let farmer_summary = runtime.summary();
+ assert_eq!(farmer_summary.startup_gate, AppStartupGate::Farmer);
+ assert_eq!(
+ farmer_summary
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .map(|account| account.active_surface()),
+ Some(ActiveSurface::Farmer)
+ );
+ assert_eq!(
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .load_surface_activation(account_id.as_str())
+ .expect("surface activation should load")
+ .expect("surface activation should exist")
+ .active_surface(),
+ ActiveSurface::Farmer
+ );
+ }
+
+ #[test]
fn runtime_reset_local_device_state_clears_store_file_and_projection() {
let (runtime, paths) = file_backed_runtime("reset");
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -1,9 +1,13 @@
use gpui::{
- AnyElement, App, AppContext, Context, InteractiveElement, IntoElement, ParentElement, Render,
- SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px,
- relative, rgb,
+ AnyElement, App, AppContext, Bounds, ClickEvent, Context, InteractiveElement, IntoElement,
+ ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
+ WindowBackgroundAppearance, WindowBounds, WindowOptions, div, prelude::FluentBuilder, px,
+ relative, rgb, size, transparent_black,
+};
+use gpui_component::{
+ IconName, Root,
+ button::{Button, ButtonCustomVariant, ButtonRounded, ButtonVariants},
};
-use gpui_component::{IconName, Root};
use radroots_app_i18n::AppTextKey;
pub use radroots_app_models::SettingsSection as SettingsPanelViewKey;
use radroots_app_models::{
@@ -13,9 +17,9 @@ use radroots_app_models::{
};
use radroots_app_ui::{
APP_UI_THEME, AppCheckboxFieldSpec, IconSegmentButtonSpec, LabelValueRow, action_button,
- action_button_compact, action_icon_button, app_checkbox_field, app_shared_label_text,
- app_shared_text, app_window_shell, icon_segment_button, label_value_list, section_divider,
- status_indicator, utility_title_row,
+ action_button_compact, action_icon_button, app_center_stage, app_checkbox_field,
+ app_shared_label_text, app_shared_text, app_window_shell, icon_segment_button,
+ label_value_list, section_divider, status_indicator, utility_title_row,
};
use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary};
@@ -36,6 +40,90 @@ pub fn settings_titlebar_options() -> gpui::TitlebarOptions {
}
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum PrimaryWindowTarget {
+ Home,
+ SettingsAccount,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum HomeStage {
+ Setup,
+ PersonalHolding,
+ FarmerWorkspace,
+}
+
+pub fn primary_window_target(summary: &DesktopAppRuntimeSummary) -> PrimaryWindowTarget {
+ if summary.startup_issue.is_some()
+ || matches!(
+ summary.startup_gate,
+ AppStartupGate::Blocked | AppStartupGate::SetupRequired
+ )
+ {
+ PrimaryWindowTarget::SettingsAccount
+ } else {
+ PrimaryWindowTarget::Home
+ }
+}
+
+pub fn home_stage(summary: &DesktopAppRuntimeSummary) -> HomeStage {
+ if summary.startup_issue.is_some()
+ || matches!(
+ summary.startup_gate,
+ AppStartupGate::Blocked | AppStartupGate::SetupRequired
+ )
+ {
+ HomeStage::Setup
+ } else if summary.startup_gate == AppStartupGate::Farmer {
+ HomeStage::FarmerWorkspace
+ } else {
+ HomeStage::PersonalHolding
+ }
+}
+
+pub fn home_window_options(cx: &mut App) -> WindowOptions {
+ let bounds = Bounds::centered(
+ None,
+ size(
+ px(APP_UI_THEME.windows.home_min_width_px),
+ px(APP_UI_THEME.windows.home_min_height_px),
+ ),
+ cx,
+ );
+
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(bounds)),
+ window_min_size: Some(size(
+ px(APP_UI_THEME.windows.home_min_width_px),
+ px(APP_UI_THEME.windows.home_min_height_px),
+ )),
+ titlebar: Some(home_titlebar_options()),
+ ..Default::default()
+ }
+}
+
+pub fn settings_window_options(cx: &mut App) -> WindowOptions {
+ let bounds = Bounds::centered(
+ None,
+ size(
+ px(APP_UI_THEME.windows.settings_width_px),
+ px(APP_UI_THEME.windows.settings_height_px),
+ ),
+ cx,
+ );
+
+ 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()),
+ window_background: WindowBackgroundAppearance::Transparent,
+ ..Default::default()
+ }
+}
+
pub fn open_home_window(
window: &mut Window,
cx: &mut App,
@@ -71,76 +159,12 @@ impl HomeView {
impl Render for HomeView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let runtime_summary = self.runtime.summary();
- let home_status = home_status_presentation(&runtime_summary);
-
- app_window_shell(
- APP_UI_THEME.surfaces.window_background,
- div()
- .size_full()
- .overflow_hidden()
- .flex()
- .child(
- div()
- .h_full()
- .w(px(APP_UI_THEME.layout.home_sidebar_width_px))
- .bg(rgb(APP_UI_THEME.surfaces.card_background))
- .p(px(APP_UI_THEME.layout.home_window_padding_px))
- .flex()
- .flex_col()
- .justify_between()
- .child(
- div()
- .flex()
- .flex_col()
- .gap(px(APP_UI_THEME.layout.home_stack_gap_px))
- .child(
- div()
- .text_size(px(APP_UI_THEME.typography.body_text_px * 2.0))
- .font_weight(gpui::FontWeight::BOLD)
- .text_color(rgb(APP_UI_THEME.text.primary))
- .child(app_shared_text(AppTextKey::HomeTodayTitle)),
- )
- .child(home_status_row(&home_status)),
- )
- .child(
- div().child(
- div()
- .text_size(px(APP_UI_THEME.typography.body_text_px))
- .line_height(relative(1.2))
- .text_color(rgb(APP_UI_THEME.text.secondary))
- .when_some(
- runtime_summary.today_projection.farm.as_ref(),
- |this, farm| this.child(farm.display_name.clone()),
- ),
- ),
- ),
- )
- .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()
- .bg(rgb(APP_UI_THEME.surfaces.window_background))
- .overflow_hidden()
- .child(
- div()
- .size_full()
- .p(px(APP_UI_THEME.layout.home_window_padding_px))
- .child(
- div()
- .id("home-today-scroll")
- .size_full()
- .overflow_y_scroll()
- .child(home_view_content(&runtime_summary)),
- ),
- ),
- ),
- )
+ match home_stage(&runtime_summary) {
+ HomeStage::FarmerWorkspace => farmer_home_shell(&runtime_summary).into_any_element(),
+ HomeStage::Setup | HomeStage::PersonalHolding => {
+ holding_home_shell(&runtime_summary).into_any_element()
+ }
+ }
}
}
@@ -174,6 +198,89 @@ impl SettingsWindowView {
}
}
+ fn finish_account_runtime_change(
+ &mut self,
+ changed: bool,
+ previous_target: PrimaryWindowTarget,
+ cx: &mut Context<Self>,
+ ) {
+ if changed {
+ cx.refresh_windows();
+ cx.notify();
+ }
+
+ if previous_target == PrimaryWindowTarget::SettingsAccount
+ && primary_window_target(&self.runtime.summary()) == PrimaryWindowTarget::Home
+ {
+ self.ensure_home_window_if_ready(cx);
+ }
+ }
+
+ fn ensure_home_window_if_ready(&self, cx: &mut Context<Self>) {
+ if primary_window_target(&self.runtime.summary()) != PrimaryWindowTarget::Home {
+ return;
+ }
+
+ if cx.windows().len() > 1 {
+ cx.refresh_windows();
+ return;
+ }
+
+ let runtime = self.runtime.clone();
+ let options = home_window_options(cx);
+ let _ = cx.open_window(options, |window, cx| {
+ window.activate_window();
+ open_home_window(window, cx, runtime.clone())
+ });
+ }
+
+ fn generate_local_account(&mut self, cx: &mut Context<Self>) {
+ let previous_target = primary_window_target(&self.runtime.summary());
+ let changed = self.runtime.generate_local_account(None).unwrap_or(false);
+ self.finish_account_runtime_change(changed, previous_target, cx);
+ }
+
+ fn select_local_account(&mut self, account_id: &str, cx: &mut Context<Self>) {
+ let previous_target = primary_window_target(&self.runtime.summary());
+ let changed = self
+ .runtime
+ .select_local_account(account_id)
+ .unwrap_or(false);
+ self.finish_account_runtime_change(changed, previous_target, cx);
+ }
+
+ fn remove_selected_local_key(&mut self, cx: &mut Context<Self>) {
+ let previous_target = primary_window_target(&self.runtime.summary());
+ let changed = self.runtime.remove_selected_local_key().unwrap_or(false);
+ self.finish_account_runtime_change(changed, previous_target, cx);
+ }
+
+ fn open_selected_workspace(&mut self, cx: &mut Context<Self>) {
+ let runtime_summary = self.runtime.summary();
+ let previous_target = primary_window_target(&runtime_summary);
+ let Some(target_surface) = runtime_summary
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .map(|account| {
+ if account.farmer_activation.is_active() {
+ ActiveSurface::Farmer
+ } else {
+ ActiveSurface::Personal
+ }
+ })
+ else {
+ return;
+ };
+
+ let changed = self
+ .runtime
+ .select_active_surface(target_surface)
+ .unwrap_or(false);
+ self.finish_account_runtime_change(changed, previous_target, cx);
+ self.ensure_home_window_if_ready(cx);
+ }
+
fn navigation_button(
&mut self,
view: SettingsPanelViewKey,
@@ -192,7 +299,7 @@ impl SettingsWindowView {
)
}
- fn account_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ fn account_panel(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let detail_text_px = APP_UI_THEME.typography.settings_account_detail_text_px;
let runtime_summary = self.runtime.summary();
let account_projection = &runtime_summary.settings_account_projection;
@@ -273,9 +380,14 @@ impl SettingsWindowView {
.roster
.iter()
.map(|account| {
+ let account_id = account.account_id.clone();
settings_account_sidebar_row(
account,
selected_account_id,
+ cx.listener(move |this, _, _, cx| {
+ this.select_local_account(account_id.as_str(), cx);
+ }),
+ cx,
)
})
.collect::<Vec<_>>(),
@@ -310,7 +422,9 @@ impl SettingsWindowView {
.child(action_button(
"account-add",
app_shared_text(AppTextKey::SettingsAccountAddAction),
- |_, _, _| {},
+ cx.listener(|this, _, _, cx| {
+ this.generate_local_account(cx);
+ }),
cx,
))
.child(action_icon_button(
@@ -515,7 +629,9 @@ impl SettingsWindowView {
app_shared_text(
AppTextKey::SettingsAccountLogOutAction,
),
- |_, _, _| {},
+ cx.listener(|this, _, _, cx| {
+ this.remove_selected_local_key(cx);
+ }),
cx,
)),
)
@@ -525,7 +641,9 @@ impl SettingsWindowView {
app_shared_text(
AppTextKey::SettingsAccountAdminConsoleAction,
),
- |_, _, _| {},
+ cx.listener(|this, _, _, cx| {
+ this.open_selected_workspace(cx);
+ }),
cx,
)),
),
@@ -858,70 +976,85 @@ fn settings_account_activation_key(account: &SelectedAccountProjection) -> AppTe
fn settings_account_sidebar_row(
account: &AccountSummary,
selected_account_id: Option<&str>,
+ on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
) -> AnyElement {
let is_selected = selected_account_id
.map(|account_id| account_id == account.account_id.as_str())
.unwrap_or(false);
-
- div()
- .w_full()
+ let background = if is_selected {
+ APP_UI_THEME.surfaces.card_background
+ } else {
+ APP_UI_THEME.surfaces.chrome_background
+ };
+
+ Button::new(SharedString::from(account.account_id.clone()))
+ .custom(
+ ButtonCustomVariant::new(cx)
+ .color(rgb(background).into())
+ .foreground(rgb(APP_UI_THEME.text.primary).into())
+ .border(transparent_black())
+ .hover(rgb(APP_UI_THEME.surfaces.card_background).into())
+ .active(rgb(APP_UI_THEME.surfaces.card_background).into()),
+ )
+ .rounded(ButtonRounded::Size(px(APP_UI_THEME
+ .layout
+ .settings_account_sidebar_button_corner_radius_px)))
.h(px(APP_UI_THEME
.layout
.settings_account_sidebar_button_height_px))
- .bg(rgb(if is_selected {
- APP_UI_THEME.surfaces.card_background
- } else {
- APP_UI_THEME.surfaces.chrome_background
- }))
- .rounded(px(APP_UI_THEME
- .layout
- .settings_account_sidebar_button_corner_radius_px))
- .p(px(APP_UI_THEME
- .layout
- .settings_account_sidebar_button_padding_px))
- .flex()
- .flex_row()
- .justify_start()
- .items_center()
- .gap(px(APP_UI_THEME
- .layout
- .settings_account_sidebar_button_gap_px))
+ .w_full()
+ .on_click(on_click)
.child(
div()
- .size(px(APP_UI_THEME
- .layout
- .settings_account_sidebar_avatar_size_px))
- .bg(rgb(APP_UI_THEME.surfaces.window_background))
- .rounded(px(APP_UI_THEME
+ .size_full()
+ .p(px(APP_UI_THEME
.layout
- .settings_account_sidebar_avatar_size_px
- / 2.0)),
- )
- .child(
- div()
- .min_w_0()
+ .settings_account_sidebar_button_padding_px))
.flex()
- .flex_col()
+ .flex_row()
+ .justify_start()
+ .items_center()
.gap(px(APP_UI_THEME
.layout
- .settings_account_identity_text_gap_px))
- .justify_center()
+ .settings_account_sidebar_button_gap_px))
.child(
div()
- .text_size(px(APP_UI_THEME
- .typography
- .settings_account_identity_text_px))
- .font_weight(gpui::FontWeight::MEDIUM)
- .text_color(rgb(APP_UI_THEME.text.primary))
- .child(settings_account_display_name(account)),
+ .size(px(APP_UI_THEME
+ .layout
+ .settings_account_sidebar_avatar_size_px))
+ .bg(rgb(APP_UI_THEME.surfaces.window_background))
+ .rounded(px(APP_UI_THEME
+ .layout
+ .settings_account_sidebar_avatar_size_px
+ / 2.0)),
)
.child(
div()
- .text_size(px(APP_UI_THEME
- .typography
- .settings_account_identity_text_px))
- .text_color(rgb(APP_UI_THEME.text.secondary))
- .child(account.npub.clone()),
+ .min_w_0()
+ .flex()
+ .flex_col()
+ .gap(px(APP_UI_THEME
+ .layout
+ .settings_account_identity_text_gap_px))
+ .justify_center()
+ .child(
+ div()
+ .text_size(px(APP_UI_THEME
+ .typography
+ .settings_account_identity_text_px))
+ .font_weight(gpui::FontWeight::MEDIUM)
+ .text_color(rgb(APP_UI_THEME.text.primary))
+ .child(settings_account_display_name(account)),
+ )
+ .child(
+ div()
+ .text_size(px(APP_UI_THEME
+ .typography
+ .settings_account_identity_text_px))
+ .text_color(rgb(APP_UI_THEME.text.secondary))
+ .child(account.npub.clone()),
+ ),
),
)
.into_any_element()
@@ -949,6 +1082,121 @@ struct HomeStatusPresentation {
label_key: AppTextKey,
}
+fn farmer_home_shell(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
+ let home_status = home_status_presentation(runtime);
+
+ app_window_shell(
+ APP_UI_THEME.surfaces.window_background,
+ div()
+ .size_full()
+ .overflow_hidden()
+ .flex()
+ .child(
+ div()
+ .h_full()
+ .w(px(APP_UI_THEME.layout.home_sidebar_width_px))
+ .bg(rgb(APP_UI_THEME.surfaces.card_background))
+ .p(px(APP_UI_THEME.layout.home_window_padding_px))
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .gap(px(APP_UI_THEME.layout.home_stack_gap_px))
+ .child(
+ div()
+ .text_size(px(APP_UI_THEME.typography.body_text_px * 2.0))
+ .font_weight(gpui::FontWeight::BOLD)
+ .text_color(rgb(APP_UI_THEME.text.primary))
+ .child(app_shared_text(AppTextKey::HomeTodayTitle)),
+ )
+ .child(home_status_row(&home_status)),
+ )
+ .child(
+ div().child(
+ div()
+ .text_size(px(APP_UI_THEME.typography.body_text_px))
+ .line_height(relative(1.2))
+ .text_color(rgb(APP_UI_THEME.text.secondary))
+ .when_some(runtime.today_projection.farm.as_ref(), |this, farm| {
+ this.child(farm.display_name.clone())
+ }),
+ ),
+ ),
+ )
+ .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()
+ .bg(rgb(APP_UI_THEME.surfaces.window_background))
+ .overflow_hidden()
+ .child(
+ div()
+ .size_full()
+ .p(px(APP_UI_THEME.layout.home_window_padding_px))
+ .child(
+ div()
+ .id("home-today-scroll")
+ .size_full()
+ .overflow_y_scroll()
+ .child(home_view_content(runtime)),
+ ),
+ ),
+ ),
+ )
+}
+
+fn holding_home_shell(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
+ let home_status = home_status_presentation(runtime);
+ let (title_key, body_key) = match home_stage(runtime) {
+ HomeStage::Setup => (
+ AppTextKey::HomeTodayEmptySetupTitle,
+ AppTextKey::HomeTodayEmptySetupBody,
+ ),
+ HomeStage::PersonalHolding => (
+ AppTextKey::HomeTodayEmptyNoFarmTitle,
+ AppTextKey::HomeTodayEmptyNoFarmBody,
+ ),
+ HomeStage::FarmerWorkspace => (
+ AppTextKey::HomeTodayEmptyQuietTitle,
+ AppTextKey::HomeTodayEmptyQuietBody,
+ ),
+ };
+ let mut sections = vec![home_empty_state_card(title_key, body_key).into_any_element()];
+
+ if let Some(issue) = runtime.startup_issue.as_ref() {
+ sections.push(
+ home_card(
+ app_shared_text(AppTextKey::MetadataStartupIssue),
+ home_body_text(issue.clone()),
+ )
+ .into_any_element(),
+ );
+ }
+
+ app_window_shell(
+ APP_UI_THEME.surfaces.window_background,
+ app_center_stage(
+ div()
+ .w_full()
+ .max_w(px(APP_UI_THEME.layout.home_card_max_width_px))
+ .flex()
+ .flex_col()
+ .gap(px(APP_UI_THEME.layout.home_stack_gap_px))
+ .child(home_status_row(&home_status))
+ .children(sections),
+ ),
+ )
+}
+
fn home_view_content(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
let projection = &runtime.today_projection;
let home_status = home_status_presentation(runtime);