diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 0ecb620..114634d 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -108,8 +108,13 @@ impl BrowserCore { pub fn new(config: InitialBrowserConfig) -> Result { let profile = Profile::new(config.profile_name, 0x26251e, ProfileKind::Standard); let active_profile_id = profile.id().clone(); - let space = - Space::new(config.space_name, config.space_icon, 0xf54e00, active_profile_id.clone()); + let space = Space::new( + config.space_name, + config.space_icon, + 0xf54e00, + active_profile_id.clone(), + 0, + ); let new_tab_destination = config.new_tab_destination; let new_tab_url = new_tab_destination.url()?; let new_tab_title = tab_title(&new_tab_url); @@ -163,7 +168,8 @@ impl BrowserCore { icon: impl Into, accent_hex: u32, ) -> Result { - let space = Space::new(name, icon, accent_hex, self.active_profile_id.clone()); + let sort_key = self.next_space_sort_key(); + let space = Space::new(name, icon, accent_hex, self.active_profile_id.clone(), sort_key); let space_id = space.id().clone(); let default_profile_id = space.default_profile_id().clone(); let tab = self.build_tab_for(space_id.clone(), default_profile_id, self.new_tab_url()?); @@ -266,6 +272,20 @@ impl BrowserCore { Ok(()) } + pub fn set_space_sort_key( + &mut self, + space_id: &SpaceId, + sort_key: u64, + ) -> Result<(), CoreError> { + let space = self + .spaces + .iter_mut() + .find(|space| space.id() == space_id) + .ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })?; + space.set_sort_key(sort_key); + Ok(()) + } + pub fn set_search_engine(&mut self, search_engine: SearchEngine) { self.search_engine = search_engine; } @@ -329,7 +349,7 @@ impl BrowserCore { split_layouts: self.visible_split_layouts(), installed_plugins: self.installed_plugins.clone(), plugin_audit_events: self.plugin_audit_events.clone(), - spaces: self.spaces.clone(), + spaces: self.sorted_spaces(), profiles: self.profiles.clone(), sync_status: self.sync_status(), tabs: self.visible_tabs(), @@ -365,6 +385,22 @@ impl BrowserCore { .ok_or_else(|| CoreError::SpaceNotFound { id: self.active_space_id.clone() }) } + fn next_space_sort_key(&self) -> u64 { + self.spaces + .iter() + .map(Space::sort_key) + .max() + .map_or(0, |sort_key| sort_key.saturating_add(1)) + } + + fn sorted_spaces(&self) -> Vec { + let mut spaces = self.spaces.clone(); + spaces.sort_by(|left, right| { + left.sort_key().cmp(&right.sort_key()).then_with(|| left.id().cmp(right.id())) + }); + spaces + } + fn favorites(&self) -> Vec { self.tabs.iter().filter(|tab| tab.flags().favorite).cloned().collect() } diff --git a/crates/ely_browser_core/tests/spaces.rs b/crates/ely_browser_core/tests/spaces.rs index 7e68822..7abe71d 100644 --- a/crates/ely_browser_core/tests/spaces.rs +++ b/crates/ely_browser_core/tests/spaces.rs @@ -51,6 +51,42 @@ fn created_space_uses_default_sidebar_width() -> Result<(), Box> { Ok(()) } +#[test] +fn created_spaces_receive_incrementing_sort_keys() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let work_space_id = core.snapshot()?.active_space_id; + let research_space_id = core.create_space("Research", "R", 0x9fc9a2)?; + + let snapshot = core.snapshot()?; + let Some(work_space) = snapshot.spaces.iter().find(|space| space.id() == &work_space_id) else { + return Err("missing work space".into()); + }; + let Some(research_space) = + snapshot.spaces.iter().find(|space| space.id() == &research_space_id) + else { + return Err("missing research space".into()); + }; + + assert_eq!(work_space.sort_key(), 0); + assert_eq!(research_space.sort_key(), 1); + Ok(()) +} + +#[test] +fn snapshot_orders_spaces_by_sort_key() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let work_space_id = core.snapshot()?.active_space_id; + let research_space_id = core.create_space("Research", "R", 0x9fc9a2)?; + + core.set_space_sort_key(&work_space_id, 20)?; + core.set_space_sort_key(&research_space_id, 10)?; + let snapshot = core.snapshot()?; + + assert_eq!(snapshot.spaces[0].id(), &research_space_id); + assert_eq!(snapshot.spaces[1].id(), &work_space_id); + Ok(()) +} + #[test] fn space_default_profile_updates_with_profile_validation() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; @@ -91,9 +127,13 @@ fn space_settings_refresh_updated_at() -> Result<(), Box> { core.set_space_sidebar_width(&work_space_id, 320)?; let after_sidebar_update = active_space_updated_at(&core, &work_space_id)?; + core.set_space_sort_key(&work_space_id, 8)?; + let after_sort_update = active_space_updated_at(&core, &work_space_id)?; + assert!(after_archive_update > before_update); assert!(after_profile_update > after_archive_update); assert!(after_sidebar_update > after_profile_update); + assert!(after_sort_update > after_sidebar_update); Ok(()) } diff --git a/crates/ely_domain/src/space.rs b/crates/ely_domain/src/space.rs index 43d0a3c..c802bca 100644 --- a/crates/ely_domain/src/space.rs +++ b/crates/ely_domain/src/space.rs @@ -19,6 +19,7 @@ pub struct Space { default_profile_id: ProfileId, archive_policy: ArchivePolicy, sidebar_width_px: u16, + sort_key: u64, created_at: SystemTime, updated_at: SystemTime, } @@ -30,6 +31,7 @@ impl Space { icon: impl Into, accent_hex: u32, default_profile_id: ProfileId, + sort_key: u64, ) -> Self { let created_at = SystemTime::now(); Self { @@ -40,6 +42,7 @@ impl Space { default_profile_id, archive_policy: ArchivePolicy::Manual, sidebar_width_px: DEFAULT_SIDEBAR_WIDTH_PX, + sort_key, created_at, updated_at: created_at, } @@ -95,6 +98,16 @@ impl Space { self.record_update(); } + #[must_use] + pub fn sort_key(&self) -> u64 { + self.sort_key + } + + pub fn set_sort_key(&mut self, sort_key: u64) { + self.sort_key = sort_key; + self.record_update(); + } + #[must_use] pub fn created_at(&self) -> SystemTime { self.created_at