//! The application shell: a left sidebar (on by default), a right sidebar //! (off by default), and the center region that hosts the dock. use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px, }; #[cfg(feature = "alert")] use gpui_native_core::alert::{Alert, Alerts, AlertsView}; use gpui_native_core::button::{Button, ButtonVariant}; use gpui_native_core::common::{Sizable as _, h_flex, v_flex}; #[cfg(feature = "toast")] use gpui_native_core::toast::{Toast, Toasts, ToastsView}; use gpui_native_data::sidebar::Sidebar; use gpui_native_icons::IconRole; use gpui_native_theme::{ActiveTheme as _, Size}; use crate::resize_handle::ResizeHandle; use crate::window_controls::{TRAFFIC_LIGHT_PAD, WindowControls, titlebar_strip}; use crate::window_frame::frame; /// The width a sidebar opens at. const DEFAULT_SIDEBAR_WIDTH: f32 = 240.0; /// The narrowest a sidebar can be dragged to. const MIN_SIDEBAR_WIDTH: f32 = 160.0; /// The widest a sidebar can be dragged to. const MAX_SIDEBAR_WIDTH: f32 = 480.0; /// The layout the application owns. pub struct LayoutState { left_visible: bool, right_visible: bool, left_width: Pixels, right_width: Pixels, #[cfg(feature = "toast")] toasts: Entity, #[cfg(feature = "alert")] alerts: Entity, } impl LayoutState { /// Creates the state: left sidebar visible, right sidebar hidden. pub fn new(cx: &mut App) -> Entity { cx.new(|cx| Self { left_visible: true, right_visible: false, left_width: px(DEFAULT_SIDEBAR_WIDTH), right_width: px(DEFAULT_SIDEBAR_WIDTH), #[cfg(feature = "toast")] toasts: cx.new(|_| Toasts::default()), #[cfg(feature = "alert")] alerts: cx.new(|_| Alerts::default()), }) } /// Shows or hides the left sidebar. pub fn set_left_sidebar_visible(&mut self, visible: bool, cx: &mut Context) { if self.left_visible != visible { self.left_visible = visible; cx.notify(); } } /// Shows or hides the right sidebar. pub fn set_right_sidebar_visible(&mut self, visible: bool, cx: &mut Context) { if self.right_visible != visible { self.right_visible = visible; cx.notify(); } } /// Whether the left sidebar is visible. pub fn is_left_sidebar_visible(&self) -> bool { self.left_visible } /// Whether the right sidebar is visible. pub fn is_right_sidebar_visible(&self) -> bool { self.right_visible } /// The left sidebar's current width. pub fn left_sidebar_width(&self) -> Pixels { self.left_width } /// The right sidebar's current width. pub fn right_sidebar_width(&self) -> Pixels { self.right_width } /// Applies a drag delta to the left sidebar's width, clamped. fn resize_left(&mut self, delta: Pixels, cx: &mut Context) { self.left_width = (self.left_width + delta).clamp(px(MIN_SIDEBAR_WIDTH), px(MAX_SIDEBAR_WIDTH)); cx.notify(); } /// Applies a drag delta to the right sidebar's width, clamped. fn resize_right(&mut self, delta: Pixels, cx: &mut Context) { self.right_width = (self.right_width - delta).clamp(px(MIN_SIDEBAR_WIDTH), px(MAX_SIDEBAR_WIDTH)); cx.notify(); } /// Raises a toast in the shell's overlay. Returns its id. #[cfg(feature = "toast")] pub fn push_toast(&mut self, toast: Toast, cx: &mut Context) -> usize { self.toasts.update(cx, |toasts, cx| toasts.push(toast, cx)) } /// Dismisses one toast by id. #[cfg(feature = "toast")] pub fn dismiss_toast(&mut self, id: usize, cx: &mut Context) { self.toasts.update(cx, |toasts, cx| toasts.dismiss(id, cx)); } /// Dismisses the toast on screen. #[cfg(feature = "toast")] pub fn clear_toasts(&mut self, cx: &mut Context) { self.toasts.update(cx, |toasts, cx| toasts.clear(cx)); } /// Pushes an alert onto the shell's stack. Returns its id. #[cfg(feature = "alert")] pub fn push_alert(&mut self, alert: Alert, cx: &mut Context) -> usize { self.alerts.update(cx, |alerts, cx| alerts.push(alert, cx)) } /// Dismisses one alert by id, running its dismiss handler. #[cfg(feature = "alert")] pub fn dismiss_alert(&mut self, id: usize, cx: &mut Context) { self.alerts.update(cx, |alerts, cx| alerts.dismiss(id, cx)); } /// Dismisses every alert, running each entry's `on_dismiss` handler. #[cfg(feature = "alert")] pub fn clear_alerts(&mut self, cx: &mut Context) { self.alerts.update(cx, |alerts, cx| alerts.clear(cx)); } } /// Which sidebar a region or toggle belongs to. #[derive(Clone, Copy, PartialEq, Eq)] enum Side { Left, Right, } impl Side { fn name(self) -> &'static str { match self { Side::Left => "left", Side::Right => "right", } } } /// The application shell. #[derive(IntoElement)] pub struct Shell { id: ElementId, state: Entity, left_sidebar: Option, right_sidebar: Option, center: Option, } impl Shell { /// Creates the shell bound to `state`. pub fn new(id: I, state: Entity) -> Self where I: Into, { Self { id: id.into(), state, left_sidebar: None, right_sidebar: None, center: None, } } /// Sets the left sidebar's navigation list. pub fn left_sidebar(mut self, sidebar: Sidebar) -> Self { self.left_sidebar = Some(sidebar); self } /// Sets the right sidebar's content: a details or inspector pane. pub fn right_sidebar(mut self, content: E) -> Self where E: IntoElement, { self.right_sidebar = Some(content.into_any_element()); self } /// Sets the center region's content, which hosts the dock. pub fn center(mut self, content: E) -> Self where E: IntoElement, { self.center = Some(content.into_any_element()); self } } impl RenderOnce for Shell { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let metrics = theme.sizes.resolve(Size::default()); let background = theme.colors.background; let titlebar_height = theme.titlebar_height; let id = self.id.clone(); let state = self.state; let mut left = self.left_sidebar; let mut right = self.right_sidebar; let mut center = self.center; let left_visible = state.read(cx).is_left_sidebar_visible(); let right_visible = state.read(cx).is_right_sidebar_visible(); let left_material = left .as_ref() .is_some_and(|sidebar| sidebar.is_transparent()); let layout = div() .flex() .flex_row() .w_full() .h_full() .overflow_hidden() .when(left_visible && left.is_some(), |layout| { let width = state.read(cx).left_sidebar_width(); layout .child(sidebar_region( (id.clone(), "left"), width, left.take() .map(|sidebar| sidebar.into_any_element()) .unwrap_or_else(|| div().into_any_element()), Side::Left, left_material, &state, titlebar_height, metrics.dense_padding_x, window, cx, )) .child(ResizeHandle::new((id.clone(), "left-resize")).on_resize({ let state = state.clone(); move |delta, _, cx| { state.update(cx, |state, cx| state.resize_left(delta, cx)); } })) }) .child( div() .flex_1() .h_full() .min_w_0() .overflow_hidden() .bg(background) .child(center.take().unwrap_or_else(|| div().into_any_element())), ) .when(right_visible && right.is_some(), |layout| { let width = state.read(cx).right_sidebar_width(); layout .child(ResizeHandle::new((id.clone(), "right-resize")).on_resize({ let state = state.clone(); move |delta, _, cx| { state.update(cx, |state, cx| state.resize_right(delta, cx)); } })) .child(sidebar_region( (id.clone(), "right"), width, right.take().unwrap_or_else(|| div().into_any_element()), Side::Right, false, &state, titlebar_height, metrics.dense_padding_x, window, cx, )) }); // Wraps the layout in a window frame let mut shell = frame(layout, window, cx); #[cfg(feature = "toast")] { let toasts = state.read(cx).toasts.clone(); shell = shell.child(ToastsView::new((id.clone(), "toasts"), toasts)); } #[cfg(feature = "alert")] { let alerts = state.read(cx).alerts.clone(); shell = shell.child(AlertsView::new((id, "alerts"), alerts)); } shell } } /// One sidebar: a header strip with the hide toggle over the app's content. #[allow(clippy::too_many_arguments)] fn sidebar_region( id: impl Into, width: Pixels, content: AnyElement, side: Side, material: bool, state: &Entity, strip_height: Pixels, padding_x: Pixels, window: &mut Window, cx: &mut App, ) -> AnyElement { let id = id.into(); let theme = cx.theme(); let background = theme.colors.background; let strip_background = match (side, material) { (Side::Left, true) => theme.material.background, _ => theme.colors.background, }; let strip = h_flex() .id((id.clone(), "strip")) .debug_selector(move || format!("sidebar-strip-{}", side.name())) .flex_none() .h(strip_height) .bg(strip_background) .items_center() .map(|strip| match side { Side::Left => strip.when(cfg!(target_os = "macos"), |strip| { strip.pl(TRAFFIC_LIGHT_PAD) }), Side::Right => strip, }) .px(padding_x) .child(titlebar_strip( div().id((id.clone(), "strip-drag")).flex_1().self_stretch(), window, cx, )) .map(|strip| match side { Side::Left => strip.child( Button::new((id, "sidebar-toggle")) .icon(IconRole::PanelLeft) .variant(ButtonVariant::Ghost) .small() .accessibility_label("Hide left sidebar") .on_click({ let state = state.clone(); move |_, _, cx| { state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx)) } }), ), Side::Right => strip.child(WindowControls::new((id, "window-controls"))), }); v_flex() .w(width) .h_full() .overflow_hidden() .child(strip) .child( div() .flex_1() .min_h_0() .overflow_hidden() .when(!material, |content| content.bg(background)) .child(content), ) .into_any_element() }