//! The dock-area appearance: the outer frame, the split frames, and one //! dock's chrome. Ported from the vendored dock's `DockArea`/`Dock` render //! onto `gpui_base::dock::DockAreaRenderer`. use std::cell::Cell; use std::ops::Deref as _; use std::rc::Rc; use std::sync::Arc; use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, AppContext as _, Axis, Context, Div, Element, Empty, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Stateful, Style, Styled as _, WeakEntity, Window, div, px, }; use gpui_base::dock::{ DockArea, DockAreaRenderer, DockContext, DockEvent, DockPlacement, NodeId, PanelState, PanelView, TabGroupRenderer, TilesRenderer, }; use gpui_base::resize_handle; use gpui_component::scroll::ScrollbarMode; use gpui_component::{ActiveTheme as _, Side, StyledExt as _}; use crate::invalid_panel::InvalidPanel; use crate::tab_panel::SignedTabGroupSkin; use crate::tiles::SignedTilesSkin; use crate::{TAB_BAR_HEIGHT, panel_handle}; /// What every part of the skin reads, and the dock area it belongs to. /// Shared by reference with the per-container renderers. pub(crate) struct SkinShared { area: WeakEntity, toggle_button_visible: Cell, tiles_scrollbar_mode: Cell>, /// The dock whose resize handle is being dragged, if any. Only one can be. resizing_dock: Cell>, } impl SkinShared { pub(crate) fn area(&self) -> &WeakEntity { &self.area } pub(crate) fn is_toggle_button_visible(&self) -> bool { self.toggle_button_visible.get() } pub(crate) fn tiles_scrollbar_mode(&self) -> Option { self.tiles_scrollbar_mode.get() } pub(crate) fn resizing_dock(&self) -> &Cell> { &self.resizing_dock } /// Redraw the area after a setting changed. The skin is not an entity, so /// nothing else would notice. pub(crate) fn notify(&self, cx: &mut App) { _ = self.area.update(cx, |_, cx| cx.notify()); } } /// The Signed appearance for a [`DockArea`]. /// /// Install it at construction, where the area's own weak handle is available: /// /// ```ignore /// let dock = cx.new(|cx| { /// let skin = SignedDockSkin::new(cx); /// DockArea::new("dock", Some(1), window, cx).with_renderer(skin) /// }); /// ``` pub struct SignedDockSkin { shared: Rc, } impl SignedDockSkin { pub fn new(cx: &mut Context) -> Rc { Rc::new(Self { shared: Rc::new(SkinShared { area: cx.weak_entity(), toggle_button_visible: Cell::new(true), tiles_scrollbar_mode: Cell::new(None), resizing_dock: Cell::new(None), }), }) } pub(crate) fn shared(&self) -> &Rc { &self.shared } /// Whether tab bars offer the affordance that collapses a neighbouring /// dock. pub fn is_toggle_button_visible(&self) -> bool { self.shared.is_toggle_button_visible() } pub fn set_toggle_button_visible(&self, visible: bool, cx: &mut App) { self.shared.toggle_button_visible.set(visible); self.shared.notify(cx); } /// When a tiles canvas shows its scrollbar. `None` follows the theme. pub fn tiles_scrollbar_mode(&self) -> Option { self.shared.tiles_scrollbar_mode() } pub fn set_tiles_scrollbar_mode(&self, mode: Option, cx: &mut App) { self.shared.tiles_scrollbar_mode.set(mode); self.shared.notify(cx); } } /// The payload a dock's resize handle drags. It draws nothing: the handle /// itself is the affordance. #[derive(Clone)] struct ResizePanel; impl Render for ResizePanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { Empty } } impl DockAreaRenderer for SignedDockSkin { fn frame(&self, _: &mut Window, _: &mut App) -> Stateful
{ div() .id("dock-area") .relative() .size_full() .overflow_hidden() .flex() .flex_row() } fn center_frame(&self, _: &mut Window, _: &mut App) -> Stateful
{ div() .id("dock-area-center") .flex() .flex_1() .flex_col() .overflow_hidden() } fn split_frame(&self, node: NodeId, _: Axis, _: &mut Window, cx: &mut App) -> Stateful
{ // `size_full` is what the old `StackPanel::render` carried; `flex_1` // is belt and braces so the frame never collapses to zero height in // an unsizing parent. div() .id(("dock-split-frame", node.as_u64())) .size_full() .flex_1() .min_h(px(0.)) .overflow_hidden() .bg(cx.theme().tokens.tab_bar) } fn render_dock( &self, dock: &DockContext, content: AnyElement, window: &mut Window, cx: &mut App, ) -> AnyElement { let placement = dock.placement(); let open = dock.is_open(); // A closed left or right dock takes no space at all; a closed bottom // dock keeps a strip so its tab bar stays clickable. if !open && !placement.is_bottom() { return div().into_any_element(); } div() .flex() .flex_none() .relative() .overflow_hidden() .map(|this| match placement { DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(dock.size()), DockPlacement::Bottom => this.w_full().h(dock.size()), // Base never builds a dock for the centre. DockPlacement::Center => this, }) // The closed bottom dock's strip is the tab bar itself, which is // a full tab bar tall. .when(!open && placement.is_bottom(), |this| { this.h(TAB_BAR_HEIGHT) }) .child(content) .child(self.render_resize_handle(dock, window, cx)) .child(DockResizeTracker { dock: dock.clone(), shared: self.shared().clone(), }) .into_any_element() } /// The "unknown panel" message the old `InvalidPanel` drew. It answers /// `dump` with the state it was handed, so a layout written by a build /// that knows the panel survives a load and save here. fn build_placeholder( &self, state: &PanelState, _: &mut Window, cx: &mut App, ) -> Option> { let state = state.clone(); Some(panel_handle(cx.new(|cx| { InvalidPanel::new(state.panel_name.clone(), state, cx) }))) } fn tab_group_renderer(&self) -> Rc { Rc::new(SignedTabGroupSkin::new(self.shared().clone())) } fn tiles_renderer(&self) -> Rc { Rc::new(SignedTilesSkin::new(self.shared().clone())) } } impl SignedDockSkin { fn render_resize_handle( &self, dock: &DockContext, _: &mut Window, _: &mut App, ) -> impl IntoElement { let placement = dock.placement(); let shared = self.shared().clone(); resize_handle("resize-handle", placement.axis()) .when(placement.is_left(), |this| this.placement(Side::Left)) .on_drag(ResizePanel, move |info, _, _, cx| { cx.stop_propagation(); shared.resizing_dock().set(Some(placement)); cx.new(|_| info.deref().clone()) }) } } /// Turns the window's mouse stream into dock resizing. A resize is driven /// by pointer moves anywhere in the window, so this paints nothing and /// exists for its `paint` hook — the only place a window-level mouse /// listener can be registered. struct DockResizeTracker { dock: DockContext, shared: Rc, } impl IntoElement for DockResizeTracker { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for DockResizeTracker { type PrepaintState = (); type RequestLayoutState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&gpui::GlobalElementId>, _: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { (window.request_layout(Style::default(), None, cx), ()) } fn prepaint( &mut self, _: Option<&gpui::GlobalElementId>, _: Option<&gpui::InspectorElementId>, _: gpui::Bounds, _: &mut Self::RequestLayoutState, _: &mut Window, _: &mut App, ) -> Self::PrepaintState { } fn paint( &mut self, _: Option<&gpui::GlobalElementId>, _: Option<&gpui::InspectorElementId>, _: gpui::Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, window: &mut Window, _: &mut App, ) { let placement = self.dock.placement(); window.on_mouse_event({ let dock = self.dock.clone(); let shared = self.shared.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if !phase.bubble() || shared.resizing_dock().get() != Some(placement) { return; } // Dragging a closed dock's handle reopens it. The live // state is read rather than the render-time snapshot in // `dock`, which would still say closed for the rest of the // frame and toggle it shut again on the next move. let open = shared .area() .upgrade() .is_some_and(|area| area.read(cx).is_dock_open(placement)); if !open { dock.toggle(window, cx); } dock.resize_to(event.position, window, cx); } }); window.on_mouse_event({ let shared = self.shared.clone(); move |_: &MouseUpEvent, phase, _, cx| { if !phase.bubble() || shared.resizing_dock().get() != Some(placement) { return; } shared.resizing_dock().set(None); // The size lives on the dock, not in the layout tree, so // nothing else tells a subscriber to persist it. _ = shared .area() .update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged)); } }); } }