Add workspace switcher disclosure list

Clicking the picker pill now opens an in-flow space-list disclosure
beneath the workspace row instead of cycling to the next space. The
list shows every space's emoji glyph + name, highlights the active one
with the accent check, and on row click switches to that space and
closes the disclosure.

Sidebar disclosure rules:
- The chevron flips ChevronDown → ChevronUp when open so the affordance
  reads at a glance.
- A "Manage spaces" footer routes to ely://settings/spaces and closes
  the picker.
- ElyShell carries a workspace_picker_open: bool with toggle / close /
  select_space_from_picker helpers; render_sidebar_header receives a
  &ElyShell so it can read the open flag without leaking shell internals
  to free helpers.

The existing SelectNextSpace shortcut still routes through
cycle_to_next_space, so the keyboard cycle behaviour is unchanged.
This commit is contained in:
2026-05-09 19:49:37 -04:00
parent 797560d71c
commit c98ca18613
4 changed files with 156 additions and 10 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ impl ElyShell {
.bg(rgba(panel_color))
.shadow(panel_shadow())
.overflow_hidden()
.child(render_sidebar_header(snapshot, cx))
.child(render_sidebar_header(self, snapshot, cx))
.child(
div()
.flex_1()
@@ -3,14 +3,15 @@ use ely_design_system::colors;
use ely_domain::Space;
use gpui::{
AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point, px,
rgb, rgba,
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient,
prelude::FluentBuilder, point, px, rgb, rgba,
};
use gpui_component::IconName;
use crate::shell::ElyShell;
pub(crate) fn render_sidebar_header(
shell: &ElyShell,
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> AnyElement {
@@ -18,6 +19,7 @@ pub(crate) fn render_sidebar_header(
.spaces
.iter()
.find(|space| space.id() == &snapshot.active_space_id);
let picker_open = shell.workspace_picker_open;
div()
.flex()
@@ -28,7 +30,10 @@ pub(crate) fn render_sidebar_header(
.gap(px(8.0))
.flex_shrink_0()
.child(render_title_row())
.child(render_workspace_picker(active_space, cx))
.child(render_workspace_picker(active_space, picker_open, cx))
.when(picker_open, |el| {
el.child(render_workspace_disclosure(snapshot, cx))
})
.into_any_element()
}
@@ -56,6 +61,7 @@ fn render_title_row() -> AnyElement {
fn render_workspace_picker(
active_space: Option<&Space>,
picker_open: bool,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
@@ -65,7 +71,7 @@ fn render_workspace_picker(
.py(px(4.0))
.px(px(2.0))
.child(render_workspaces_tile(cx))
.child(render_picker_pill(active_space, cx))
.child(render_picker_pill(active_space, picker_open, cx))
.child(render_add_workspace_button(cx))
.into_any_element()
}
@@ -95,13 +101,23 @@ fn render_workspaces_tile(cx: &mut Context<ElyShell>) -> AnyElement {
.into_any_element()
}
fn render_picker_pill(active_space: Option<&Space>, cx: &mut Context<ElyShell>) -> AnyElement {
fn render_picker_pill(
active_space: Option<&Space>,
picker_open: bool,
cx: &mut Context<ElyShell>,
) -> 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 chevron = if picker_open {
IconName::ChevronUp
} else {
IconName::ChevronDown
};
let bg = if picker_open { PICKER_BG_HOVER } else { PICKER_BG };
div()
.id(SharedString::from("workspace-picker"))
@@ -113,13 +129,13 @@ fn render_picker_pill(active_space: Option<&Space>, cx: &mut Context<ElyShell>)
.px(px(8.0))
.py(px(6.0))
.rounded(px(9.0))
.bg(rgba(PICKER_BG))
.bg(rgba(bg))
.shadow(soft_shadow())
.cursor_pointer()
.hover(|style| style.bg(rgba(PICKER_BG_HOVER)))
.active(|style| style.opacity(0.85))
.on_click(cx.listener(|shell, _, window, cx| {
shell.cycle_to_next_space(window, cx);
.on_click(cx.listener(|shell, _, _, cx| {
shell.toggle_workspace_picker(cx);
}))
.child(render_workspace_glyph(space_glyph))
.child(
@@ -135,11 +151,114 @@ fn render_picker_pill(active_space: Option<&Space>, cx: &mut Context<ElyShell>)
.child(
div()
.text_color(rgb(colors::INK_3))
.child(IconName::ChevronDown),
.child(chevron),
)
.into_any_element()
}
fn render_workspace_disclosure(
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let active_id = snapshot.active_space_id.clone();
div()
.flex()
.flex_col()
.gap(px(2.0))
.p(px(4.0))
.rounded(px(10.0))
.bg(rgba(DISCLOSURE_BG))
.shadow(soft_shadow())
.children(
snapshot
.spaces
.iter()
.enumerate()
.map(|(index, space)| {
render_disclosure_row(index, space, &active_id, cx)
}),
)
.child(render_disclosure_footer(cx))
.into_any_element()
}
fn render_disclosure_row(
index: usize,
space: &Space,
active_id: &ely_domain::SpaceId,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let space_id = space.id().clone();
let active = space.id() == active_id;
let bg = if active { DISCLOSURE_ROW_ACTIVE_BG } else { 0x00000000 };
div()
.id(SharedString::from(format!("workspace-row-{index}")))
.flex()
.items_center()
.gap(px(8.0))
.px(px(8.0))
.py(px(6.0))
.rounded(px(8.0))
.bg(rgba(bg))
.cursor_pointer()
.hover(|style| style.bg(rgba(DISCLOSURE_ROW_HOVER_BG)))
.active(|style| style.opacity(0.85))
.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(
div()
.flex_1()
.min_w_0()
.truncate()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK))
.child(space.name().to_string()),
)
.when(active, |el| {
el.child(
div()
.text_color(rgb(colors::ACCENT))
.child(IconName::Check),
)
})
.into_any_element()
}
fn render_disclosure_footer(cx: &mut Context<ElyShell>) -> AnyElement {
div()
.id(SharedString::from("workspace-manage"))
.flex()
.items_center()
.gap(px(8.0))
.px(px(8.0))
.py(px(6.0))
.mt(px(2.0))
.rounded(px(8.0))
.border_t_1()
.border_color(rgba(colors::DIVIDER))
.text_size(px(12.0))
.text_color(rgb(colors::INK_3))
.cursor_pointer()
.hover(|style| style.bg(rgba(DISCLOSURE_ROW_HOVER_BG)).text_color(rgb(colors::INK)))
.active(|style| style.opacity(0.85))
.on_click(cx.listener(|shell, _, window, cx| {
shell.close_workspace_picker(cx);
shell.open_internal_tab("ely://settings/spaces", window, cx);
}))
.child(
div()
.text_color(rgb(colors::INK_4))
.child(IconName::Settings),
)
.child("Manage spaces")
.into_any_element()
}
fn render_workspace_glyph(emoji: String) -> AnyElement {
div()
.size(px(18.0))
@@ -182,6 +301,9 @@ fn render_add_workspace_button(cx: &mut Context<ElyShell>) -> AnyElement {
const PICKER_BG: u32 = 0xffffff99;
const PICKER_BG_HOVER: u32 = 0xffffffd9;
const ADD_BUTTON_BG: u32 = 0xffffff66;
const DISCLOSURE_BG: u32 = 0xffffffd9;
const DISCLOSURE_ROW_ACTIVE_BG: u32 = 0xffffffeb;
const DISCLOSURE_ROW_HOVER_BG: u32 = 0xffffffb3;
fn soft_shadow() -> Vec<BoxShadow> {
vec![
+2
View File
@@ -65,6 +65,7 @@ pub struct ElyShell {
command_input: Entity<InputState>,
pub(crate) plugin_search_input: Entity<InputState>,
pub(crate) translucency_slider: Entity<SliderState>,
pub(crate) workspace_picker_open: bool,
download_action_error: Option<String>,
download_clear_confirmation: bool,
download_security_confirmation: Option<PendingDownloadFileAction>,
@@ -175,6 +176,7 @@ impl ElyShell {
command_input,
plugin_search_input,
translucency_slider,
workspace_picker_open: false,
download_action_error: None,
download_clear_confirmation: false,
download_security_confirmation: None,
+22
View File
@@ -99,6 +99,28 @@ impl ElyShell {
}
}
pub(crate) fn toggle_workspace_picker(&mut self, cx: &mut Context<Self>) {
self.workspace_picker_open = !self.workspace_picker_open;
cx.notify();
}
pub(crate) fn close_workspace_picker(&mut self, cx: &mut Context<Self>) {
if self.workspace_picker_open {
self.workspace_picker_open = false;
cx.notify();
}
}
pub(crate) fn select_space_from_picker(
&mut self,
space_id: &SpaceId,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_space(space_id, window, cx);
self.close_workspace_picker(cx);
}
pub(super) fn on_select_previous_space(
&mut self,
_: &SelectPreviousSpace,