diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index daab449..efc2010 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -24,7 +24,9 @@ pub(crate) use home::render_home_page; pub(crate) use plugin_detail_view::render_plugin_detail_view; pub(crate) use settings_layout::render_settings_landing; pub(crate) use sidebar::{panel_bg, panel_shadow}; -pub(crate) use sidebar_header::render_sidebar_header; +pub(crate) use sidebar_header::{ + render_sidebar_header, render_workspace_disclosure, render_workspace_disclosure_backdrop, +}; pub(crate) use split_pane::{ pane_host_label, pane_url_is_secure, render_compact_split_canvas, render_split_pane_header, diff --git a/crates/ely_app/src/shell/chrome/sidebar_header.rs b/crates/ely_app/src/shell/chrome/sidebar_header.rs index 5863ec8..c4656e3 100644 --- a/crates/ely_app/src/shell/chrome/sidebar_header.rs +++ b/crates/ely_app/src/shell/chrome/sidebar_header.rs @@ -69,13 +69,16 @@ fn render_title_row() -> AnyElement { const TRAFFIC_LIGHT_RESERVE: f32 = 68.0; fn render_workspace_picker( - snapshot: &BrowserSnapshot, + _snapshot: &BrowserSnapshot, active_space: Option<&Space>, picker_open: bool, cx: &mut Context, ) -> AnyElement { + // The disclosure renders at the window root (see render.rs) so it can + // sit above a fullscreen click-out backdrop and extend past the + // sidebar's overflow_hidden clip. Picker row keeps just the tile, the + // pill, and the add button. div() - .relative() .flex() .items_center() .gap(px(6.0)) @@ -84,12 +87,6 @@ fn render_workspace_picker( .child(render_workspaces_tile(cx)) .child(render_picker_pill(active_space, picker_open, cx)) .child(render_add_workspace_button(cx)) - // Disclosure floats below the picker row instead of taking inline - // space so the tab list never gets pushed when a workspace is being - // picked. Anchor: picker row's bottom-left, full row width. - .when(picker_open, |el| { - el.child(render_workspace_disclosure(snapshot, cx)) - }) .into_any_element() } @@ -173,7 +170,12 @@ fn render_picker_pill( .into_any_element() } -fn render_workspace_disclosure( +/// Window-root popover surface for the workspace picker. Rendered by +/// render_browser when `workspace_picker_open` is true. Pinned to a +/// fixed window-relative origin matched to the picker pill's row in +/// the sidebar header, so the surface lines up with the trigger +/// without needing to capture bounds at paint time. +pub(crate) fn render_workspace_disclosure( snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { @@ -181,10 +183,9 @@ fn render_workspace_disclosure( let body = div() .absolute() - .top_full() - .left_0() - .right_0() - .mt(px(6.0)) + .top(px(DISCLOSURE_TOP_PX)) + .left(px(DISCLOSURE_LEFT_PX)) + .w(px(DISCLOSURE_WIDTH_PX)) .flex() .flex_col() .gap(px(2.0)) @@ -208,6 +209,43 @@ fn render_workspace_disclosure( fade_in("workspace-disclosure", 140, body).into_any_element() } +/// Transparent fullscreen backdrop layered between the layout grid +/// and the disclosure. Any click on it closes the picker; the +/// disclosure paints after this backdrop so it remains interactive. +pub(crate) fn render_workspace_disclosure_backdrop( + cx: &mut Context, +) -> AnyElement { + div() + .id(SharedString::from("workspace-picker-backdrop")) + .absolute() + .inset_0() + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|shell, _: &gpui::MouseDownEvent, _, cx| { + shell.close_workspace_picker(cx); + }), + ) + .into_any_element() +} + +/// Anchor of the disclosure popover relative to the window's top-left. +/// Walks the sidebar layout: SHELL_INSET (16) + sidebar header pt (8) +/// + title row h (20) + header gap (8) + picker row py-top (4) + tile +/// height (32) + picker row py-bot (4) + popover lift (6) = 98. +const DISCLOSURE_TOP_PX: f32 = 98.0; + +/// Left edge of the disclosure: SHELL_INSET (16) + header px-left (10) +/// + picker row px-left (2) + tile width (32) + picker gap (6) = 66. +/// That lines the disclosure up with the picker pill, not the tile. +const DISCLOSURE_LEFT_PX: f32 = 66.0; + +/// Default sidebar (280 px) → pill width 180 px. The disclosure inherits +/// that width so the popover and trigger frame to the same column. If +/// the sidebar is resized the disclosure stays a fixed width — picker +/// is closed before the user can drag, so the misalignment is +/// transient at worst. +const DISCLOSURE_WIDTH_PX: f32 = 180.0; + fn render_disclosure_row( index: usize, space: &Space, diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index 3703113..77d41ae 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -10,7 +10,8 @@ use gpui::{ use super::chrome::command_match::visible_command_rows; use super::chrome::{ SANS_FAMILY, panel_bg, panel_shadow, render_command_overlay, - render_topbar as render_topbar_chrome, render_wallpaper, + render_topbar as render_topbar_chrome, render_wallpaper, render_workspace_disclosure, + render_workspace_disclosure_backdrop, }; use super::sidebar::collapsed_sidebar_active; use super::{ElyShell, ShellState}; @@ -92,6 +93,10 @@ impl ElyShell { .when(hover_expanded, |el| { el.child(self.render_hidden_sidebar_overlay(&snapshot, cx)) }) + .when(self.workspace_picker_open, |el| { + el.child(render_workspace_disclosure_backdrop(cx)) + .child(render_workspace_disclosure(&snapshot, cx)) + }) .children(render_command_overlay(self, &snapshot, cx)) .into_any_element() }