Skip to main content

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        self.sync_focus(cx);
144        cx.notify();
145    }
146
147    /// Splits the active pane in `dir`: appends a sibling into the active
148    /// pane's parent axis if it already runs along `dir`'s axis (N-way
149    /// split), otherwise wraps the active pane in a new two-child axis. The
150    /// new pane becomes active.
151    pub fn split(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
152        let new_pane = cx.new(|pane_cx| (self.pane_factory)(pane_cx));
153        let subscription = Self::watch_pane(&new_pane, cx);
154        self._subscriptions.push(subscription);
155
156        let active_id = self.active_pane.entity_id();
157        let inserted = self.root.split_active(
158            active_id,
159            dir.axis(),
160            dir.inserts_before(),
161            new_pane.clone(),
162        );
163        debug_assert!(
164            inserted,
165            "PaneGroup's active pane must exist in its own tree"
166        );
167
168        self.active_pane = new_pane;
169        self.sync_focus(cx);
170        cx.notify();
171    }
172
173    /// Marks the active pane focused and every other leaf pane unfocused, so
174    /// only the active pane's active tab renders "selected".
175    pub(super) fn sync_focus(&mut self, cx: &mut Context<Self>) {
176        let active_id = self.active_pane.entity_id();
177        let mut leaves = Vec::new();
178        self.root.collect_leaves(&mut leaves);
179        for pane in leaves {
180            let focused = pane.entity_id() == active_id;
181            pane.update(cx, |pane, cx| pane.set_focused(focused, cx));
182        }
183    }
184
185    /// Removes the active pane from the tree. Errs if it is the last pane.
186    pub fn close_active(&mut self, cx: &mut Context<Self>) -> Result<(), CannotRemoveLastPane> {
187        let target = self.active_pane.clone();
188        self.remove_pane(&target, cx)
189    }
190
191    /// Moves the active pane to its neighbor in `dir`, if one exists in the
192    /// same axis as the active pane's immediate parent split.
193    pub fn focus(&mut self, dir: SplitDirection, cx: &mut Context<Self>) {
194        let active_id = self.active_pane.entity_id();
195        let neighbor = match &self.root {
196            Member::Leaf(_) => None,
197            Member::Split(axis) => axis.find_neighbor(active_id, dir),
198        };
199        if let Some(neighbor) = neighbor {
200            self.active_pane = neighbor;
201            self.sync_focus(cx);
202            cx.notify();
203        }
204    }
205}
206
207impl Focusable for PaneGroup {
208    fn focus_handle(&self, _cx: &App) -> FocusHandle {
209        self.focus_handle.clone()
210    }
211}