commit 622aa11b1318bf8d8e9f8421fcef0cb52ca256af
parent 98d76b02531583de90d7a03911f6bd58226066dd
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Apr 2026 02:26:48 +0000
app: replace shell mode with active surface
Diffstat:
3 files changed, 154 insertions(+), 70 deletions(-)
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::{AppRuntimePathsError, AppRuntimeRoots};
use radroots_app_models::{
- AppActivityContext, AppActivityKind, AppMode, SettingsPreference, SettingsSection,
+ ActiveSurface, AppActivityContext, AppActivityKind, SettingsPreference, SettingsSection,
TodayAgendaProjection,
};
use radroots_app_sqlite::{
@@ -184,7 +184,7 @@ impl DesktopAppRuntimeState {
fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self {
Self {
state_store: AppStateStore::in_memory(AppShellProjection {
- app_mode: AppMode::Farmer,
+ active_surface: ActiveSurface::Farmer,
..AppShellProjection::default()
}),
sqlite_store: None,
@@ -216,8 +216,9 @@ mod tests {
use radroots_app_core::{AppRuntimeHostEnvironment, AppRuntimePlatform, AppRuntimeRoots};
use radroots_app_models::{
- AppActivityKind, AppMode, FarmReadiness, FarmSummary, SettingsPreference, SettingsSection,
- ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary,
+ ActiveSurface, AppActivityKind, FarmReadiness, FarmSummary, SettingsPreference,
+ SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
+ TodaySummary,
};
use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget};
use radroots_app_state::{
@@ -321,7 +322,10 @@ mod tests {
let summary = runtime.summary();
assert_eq!(summary.today_projection, today_agenda);
- assert_eq!(summary.shell_projection.app_mode, AppMode::Farmer);
+ assert_eq!(
+ summary.shell_projection.active_surface,
+ ActiveSurface::Farmer
+ );
assert_eq!(
summary.shell_projection.selected_section,
ShellSection::Home
@@ -343,7 +347,10 @@ mod tests {
let summary = runtime.summary();
- assert_eq!(summary.shell_projection.app_mode, AppMode::Farmer);
+ assert_eq!(
+ summary.shell_projection.active_surface,
+ ActiveSurface::Farmer
+ );
assert_eq!(
summary.shell_projection.selected_section,
ShellSection::Home
diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs
@@ -6,17 +6,17 @@ use uuid::Uuid;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
-pub enum AppMode {
+pub enum ActiveSurface {
#[default]
Farmer,
- Buyer,
+ Personal,
}
-impl AppMode {
+impl ActiveSurface {
pub const fn storage_key(self) -> &'static str {
match self {
Self::Farmer => "farmer",
- Self::Buyer => "buyer",
+ Self::Personal => "personal",
}
}
}
@@ -46,27 +46,6 @@ impl FarmerSection {
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
-pub enum BuyerSection {
- #[default]
- Marketplace,
- Search,
- Cart,
- Orders,
-}
-
-impl BuyerSection {
- pub const fn storage_key(self) -> &'static str {
- match self {
- Self::Marketplace => "buyer.marketplace",
- Self::Search => "buyer.search",
- Self::Cart => "buyer.cart",
- Self::Orders => "buyer.orders",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
pub enum SettingsSection {
#[default]
Account,
@@ -110,15 +89,21 @@ pub enum ShellSection {
#[default]
Home,
Farmer(FarmerSection),
- Buyer(BuyerSection),
Settings(SettingsSection),
}
impl ShellSection {
- pub const fn mode(self) -> AppMode {
+ pub const fn surface(self) -> Option<ActiveSurface> {
match self {
- Self::Buyer(_) => AppMode::Buyer,
- Self::Home | Self::Farmer(_) | Self::Settings(_) => AppMode::Farmer,
+ Self::Home | Self::Settings(_) => None,
+ Self::Farmer(_) => Some(ActiveSurface::Farmer),
+ }
+ }
+
+ pub const fn default_for_surface(surface: ActiveSurface) -> Self {
+ match surface {
+ ActiveSurface::Personal => Self::Home,
+ ActiveSurface::Farmer => Self::Farmer(FarmerSection::Today),
}
}
@@ -126,7 +111,6 @@ impl ShellSection {
match self {
Self::Home => "home",
Self::Farmer(section) => section.storage_key(),
- Self::Buyer(section) => section.storage_key(),
Self::Settings(section) => section.storage_key(),
}
}
@@ -154,10 +138,6 @@ impl FromStr for ShellSection {
"farmer.orders" => Ok(Self::Farmer(FarmerSection::Orders)),
"farmer.pack_day" => Ok(Self::Farmer(FarmerSection::PackDay)),
"farmer.farm" => Ok(Self::Farmer(FarmerSection::Farm)),
- "buyer.marketplace" => Ok(Self::Buyer(BuyerSection::Marketplace)),
- "buyer.search" => Ok(Self::Buyer(BuyerSection::Search)),
- "buyer.cart" => Ok(Self::Buyer(BuyerSection::Cart)),
- "buyer.orders" => Ok(Self::Buyer(BuyerSection::Orders)),
"settings.account" => Ok(Self::Settings(SettingsSection::Account)),
"settings.settings" => Ok(Self::Settings(SettingsSection::Settings)),
"settings.about" => Ok(Self::Settings(SettingsSection::About)),
@@ -385,10 +365,9 @@ impl TodayAgendaProjection {
#[cfg(test)]
mod tests {
use super::{
- ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, AppMode,
- BuyerSection, FarmId, FarmerSection, OrderListRow, ProductListRow, SettingsPreference,
- SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
- TodaySummary,
+ ActiveSurface, ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind,
+ FarmId, FarmerSection, OrderListRow, ProductListRow, SettingsPreference, SettingsSection,
+ ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary,
};
use std::{collections::BTreeSet, str::FromStr};
use uuid::Uuid;
@@ -402,10 +381,6 @@ mod tests {
ShellSection::Farmer(FarmerSection::Orders),
ShellSection::Farmer(FarmerSection::PackDay),
ShellSection::Farmer(FarmerSection::Farm),
- ShellSection::Buyer(BuyerSection::Marketplace),
- ShellSection::Buyer(BuyerSection::Search),
- ShellSection::Buyer(BuyerSection::Cart),
- ShellSection::Buyer(BuyerSection::Orders),
ShellSection::Settings(SettingsSection::Account),
ShellSection::Settings(SettingsSection::Settings),
ShellSection::Settings(SettingsSection::About),
@@ -425,19 +400,27 @@ mod tests {
}
#[test]
- fn shell_section_mode_tracks_farmer_and_buyer_surfaces() {
- assert_eq!(ShellSection::Home.mode(), AppMode::Farmer);
+ fn shell_section_surface_is_explicit_only_for_farmer_routes() {
+ assert_eq!(ShellSection::Home.surface(), None);
assert_eq!(
- ShellSection::Farmer(FarmerSection::Today).mode(),
- AppMode::Farmer
+ ShellSection::Farmer(FarmerSection::Today).surface(),
+ Some(ActiveSurface::Farmer)
);
assert_eq!(
- ShellSection::Buyer(BuyerSection::Marketplace).mode(),
- AppMode::Buyer
+ ShellSection::Settings(SettingsSection::Settings).surface(),
+ None
+ );
+ }
+
+ #[test]
+ fn shell_section_default_for_surface_preserves_current_farmer_entry() {
+ assert_eq!(
+ ShellSection::default_for_surface(ActiveSurface::Personal),
+ ShellSection::Home
);
assert_eq!(
- ShellSection::Settings(SettingsSection::Settings).mode(),
- AppMode::Farmer
+ ShellSection::default_for_surface(ActiveSurface::Farmer),
+ ShellSection::Farmer(FarmerSection::Today)
);
}
diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs
@@ -1,7 +1,7 @@
#![forbid(unsafe_code)]
use radroots_app_models::{
- AppMode, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection,
+ ActiveSurface, SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection,
};
use thiserror::Error;
@@ -66,37 +66,43 @@ impl SettingsShellProjection {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppShellProjection {
- pub app_mode: AppMode,
+ pub active_surface: ActiveSurface,
pub selected_section: ShellSection,
pub settings: SettingsShellProjection,
}
impl Default for AppShellProjection {
fn default() -> Self {
- Self::new(ShellSection::default())
+ Self::new(ActiveSurface::Farmer, ShellSection::Home)
}
}
impl AppShellProjection {
- pub fn new(selected_section: ShellSection) -> Self {
+ pub fn new(active_surface: ActiveSurface, selected_section: ShellSection) -> Self {
let settings = match selected_section {
ShellSection::Settings(section) => SettingsShellProjection::new(section),
_ => SettingsShellProjection::default(),
};
Self {
- app_mode: selected_section.mode(),
+ active_surface: selected_section.surface().unwrap_or(active_surface),
selected_section,
settings,
}
}
- pub fn for_settings(selected_section: SettingsSection) -> Self {
- Self::new(ShellSection::Settings(selected_section))
+ pub fn for_surface(active_surface: ActiveSurface) -> Self {
+ Self::new(active_surface, ShellSection::default_for_surface(active_surface))
+ }
+
+ pub fn for_settings(active_surface: ActiveSurface, selected_section: SettingsSection) -> Self {
+ Self::new(active_surface, ShellSection::Settings(selected_section))
}
fn select_section(&mut self, selected_section: ShellSection) {
- self.app_mode = selected_section.mode();
+ if let Some(active_surface) = selected_section.surface() {
+ self.active_surface = active_surface;
+ }
self.selected_section = selected_section;
if let ShellSection::Settings(settings_section) = selected_section {
@@ -104,6 +110,26 @@ impl AppShellProjection {
}
}
+ fn select_active_surface(&mut self, active_surface: ActiveSurface) {
+ if self.active_surface == active_surface {
+ return;
+ }
+
+ self.active_surface = active_surface;
+ match active_surface {
+ ActiveSurface::Personal => {
+ if matches!(self.selected_section, ShellSection::Farmer(_)) {
+ self.selected_section = ShellSection::default_for_surface(active_surface);
+ }
+ }
+ ActiveSurface::Farmer => {
+ if matches!(self.selected_section, ShellSection::Home) {
+ self.selected_section = ShellSection::default_for_surface(active_surface);
+ }
+ }
+ }
+ }
+
fn select_settings_section(&mut self, selected_section: SettingsSection) {
self.settings.selected_section = selected_section;
@@ -127,6 +153,7 @@ impl AppProjection {
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AppStateCommand {
+ SelectActiveSurface(ActiveSurface),
SelectSection(ShellSection),
SelectSettingsSection(SettingsSection),
SetSettingsPreference {
@@ -137,6 +164,10 @@ pub enum AppStateCommand {
}
impl AppStateCommand {
+ pub const fn select_active_surface(surface: ActiveSurface) -> Self {
+ Self::SelectActiveSurface(surface)
+ }
+
pub const fn select_settings_section(section: SettingsSection) -> Self {
Self::SelectSettingsSection(section)
}
@@ -317,6 +348,9 @@ fn apply_command(projection: &mut AppProjection, command: AppStateCommand) -> Ap
let before = projection.clone();
match command {
+ AppStateCommand::SelectActiveSurface(active_surface) => {
+ projection.shell.select_active_surface(active_surface);
+ }
AppStateCommand::SelectSection(selected_section) => {
projection.shell.select_section(selected_section);
}
@@ -355,7 +389,7 @@ mod tests {
SettingsPreference,
};
use radroots_app_models::{
- AppMode, FarmerSection, SettingsSection, ShellSection, TodayAgendaProjection,
+ ActiveSurface, FarmerSection, SettingsSection, ShellSection, TodayAgendaProjection,
TodaySetupTask, TodaySetupTaskKind,
};
@@ -378,7 +412,7 @@ mod tests {
fn default_projection_starts_on_farmer_home() {
let projection = AppProjection::default();
- assert_eq!(projection.shell.app_mode, AppMode::Farmer);
+ assert_eq!(projection.shell.active_surface, ActiveSurface::Farmer);
assert_eq!(projection.shell.selected_section, ShellSection::Home);
assert_eq!(
projection.shell.settings.selected_section,
@@ -394,11 +428,12 @@ mod tests {
#[test]
fn load_uses_repository_projection() {
let repository = InMemoryAppStateRepository::new(AppShellProjection::for_settings(
+ ActiveSurface::Farmer,
SettingsSection::About,
));
let store = AppStateStore::load(repository).expect("in-memory repository should load");
- assert_eq!(store.projection().shell.app_mode, AppMode::Farmer);
+ assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer);
assert_eq!(
store.projection().shell.selected_section,
ShellSection::Settings(SettingsSection::About)
@@ -420,7 +455,7 @@ mod tests {
));
assert_eq!(changed, Ok(true));
- assert_eq!(store.projection().shell.app_mode, AppMode::Farmer);
+ assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer);
assert_eq!(
store.projection().shell.selected_section,
ShellSection::Home
@@ -457,6 +492,63 @@ mod tests {
store.repository().projection().selected_section,
ShellSection::Farmer(FarmerSection::Products)
);
+ assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer);
+ }
+
+ #[test]
+ fn select_active_surface_moves_personal_home_to_farmer_today() {
+ let repository = InMemoryAppStateRepository::new(AppShellProjection::for_surface(
+ ActiveSurface::Personal,
+ ));
+ let mut store = AppStateStore::load(repository).expect("in-memory repository should load");
+
+ let changed = store.apply(AppStateCommand::select_active_surface(
+ ActiveSurface::Farmer,
+ ));
+
+ assert_eq!(changed, Ok(true));
+ assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer);
+ assert_eq!(
+ store.projection().shell.selected_section,
+ ShellSection::Farmer(FarmerSection::Today)
+ );
+ }
+
+ #[test]
+ fn select_active_surface_moves_farmer_routes_back_to_home_for_personal() {
+ let repository = InMemoryAppStateRepository::new(AppShellProjection::new(
+ ActiveSurface::Farmer,
+ ShellSection::Farmer(FarmerSection::Products),
+ ));
+ let mut store = AppStateStore::load(repository).expect("in-memory repository should load");
+
+ let changed = store.apply(AppStateCommand::select_active_surface(
+ ActiveSurface::Personal,
+ ));
+
+ assert_eq!(changed, Ok(true));
+ assert_eq!(store.projection().shell.active_surface, ActiveSurface::Personal);
+ assert_eq!(store.projection().shell.selected_section, ShellSection::Home);
+ }
+
+ #[test]
+ fn select_active_surface_preserves_settings_route() {
+ let repository = InMemoryAppStateRepository::new(AppShellProjection::for_settings(
+ ActiveSurface::Personal,
+ SettingsSection::About,
+ ));
+ let mut store = AppStateStore::load(repository).expect("in-memory repository should load");
+
+ let changed = store.apply(AppStateCommand::select_active_surface(
+ ActiveSurface::Farmer,
+ ));
+
+ assert_eq!(changed, Ok(true));
+ assert_eq!(store.projection().shell.active_surface, ActiveSurface::Farmer);
+ assert_eq!(
+ store.projection().shell.selected_section,
+ ShellSection::Settings(SettingsSection::About)
+ );
}
#[test]
@@ -532,8 +624,10 @@ mod tests {
#[test]
fn in_memory_store_construction_and_updates_are_infallible() {
- let mut store =
- AppStateStore::in_memory(AppShellProjection::for_settings(SettingsSection::Account));
+ let mut store = AppStateStore::in_memory(AppShellProjection::for_settings(
+ ActiveSurface::Farmer,
+ SettingsSection::Account,
+ ));
let changed = store.apply_in_memory(AppStateCommand::SetSettingsPreference {
preference: SettingsPreference::AllowRelayConnections,