344 lines
11 KiB
Rust
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));
|
|
}
|
|
});
|
|
}
|
|
}
|