Move expanded sidebar list out of render.rs

render.rs was 525 lines because it carried both the shell orchestrator
and every sidebar row helper. The expanded sidebar (favorites, pinned,
spaces, tabs, archive sections plus the profile footer and the row
helpers) now lives in chrome::sidebar, where panel/soft shadow tokens
also moved. render.rs is back to a 150-line orchestrator that only
composes wallpaper, sidebar, topbar, and content area.

Behaviour preserved: render_tab_row stays available for tab_groups via
pub(crate) on impl ElyShell, and all existing on_action listeners and
keyboard captures remain wired in render_browser.
This commit is contained in:
2026-05-09 18:08:42 -04:00
parent 4d44057367
commit f136ac55c1
3 changed files with 406 additions and 384 deletions
+392
View File
@@ -0,0 +1,392 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing};
use ely_domain::{ArchivedTab, BrowserTab, Profile, Space};
use gpui::{
AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, point,
prelude::FluentBuilder, px, rgb, rgba,
};
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
use crate::shell::ElyShell;
use crate::shell::archive_labels::archive_detail_label;
use crate::shell::chrome::render_sidebar_header;
impl ElyShell {
pub(crate) fn render_expanded_sidebar(
&mut self,
snapshot: &BrowserSnapshot,
sidebar_width: f32,
cx: &mut Context<Self>,
) -> AnyElement {
div()
.w(px(sidebar_width))
.h_full()
.flex()
.flex_col()
.rounded(px(spacing::RADIUS_CARD))
.bg(rgba(PANEL_BG))
.shadow(panel_shadow())
.overflow_hidden()
.child(render_sidebar_header(snapshot, cx))
.child(
div()
.flex_1()
.overflow_y_scrollbar()
.p(px(10.0))
.flex()
.flex_col()
.gap(px(6.0))
.child(section_label("FAVORITES"))
.children(snapshot.favorites.iter().map(|tab| {
self.render_favorite_row(
tab,
&snapshot.profiles,
tab.id() == &snapshot.active_tab_id,
cx,
)
}))
.child(section_label("PINNED"))
.children(snapshot.pinned_tabs.iter().map(|tab| {
self.render_pinned_row(
tab,
&snapshot.profiles,
tab.id() == &snapshot.active_tab_id,
cx,
)
}))
.child(section_label("SPACES"))
.children(snapshot.spaces.iter().map(|space| {
self.render_space_row(
space,
space.id() == &snapshot.active_space_id,
cx,
)
}))
.child(section_label("TABS"))
.children(self.render_sidebar_tab_rows(snapshot, cx))
.child(section_label("ARCHIVE"))
.children(
snapshot
.archived_tabs
.iter()
.rev()
.map(|archived_tab| {
self.render_archived_row(archived_tab, snapshot, cx)
}),
),
)
.child(self.render_sidebar_footer(snapshot))
.into_any_element()
}
fn render_sidebar_footer(&self, snapshot: &BrowserSnapshot) -> AnyElement {
div()
.px(px(14.0))
.py(px(12.0))
.flex()
.items_center()
.gap_2()
.flex_shrink_0()
.border_t_1()
.border_color(rgba(colors::DIVIDER))
.child(
div()
.size(px(26.0))
.rounded_full()
.bg(linear_gradient(
135.0,
linear_color_stop(hsla(20.0 / 360.0, 0.6, 0.84, 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()
.child(
div()
.text_size(px(10.0))
.font_semibold()
.text_color(rgb(0xffffff))
.child(profile_initial(&snapshot.active_profile_name)),
),
)
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_2))
.child(snapshot.active_profile_name.clone()),
)
.into_any_element()
}
fn render_space_row(
&mut self,
space: &Space,
active: bool,
cx: &mut Context<Self>,
) -> AnyElement {
let space_id = space.id().clone();
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
let text_color = if active { colors::INK } else { colors::INK_2 };
div()
.id(SharedString::from(format!("space-{}", space.id().as_str())))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(10.0))
.flex()
.items_center()
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
.active(|style| style.opacity(0.82))
.bg(rgba(bg_color))
.when(active, |el| el.shadow(soft_shadow()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_space(&space_id, window, cx);
}))
.child(render_workspace_tile(space))
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(text_color))
.child(space.name().to_string()),
)
.into_any_element()
}
fn render_favorite_row(
&mut self,
tab: &BrowserTab,
profiles: &[Profile],
active: bool,
cx: &mut Context<Self>,
) -> AnyElement {
self.render_nav_row(tab, profiles, active, IconName::Star, colors::ACCENT, cx)
}
fn render_pinned_row(
&mut self,
tab: &BrowserTab,
profiles: &[Profile],
active: bool,
cx: &mut Context<Self>,
) -> AnyElement {
self.render_nav_row(tab, profiles, active, IconName::Asterisk, colors::INK_3, cx)
}
fn render_nav_row(
&mut self,
tab: &BrowserTab,
_profiles: &[Profile],
active: bool,
icon: IconName,
icon_color: u32,
cx: &mut Context<Self>,
) -> AnyElement {
let tab_id = tab.id().clone();
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
let text_color = if active { colors::INK } else { colors::INK_2 };
div()
.id(SharedString::from(format!("nav-{}", tab.id().as_str())))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(10.0))
.flex()
.items_center()
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
.active(|style| style.opacity(0.82))
.bg(rgba(bg_color))
.when(active, |el| el.shadow(soft_shadow()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_tab(&tab_id, window, cx);
}))
.child(div().text_color(rgb(icon_color)).child(icon))
.child(
div()
.min_w_0()
.overflow_hidden()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(text_color))
.child(tab.title().to_string()),
)
.into_any_element()
}
pub(crate) fn render_tab_row(
&mut self,
tab: &BrowserTab,
active: bool,
cx: &mut Context<Self>,
) -> AnyElement {
let tab_id = tab.id().clone();
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
let text_color = if active { colors::INK } else { colors::INK_2 };
div()
.id(SharedString::from(tab.id().as_str().to_string()))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(6.0))
.flex()
.flex_col()
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
.active(|style| style.opacity(0.82))
.bg(rgba(bg_color))
.when(active, |el| el.shadow(soft_shadow()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_tab(&tab_id, window, cx);
}))
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(text_color))
.overflow_hidden()
.child(tab.title().to_string()),
)
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_4))
.overflow_hidden()
.child(tab.display_url()),
)
.into_any_element()
}
fn render_archived_row(
&mut self,
archived_tab: &ArchivedTab,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
let tab = archived_tab.tab();
let tab_id = tab.id().clone();
let detail = archive_detail_label(archived_tab, &snapshot.spaces, &snapshot.profiles);
div()
.id(SharedString::from(format!("archived-{}", tab.id().as_str())))
.rounded(px(spacing::RADIUS_NAV))
.px(px(10.0))
.py(px(7.0))
.gap(px(10.0))
.flex()
.items_center()
.cursor_pointer()
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
.active(|style| style.opacity(0.82))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.restore_archived_tab(&tab_id, window, cx);
}))
.child(div().text_color(rgb(colors::INK_4)).child(IconName::Undo2))
.child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_size(px(13.0))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_2))
.overflow_hidden()
.child(tab.title().to_string()),
)
.child(
div()
.text_size(px(11.0))
.text_color(rgb(colors::INK_4))
.overflow_hidden()
.child(detail),
),
)
.into_any_element()
}
}
fn profile_initial(name: &str) -> String {
name.chars()
.next()
.unwrap_or('P')
.to_uppercase()
.to_string()
}
fn render_workspace_tile(space: &Space) -> impl IntoElement {
div()
.size(px(32.0))
.rounded(px(9.0))
.bg(linear_gradient(
135.0,
linear_color_stop(hsla(0., 0., 1., 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()
.child(
div()
.text_size(px(14.0))
.text_color(rgb(colors::ACCENT))
.child(space.icon().to_string()),
)
}
fn section_label(label: &'static str) -> impl IntoElement {
div()
.pt(px(8.0))
.pb(px(4.0))
.px(px(10.0))
.text_size(px(10.5))
.font_weight(gpui::FontWeight(500.0))
.text_color(rgb(colors::INK_4))
.child(label)
}
pub(crate) const ACTIVE_NAV_BG: u32 = 0xffffffd9;
pub(crate) const PANEL_BG: u32 = 0xffffffe0;
pub(crate) fn panel_shadow() -> Vec<BoxShadow> {
vec![
BoxShadow {
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.30),
offset: point(px(0.), px(20.)),
blur_radius: px(50.),
spread_radius: px(-15.),
},
BoxShadow {
color: hsla(0., 0., 1., 0.5),
offset: point(px(0.), px(0.)),
blur_radius: px(0.),
spread_radius: px(1.),
},
]
}
pub(crate) fn soft_shadow() -> Vec<BoxShadow> {
vec![
BoxShadow {
color: hsla(0., 0., 1., 0.7),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
},
BoxShadow {
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08),
offset: point(px(0.), px(0.)),
blur_radius: px(0.),
spread_radius: px(1.),
},
]
}