diff --git a/crates/ely_app/src/shell/chrome/sidebar_header.rs b/crates/ely_app/src/shell/chrome/sidebar_header.rs index 3e363b9..d1f652b 100644 --- a/crates/ely_app/src/shell/chrome/sidebar_header.rs +++ b/crates/ely_app/src/shell/chrome/sidebar_header.rs @@ -97,6 +97,7 @@ fn render_picker_pill( ) -> AnyElement { let space_name = active_space.map(|space| space.name().to_string()).unwrap_or_default(); let space_glyph = active_space.map(|space| space.icon().to_string()).unwrap_or_default(); + let space_accent = active_space.map(ely_domain::Space::accent_hex); let chevron = if picker_open { IconName::ChevronUp } else { IconName::ChevronDown }; let bg = if picker_open { picker_bg_hover() } else { picker_bg() }; @@ -118,7 +119,7 @@ fn render_picker_pill( .on_click(cx.listener(|shell, _, _, cx| { shell.toggle_workspace_picker(cx); })) - .child(render_workspace_glyph(space_glyph)) + .child(render_workspace_glyph(space_glyph, space_accent)) .child( div() .flex_1() @@ -260,7 +261,7 @@ fn render_disclosure_row( .on_click(cx.listener(move |shell, _, window, cx| { shell.select_space_from_picker(&space_id, window, cx); })) - .child(render_workspace_glyph(space.icon().to_string())) + .child(render_workspace_glyph(space.icon().to_string(), Some(space.accent_hex()))) .child( div() .flex_1() @@ -301,15 +302,17 @@ fn render_disclosure_footer(cx: &mut Context) -> AnyElement { .into_any_element() } -fn render_workspace_glyph(emoji: String) -> AnyElement { - div() - .size(px(18.0)) - .rounded(px(5.0)) - .bg(linear_gradient( +fn render_workspace_glyph(emoji: String, accent_hex: Option) -> AnyElement { + let glyph = div().size(px(18.0)).rounded(px(5.0)); + let glyph = match accent_hex { + Some(accent) => glyph.bg(rgb(accent)), + None => glyph.bg(linear_gradient( 135.0, linear_color_stop(hsla(341.0 / 360.0, 0.78, 0.67, 1.0), 0.0), linear_color_stop(hsla(15.0 / 360.0, 0.55, 0.53, 1.0), 1.0), - )) + )), + }; + glyph .flex() .items_center() .justify_center() diff --git a/crates/ely_browser_core/src/navigation.rs b/crates/ely_browser_core/src/navigation.rs index 2987b74..77b5c70 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -123,6 +123,14 @@ pub(crate) fn tab_group_color_hex(command: &str) -> Option { parse_color_hex(value) } +pub(crate) fn space_accent_hex(command: &str) -> Option { + let value = command_argument( + command, + &["set-space-accent ", "set space accent ", "space-accent ", "space accent "], + )?; + parse_color_hex(value) +} + pub(crate) fn split_group_name(command: &str) -> Option<&str> { command_argument( command, diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 5ea3aea..8261c83 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -262,6 +262,17 @@ impl BrowserCore { self.set_space_archive_policy(&active_space_id, archive_policy) } + pub fn set_active_space_accent(&mut self, accent_hex: u32) -> Result<(), CoreError> { + let active_space_id = self.active_space_id.clone(); + let space = self + .spaces + .iter_mut() + .find(|space| space.id() == &active_space_id) + .ok_or_else(|| CoreError::SpaceNotFound { id: active_space_id.clone() })?; + space.set_accent_hex(accent_hex); + Ok(()) + } + pub fn set_space_archive_policy( &mut self, space_id: &SpaceId, diff --git a/crates/ely_browser_core/src/state/commands.rs b/crates/ely_browser_core/src/state/commands.rs index 292b764..01fa202 100644 --- a/crates/ely_browser_core/src/state/commands.rs +++ b/crates/ely_browser_core/src/state/commands.rs @@ -11,7 +11,7 @@ use crate::{ move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name, note_body, notes_url, plugin_detail_url, plugin_settings_url, plugins_url, reading_list_url, reading_progress_percent, rename_tab_group_name, search_url, settings_page_url, - settings_url, shortcut_settings_url, site_compatibility_url, space_icon, + settings_url, shortcut_settings_url, site_compatibility_url, space_accent_hex, space_icon, space_settings_url, split_group_name, switch_profile_name, sync_status_url, tab_group_color_hex, tab_group_name, tab_note_body, task_manager_url, }, @@ -111,7 +111,11 @@ impl BrowserCore { fn submit_named_command(&mut self, command: &str) -> Result { let command = command.trim(); if let Some(name) = new_space_name(command) { - self.create_space(name.to_string(), space_icon(name), 0xf54e00)?; + self.create_space(name.to_string(), space_icon(name), self.next_space_accent())?; + return Ok(true); + } + if let Some(accent_hex) = space_accent_hex(command) { + self.set_active_space_accent(accent_hex)?; return Ok(true); } if let Some(name) = new_profile_name(command) { diff --git a/crates/ely_browser_core/src/state/spaces.rs b/crates/ely_browser_core/src/state/spaces.rs index c1a6aa5..c351b20 100644 --- a/crates/ely_browser_core/src/state/spaces.rs +++ b/crates/ely_browser_core/src/state/spaces.rs @@ -59,6 +59,15 @@ impl TrashedSpace { } impl BrowserCore { + /// Rotate new spaces through distinct accents so workspace identity + /// reads at a glance; `>space-accent` overrides per space. + #[must_use] + pub fn next_space_accent(&self) -> u32 { + const SPACE_ACCENT_PALETTE: &[u32] = + &[0xf54e00, 0x0f7b6c, 0x2f5fe0, 0x8a4bd8, 0xb26b00, 0xc23a63]; + SPACE_ACCENT_PALETTE[self.spaces.len() % SPACE_ACCENT_PALETTE.len()] + } + pub fn create_space( &mut self, name: impl Into, diff --git a/crates/ely_browser_core/tests/space_accent.rs b/crates/ely_browser_core/tests/space_accent.rs new file mode 100644 index 0000000..9a757ce --- /dev/null +++ b/crates/ely_browser_core/tests/space_accent.rs @@ -0,0 +1,53 @@ +use std::error::Error; + +use ely_browser_core::{BrowserCore, InitialBrowserConfig}; +use ely_domain::CommandIntent; + +#[test] +fn space_accent_command_updates_the_active_space() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + core.set_command_query(">space-accent #123ABC"); + let intent = core.submit_command()?; + let snapshot = core.snapshot()?; + let active_space = snapshot + .spaces + .iter() + .find(|space| space.id() == &snapshot.active_space_id) + .ok_or("missing active space")?; + + assert_eq!(intent, Some(CommandIntent::Command("space-accent #123ABC".to_string()))); + assert_eq!(snapshot.command_query, ""); + assert_eq!(active_space.accent_hex(), 0x123abc); + Ok(()) +} + +#[test] +fn space_accent_command_preserves_query_for_invalid_hex() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let original_accent = core.snapshot()?.spaces[0].accent_hex(); + + core.set_command_query(">space-accent #12"); + core.submit_command()?; + let snapshot = core.snapshot()?; + + assert_eq!(snapshot.command_query, ">space-accent #12"); + assert_eq!(snapshot.spaces[0].accent_hex(), original_accent); + Ok(()) +} + +#[test] +fn new_spaces_rotate_through_the_accent_palette() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + core.set_command_query(">new-space Research"); + core.submit_command()?; + core.set_command_query(">new-space Notes"); + core.submit_command()?; + + let snapshot = core.snapshot()?; + let accents: Vec = snapshot.spaces.iter().map(ely_domain::Space::accent_hex).collect(); + assert_eq!(accents.len(), 3); + assert_ne!(accents[1], accents[2], "consecutive spaces must not share one accent"); + Ok(()) +} diff --git a/crates/ely_domain/src/space.rs b/crates/ely_domain/src/space.rs index dce269d..2d0bd09 100644 --- a/crates/ely_domain/src/space.rs +++ b/crates/ely_domain/src/space.rs @@ -80,6 +80,11 @@ impl Space { self.record_update(); } + pub fn set_accent_hex(&mut self, accent_hex: u32) { + self.accent_hex = accent_hex; + self.record_update(); + } + pub fn set_presentation( &mut self, name: impl Into,