Files
signed/crates/dock/src/dock_area.rs
T

344 lines
11 KiB
Rust

//! 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<DockArea>,
toggle_button_visible: Cell<bool>,
tiles_scrollbar_mode: Cell<Option<ScrollbarMode>>,
/// The dock whose resize handle is being dragged, if any. Only one can be.
resizing_dock: Cell<Option<DockPlacement>>,
}
impl SkinShared {
pub(crate) fn area(&self) -> &WeakEntity<DockArea> {
&self.area
}
pub(crate) fn is_toggle_button_visible(&self) -> bool {
self.toggle_button_visible.get()
}
pub(crate) fn tiles_scrollbar_mode(&self) -> Option<ScrollbarMode> {
self.tiles_scrollbar_mode.get()
}
pub(crate) fn resizing_dock(&self) -> &Cell<Option<DockPlacement>> {
&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<SkinShared>,
}
impl SignedDockSkin {
pub fn new(cx: &mut Context<DockArea>) -> Rc<Self> {
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<SkinShared> {
&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<ScrollbarMode> {
self.shared.tiles_scrollbar_mode()
}
pub fn set_tiles_scrollbar_mode(&self, mode: Option<ScrollbarMode>, 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<Self>) -> impl IntoElement {
Empty
}
}
impl DockAreaRenderer for SignedDockSkin {
fn frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
div()
.id("dock-area")
.relative()
.size_full()
.overflow_hidden()
.flex()
.flex_row()
}
fn center_frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
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<Div> {
// `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<Arc<dyn PanelView>> {
let state = state.clone();
Some(panel_handle(cx.new(|cx| {
InvalidPanel::new(state.panel_name.clone(), state, cx)
})))
}
fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
Rc::new(SignedTabGroupSkin::new(self.shared().clone()))
}
fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
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<SkinShared>,
}
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<gpui::ElementId> {
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<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Window,
_: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_: gpui::Bounds<Pixels>,
_: &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));
}
});
}
}