use std::{collections::HashMap, sync::Arc, time::Instant};
use gpui_kit::{
App, Axis, Bounds, Context, Pixels, Point, Task, WeakEntity, Window,
base::{
Interpolate as _, Placement,
dock::{
DockArea, DockLayout, DockPlacement, InsertTarget, NodeId, PaneNode, PaneRef, PaneTree,
PanelId, PanelView,
},
},
size,
};
use super::motion::{MOVING, SETTLING, Track};
use crate::Theme;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Target {
node: NodeId,
placement: Placement,
}
#[derive(Clone)]
pub(crate) struct Lift {
panel: PanelId,
view: Arc<dyn PanelView>,
origin: PaneTree,
slot: Bounds<Pixels>,
grab: Point<Pixels>,
pointer: Point<Pixels>,
lifted_at: Instant,
}
pub(crate) struct Card {
pub(crate) view: Arc<dyn PanelView>,
pub(crate) rect: Bounds<Pixels>,
pub(crate) content: Bounds<Pixels>,
pub(crate) growing: bool,
}
#[derive(Clone, Default)]
pub(crate) enum PaneDrag {
#[default]
Idle,
Pressed {
panel: PanelId,
grab: Point<Pixels>,
start: Point<Pixels>,
},
Lifted { lift: Lift, pending: Option<Target> },
Previewing {
lift: Lift,
target: Target,
pending: Option<Target>,
},
Settling { panel: PanelId },
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Hover {
Nothing,
Own,
Target(Target),
}
pub(crate) struct DockSkinState {
area: WeakEntity<DockArea>,
drag: PaneDrag,
debounce: Option<Task<()>>,
settle: Option<Task<()>>,
frame: Bounds<Pixels>,
groups: HashMap<NodeId, Bounds<Pixels>>,
tracks: HashMap<PanelId, Track>,
revealed: Option<PanelId>,
hovered: Option<PanelId>,
split: bool,
}
impl DockSkinState {
pub(crate) fn new(area: WeakEntity<DockArea>) -> Self {
Self {
area,
drag: PaneDrag::Idle,
debounce: None,
settle: None,
frame: Bounds::default(),
groups: HashMap::new(),
tracks: HashMap::new(),
revealed: None,
hovered: None,
split: false,
}
}
pub(crate) fn is_dragging(&self) -> bool {
matches!(
self.drag,
PaneDrag::Pressed { .. } | PaneDrag::Lifted { .. } | PaneDrag::Previewing { .. }
)
}
pub(crate) fn lifted(&self) -> Option<PanelId> {
self.lift().map(|lift| lift.panel)
}
pub(crate) fn split(&self) -> bool {
self.split
}
pub(crate) fn revealed(&self) -> Option<PanelId> {
self.revealed
}
pub(crate) fn hovered(&self) -> Option<PanelId> {
self.hovered
}
fn lift(&self) -> Option<&Lift> {
match &self.drag {
PaneDrag::Lifted { lift, .. } | PaneDrag::Previewing { lift, .. } => Some(lift),
_ => None,
}
}
pub(crate) fn frame(&self) -> Bounds<Pixels> {
self.frame
}
pub(crate) fn record_frame(&mut self, bounds: Bounds<Pixels>) {
self.frame = bounds;
}
pub(crate) fn record_group(&mut self, node: NodeId, bounds: Bounds<Pixels>) {
self.groups.insert(node, bounds);
}
pub(crate) fn place(
&mut self,
panel: PanelId,
slot: Bounds<Pixels>,
cx: &App,
) -> (Bounds<Pixels>, bool, usize, Bounds<Pixels>) {
let now = cx.background_executor().now();
let motion = &Theme::global(cx).motion;
let (duration, curve) = (motion.layout, motion.layout_curve.clone());
let track = self
.tracks
.entry(panel)
.or_insert_with(|| Track::still(slot, now));
if cx.reduce_motion() {
*track = Track::still(slot, now);
} else {
track.retarget(slot, now, duration, &curve);
}
let (painted, moving) = track.sample(now, duration, &curve);
let priority = match self.drag {
PaneDrag::Settling { panel: settling } if settling == panel => SETTLING,
_ => MOVING,
};
(painted, moving, priority, self.frame)
}
pub(crate) fn card(&self, cx: &App) -> Option<Card> {
let lift = self.lift()?;
let (rect, growing) = card_rect(lift, cx);
Some(Card {
view: lift.view.clone(),
rect,
content: lift.slot,
growing,
})
}
pub(crate) fn reveal(&mut self, panel: PanelId, inside: bool, cx: &mut Context<Self>) {
if toggle(&mut self.revealed, panel, inside) {
cx.notify();
}
}
pub(crate) fn hover_handle(&mut self, panel: PanelId, hovered: bool, cx: &mut Context<Self>) {
if toggle(&mut self.hovered, panel, hovered) {
cx.notify();
}
}
pub(crate) fn layout_changed(&mut self, tree: &PaneTree, cx: &mut Context<Self>) {
let mut groups = 0;
tree.root().walk(&mut |node| {
if matches!(node.kind(), PaneRef::Tabs { .. }) {
groups += 1;
}
});
let split = groups > 1;
if split != self.split {
self.split = split;
cx.notify();
}
self.tracks.retain(|panel, _| tree.contains_panel(*panel));
self.groups
.retain(|node, _| tree.find_node(*node).is_some());
}
pub(crate) fn press(
&mut self,
panel: PanelId,
position: Point<Pixels>,
cx: &mut Context<Self>,
) {
if self.is_dragging() {
return;
}
let Some(slot) = self.tracks.get(&panel).map(Track::target) else {
return;
};
self.settle = None;
self.drag = PaneDrag::Pressed {
panel,
grab: position - slot.origin,
start: position,
};
cx.notify();
}
pub(crate) fn moved(
&mut self,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
match &mut self.drag {
PaneDrag::Pressed { panel, grab, start } => {
let threshold = f32::from(Theme::global(cx).tokens().metrics.dock_drag_threshold);
if (position - *start).magnitude() > f64::from(threshold) {
let (panel, grab) = (*panel, *grab);
self.lift_pane(panel, grab, position, window, cx);
}
}
PaneDrag::Lifted { lift, .. } | PaneDrag::Previewing { lift, .. } => {
lift.pointer = position;
self.retarget(position, window, cx);
cx.notify();
}
PaneDrag::Idle | PaneDrag::Settling { .. } => {}
}
}
fn lift_pane(
&mut self,
panel: PanelId,
grab: Point<Pixels>,
pointer: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(area) = self.area.upgrade() else {
return;
};
let (origin, view) = {
let area = area.read(cx);
(
area.layout(DockPlacement::Center).cloned(),
area.panel(panel).cloned(),
)
};
let (Some(origin), Some(view), Some(track)) = (origin, view, self.tracks.get(&panel))
else {
self.drag = PaneDrag::Idle;
return;
};
let neighbor = neighbor_group(origin.root(), panel);
self.drag = PaneDrag::Lifted {
lift: Lift {
panel,
view: view.clone(),
origin,
slot: track.target(),
grab,
pointer,
lifted_at: cx.background_executor().now(),
},
pending: None,
};
if let Some(node) = neighbor {
area.update(cx, |area, cx| {
area.move_panel(
panel,
InsertTarget::Tabs {
node,
ix: None,
activate: false,
},
window,
cx,
);
});
}
view.focus_handle(cx).focus(window, cx);
cx.notify();
}
fn hover_at(&self, position: Point<Pixels>, cx: &App) -> Hover {
let (Some(lift), Some(area)) = (self.lift(), self.area.upgrade()) else {
return Hover::Nothing;
};
let area = area.read(cx);
let Some(tree) = area.layout(DockPlacement::Center) else {
return Hover::Nothing;
};
let own = tree.find_panel_node(lift.panel).filter(|node| {
tree.find_node(*node).is_some_and(
|found| matches!(found.kind(), PaneRef::Tabs { panels, .. } if panels.len() == 1),
)
});
let hit = self.groups.iter().find(|(node, bounds)| {
bounds.contains(&position)
&& tree
.find_node(**node)
.is_some_and(|found| matches!(found.kind(), PaneRef::Tabs { .. }))
});
match hit {
None => Hover::Nothing,
Some((node, _)) if Some(*node) == own => Hover::Own,
Some((node, bounds)) => Hover::Target(Target {
node: *node,
placement: nearest_edge(*bounds, position),
}),
}
}
fn retarget(&mut self, position: Point<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
let hover = self.hover_at(position, cx);
let (applied, pending) = match &mut self.drag {
PaneDrag::Lifted { pending, .. } => (None, pending),
PaneDrag::Previewing {
target, pending, ..
} => (Some(*target), pending),
_ => return,
};
let wanted = match hover {
Hover::Target(target) if Some(target) != applied => Some(target),
_ => None,
};
if *pending == wanted {
return;
}
*pending = wanted;
self.debounce = wanted.map(|_| {
let delay = Theme::global(cx).motion.retarget;
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(delay).await;
_ = this.update_in(cx, |this, window, cx| {
if let Some(task) = this.debounce.take() {
task.detach();
}
this.apply_pending(window, cx);
});
})
});
}
fn apply_pending(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let pending = match &self.drag {
PaneDrag::Lifted { pending, .. } | PaneDrag::Previewing { pending, .. } => *pending,
_ => None,
};
if let Some(target) = pending {
self.apply(target, window, cx);
}
}
fn apply(&mut self, target: Target, window: &mut Window, cx: &mut Context<Self>) {
let Some(lift) = self.lift().cloned() else {
return;
};
let Some(area) = self.area.upgrade() else {
return;
};
let live = area
.read(cx)
.layout(DockPlacement::Center)
.and_then(|tree| tree.find_node(target.node))
.is_some();
if live {
area.update(cx, |area, cx| {
area.move_panel(
lift.panel,
InsertTarget::Split {
node: target.node,
placement: target.placement,
size: None,
},
window,
cx,
);
});
}
self.debounce = None;
self.drag = PaneDrag::Previewing {
lift,
target,
pending: None,
};
cx.notify();
}
pub(crate) fn drop_at(
&mut self,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
match &self.drag {
PaneDrag::Pressed { .. } => self.rest(cx),
PaneDrag::Lifted { .. } | PaneDrag::Previewing { .. } => {
match self.hover_at(position, cx) {
Hover::Nothing => self.cancel(window, cx),
Hover::Own => self.commit(window, cx),
Hover::Target(target) => {
let applied = match &self.drag {
PaneDrag::Previewing { target, .. } => Some(*target),
_ => None,
};
if applied != Some(target) {
self.apply(target, window, cx);
}
self.commit(window, cx);
}
}
}
PaneDrag::Idle | PaneDrag::Settling { .. } => {}
}
}
pub(crate) fn released(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match self.drag {
PaneDrag::Pressed { .. } => self.rest(cx),
PaneDrag::Lifted { .. } | PaneDrag::Previewing { .. } => self.cancel(window, cx),
PaneDrag::Idle | PaneDrag::Settling { .. } => {}
}
}
pub(crate) fn escape(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
match self.drag {
PaneDrag::Pressed { .. } => {
cx.stop_active_drag(window);
self.rest(cx);
true
}
PaneDrag::Lifted { .. } | PaneDrag::Previewing { .. } => {
self.cancel(window, cx);
true
}
PaneDrag::Idle | PaneDrag::Settling { .. } => false,
}
}
fn commit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(lift) = self.lift().cloned() else {
return;
};
self.settle_from_card(&lift, window, cx);
}
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(lift) = self.lift().cloned() else {
return;
};
if let Some(area) = self.area.upgrade() {
let layout = {
let area = area.read(cx);
layout_of(lift.origin.root(), area, cx)
};
area.update(cx, |area, cx| area.set_center(layout, window, cx));
}
cx.stop_active_drag(window);
self.settle_from_card(&lift, window, cx);
}
fn settle_from_card(&mut self, lift: &Lift, window: &mut Window, cx: &mut Context<Self>) {
let now = cx.background_executor().now();
let (card, _) = card_rect(lift, cx);
self.tracks.insert(lift.panel, Track::still(card, now));
self.debounce = None;
self.drag = PaneDrag::Settling { panel: lift.panel };
lift.view.focus_handle(cx).focus(window, cx);
let duration = Theme::global(cx).motion.layout;
self.settle = (!cx.reduce_motion() && !duration.is_zero()).then(|| {
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(duration).await;
_ = this.update(cx, |this, cx| {
if let Some(task) = this.settle.take() {
task.detach();
}
this.rest(cx);
});
})
});
if self.settle.is_none() {
self.drag = PaneDrag::Idle;
}
cx.notify();
}
fn rest(&mut self, cx: &mut Context<Self>) {
self.drag = PaneDrag::Idle;
self.debounce = None;
cx.notify();
}
}
fn toggle(slot: &mut Option<PanelId>, panel: PanelId, on: bool) -> bool {
let next = match (on, *slot) {
(true, _) => Some(panel),
(false, Some(shown)) if shown == panel => None,
(false, shown) => shown,
};
let changed = next != *slot;
*slot = next;
changed
}
fn card_rect(lift: &Lift, cx: &App) -> (Bounds<Pixels>, bool) {
let theme = Theme::global(cx);
let compact = theme.tokens().metrics.dock_card;
let compact = size(
compact.width.min(lift.slot.size.width),
compact.height.min(lift.slot.size.height),
);
let appear = theme.motion.appear;
let elapsed = cx
.background_executor()
.now()
.saturating_duration_since(lift.lifted_at);
let (progress, growing) = if cx.reduce_motion() || appear.is_zero() || elapsed >= appear {
(1., false)
} else {
let linear = elapsed.as_secs_f32() / appear.as_secs_f32();
(theme.motion.layout_curve.sample(linear), true)
};
let card = lift.slot.size.interpolate(&compact, progress);
let scale = |part: Pixels, of: Pixels, to: Pixels| {
if of > Pixels::ZERO {
to * (part / of)
} else {
Pixels::ZERO
}
};
let grab = Point::new(
scale(lift.grab.x, lift.slot.size.width, card.width),
scale(lift.grab.y, lift.slot.size.height, card.height),
);
(Bounds::new(lift.pointer - grab, card), growing)
}
pub(crate) fn nearest_edge(bounds: Bounds<Pixels>, position: Point<Pixels>) -> Placement {
let x = (position.x - bounds.left()) / bounds.size.width;
let y = (position.y - bounds.top()) / bounds.size.height;
let edges = [
(x, Placement::Left),
(1. - x, Placement::Right),
(y, Placement::Top),
(1. - y, Placement::Bottom),
];
edges
.into_iter()
.fold((f32::MAX, Placement::Left), |best, (distance, edge)| {
if distance < best.0 {
(distance, edge)
} else {
best
}
})
.1
}
fn neighbor_group(root: &PaneNode, panel: PanelId) -> Option<NodeId> {
let PaneRef::Split { children, .. } = root.kind() else {
return None;
};
let index = children.iter().position(|child| holds(child, panel))?;
let holds_only = matches!(
children[index].kind(),
PaneRef::Tabs { panels, .. } if panels.len() == 1
);
if !holds_only {
return neighbor_group(&children[index], panel);
}
let (sibling, from_start) = match index.checked_sub(1) {
Some(before) => (&children[before], false),
None => (children.get(index + 1)?, true),
};
Some(edge_group(sibling, from_start))
}
fn holds(node: &PaneNode, panel: PanelId) -> bool {
match node.kind() {
PaneRef::Split { children, .. } => children.iter().any(|child| holds(child, panel)),
PaneRef::Tabs { panels, .. } => panels.contains(&panel),
}
}
fn edge_group(node: &PaneNode, from_start: bool) -> NodeId {
match node.kind() {
PaneRef::Split { children, .. } => {
let child = if from_start {
children.first()
} else {
children.last()
};
child.map_or(node.id(), |child| edge_group(child, from_start))
}
PaneRef::Tabs { .. } => node.id(),
}
}
fn layout_of(node: &PaneNode, area: &DockArea, cx: &App) -> DockLayout {
match node.kind() {
PaneRef::Split {
axis,
children,
sizes,
} => {
let split = match axis {
Axis::Horizontal => DockLayout::h_split(),
Axis::Vertical => DockLayout::v_split(),
};
children
.iter()
.zip(sizes.iter())
.fold(split, |split, (child, size)| {
split.child(layout_of(child, area, cx), *size)
})
}
PaneRef::Tabs { panels, active_ix } => panels
.iter()
.filter_map(|panel| area.panel(*panel))
.fold(DockLayout::tabs(), |tabs, view| {
tabs.panel_view(view.clone(), cx)
})
.active_index(active_ix),
}
}
#[cfg(test)]
mod tests {
use gpui_kit::{point, px};
use super::*;
#[test]
fn the_nearest_edge_wins_and_there_is_no_center() {
let bounds = Bounds::new(point(px(100.), px(0.)), size(px(400.), px(200.)));
let at = |x: f32, y: f32| nearest_edge(bounds, point(px(x), px(y)));
assert_eq!(at(110., 100.), Placement::Left);
assert_eq!(at(490., 100.), Placement::Right);
assert_eq!(at(300., 10.), Placement::Top);
assert_eq!(at(300., 190.), Placement::Bottom);
assert_eq!(at(320., 100.), Placement::Right);
assert_eq!(at(280., 100.), Placement::Left);
}
}