//! 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, SharedString, Styled as _, Window, div, px, }; use gpui_native_core::button::{Button, ButtonVariant}; use gpui_native_core::common::{Sizable as _, h_flex, v_flex}; 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, } impl LayoutState { /// Creates the state: left sidebar visible, right sidebar hidden. pub fn new(cx: &mut App) -> Entity { cx.new(|_| Self { left_visible: true, right_visible: false, left_width: px(DEFAULT_SIDEBAR_WIDTH), right_width: px(DEFAULT_SIDEBAR_WIDTH), }) } /// 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(); } } /// 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", } } fn toggle_icon(self) -> IconRole { match self { Side::Left => IconRole::PanelLeft, Side::Right => IconRole::PanelRight, } } fn toggle_label(self) -> SharedString { match self { Side::Left => SharedString::from("Hide left sidebar"), Side::Right => SharedString::from("Hide right sidebar"), } } } /// 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 state = self.state; let id = self.id.clone(); 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 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( width, left.take() .map(|sidebar| sidebar.into_any_element()) .unwrap_or_else(|| div().into_any_element()), Side::Left, &state, metrics.strip_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() .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( width, right.take().unwrap_or_else(|| div().into_any_element()), Side::Right, &state, metrics.strip_height, metrics.dense_padding_x, window, cx, )) }); // Wraps the layout in a window frame. frame(layout, window, cx) } } /// One sidebar: a header strip with the hide toggle over the app's content. #[allow(clippy::too_many_arguments)] fn sidebar_region( width: Pixels, content: AnyElement, side: Side, state: &Entity, strip_height: Pixels, padding_x: Pixels, window: &mut Window, cx: &mut App, ) -> AnyElement { let strip = h_flex() .id(SharedString::from(format!("sidebar-strip-{}", side.name()))) .debug_selector(move || format!("sidebar-strip-{}", side.name())) .flex_none() .h(strip_height) .items_center() .justify_end() .map(|strip| titlebar_strip(strip, window, cx)) .map(|strip| match side { Side::Left => strip.when(cfg!(target_os = "macos"), |strip| { strip.pl(px(TRAFFIC_LIGHT_PAD)) }), Side::Right => strip.child(WindowControls::new("sidebar-window-controls")), }) .px(padding_x) .child( Button::new(SharedString::from(format!( "sidebar-toggle-{}", side.name() ))) .icon(side.toggle_icon()) .variant(ButtonVariant::Ghost) .small() .accessibility_label(side.toggle_label()) .on_click({ let state = state.clone(); move |_, _, cx| match side { Side::Left => { state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx)) } Side::Right => { state.update(cx, |state, cx| state.set_right_sidebar_visible(false, cx)) } } }), ); v_flex() .w(width) .h_full() .overflow_hidden() .child(strip) .child(div().flex_1().min_h_0().overflow_hidden().child(content)) .into_any_element() }