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, prelude::FluentBuilder, point, px, rgb, rgba, }; use gpui_component::IconName; use crate::shell::ElyShell; use crate::shell::chrome::animations::fade_in; pub(crate) fn render_sidebar_header( shell: &ElyShell, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { let active_space = snapshot .spaces .iter() .find(|space| space.id() == &snapshot.active_space_id); let picker_open = shell.workspace_picker_open; div() .flex() .flex_col() .pt(px(8.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, picker_open, cx)) .when(picker_open, |el| { el.child(render_workspace_disclosure(snapshot, cx)) }) .into_any_element() } fn render_title_row() -> AnyElement { div() .h(px(20.0)) .flex() .items_center() .gap(px(4.0)) // macOS draws the traffic lights at window (12, 14) — roughly the // first 70 px of any sidebar that touches the window's left edge. // Pad the title past that span so "ELY Browser ⌄" sits inline with // the dots instead of stranded on a row of its own. .pl(px(TRAFFIC_LIGHT_RESERVE)) .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() } /// Width reserved at the start of the sidebar's top row for the macOS /// traffic lights. Three 12 px dots with 8 px gaps, plus 12 px from the /// window left edge minus our 16 px shell inset, lands at ~62; we add /// breathing room and call it 68. const TRAFFIC_LIGHT_RESERVE: f32 = 68.0; fn render_workspace_picker( active_space: Option<&Space>, picker_open: bool, cx: &mut Context, ) -> 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, picker_open, cx)) .child(render_add_workspace_button(cx)) .into_any_element() } fn render_workspaces_tile(cx: &mut Context) -> 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.92)) .active(|style| style.opacity(0.82)) .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>, picker_open: bool, cx: &mut Context, ) -> 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")) .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(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, _, _, cx| { shell.toggle_workspace_picker(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(chevron), ) .into_any_element() } fn render_workspace_disclosure( snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { let active_id = snapshot.active_space_id.clone(); let body = 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)); fade_in("workspace-disclosure", 140, body).into_any_element() } fn render_disclosure_row( index: usize, space: &Space, active_id: &ely_domain::SpaceId, cx: &mut Context, ) -> 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) -> 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)) .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) -> 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.82)) .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; const DISCLOSURE_BG: u32 = 0xffffffd9; const DISCLOSURE_ROW_ACTIVE_BG: u32 = 0xffffffeb; const DISCLOSURE_ROW_HOVER_BG: u32 = 0xffffffb3; fn soft_shadow() -> Vec { 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), }, ] }