Add hidden-on-hover sidebar mode
Domain ships HIDDEN_SIDEBAR_WIDTH_PX = 8 alongside the existing collapsed/default tiers. ElyShell tracks sidebar_hover_expanded with expand_hidden_sidebar / collapse_hidden_sidebar helpers; switching back to a non-hidden width via the layout cards or core API resets the flag automatically so the sidebar can never be both hidden and expanded after a mode change. Renderer: - render_sidebar takes a sidebar_hidden flag and routes to a thin 8 px clickable rail (hover bg + click expands) when the active space's width is at HIDDEN. - While the rail is expanded, render_browser overlays a transparent backdrop + the full default-width sidebar absolutely positioned in the shell inset, so the main pane content never reflows. - collapsed_sidebar_active still drives the COLLAPSED-tier compact sidebar; the hidden tier is opted out of that path. Appearance form: - Layout cards section gains the design's third "Hidden on hover" card with a 6 px sliver preview that mutates the active space to HIDDEN_SIDEBAR_WIDTH_PX. LayoutMode now derives id/width/preview from a small enum so adding a fourth mode would be one match arm. The hover-expanded state never persists into the domain; once the user switches modes or clicks the backdrop, it collapses cleanly.
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use ely_domain::{COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX};
|
use ely_domain::{
|
||||||
|
COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX,
|
||||||
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba,
|
StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba,
|
||||||
@@ -19,7 +21,7 @@ pub(crate) fn render_sidebar_layout_section(
|
|||||||
.find(|space| space.id() == &snapshot.active_space_id)
|
.find(|space| space.id() == &snapshot.active_space_id)
|
||||||
.map(|space| space.sidebar_width_px())
|
.map(|space| space.sidebar_width_px())
|
||||||
.unwrap_or(DEFAULT_SIDEBAR_WIDTH_PX);
|
.unwrap_or(DEFAULT_SIDEBAR_WIDTH_PX);
|
||||||
let compact = active_width <= COLLAPSED_SIDEBAR_WIDTH_PX;
|
let mode = LayoutMode::from_width(active_width);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -43,30 +45,74 @@ pub(crate) fn render_sidebar_layout_section(
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.grid()
|
.grid()
|
||||||
.grid_cols(2)
|
.grid_cols(3)
|
||||||
.gap(px(10.0))
|
.gap(px(10.0))
|
||||||
.child(render_layout_card(
|
.child(render_layout_card(
|
||||||
"Single column",
|
"Single column",
|
||||||
"Default · workspace + tabs.",
|
"Default · workspace + tabs.",
|
||||||
!compact,
|
mode == LayoutMode::Single,
|
||||||
LayoutMode::Single,
|
LayoutMode::Single,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(render_layout_card(
|
.child(render_layout_card(
|
||||||
"Compact",
|
"Compact",
|
||||||
"Icons-only with launcher rail.",
|
"Icons-only with launcher rail.",
|
||||||
compact,
|
mode == LayoutMode::Compact,
|
||||||
LayoutMode::Compact,
|
LayoutMode::Compact,
|
||||||
cx,
|
cx,
|
||||||
|
))
|
||||||
|
.child(render_layout_card(
|
||||||
|
"Hidden on hover",
|
||||||
|
"Slide in on cursor reach.",
|
||||||
|
mode == LayoutMode::Hidden,
|
||||||
|
LayoutMode::Hidden,
|
||||||
|
cx,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
enum LayoutMode {
|
enum LayoutMode {
|
||||||
Single,
|
Single,
|
||||||
Compact,
|
Compact,
|
||||||
|
Hidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutMode {
|
||||||
|
fn from_width(width: u16) -> Self {
|
||||||
|
if width <= HIDDEN_SIDEBAR_WIDTH_PX {
|
||||||
|
Self::Hidden
|
||||||
|
} else if width <= COLLAPSED_SIDEBAR_WIDTH_PX {
|
||||||
|
Self::Compact
|
||||||
|
} else {
|
||||||
|
Self::Single
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(self) -> u16 {
|
||||||
|
match self {
|
||||||
|
Self::Single => DEFAULT_SIDEBAR_WIDTH_PX,
|
||||||
|
Self::Compact => COLLAPSED_SIDEBAR_WIDTH_PX,
|
||||||
|
Self::Hidden => HIDDEN_SIDEBAR_WIDTH_PX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Single => "layout-single",
|
||||||
|
Self::Compact => "layout-compact",
|
||||||
|
Self::Hidden => "layout-hidden",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_sidebar(self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Self::Single => 56.0,
|
||||||
|
Self::Compact => 18.0,
|
||||||
|
Self::Hidden => 6.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_layout_card(
|
fn render_layout_card(
|
||||||
@@ -76,13 +122,8 @@ fn render_layout_card(
|
|||||||
mode: LayoutMode,
|
mode: LayoutMode,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let id = SharedString::from(match mode {
|
|
||||||
LayoutMode::Single => "layout-single",
|
|
||||||
LayoutMode::Compact => "layout-compact",
|
|
||||||
});
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(id)
|
.id(SharedString::from(mode.id()))
|
||||||
.p(px(14.0))
|
.p(px(14.0))
|
||||||
.rounded(px(14.0))
|
.rounded(px(14.0))
|
||||||
.bg(rgba(LAYOUT_CARD_BG))
|
.bg(rgba(LAYOUT_CARD_BG))
|
||||||
@@ -95,11 +136,7 @@ fn render_layout_card(
|
|||||||
.hover(|style| style.opacity(0.94))
|
.hover(|style| style.opacity(0.94))
|
||||||
.active(|style| style.opacity(0.85))
|
.active(|style| style.opacity(0.85))
|
||||||
.on_click(cx.listener(move |shell, _, _, cx| {
|
.on_click(cx.listener(move |shell, _, _, cx| {
|
||||||
let width = match mode {
|
shell.set_active_sidebar_width(mode.width(), cx);
|
||||||
LayoutMode::Single => DEFAULT_SIDEBAR_WIDTH_PX,
|
|
||||||
LayoutMode::Compact => COLLAPSED_SIDEBAR_WIDTH_PX,
|
|
||||||
};
|
|
||||||
shell.set_active_sidebar_width(width, cx);
|
|
||||||
}))
|
}))
|
||||||
.child(render_layout_preview(mode))
|
.child(render_layout_preview(mode))
|
||||||
.child(
|
.child(
|
||||||
@@ -125,10 +162,7 @@ fn render_layout_card(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_layout_preview(mode: LayoutMode) -> AnyElement {
|
fn render_layout_preview(mode: LayoutMode) -> AnyElement {
|
||||||
let sidebar_width = match mode {
|
let sidebar_width = mode.preview_sidebar();
|
||||||
LayoutMode::Single => 56.0,
|
|
||||||
LayoutMode::Compact => 18.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.h(px(96.0))
|
.h(px(96.0))
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ pub struct ElyShell {
|
|||||||
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,
|
pub(crate) workspace_picker_open: bool,
|
||||||
|
pub(crate) sidebar_hover_expanded: 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>,
|
||||||
@@ -177,6 +178,7 @@ impl ElyShell {
|
|||||||
plugin_search_input,
|
plugin_search_input,
|
||||||
translucency_slider,
|
translucency_slider,
|
||||||
workspace_picker_open: false,
|
workspace_picker_open: false,
|
||||||
|
sidebar_hover_expanded: 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,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::{colors, spacing};
|
use ely_design_system::{colors, spacing};
|
||||||
use ely_domain::BrowserTab;
|
use ely_domain::{BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, Render, Styled, Window,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
div, px, rgb, rgba,
|
StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px, rgb, rgba,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::chrome::{
|
use super::chrome::{
|
||||||
@@ -36,7 +36,9 @@ impl ElyShell {
|
|||||||
Ok(sidebar_width) => sidebar_width,
|
Ok(sidebar_width) => sidebar_width,
|
||||||
Err(message) => return render_error(message),
|
Err(message) => return render_error(message),
|
||||||
};
|
};
|
||||||
let sidebar_collapsed = collapsed_sidebar_active(sidebar_width);
|
let sidebar_hidden = sidebar_width <= f32::from(HIDDEN_SIDEBAR_WIDTH_PX);
|
||||||
|
let sidebar_collapsed = collapsed_sidebar_active(sidebar_width) && !sidebar_hidden;
|
||||||
|
let hover_expanded = sidebar_hidden && self.sidebar_hover_expanded;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -72,13 +74,52 @@ impl ElyShell {
|
|||||||
.p(px(spacing::SHELL_INSET))
|
.p(px(spacing::SHELL_INSET))
|
||||||
.gap(px(spacing::SIDEBAR_MAIN_GAP))
|
.gap(px(spacing::SIDEBAR_MAIN_GAP))
|
||||||
.flex()
|
.flex()
|
||||||
.child(self.render_sidebar(&snapshot, sidebar_width, sidebar_collapsed, cx))
|
.child(self.render_sidebar(
|
||||||
|
&snapshot,
|
||||||
|
sidebar_width,
|
||||||
|
sidebar_collapsed,
|
||||||
|
sidebar_hidden,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
.child(self.render_main_pane(&snapshot, &active_tab, sidebar_collapsed, cx)),
|
.child(self.render_main_pane(&snapshot, &active_tab, sidebar_collapsed, cx)),
|
||||||
)
|
)
|
||||||
|
.when(hover_expanded, |el| {
|
||||||
|
el.child(self.render_hidden_sidebar_overlay(&snapshot, cx))
|
||||||
|
})
|
||||||
.children(render_command_overlay(&snapshot, cx))
|
.children(render_command_overlay(&snapshot, cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_hidden_sidebar_overlay(
|
||||||
|
&mut self,
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.inset_0()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("hidden-sidebar-backdrop"))
|
||||||
|
.absolute()
|
||||||
|
.inset_0()
|
||||||
|
.on_click(cx.listener(|shell, _, _, cx| shell.collapse_hidden_sidebar(cx))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top(px(spacing::SHELL_INSET))
|
||||||
|
.left(px(spacing::SHELL_INSET))
|
||||||
|
.bottom(px(spacing::SHELL_INSET))
|
||||||
|
.child(self.render_expanded_sidebar(
|
||||||
|
snapshot,
|
||||||
|
f32::from(DEFAULT_SIDEBAR_WIDTH_PX),
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_main_pane(
|
fn render_main_pane(
|
||||||
&mut self,
|
&mut self,
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
@@ -112,13 +153,31 @@ impl ElyShell {
|
|||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
sidebar_width: f32,
|
sidebar_width: f32,
|
||||||
sidebar_collapsed: bool,
|
sidebar_collapsed: bool,
|
||||||
|
sidebar_hidden: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
if sidebar_hidden {
|
||||||
|
return self.render_hidden_sidebar_rail(cx);
|
||||||
|
}
|
||||||
if sidebar_collapsed {
|
if sidebar_collapsed {
|
||||||
return self.render_compact_sidebar(snapshot, sidebar_width, cx);
|
return self.render_compact_sidebar(snapshot, sidebar_width, cx);
|
||||||
}
|
}
|
||||||
self.render_expanded_sidebar(snapshot, sidebar_width, cx)
|
self.render_expanded_sidebar(snapshot, sidebar_width, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_hidden_sidebar_rail(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("hidden-sidebar-rail"))
|
||||||
|
.w(px(f32::from(HIDDEN_SIDEBAR_WIDTH_PX)))
|
||||||
|
.h_full()
|
||||||
|
.rounded(px(spacing::RADIUS_PANE))
|
||||||
|
.bg(rgba(0xffffff8c))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(0xffffffd9)))
|
||||||
|
.active(|style| style.opacity(0.78))
|
||||||
|
.on_click(cx.listener(|shell, _, _, cx| shell.expand_hidden_sidebar(cx)))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_error(message: String) -> AnyElement {
|
fn render_error(message: String) -> AnyElement {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::{colors, spacing};
|
use ely_design_system::{colors, spacing};
|
||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
ArchivedTab, BrowserTab, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, Space,
|
ArchivedTab, BrowserTab, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX,
|
||||||
|
HIDDEN_SIDEBAR_WIDTH_PX, Space,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, BoxShadow, Context, IntoElement, ParentElement, Styled, Window, div, hsla, point,
|
AnyElement, BoxShadow, Context, IntoElement, ParentElement, Styled, Window, div, hsla, point,
|
||||||
@@ -124,6 +125,23 @@ impl ElyShell {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if core.set_space_sidebar_width(&active_space_id, width_px).is_ok() {
|
if core.set_space_sidebar_width(&active_space_id, width_px).is_ok() {
|
||||||
|
if width_px > HIDDEN_SIDEBAR_WIDTH_PX {
|
||||||
|
self.sidebar_hover_expanded = false;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn expand_hidden_sidebar(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if !self.sidebar_hover_expanded {
|
||||||
|
self.sidebar_hover_expanded = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collapse_hidden_sidebar(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.sidebar_hover_expanded {
|
||||||
|
self.sidebar_hover_expanded = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ pub use site_permission::{
|
|||||||
SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision,
|
SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision,
|
||||||
SitePermissionEntry, SitePermissionFeature,
|
SitePermissionEntry, SitePermissionFeature,
|
||||||
};
|
};
|
||||||
pub use space::{ArchivePolicy, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, Space};
|
pub use space::{
|
||||||
|
ArchivePolicy, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX,
|
||||||
|
Space,
|
||||||
|
};
|
||||||
pub use split::{MAX_SPLIT_PANES, SplitAxis, SplitLayout, SplitPane};
|
pub use split::{MAX_SPLIT_PANES, SplitAxis, SplitLayout, SplitPane};
|
||||||
pub use sync::{
|
pub use sync::{
|
||||||
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
|
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{ProfileId, SpaceId};
|
|||||||
|
|
||||||
pub const DEFAULT_SIDEBAR_WIDTH_PX: u16 = 280;
|
pub const DEFAULT_SIDEBAR_WIDTH_PX: u16 = 280;
|
||||||
pub const COLLAPSED_SIDEBAR_WIDTH_PX: u16 = 56;
|
pub const COLLAPSED_SIDEBAR_WIDTH_PX: u16 = 56;
|
||||||
|
pub const HIDDEN_SIDEBAR_WIDTH_PX: u16 = 8;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum ArchivePolicy {
|
pub enum ArchivePolicy {
|
||||||
|
|||||||
Reference in New Issue
Block a user