ui/components/pane_group/mod.rs
1//! Recursive split-tree layout: [`PaneGroup`] owns a tree of [`Pane`]s
2//! joined by [`PaneAxis`] splits, tracks which pane is active, and exposes
3//! [`SplitDirection`]-driven split/close/focus operations plus the default
4//! [`crate::register_pane_keybindings`] that drive them.
5//!
6//! Deliberately reuses [`gpui::Axis`] (exactly `Horizontal`/`Vertical`)
7//! rather than a redundant parallel enum, and keeps the tree shape
8//! (`Member`/`PaneAxis`, per-axis `flexes`) close to the proven shape used
9//! by editors with recursive pane splitting — reused as an *idea*, not code
10//! (no shared dependency, no copied source).
11
12mod divider;
13mod lifecycle;
14mod preview;
15mod render;
16mod tree;
17
18use std::cell::Cell;
19use std::rc::Rc;
20
21use gpui::{Axis, Bounds, Entity, FocusHandle, Focusable, Subscription};
22
23pub use preview::PaneGroupPreview;
24
25use crate::{Pane, prelude::*};
26
27/// Direction a new pane is inserted relative to the currently active pane.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SplitDirection {
30 Left,
31 Right,
32 Up,
33 Down,
34}
35
36impl SplitDirection {
37 /// The layout axis a split in this direction runs along.
38 pub fn axis(self) -> Axis {
39 match self {
40 SplitDirection::Left | SplitDirection::Right => Axis::Horizontal,
41 SplitDirection::Up | SplitDirection::Down => Axis::Vertical,
42 }
43 }
44
45 /// Whether the new pane is inserted before (`Left`/`Up`) or after
46 /// (`Right`/`Down`) the active pane along [`Self::axis`].
47 fn inserts_before(self) -> bool {
48 matches!(self, SplitDirection::Left | SplitDirection::Up)
49 }
50}
51
52/// A node in [`PaneGroup`]'s recursive split tree: either a leaf pane or a
53/// nested split along some [`Axis`].
54pub enum Member {
55 Leaf(Entity<Pane>),
56 Split(PaneAxis),
57}
58
59/// A row (`Horizontal`) or column (`Vertical`) of `N` [`Member`]s, each
60/// sized by the parallel `flexes` fraction (sums to `1.0`).
61pub struct PaneAxis {
62 pub axis: Axis,
63 pub members: Vec<Member>,
64 pub flexes: Vec<f32>,
65 /// Last-measured container bounds, populated by a `canvas()` during
66 /// render and read back by the divider-drag handler to convert pointer
67 /// deltas into fraction deltas. Persists on the node across re-renders.
68 bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
69}
70
71impl PaneAxis {
72 fn new(axis: Axis, members: Vec<Member>, flexes: Vec<f32>) -> Self {
73 Self {
74 axis,
75 members,
76 flexes,
77 bounds: Rc::new(Cell::new(None)),
78 }
79 }
80}
81
82/// Returned by [`PaneGroup::close_active`] when the active pane is the only
83/// leaf left in the tree — a `PaneGroup` always keeps at least one pane.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct CannotRemoveLastPane;
86
87/// Stateful owner of a recursive pane split tree: which pane is active, how
88/// new panes' content is created, and (via its [`Render`] impl) the
89/// split/close/focus keybinding wiring and active-pane highlight.
90///
91/// Create with `cx.new(|cx| PaneGroup::new(cx, pane))`, optionally
92/// `.with_pane_factory(..)` to control what split-created panes contain.
93pub struct PaneGroup {
94 root: Member,
95 active_pane: Entity<Pane>,
96 pane_factory: Box<dyn Fn(&mut Context<Pane>) -> Pane>,
97 focus_handle: FocusHandle,
98 _subscriptions: Vec<Subscription>,
99}
100
101impl PaneGroup {
102 /// Boots a single-leaf tree containing `pane`.
103 ///
104 /// Takes `cx: &mut Context<Self>` (a refinement over phase-01's
105 /// `new(pane: Entity<Pane>)` pseudocode) — needed to allocate this
106 /// group's own [`FocusHandle`] and subscribe to `pane`'s
107 /// [`PaneEvent::Empty`].
108 pub fn new(cx: &mut Context<Self>, pane: Entity<Pane>) -> Self {
109 let subscription = Self::watch_pane(&pane, cx);
110 Self {
111 root: Member::Leaf(pane.clone()),
112 active_pane: pane,
113 pane_factory: Box::new(|_| Pane::new()),
114 focus_handle: cx.focus_handle(),
115 _subscriptions: vec![subscription],
116 }
117 }
118
119 /// Sets the factory used to construct the [`Pane`] for every future
120 /// split. Defaults to an empty [`Pane::new`].
121 pub fn with_pane_factory(
122 mut self,
123 factory: impl Fn(&mut Context<Pane>) -> Pane + 'static,
124 ) -> Self {
125 self.pane_factory = Box::new(factory);
126 self
127 }
128
129 /// The currently active (keyboard/action target) pane.
130 pub fn active_pane(&self) -> &Entity<Pane> {
131 &self.active_pane
132 }
133
134 /// Sets the active pane directly, for callers holding a specific
135 /// `Entity<Pane>` handle (e.g. programmatically laying out an initial
136 /// grid, or a "reveal this file's pane" flow) rather than a keyboard
137 /// direction. No tree-membership validation: setting a pane outside
138 /// this group's tree only makes subsequent `split`/`close_active`
139 /// target a detached pane, which is inert (not a crash risk) but almost
140 /// certainly not what the caller wants.
141 pub fn set_active_pane(&mut self, pane: Entity<Pane>, cx: &mut Context<Self>) {
142 self.active_pane = pane;
143 cx.notify();
144 }
145
146 /// Splits the active pane in `dir`: appends a sibling into the active
147 /// pane's parent axis if it already runs along `dir`'s axis (N-way
148 /// split), otherwise wraps the active pane in a new two-child axis. The
149 /// new pane becomes active.
150 pub fn split(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
151 let new_pane = cx.new(|pane_cx| (self.pane_factory)(pane_cx));
152 let subscription = Self::watch_pane(&new_pane, cx);
153 self._subscriptions.push(subscription);
154
155 let active_id = self.active_pane.entity_id();
156 let inserted = self.root.split_active(
157 active_id,
158 dir.axis(),
159 dir.inserts_before(),
160 new_pane.clone(),
161 );
162 debug_assert!(
163 inserted,
164 "PaneGroup's active pane must exist in its own tree"
165 );
166
167 self.active_pane = new_pane;
168 cx.notify();
169 }
170
171 /// Removes the active pane from the tree. Errs if it is the last pane.
172 pub fn close_active(&mut self, cx: &mut Context<Self>) -> Result<(), CannotRemoveLastPane> {
173 let target = self.active_pane.clone();
174 self.remove_pane(&target, cx)
175 }
176
177 /// Moves the active pane to its neighbor in `dir`, if one exists in the
178 /// same axis as the active pane's immediate parent split.
179 pub fn focus(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
180 let active_id = self.active_pane.entity_id();
181 let neighbor = match &self.root {
182 Member::Leaf(_) => None,
183 Member::Split(axis) => axis.find_neighbor(active_id, dir),
184 };
185 if let Some(neighbor) = neighbor {
186 self.active_pane = neighbor;
187 cx.notify();
188 }
189 }
190}
191
192impl Focusable for PaneGroup {
193 fn focus_handle(&self, _cx: &App) -> FocusHandle {
194 self.focus_handle.clone()
195 }
196}