From 668557773be74014ef1c1e514a2896a4f7341d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 05:47:50 -0400 Subject: [PATCH] Track tab sort keys --- crates/ely_browser_core/src/state.rs | 20 ++++---- .../ely_browser_core/src/state/tab_order.rs | 47 +++++++++++++++++++ crates/ely_browser_core/src/state/tabs.rs | 22 ++++++++- crates/ely_browser_core/tests/tabs.rs | 33 +++++++++++++ crates/ely_domain/src/tab.rs | 17 +++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 crates/ely_browser_core/src/state/tab_order.rs diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 114634d..586857a 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -20,6 +20,7 @@ mod reading_list; mod site_permissions; mod splits; mod sync; +mod tab_order; mod tabs; pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent}; @@ -125,7 +126,8 @@ impl BrowserCore { active_profile_id.clone(), new_tab_title, new_tab_url.clone(), - ); + ) + .with_sort_key(0); let active_tab_id = tab.id().clone(); let mut active_tabs_by_space = BTreeMap::new(); let mut active_tabs_by_space_profile = BTreeMap::new(); @@ -406,16 +408,18 @@ impl BrowserCore { } fn pinned_tabs(&self) -> Vec { - self.tabs - .iter() - .filter(|tab| tab.space_id() == &self.active_space_id) - .filter(|tab| tab.flags().pinned && !tab.flags().favorite) - .cloned() - .collect() + tab_order::sorted_tabs( + self.tabs + .iter() + .filter(|tab| tab.space_id() == &self.active_space_id) + .filter(|tab| tab.flags().pinned && !tab.flags().favorite), + ) } fn visible_tabs(&self) -> Vec { - self.tabs.iter().filter(|tab| tab.space_id() == &self.active_space_id).cloned().collect() + tab_order::sorted_tabs( + self.tabs.iter().filter(|tab| tab.space_id() == &self.active_space_id), + ) } fn record_tab_activity(&mut self, tab_id: &TabId, active_at: SystemTime) { diff --git a/crates/ely_browser_core/src/state/tab_order.rs b/crates/ely_browser_core/src/state/tab_order.rs new file mode 100644 index 0000000..a4052b9 --- /dev/null +++ b/crates/ely_browser_core/src/state/tab_order.rs @@ -0,0 +1,47 @@ +use ely_domain::{BrowserTab, SpaceId}; + +use super::BrowserCore; + +impl BrowserCore { + pub(super) fn next_tab_sort_key(&self, space_id: &SpaceId) -> u64 { + self.tabs + .iter() + .filter(|tab| tab.space_id() == space_id) + .map(BrowserTab::sort_key) + .max() + .map_or(0, |sort_key| sort_key.saturating_add(1)) + } + + pub(super) fn normalize_tab_sort_keys(&mut self, space_id: &SpaceId) { + let mut sort_key = 0; + for tab in self.tabs.iter_mut().filter(|tab| tab.space_id() == space_id) { + tab.set_sort_key(sort_key); + sort_key = sort_key.saturating_add(1); + } + } + + pub(super) fn sort_tabs_within_space(&mut self, space_id: &SpaceId) { + let indices = self + .tabs + .iter() + .enumerate() + .filter_map(|(index, tab)| (tab.space_id() == space_id).then_some(index)) + .collect::>(); + let mut tabs = indices.iter().map(|index| self.tabs[*index].clone()).collect::>(); + tabs.sort_by(compare_tabs); + + for (index, tab) in indices.into_iter().zip(tabs) { + self.tabs[index] = tab; + } + } +} + +pub(super) fn sorted_tabs<'a>(tabs: impl Iterator) -> Vec { + let mut tabs = tabs.cloned().collect::>(); + tabs.sort_by(compare_tabs); + tabs +} + +fn compare_tabs(left: &BrowserTab, right: &BrowserTab) -> std::cmp::Ordering { + left.sort_key().cmp(&right.sort_key()).then_with(|| left.id().cmp(right.id())) +} diff --git a/crates/ely_browser_core/src/state/tabs.rs b/crates/ely_browser_core/src/state/tabs.rs index ce3f9cf..ced2337 100644 --- a/crates/ely_browser_core/src/state/tabs.rs +++ b/crates/ely_browser_core/src/state/tabs.rs @@ -20,6 +20,7 @@ impl BrowserCore { pub fn open_tab(&mut self, url: UrlText) -> TabId { let tab = self.build_tab(url); let tab_id = tab.id().clone(); + let space_id = tab.space_id().clone(); let insert_index = self .tabs .iter() @@ -27,6 +28,7 @@ impl BrowserCore { .map_or(self.tabs.len(), |index| index + 1); self.record_history_entry(&tab); self.tabs.insert(insert_index, tab); + self.normalize_tab_sort_keys(&space_id); self.active_tab_id = tab_id.clone(); self.active_tabs_by_space.insert(self.active_space_id.clone(), tab_id.clone()); self.active_tabs_by_space_profile @@ -49,7 +51,10 @@ impl BrowserCore { self.detach_tab_from_split(&tab_id); let tab_index = self.active_tab_index()?; + let target_sort_key = self.next_tab_sort_key(space_id); self.tabs[tab_index].move_to_space(space_id.clone()); + self.tabs[tab_index].set_sort_key(target_sort_key); + self.sort_tabs_within_space(space_id); self.active_tabs_by_space.insert(space_id.clone(), tab_id.clone()); self.active_tabs_by_space_profile.remove(&(source_space_id.clone(), profile_id.clone())); @@ -257,6 +262,18 @@ impl BrowserCore { Ok(()) } + pub fn set_tab_sort_key(&mut self, tab_id: &TabId, sort_key: u64) -> Result<(), CoreError> { + let tab = self + .tabs + .iter_mut() + .find(|tab| tab.id() == tab_id) + .ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?; + let space_id = tab.space_id().clone(); + tab.set_sort_key(sort_key); + self.sort_tabs_within_space(&space_id); + Ok(()) + } + pub fn active_tab(&self) -> Result<&BrowserTab, CoreError> { self.tabs .iter() @@ -279,7 +296,8 @@ impl BrowserCore { url: UrlText, ) -> BrowserTab { let title = tab_title(&url); - BrowserTab::new(TabId::new(), space_id, profile_id, title, url) + let sort_key = self.next_tab_sort_key(&space_id); + BrowserTab::new(TabId::new(), space_id, profile_id, title, url).with_sort_key(sort_key) } pub(super) fn tab_belongs_to_space(&self, tab_id: &TabId, space_id: &SpaceId) -> bool { @@ -294,7 +312,9 @@ impl BrowserCore { .position(|existing| existing.id() == &self.active_tab_id) .map_or(self.tabs.len(), |index| index + 1); + let space_id = tab.space_id().clone(); self.tabs.insert(insert_index, tab); + self.normalize_tab_sort_keys(&space_id); self.select_tab(&tab_id)?; Ok(tab_id) } diff --git a/crates/ely_browser_core/tests/tabs.rs b/crates/ely_browser_core/tests/tabs.rs index 31fc06f..164454b 100644 --- a/crates/ely_browser_core/tests/tabs.rs +++ b/crates/ely_browser_core/tests/tabs.rs @@ -22,6 +22,39 @@ fn opens_new_tab_below_active_tab() -> Result<(), Box> { Ok(()) } +#[test] +fn opened_tabs_receive_visible_sort_keys() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let first_tab_id = core.active_tab()?.id().clone(); + let second_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + + core.select_tab(&first_tab_id)?; + let third_tab_id = core.open_tab(UrlText::parse("https://servo.org")?); + let snapshot = core.snapshot()?; + let ordered = + snapshot.tabs.iter().map(|tab| (tab.id().clone(), tab.sort_key())).collect::>(); + + assert_eq!(ordered, vec![(first_tab_id, 0), (third_tab_id, 1), (second_tab_id, 2)]); + Ok(()) +} + +#[test] +fn snapshot_orders_tabs_by_sort_key() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let first_tab_id = core.active_tab()?.id().clone(); + let second_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + let third_tab_id = core.open_tab(UrlText::parse("https://servo.org")?); + + core.set_tab_sort_key(&second_tab_id, 30)?; + core.set_tab_sort_key(&third_tab_id, 10)?; + core.set_tab_sort_key(&first_tab_id, 20)?; + let snapshot = core.snapshot()?; + let ordered_ids = snapshot.tabs.iter().map(|tab| tab.id().clone()).collect::>(); + + assert_eq!(ordered_ids, vec![third_tab_id, first_tab_id, second_tab_id]); + Ok(()) +} + #[test] fn opened_tabs_record_active_tab_as_parent() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/crates/ely_domain/src/tab.rs b/crates/ely_domain/src/tab.rs index 1172725..0c7056c 100644 --- a/crates/ely_domain/src/tab.rs +++ b/crates/ely_domain/src/tab.rs @@ -30,6 +30,7 @@ pub struct BrowserTab { state: TabState, flags: TabFlags, split_id: Option, + sort_key: u64, sync_enabled: bool, created_at: SystemTime, last_active_at: SystemTime, @@ -55,6 +56,7 @@ impl BrowserTab { state: TabState::Ready, flags: TabFlags::default(), split_id: None, + sort_key: 0, sync_enabled: true, created_at, last_active_at: created_at, @@ -67,6 +69,12 @@ impl BrowserTab { self } + #[must_use] + pub fn with_sort_key(mut self, sort_key: u64) -> Self { + self.sort_key = sort_key; + self + } + #[must_use] pub fn id(&self) -> &TabId { &self.id @@ -164,6 +172,15 @@ impl BrowserTab { self.split_id = None; } + #[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; + } + pub fn set_sync_enabled(&mut self, sync_enabled: bool) { self.sync_enabled = sync_enabled; }