Replace sidebar header with workspace picker row

The design's sidebar opens with an ELY title row plus a three-part
workspace picker: a layout-grid tile (opens spaces settings), a glass
pill showing the active space's emoji + name (cycles to next space on
click), and a plus button (opens spaces settings). The previous header
was a static title + space-name label that did not match the design.

Cycle behaviour reuses the existing select_next_space core API to avoid
introducing a new state flow; the existing SelectNextSpace shortcut
delegates through the same helper.
This commit is contained in:
2026-05-09 17:55:35 -04:00
parent dcd4bb4c48
commit 7508767014
4 changed files with 209 additions and 34 deletions
+2
View File
@@ -1,3 +1,5 @@
pub(crate) mod sidebar_header;
pub(crate) mod wallpaper;
pub(crate) use sidebar_header::render_sidebar_header;
pub(crate) use wallpaper::{WallpaperTheme, render_wallpaper};
@@ -0,0 +1,201 @@
use ely_browser_core::BrowserSnapshot;
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,
};
use gpui_component::IconName;
use crate::shell::ElyShell;
pub(crate) fn render_sidebar_header(
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let active_space = snapshot
.spaces
.iter()
.find(|space| space.id() == &snapshot.active_space_id);
div()
.flex()
.flex_col()
.pt(px(36.0))
.px(px(10.0))
.pb(px(10.0))
.gap(px(8.0))
.flex_shrink_0()
.child(render_title_row())
.child(render_workspace_picker(active_space, cx))
.into_any_element()
}
fn render_title_row() -> AnyElement {
div()
.flex()
.items_center()
.gap(px(4.0))
.pl(px(6.0))
.child(
div()
.text_size(px(12.5))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_2))
.child("ELY Browser"),
)
.child(
div()
.text_color(rgb(colors::INK_3))
.opacity(0.6)
.child(IconName::ChevronDown),
)
.into_any_element()
}
fn render_workspace_picker(
active_space: Option<&Space>,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div()
.flex()
.items_center()
.gap(px(6.0))
.py(px(4.0))
.px(px(2.0))
.child(render_workspaces_tile(cx))
.child(render_picker_pill(active_space, cx))
.child(render_add_workspace_button(cx))
.into_any_element()
}
fn render_workspaces_tile(cx: &mut Context<ElyShell>) -> AnyElement {
div()
.id(SharedString::from("workspace-tile"))
.size(px(32.0))
.rounded(px(9.0))
.bg(linear_gradient(
135.0,
linear_color_stop(hsla(0.0, 0.0, 1.0, 1.0), 0.0),
linear_color_stop(hsla(20.0 / 360.0, 0.6, 0.94, 1.0), 1.0),
))
.shadow(soft_shadow())
.flex()
.items_center()
.justify_center()
.text_color(rgb(colors::ACCENT))
.cursor_pointer()
.hover(|style| style.opacity(0.85))
.active(|style| style.opacity(0.7))
.on_click(cx.listener(|shell, _, window, cx| {
shell.open_internal_tab("ely://settings/spaces", window, cx);
}))
.child(IconName::LayoutDashboard)
.into_any_element()
}
fn render_picker_pill(active_space: Option<&Space>, 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();
div()
.id(SharedString::from("workspace-picker"))
.flex_1()
.min_w_0()
.flex()
.items_center()
.gap(px(8.0))
.px(px(8.0))
.py(px(6.0))
.rounded(px(9.0))
.bg(rgba(PICKER_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);
}))
.child(render_workspace_glyph(space_glyph))
.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),
)
.child(
div()
.text_color(rgb(colors::INK_3))
.child(IconName::ChevronDown),
)
.into_any_element()
}
fn render_workspace_glyph(emoji: String) -> AnyElement {
div()
.size(px(18.0))
.rounded(px(5.0))
.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),
))
.flex()
.items_center()
.justify_center()
.text_size(px(10.0))
.text_color(rgb(0xffffff))
.child(emoji)
.into_any_element()
}
fn render_add_workspace_button(cx: &mut Context<ElyShell>) -> AnyElement {
div()
.id(SharedString::from("workspace-add"))
.size(px(32.0))
.rounded(px(9.0))
.bg(rgba(ADD_BUTTON_BG))
.shadow(soft_shadow())
.flex()
.items_center()
.justify_center()
.text_color(rgb(colors::INK_3))
.cursor_pointer()
.hover(|style| style.bg(rgba(PICKER_BG)).text_color(rgb(colors::INK)))
.active(|style| style.opacity(0.7))
.on_click(cx.listener(|shell, _, window, cx| {
shell.open_internal_tab("ely://settings/spaces", window, cx);
}))
.child(IconName::Plus)
.into_any_element()
}
const PICKER_BG: u32 = 0xffffff99;
const PICKER_BG_HOVER: u32 = 0xffffffd9;
const ADD_BUTTON_BG: u32 = 0xffffff66;
fn soft_shadow() -> Vec<BoxShadow> {
vec![
BoxShadow {
color: hsla(0.0, 0.0, 1.0, 0.7),
offset: point(px(0.0), px(1.0)),
blur_radius: px(0.0),
spread_radius: px(0.0),
},
BoxShadow {
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08),
offset: point(px(0.0), px(0.0)),
blur_radius: px(0.0),
spread_radius: px(1.0),
},
]
}
+2 -34
View File
@@ -13,7 +13,7 @@ use gpui_component::{
scroll::ScrollableElement,
};
use super::chrome::{WallpaperTheme, render_wallpaper};
use super::chrome::{WallpaperTheme, render_sidebar_header, render_wallpaper};
use super::sidebar::{collapsed_sidebar_active, render_command_bar_identity};
use super::{ElyShell, ShellState, archive_labels::archive_detail_label};
@@ -213,7 +213,7 @@ impl ElyShell {
.bg(rgba(PANEL_BG))
.shadow(panel_shadow())
.overflow_hidden()
.child(self.render_sidebar_header(snapshot))
.child(render_sidebar_header(snapshot, cx))
.child(
div()
.flex_1()
@@ -265,38 +265,6 @@ impl ElyShell {
.into_any_element()
}
fn render_sidebar_header(&self, snapshot: &BrowserSnapshot) -> AnyElement {
let space_name = snapshot
.spaces
.iter()
.find(|s| s.id() == &snapshot.active_space_id)
.map(|s| s.name().to_string())
.unwrap_or_default();
div()
.pt(px(48.0))
.px(px(14.0))
.pb(px(10.0))
.flex()
.flex_col()
.gap_1()
.flex_shrink_0()
.child(
div()
.text_size(px(15.0))
.font_semibold()
.text_color(rgb(colors::INK))
.child("ELY Browser"),
)
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_3))
.child(space_name),
)
.into_any_element()
}
fn render_sidebar_footer(&self, snapshot: &BrowserSnapshot) -> AnyElement {
div()
.px(px(14.0))
+4
View File
@@ -87,6 +87,10 @@ impl ElyShell {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.cycle_to_next_space(window, cx);
}
pub(super) fn cycle_to_next_space(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state
&& core.select_next_space().is_ok()
{