feat: out-of-box experience (#2)
Reviewed-on: https://git.reya.su/reya/signed/pulls/2
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
//! 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.
|
||||
///
|
||||
/// The renderer is the only skin-owned object in the picture, so the settings
|
||||
/// the old `DockArea` carried live here. It is shared by reference with the
|
||||
/// per-container renderers, which are built once each and outlive any one
|
||||
/// frame.
|
||||
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 that land anywhere in the window, not
|
||||
/// only on the handle, so it cannot be expressed as a listener on the handle
|
||||
/// itself. This element paints nothing and exists for its `paint` hook, which
|
||||
/// is 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, as the old dock
|
||||
// did. 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user