use ely_browser_core::BrowserSnapshot; use ely_design_system::{colors, spacing}; use ely_domain::BrowserTab; 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::chrome::{render_glyph_for, render_sidebar_header}; impl ElyShell { pub(crate) fn render_expanded_sidebar( &mut self, snapshot: &BrowserSnapshot, sidebar_width: f32, cx: &mut Context, ) -> AnyElement { let panel_color = panel_bg(snapshot); div() .w(px(sidebar_width)) .h_full() .flex() .flex_col() .rounded(px(spacing::RADIUS_CARD)) .bg(rgba(panel_color)) .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(2.0)) .child(self.render_home_anchor_row(snapshot, cx)) .children(snapshot.favorites.iter().map(|tab| { self.render_launcher_row( tab, tab.id() == &snapshot.active_tab_id, cx, ) })) .child(section_label("PINNED")) .children(snapshot.pinned_tabs.iter().map(|tab| { self.render_launcher_row( tab, tab.id() == &snapshot.active_tab_id, cx, ) })) .child(section_tabs_label(snapshot.tabs.len())) .children(self.render_sidebar_tab_rows(snapshot, cx)) .child(self.render_new_tab_row(cx)), ) .child(self.render_sidebar_footer(snapshot, cx)) .into_any_element() } fn render_sidebar_footer( &mut self, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { div() .px(px(10.0)) .py(px(8.0)) .flex() .flex_col() .gap(px(2.0)) .flex_shrink_0() .border_t_1() .border_color(rgba(colors::DIVIDER)) .child(self.render_settings_row(cx)) .child(self.render_profile_row(snapshot, cx)) .into_any_element() } fn render_settings_row(&mut self, cx: &mut Context) -> AnyElement { div() .id(SharedString::from("nav-settings")) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(7.0)) .gap(px(10.0)) .flex() .items_center() .text_size(px(13.0)) .text_color(rgb(colors::INK_2)) .cursor_pointer() .hover(|style| style.bg(rgba(ACTIVE_NAV_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", window, cx); })) .child( div() .text_color(rgb(colors::INK_3)) .child(IconName::Settings), ) .child("Settings") .into_any_element() } fn render_profile_row( &mut self, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { let profile_name = snapshot.active_profile_name.clone(); div() .id(SharedString::from("nav-profile")) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(6.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(|shell, _, window, cx| { shell.open_internal_tab("ely://settings/profiles", window, cx); })) .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(&profile_name)), ), ) .child( div() .flex_1() .min_w_0() .truncate() .text_size(px(13.0)) .font_weight(gpui::FontWeight(500.0)) .text_color(rgb(colors::INK_2)) .child(profile_name), ) .child( div() .text_color(rgb(colors::INK_4)) .child(IconName::ChevronDown), ) .into_any_element() } fn render_home_anchor_row( &mut self, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { let active_tab = snapshot .tabs .iter() .find(|tab| tab.id() == &snapshot.active_tab_id); let active = active_tab .map(|tab| tab.url().as_str() == "ely://new-tab") .unwrap_or(false); 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("nav-home")) .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(|shell, _, window, cx| { shell.open_internal_tab("ely://new-tab", window, cx); })) .child(div().text_color(rgb(colors::INK_3)).child(IconName::Frame)) .child( div() .flex_1() .text_size(px(13.0)) .font_weight(gpui::FontWeight(500.0)) .text_color(rgb(text_color)) .child("Home"), ) .into_any_element() } fn render_launcher_row( &mut self, tab: &BrowserTab, active: bool, cx: &mut Context, ) -> AnyElement { let tab_id = tab.id().clone(); let close_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 }; let host = tab.url().host().map(|host| host.to_string()); let title = tab.title().to_string(); let initial = title.chars().next().unwrap_or('?').to_string(); let unread = tab.unread_count(); let group_name = SharedString::from(format!("launcher-{}", tab.id().as_str())); let close_id = SharedString::from(format!("launcher-close-{}", tab.id().as_str())); div() .id(SharedString::from(format!("nav-{}", tab.id().as_str()))) .group(group_name.clone()) .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(render_glyph_for(host.as_deref(), &initial, 18.0)) .child( div() .flex_1() .min_w_0() .overflow_hidden() .truncate() .text_size(px(13.0)) .font_weight(gpui::FontWeight(500.0)) .text_color(rgb(text_color)) .child(title), ) .when(unread > 0, |el| el.child(render_unread_badge(unread))) .child( div() .id(close_id) .size(px(16.0)) .rounded(px(4.0)) .flex() .items_center() .justify_center() .text_color(rgb(colors::INK_4)) .opacity(0.0) .group_hover(group_name, |style| style.opacity(1.0)) .hover(|style| style.bg(rgba(0x281e1414)).text_color(rgb(colors::INK))) .cursor_pointer() .on_click(cx.listener(move |shell, _, window, cx| { shell.select_tab(&close_tab_id, window, cx); shell.close_active_tab(window, cx); })) .child(IconName::Close), ) .into_any_element() } fn render_new_tab_row(&mut self, cx: &mut Context) -> AnyElement { div() .id(SharedString::from("nav-new-tab")) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(7.0)) .gap(px(10.0)) .flex() .items_center() .text_color(rgb(colors::INK_3)) .text_size(px(13.0)) .cursor_pointer() .hover(|style| style.bg(rgba(ACTIVE_NAV_BG)).text_color(rgb(colors::INK))) .active(|style| style.opacity(0.82)) .on_click(cx.listener(|shell, _, window, cx| { shell.open_new_tab(window, cx); })) .child( div() .text_color(rgb(colors::INK_4)) .child(IconName::Plus), ) .child("New Tab") .into_any_element() } pub(crate) fn render_tab_row( &mut self, tab: &BrowserTab, active: bool, cx: &mut Context, ) -> 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 profile_initial(name: &str) -> String { name.chars() .next() .unwrap_or('P') .to_uppercase() .to_string() } fn render_unread_badge(count: u32) -> impl IntoElement { let label = if count > 99 { "99+".to_string() } else { count.to_string() }; div() .px(px(6.0)) .py(px(1.0)) .rounded(px(999.0)) .bg(rgba(UNREAD_BADGE_BG)) .text_size(px(10.0)) .font_weight(gpui::FontWeight(500.0)) .text_color(rgb(colors::INK_3)) .child(label) } fn section_tabs_label(count: usize) -> impl IntoElement { div() .pt(px(12.0)) .pb(px(4.0)) .px(px(10.0)) .flex() .items_center() .gap(px(6.0)) .child( div() .text_color(rgb(colors::INK_4)) .child(IconName::Frame), ) .child( div() .text_size(px(10.5)) .font_weight(gpui::FontWeight(500.0)) .text_color(rgb(colors::INK_4)) .child(format!("TABS · {count}")), ) } 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; const UNREAD_BADGE_BG: u32 = 0x281e140f; /// Maps appearance translucency_pct to a panel rgba u32. /// /// translucency_pct 0 → alpha 0xff (fully opaque) /// translucency_pct 100 → alpha 0xb3 (mostly opaque, ~70%) so panels stay /// readable without backdrop blur. pub(crate) fn panel_bg(snapshot: &BrowserSnapshot) -> u32 { let pct = snapshot.appearance.translucency_pct().min(100) as u32; let max_alpha: u32 = 0xff; let min_alpha: u32 = 0xb3; let alpha = max_alpha - (pct * (max_alpha - min_alpha)) / 100; 0xffffff00 | alpha } pub(crate) fn panel_shadow() -> Vec { 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 { 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.), }, ] }