use ely_browser_core::BrowserSnapshot; use ely_design_system::{colors, spacing}; use ely_domain::{BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX}; use gpui::{ AnyElement, Context, InteractiveElement, IntoElement, KeyDownEvent, MouseButton, MouseMoveEvent, MouseUpEvent, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::FluentBuilder, px, rgb, rgba, }; use super::chrome::command_match::visible_command_rows; use super::chrome::{ SANS_FAMILY, WorkspaceDisclosureAnchor, panel_bg, panel_shadow, render_command_overlay, render_macos_traffic_light_hitboxes, 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}; impl Render for ElyShell { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { // Resolve the active palette before anything else so every // `colors::ink()` etc. call lower in the tree reads the right // shade. `ThemeMode::System` defers to the OS preference via // GPUI's `Window::appearance`; explicit Light / Dark wins. let appearance = window.appearance(); match &self.state { ShellState::Ready(core) => match core.snapshot() { Ok(snapshot) => { apply_color_mode( resolve_color_mode(snapshot.appearance.theme_mode(), appearance), cx, ); match active_tab_from_snapshot(&snapshot) { Some(active_tab) => self.render_browser(&snapshot, active_tab, window, cx), None => render_error("active tab missing from snapshot".to_string()), } } Err(error) => { apply_color_mode( resolve_color_mode(ely_domain::ThemeMode::default(), appearance), cx, ); render_error(error.to_string()) } }, ShellState::StartupError(message) => { apply_color_mode( resolve_color_mode(ely_domain::ThemeMode::default(), appearance), cx, ); render_error(message.clone()) } } } } fn apply_color_mode(mode: colors::Mode, cx: &mut Context) { colors::set_mode(mode); let component_mode = match mode { colors::Mode::Light => gpui_component::ThemeMode::Light, colors::Mode::Dark => gpui_component::ThemeMode::Dark, }; if gpui_component::Theme::global(cx).mode != component_mode { gpui_component::Theme::change(component_mode, None, cx); } gpui_component::Theme::global_mut(cx).font_family = SANS_FAMILY.into(); } fn resolve_color_mode( theme_mode: ely_domain::ThemeMode, window_appearance: gpui::WindowAppearance, ) -> colors::Mode { match theme_mode { ely_domain::ThemeMode::Light => colors::Mode::Light, ely_domain::ThemeMode::Dark => colors::Mode::Dark, ely_domain::ThemeMode::System => match window_appearance { gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => { colors::Mode::Dark } gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => { colors::Mode::Light } }, } } fn active_tab_from_snapshot(snapshot: &BrowserSnapshot) -> Option<&BrowserTab> { snapshot.tabs.iter().find(|tab| tab.id() == &snapshot.active_tab_id) } impl ElyShell { fn render_browser( &mut self, snapshot: &BrowserSnapshot, active_tab: &BrowserTab, window: &mut Window, cx: &mut Context, ) -> AnyElement { let sidebar_width = match active_sidebar_width(snapshot) { Ok(sidebar_width) => sidebar_width, Err(message) => return render_error(message), }; 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() .size_full() .track_focus(&self.focus_handle) .capture_key_down(cx.listener(Self::on_external_web_key_down)) .on_action(cx.listener(Self::on_close_current_tab)) .on_action(cx.listener(Self::on_focus_address_bar)) .on_action(cx.listener(Self::on_focus_command_mode)) .on_action(cx.listener(Self::on_download_current_page)) .on_action(cx.listener(Self::on_open_downloads)) .on_action(cx.listener(Self::on_open_history)) .on_action(cx.listener(Self::on_open_new_tab)) .on_action(cx.listener(Self::on_open_settings)) .on_action(cx.listener(Self::on_open_task_manager)) .on_action(cx.listener(Self::on_reset_zoom)) .on_action(cx.listener(Self::on_restore_closed_tab)) .on_action(cx.listener(Self::on_select_next_space)) .on_action(cx.listener(Self::on_select_next_tab)) .on_action(cx.listener(Self::on_select_previous_space)) .on_action(cx.listener(Self::on_select_previous_tab)) .on_action(cx.listener(Self::on_split_right)) .on_action(cx.listener(Self::on_toggle_favorite_tab)) .on_action(cx.listener(Self::on_toggle_pinned_tab)) .on_action(cx.listener(Self::on_toggle_sidebar)) .on_action(cx.listener(Self::on_zoom_in)) .on_action(cx.listener(Self::on_zoom_out)) .on_mouse_move(cx.listener(Self::on_window_mouse_move)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_window_mouse_up)) .capture_key_down(cx.listener(Self::on_command_overlay_key_down)) .font_family(SANS_FAMILY) .text_color(rgb(colors::ink())) .child(render_wallpaper(snapshot.appearance.wallpaper())) .child( div() .absolute() .inset_0() .p(px(spacing::SHELL_INSET)) .gap(px(spacing::SIDEBAR_MAIN_GAP)) .flex() .child(self.render_sidebar( snapshot, sidebar_width, sidebar_collapsed, sidebar_hidden, cx, )) .child(self.render_main_pane( snapshot, active_tab, sidebar_collapsed, window, cx, )), ) .when(hover_expanded, |el| el.child(self.render_hidden_sidebar_overlay(snapshot, cx))) // Workspace popover only makes sense when the picker pill is // visible — i.e. the sidebar is expanded. Compact/hidden // modes don't render the trigger, so showing the disclosure // would float a stranded card with no anchor. .when(self.workspace_picker_open && !sidebar_collapsed && !sidebar_hidden, |el| { let anchor = WorkspaceDisclosureAnchor::solve(sidebar_width); el.child(render_workspace_disclosure_backdrop(cx)) .child(render_workspace_disclosure(snapshot, anchor, cx)) }) .children(render_command_overlay(self, snapshot, cx)) .child(render_macos_traffic_light_hitboxes(cx)) .into_any_element() } fn render_hidden_sidebar_overlay( &mut self, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> 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( &mut self, snapshot: &BrowserSnapshot, active_tab: &BrowserTab, sidebar_collapsed: bool, window: &mut Window, cx: &mut Context, ) -> AnyElement { let panel_color = panel_bg(snapshot); div() .flex_1() .h_full() .min_w_0() .flex() .flex_col() .rounded(px(spacing::RADIUS_CARD)) .bg(rgba(panel_color)) .border_1() .border_color(rgba(main_pane_highlight_border())) .shadow(panel_shadow()) .overflow_hidden() .child(render_topbar_chrome(self, snapshot, active_tab, sidebar_collapsed, window, cx)) .child( div() .flex_1() .overflow_hidden() .child(self.render_content_area(snapshot, active_tab, cx)), ) .into_any_element() } fn render_sidebar( &mut self, snapshot: &BrowserSnapshot, sidebar_width: f32, sidebar_collapsed: bool, sidebar_hidden: bool, cx: &mut Context, ) -> AnyElement { if sidebar_hidden { return self.render_hidden_sidebar_rail(cx); } if sidebar_collapsed { return self.render_compact_sidebar(snapshot, sidebar_width, cx); } self.render_expanded_sidebar(snapshot, sidebar_width, cx) } fn on_command_overlay_key_down( &mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context, ) { let key = event.keystroke.key.as_str(); if !matches!(key, "up" | "down" | "enter") { return; } let total_rows = match &self.state { ShellState::Ready(core) => match core.snapshot() { Ok(snapshot) => { let Some(stripped) = snapshot.command_query.strip_prefix('>') else { return; }; let needle = stripped.trim().to_lowercase(); visible_command_rows(&snapshot, &needle).len() } Err(_) => return, }, ShellState::StartupError(_) => return, }; match key { "up" => self.command_select_prev(total_rows, cx), "down" => self.command_select_next(total_rows, cx), "enter" => self.activate_selected_command(window, cx), _ => {} } cx.stop_propagation(); } fn on_window_mouse_move( &mut self, event: &MouseMoveEvent, _window: &mut Window, cx: &mut Context, ) { let cursor_x = f32::from(event.position.x); if let Some((origin_x, origin_width)) = self.sidebar_resize_origin { let delta = cursor_x - origin_x; let candidate = (f32::from(origin_width) + delta) .clamp(MIN_RESIZE_WIDTH_PX, MAX_RESIZE_WIDTH_PX) .round() as u16; self.set_active_sidebar_width(candidate, cx); return; } if (REVEAL_THRESHOLD_PX..=COLLAPSE_THRESHOLD_PX).contains(&cursor_x) { return; } let ShellState::Ready(core) = &self.state else { return; }; let Some(width) = core.active_space_sidebar_width() else { return; }; if width > HIDDEN_SIDEBAR_WIDTH_PX { return; } if !self.sidebar_hover_expanded && cursor_x < REVEAL_THRESHOLD_PX { self.expand_hidden_sidebar(cx); } else if self.sidebar_hover_expanded && cursor_x > COLLAPSE_THRESHOLD_PX { self.collapse_hidden_sidebar(cx); } } pub(super) fn begin_sidebar_resize(&mut self, cursor_x: f32, cx: &mut Context) { let ShellState::Ready(core) = &self.state else { return; }; let Some(width) = core.active_space_sidebar_width() else { return; }; self.sidebar_resize_origin = Some((cursor_x, width)); cx.notify(); } pub(super) fn end_sidebar_resize(&mut self, cx: &mut Context) { if self.sidebar_resize_origin.take().is_some() { cx.notify(); } } fn on_window_mouse_up( &mut self, _event: &MouseUpEvent, _window: &mut Window, cx: &mut Context, ) { self.end_sidebar_resize(cx); } fn render_hidden_sidebar_rail(&mut self, cx: &mut Context) -> 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() } } /// 50% white inner border that traces every glass panel — the GPUI /// substitute for the design's `box-shadow: inset 0 0 0 1px rgba(255,255,255,0.5)`. /// Painted as the panel's own border so it stays part of the frame and /// never participates in hit testing. fn main_pane_highlight_border() -> u32 { colors::pick(0xffffff80, 0x1f1d1b80) } /// Lower clamp for the live sidebar-resize drag. Stays a touch above /// the per-space DEFAULT so a casual flick doesn't snap the user to a /// nearly-collapsed pane that's awkward to recover from. The toggle /// button still owns the discrete collapse-to-rail behavior. pub(super) const MIN_RESIZE_WIDTH_PX: f32 = 220.0; /// Upper clamp — wider than this and the main pane disappears on /// smaller windows. Tuned to fit comfortably on a 1280-wide window. pub(super) const MAX_RESIZE_WIDTH_PX: f32 = 480.0; /// Cursor x within this px from the left edge auto-reveals the hidden sidebar. /// Sized so the user only triggers the reveal when they actually approach the /// rail, not when they hover the page content. const REVEAL_THRESHOLD_PX: f32 = 24.0; /// Once revealed, cursor x past this px collapses the hidden sidebar back to /// the rail. Sits past the right edge of the expanded sidebar plus a small /// buffer so brief overshoots don't ping-pong the state. const COLLAPSE_THRESHOLD_PX: f32 = spacing::SHELL_INSET + DEFAULT_SIDEBAR_WIDTH_PX as f32 + 24.0; fn render_error(message: String) -> AnyElement { div() .size_full() .bg(rgb(colors::canvas())) .text_color(rgb(colors::error())) .flex() .items_center() .justify_center() .child(message) .into_any_element() } fn active_sidebar_width(snapshot: &BrowserSnapshot) -> Result { let Some(active_space) = snapshot.spaces.iter().find(|space| space.id() == &snapshot.active_space_id) else { return Err("Active Space is unavailable.".to_string()); }; Ok(f32::from(active_space.sidebar_width_px())) } pub(super) fn tab_profile_label(tab: &BrowserTab, profiles: &[ely_domain::Profile]) -> String { profiles .iter() .find(|profile| profile.id() == tab.profile_id()) .map(|profile| format!("Profile: {}", profile.name())) .unwrap_or_else(|| format!("Profile: {}", tab.profile_id().as_str())) }