From e839b05415937e559bb274546a6f83d17837950a 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 09:07:20 -0400 Subject: [PATCH] Add space order controls --- .../src/shell/internal_pages/spaces.rs | 77 ++++++++++++++++++- crates/ely_app/src/shell/spaces.rs | 17 ++++ crates/ely_browser_core/src/state.rs | 1 + crates/ely_browser_core/src/state/spaces.rs | 55 +++++++++++++ crates/ely_browser_core/tests/spaces.rs | 49 +++++++++++- 5 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 crates/ely_browser_core/src/state/spaces.rs diff --git a/crates/ely_app/src/shell/internal_pages/spaces.rs b/crates/ely_app/src/shell/internal_pages/spaces.rs index 269ca38..18f20e2 100644 --- a/crates/ely_app/src/shell/internal_pages/spaces.rs +++ b/crates/ely_app/src/shell/internal_pages/spaces.rs @@ -6,7 +6,7 @@ use gpui::{ px, rgb, }; use gpui_component::{ - IconName, Sizable, StyledExt, + Disableable, IconName, Sizable, StyledExt, button::{Button, ButtonVariants}, scroll::ScrollableElement, }; @@ -132,13 +132,21 @@ fn render_spaces_list(snapshot: &BrowserSnapshot, cx: &mut Context) -> .border_t_1() .border_color(rgb(colors::HAIRLINE)) .children(snapshot.spaces.iter().enumerate().map(|(index, space)| { - render_space_row(index, space, snapshot, space.id() == &snapshot.active_space_id, cx) + render_space_row( + index, + snapshot.spaces.len(), + space, + snapshot, + space.id() == &snapshot.active_space_id, + cx, + ) })) .into_any_element() } fn render_space_row( index: usize, + space_count: usize, space: &Space, snapshot: &BrowserSnapshot, active: bool, @@ -179,11 +187,72 @@ fn render_space_row( ), ), ) - .child(render_space_action(index, space_id, active, cx)) + .child(render_space_actions(index, space_count, space_id, active, cx)) .into_any_element() } -fn render_space_action( +fn render_space_actions( + index: usize, + space_count: usize, + space_id: SpaceId, + active: bool, + cx: &mut Context, +) -> AnyElement { + let can_move_up = index > 0; + let can_move_down = index + 1 < space_count; + + div() + .flex() + .items_center() + .gap_2() + .child(render_space_order_button( + ("move-space-up", index), + space_id.clone(), + IconName::ArrowUp, + "Move Space Up", + can_move_up, + true, + cx, + )) + .child(render_space_order_button( + ("move-space-down", index), + space_id.clone(), + IconName::ArrowDown, + "Move Space Down", + can_move_down, + false, + cx, + )) + .child(render_space_switch_action(index, space_id, active, cx)) + .into_any_element() +} + +fn render_space_order_button( + id: (&'static str, usize), + space_id: SpaceId, + icon: IconName, + tooltip: &'static str, + enabled: bool, + moves_up: bool, + cx: &mut Context, +) -> AnyElement { + Button::new(id) + .small() + .ghost() + .icon(icon) + .tooltip(tooltip) + .disabled(!enabled) + .on_click(cx.listener(move |shell, _, _, cx| { + if moves_up { + shell.move_space_up(&space_id, cx); + } else { + shell.move_space_down(&space_id, cx); + } + })) + .into_any_element() +} + +fn render_space_switch_action( index: usize, space_id: SpaceId, active: bool, diff --git a/crates/ely_app/src/shell/spaces.rs b/crates/ely_app/src/shell/spaces.rs index e992f9a..5b57250 100644 --- a/crates/ely_app/src/shell/spaces.rs +++ b/crates/ely_app/src/shell/spaces.rs @@ -1,9 +1,26 @@ +use ely_domain::SpaceId; use gpui::{Context, Window}; use super::{ElyShell, ShellState}; use crate::{SelectNextSpace, SelectPreviousSpace}; impl ElyShell { + pub(super) fn move_space_up(&mut self, space_id: &SpaceId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.move_space_up(space_id).is_ok_and(|moved| moved) + { + cx.notify(); + } + } + + pub(super) fn move_space_down(&mut self, space_id: &SpaceId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.move_space_down(space_id).is_ok_and(|moved| moved) + { + cx.notify(); + } + } + pub(super) fn on_select_next_space( &mut self, _: &SelectNextSpace, diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 949dfb6..916e8c4 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -20,6 +20,7 @@ mod plugins; mod profiles; mod reading_list; mod site_permissions; +mod spaces; mod splits; mod sync; mod tab_groups; diff --git a/crates/ely_browser_core/src/state/spaces.rs b/crates/ely_browser_core/src/state/spaces.rs new file mode 100644 index 0000000..4845e88 --- /dev/null +++ b/crates/ely_browser_core/src/state/spaces.rs @@ -0,0 +1,55 @@ +use ely_domain::SpaceId; + +use super::BrowserCore; +use crate::CoreError; + +impl BrowserCore { + pub fn move_space_up(&mut self, space_id: &SpaceId) -> Result { + let mut ordered_ids = self.sorted_space_ids(); + let Some(index) = ordered_ids.iter().position(|id| id == space_id) else { + return Err(CoreError::SpaceNotFound { id: space_id.clone() }); + }; + + if index == 0 { + return Ok(false); + } + + ordered_ids.swap(index, index - 1); + self.apply_space_order(&ordered_ids)?; + Ok(true) + } + + pub fn move_space_down(&mut self, space_id: &SpaceId) -> Result { + let mut ordered_ids = self.sorted_space_ids(); + let Some(index) = ordered_ids.iter().position(|id| id == space_id) else { + return Err(CoreError::SpaceNotFound { id: space_id.clone() }); + }; + + if index + 1 == ordered_ids.len() { + return Ok(false); + } + + ordered_ids.swap(index, index + 1); + self.apply_space_order(&ordered_ids)?; + Ok(true) + } + + fn sorted_space_ids(&self) -> Vec { + self.sorted_spaces().iter().map(|space| space.id().clone()).collect() + } + + fn apply_space_order(&mut self, ordered_ids: &[SpaceId]) -> Result<(), CoreError> { + for (sort_key, space_id) in ordered_ids.iter().enumerate() { + let Some(space) = self.spaces.iter_mut().find(|space| space.id() == space_id) else { + return Err(CoreError::SpaceNotFound { id: space_id.clone() }); + }; + + let sort_key = sort_key as u64; + if space.sort_key() != sort_key { + space.set_sort_key(sort_key); + } + } + + Ok(()) + } +} diff --git a/crates/ely_browser_core/tests/spaces.rs b/crates/ely_browser_core/tests/spaces.rs index edc63c6..76df790 100644 --- a/crates/ely_browser_core/tests/spaces.rs +++ b/crates/ely_browser_core/tests/spaces.rs @@ -1,7 +1,7 @@ use std::error::Error; use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig}; -use ely_domain::{ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, ProfileId, ProfileKind}; +use ely_domain::{ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, ProfileId, ProfileKind, SpaceId}; #[test] fn created_space_binds_current_profile_as_default() -> Result<(), Box> { @@ -87,6 +87,49 @@ fn snapshot_orders_spaces_by_sort_key() -> Result<(), Box> { Ok(()) } +#[test] +fn moving_spaces_updates_visible_order() -> 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 personal_space_id = core.create_space("Personal", "P", 0x8eb7d4)?; + + core.set_space_sort_key(&work_space_id, 20)?; + core.set_space_sort_key(&research_space_id, 10)?; + core.set_space_sort_key(&personal_space_id, 30)?; + + assert!(core.move_space_up(&work_space_id)?); + assert_eq!( + ordered_space_ids(&core)?, + vec![work_space_id.clone(), research_space_id.clone(), personal_space_id.clone()] + ); + + assert!(core.move_space_down(&work_space_id)?); + assert_eq!( + ordered_space_ids(&core)?, + vec![research_space_id, work_space_id, personal_space_id] + ); + Ok(()) +} + +#[test] +fn moving_boundary_or_missing_space_is_safe() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let work_space_id = core.snapshot()?.active_space_id; + + assert!(!core.move_space_up(&work_space_id)?); + assert!(!core.move_space_down(&work_space_id)?); + + let missing_space_id = SpaceId::new(); + let error = match core.move_space_up(&missing_space_id) { + Err(error) => error, + Ok(_) => return Err("moving a missing space should fail".into()), + }; + + assert_eq!(error, CoreError::SpaceNotFound { id: missing_space_id }); + Ok(()) +} + #[test] fn space_default_profile_updates_with_profile_validation() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; @@ -186,3 +229,7 @@ fn active_space_updated_at( Ok(space.updated_at()) } + +fn ordered_space_ids(core: &BrowserCore) -> Result, Box> { + Ok(core.snapshot()?.spaces.iter().map(|space| space.id().clone()).collect()) +}