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:
@@ -28,7 +28,7 @@ impl ElyShell {
|
|||||||
.bg(rgba(panel_color))
|
.bg(rgba(panel_color))
|
||||||
.shadow(panel_shadow())
|
.shadow(panel_shadow())
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(render_sidebar_header(snapshot, cx))
|
.child(render_sidebar_header(self, snapshot, cx))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ use ely_design_system::colors;
|
|||||||
use ely_domain::Space;
|
use ely_domain::Space;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point, px,
|
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient,
|
||||||
rgb, rgba,
|
prelude::FluentBuilder, point, px, rgb, rgba,
|
||||||
};
|
};
|
||||||
use gpui_component::IconName;
|
use gpui_component::IconName;
|
||||||
|
|
||||||
use crate::shell::ElyShell;
|
use crate::shell::ElyShell;
|
||||||
|
|
||||||
pub(crate) fn render_sidebar_header(
|
pub(crate) fn render_sidebar_header(
|
||||||
|
shell: &ElyShell,
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
@@ -18,6 +19,7 @@ pub(crate) fn render_sidebar_header(
|
|||||||
.spaces
|
.spaces
|
||||||
.iter()
|
.iter()
|
||||||
.find(|space| space.id() == &snapshot.active_space_id);
|
.find(|space| space.id() == &snapshot.active_space_id);
|
||||||
|
let picker_open = shell.workspace_picker_open;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -28,7 +30,10 @@ pub(crate) fn render_sidebar_header(
|
|||||||
.gap(px(8.0))
|
.gap(px(8.0))
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.child(render_title_row())
|
.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()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +61,7 @@ fn render_title_row() -> AnyElement {
|
|||||||
|
|
||||||
fn render_workspace_picker(
|
fn render_workspace_picker(
|
||||||
active_space: Option<&Space>,
|
active_space: Option<&Space>,
|
||||||
|
picker_open: bool,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
@@ -65,7 +71,7 @@ fn render_workspace_picker(
|
|||||||
.py(px(4.0))
|
.py(px(4.0))
|
||||||
.px(px(2.0))
|
.px(px(2.0))
|
||||||
.child(render_workspaces_tile(cx))
|
.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))
|
.child(render_add_workspace_button(cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -95,13 +101,23 @@ fn render_workspaces_tile(cx: &mut Context<ElyShell>) -> AnyElement {
|
|||||||
.into_any_element()
|
.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
|
let space_name = active_space
|
||||||
.map(|space| space.name().to_string())
|
.map(|space| space.name().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let space_glyph = active_space
|
let space_glyph = active_space
|
||||||
.map(|space| space.icon().to_string())
|
.map(|space| space.icon().to_string())
|
||||||
.unwrap_or_default();
|
.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()
|
div()
|
||||||
.id(SharedString::from("workspace-picker"))
|
.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))
|
.px(px(8.0))
|
||||||
.py(px(6.0))
|
.py(px(6.0))
|
||||||
.rounded(px(9.0))
|
.rounded(px(9.0))
|
||||||
.bg(rgba(PICKER_BG))
|
.bg(rgba(bg))
|
||||||
.shadow(soft_shadow())
|
.shadow(soft_shadow())
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.hover(|style| style.bg(rgba(PICKER_BG_HOVER)))
|
.hover(|style| style.bg(rgba(PICKER_BG_HOVER)))
|
||||||
.active(|style| style.opacity(0.85))
|
.active(|style| style.opacity(0.85))
|
||||||
.on_click(cx.listener(|shell, _, window, cx| {
|
.on_click(cx.listener(|shell, _, _, cx| {
|
||||||
shell.cycle_to_next_space(window, cx);
|
shell.toggle_workspace_picker(cx);
|
||||||
}))
|
}))
|
||||||
.child(render_workspace_glyph(space_glyph))
|
.child(render_workspace_glyph(space_glyph))
|
||||||
.child(
|
.child(
|
||||||
@@ -135,11 +151,114 @@ fn render_picker_pill(active_space: Option<&Space>, cx: &mut Context<ElyShell>)
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(rgb(colors::INK_3))
|
.text_color(rgb(colors::INK_3))
|
||||||
.child(IconName::ChevronDown),
|
.child(chevron),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.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 {
|
fn render_workspace_glyph(emoji: String) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.size(px(18.0))
|
.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: u32 = 0xffffff99;
|
||||||
const PICKER_BG_HOVER: u32 = 0xffffffd9;
|
const PICKER_BG_HOVER: u32 = 0xffffffd9;
|
||||||
const ADD_BUTTON_BG: u32 = 0xffffff66;
|
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> {
|
fn soft_shadow() -> Vec<BoxShadow> {
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ pub struct ElyShell {
|
|||||||
command_input: Entity<InputState>,
|
command_input: Entity<InputState>,
|
||||||
pub(crate) plugin_search_input: Entity<InputState>,
|
pub(crate) plugin_search_input: Entity<InputState>,
|
||||||
pub(crate) translucency_slider: Entity<SliderState>,
|
pub(crate) translucency_slider: Entity<SliderState>,
|
||||||
|
pub(crate) workspace_picker_open: bool,
|
||||||
download_action_error: Option<String>,
|
download_action_error: Option<String>,
|
||||||
download_clear_confirmation: bool,
|
download_clear_confirmation: bool,
|
||||||
download_security_confirmation: Option<PendingDownloadFileAction>,
|
download_security_confirmation: Option<PendingDownloadFileAction>,
|
||||||
@@ -175,6 +176,7 @@ impl ElyShell {
|
|||||||
command_input,
|
command_input,
|
||||||
plugin_search_input,
|
plugin_search_input,
|
||||||
translucency_slider,
|
translucency_slider,
|
||||||
|
workspace_picker_open: false,
|
||||||
download_action_error: None,
|
download_action_error: None,
|
||||||
download_clear_confirmation: false,
|
download_clear_confirmation: false,
|
||||||
download_security_confirmation: None,
|
download_security_confirmation: None,
|
||||||
|
|||||||
@@ -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(
|
pub(super) fn on_select_previous_space(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &SelectPreviousSpace,
|
_: &SelectPreviousSpace,
|
||||||
|
|||||||
Reference in New Issue
Block a user