1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! What this design system paints inside a resize handle.
//!
//! Base owns the band, the cursor and the drag; everything here is appearance.
//! A divider rests as the same hairline it has always been, and answers the
//! pointer with a pill that grows and solidifies as the pointer engages it:
//! available, held, being dragged.
use std::rc::Rc;
use gpui::{
AnyElement, App, Axis, ElementId, IntoElement, ParentElement as _, Pixels, Styled as _, Window,
deferred, div, prelude::FluentBuilder as _, px,
};
use gpui_base::{
ResizeHandleContext, ResizeHandleRenderer, ResizeHandleState, Transition, transition,
};
pub use gpui_base::{
ResizablePanel, ResizablePanelEvent, ResizablePanelGroup, ResizableState, resizable_panel,
};
use crate::theme::ActiveTheme as _;
/// How thick the indicator is across the divider it sits on.
const INDICATOR_THICKNESS: Pixels = px(3.);
/// Create a [`ResizablePanelGroup`] with horizontal resizing.
pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
gpui_base::h_resizable(id).with_handle_appearance(resize_handle_appearance())
}
/// Create a [`ResizablePanelGroup`] with vertical resizing.
pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
gpui_base::v_resizable(id).with_handle_appearance(resize_handle_appearance())
}
/// This design system's divider appearance, for a handle that base does not
/// already hand it — a dock edge, or a hand-rolled handle in an application.
pub fn resize_handle_appearance() -> ResizeHandleRenderer {
Rc::new(|handle, window, cx| Some(render_resize_handle(handle, window, cx)))
}
/// How long the indicator is at each level of engagement, and how solid.
///
/// Idle draws nothing. The hairline is the resting appearance of a divider, and
/// a pill on every divider all the time would be noise in a dock that has a
/// dozen of them.
fn indicator(state: ResizeHandleState) -> (Pixels, f32) {
match state {
ResizeHandleState::Idle => (px(0.), 0.),
ResizeHandleState::Hovered => (px(20.), 0.35),
ResizeHandleState::Pressed => (px(28.), 0.6),
ResizeHandleState::Dragging => (px(44.), 0.9),
}
}
/// The hairline, and the indicator riding on it.
pub(crate) fn render_resize_handle(
handle: &ResizeHandleContext,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let axis = handle.axis();
let (target_length, target_opacity) = indicator(handle.state());
let motion = cx.theme().motion_tokens();
let policy = Transition::new(motion.duration_fast).easing(motion.easing_move.clone());
// Both values are sampled on every frame the handle is rendered, whatever
// it is showing. A transition asks for a frame only while it is moving, so
// a resting handle costs nothing; sampling it only while the pill is up
// would instead leave the retained value frozen wherever it was when the
// pill went away, and the next hover would start from there.
let length = transition(
"resizable-handle-indicator-length",
target_length,
policy.clone(),
window,
cx,
);
let opacity = transition(
"resizable-handle-indicator-opacity",
target_opacity,
policy,
window,
cx,
);
div()
// The hairline fills the handle's content area exactly, so it has
// nothing to give: shrinking it collapses the divider.
.flex_none()
.flex()
.bg(cx.theme().border)
// Along the hairline the pill is far shorter than the line, so centring
// it there is safe. Across the hairline it is thicker than the line and
// has to overhang, and neither flex alignment can be trusted to centre
// an item that overflows -- `justify_center` returns it to the start
// instead -- so that axis is offset by hand, below.
.map(|line| match axis {
Axis::Horizontal => line.w(px(1.)).h_full().items_center(),
_ => line.h(px(1.)).w_full().items_start().justify_center(),
})
.when(length > px(0.5), |line| {
let pill = div()
// `flex_none` keeps the one-pixel line from squashing it.
.flex_none()
.rounded(cx.theme().radius_full())
.bg(cx.theme().muted_foreground)
.opacity(opacity)
// Half the overhang, pulled back so the pill straddles the
// hairline evenly.
.map(|pill| match axis {
Axis::Horizontal => pill
.w(INDICATOR_THICKNESS)
.h(length)
.ml((INDICATOR_THICKNESS - px(1.)) * -0.5),
_ => pill
.h(INDICATOR_THICKNESS)
.w(length)
.mt((INDICATOR_THICKNESS - px(1.)) * -0.5),
});
// A hugging handle's hairline is its container's outermost pixel,
// so the pill's outer pixel lies past the boundary, where a dock's
// clip would take it off. Deferring the pill -- and only the pill,
// only while it is up -- paints it after the tree under the
// window's mask, so it keeps that pixel. The hairline stays in
// tree order: a deferred element paints over the application's own
// deferred content, and a divider that cut through a popover
// opened from the neighbouring panel is what that looked like.
line.child(match handle.edge() {
Some(_) => deferred(pill).into_any_element(),
None => pill.into_any_element(),
})
})
.into_any_element()
}