Skip to main content

emux_mux/
layout.rs

1//! Layout engine for arranging panes within a window.
2
3use crate::pane::PaneId;
4
5/// Direction of a split.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SplitDirection {
8    /// Split horizontally: panes are stacked top/bottom.
9    Horizontal,
10    /// Split vertically: panes are side by side left/right.
11    Vertical,
12}
13
14/// A node in the binary layout tree.
15#[derive(Debug, Clone)]
16pub enum LayoutNode {
17    Leaf(PaneId),
18    Split {
19        direction: SplitDirection,
20        ratio: f32,
21        first: Box<LayoutNode>,
22        second: Box<LayoutNode>,
23    },
24}
25
26/// Absolute position and size of a pane within the tab area.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct PanePosition {
29    pub col: usize,
30    pub row: usize,
31    pub cols: usize,
32    pub rows: usize,
33}
34
35impl LayoutNode {
36    /// Collect all pane IDs in this subtree (left-to-right / top-to-bottom order).
37    pub fn pane_ids(&self) -> Vec<PaneId> {
38        match self {
39            LayoutNode::Leaf(id) => vec![*id],
40            LayoutNode::Split { first, second, .. } => {
41                let mut ids = first.pane_ids();
42                ids.extend(second.pane_ids());
43                ids
44            }
45        }
46    }
47
48    /// Count leaves.
49    pub fn count(&self) -> usize {
50        match self {
51            LayoutNode::Leaf(_) => 1,
52            LayoutNode::Split { first, second, .. } => first.count() + second.count(),
53        }
54    }
55
56    /// Split a target pane, returning true if found and split.
57    fn split(&mut self, target: PaneId, new_pane: PaneId, direction: SplitDirection) -> bool {
58        match self {
59            LayoutNode::Leaf(id) if *id == target => {
60                let old = LayoutNode::Leaf(target);
61                let new = LayoutNode::Leaf(new_pane);
62                *self = LayoutNode::Split {
63                    direction,
64                    ratio: 0.5,
65                    first: Box::new(old),
66                    second: Box::new(new),
67                };
68                true
69            }
70            LayoutNode::Leaf(_) => false,
71            LayoutNode::Split { first, second, .. } => {
72                first.split(target, new_pane, direction)
73                    || second.split(target, new_pane, direction)
74            }
75        }
76    }
77
78    /// Remove a pane, returning the collapsed node (or None if this was the leaf).
79    fn remove(&mut self, target: PaneId) -> RemoveResult {
80        match self {
81            LayoutNode::Leaf(id) if *id == target => RemoveResult::Removed,
82            LayoutNode::Leaf(_) => RemoveResult::NotFound,
83            LayoutNode::Split { first, second, .. } => {
84                let first_result = first.remove(target);
85                match first_result {
86                    RemoveResult::Removed => {
87                        // First child was the target; collapse to second.
88                        RemoveResult::Replaced(second.as_ref().clone())
89                    }
90                    RemoveResult::Replaced(replacement) => {
91                        **first = replacement;
92                        RemoveResult::NotFound // signal: handled internally
93                    }
94                    RemoveResult::NotFound => {
95                        let second_result = second.remove(target);
96                        match second_result {
97                            RemoveResult::Removed => RemoveResult::Replaced(first.as_ref().clone()),
98                            RemoveResult::Replaced(replacement) => {
99                                **second = replacement;
100                                RemoveResult::NotFound // handled internally
101                            }
102                            RemoveResult::NotFound => RemoveResult::NotFound,
103                        }
104                    }
105                }
106            }
107        }
108    }
109
110    /// Compute positions for all panes given a bounding rectangle.
111    fn compute_positions_inner(
112        &self,
113        col: usize,
114        row: usize,
115        cols: usize,
116        rows: usize,
117        out: &mut Vec<(PaneId, PanePosition)>,
118    ) {
119        match self {
120            LayoutNode::Leaf(id) => {
121                out.push((
122                    *id,
123                    PanePosition {
124                        col,
125                        row,
126                        cols,
127                        rows,
128                    },
129                ));
130            }
131            LayoutNode::Split {
132                direction,
133                ratio,
134                first,
135                second,
136            } => match direction {
137                SplitDirection::Horizontal => {
138                    let first_rows = ((rows as f32) * ratio).round() as usize;
139                    let first_rows = first_rows.max(1).min(rows.saturating_sub(1));
140                    let second_rows = rows - first_rows;
141                    first.compute_positions_inner(col, row, cols, first_rows, out);
142                    second.compute_positions_inner(col, row + first_rows, cols, second_rows, out);
143                }
144                SplitDirection::Vertical => {
145                    let first_cols = ((cols as f32) * ratio).round() as usize;
146                    let first_cols = first_cols.max(1).min(cols.saturating_sub(1));
147                    let second_cols = cols - first_cols;
148                    first.compute_positions_inner(col, row, first_cols, rows, out);
149                    second.compute_positions_inner(col + first_cols, row, second_cols, rows, out);
150                }
151            },
152        }
153    }
154
155    /// Adjust the split ratio of the nearest ancestor of `target` that splits
156    /// along `axis`. Returns true if a ratio was adjusted.
157    fn adjust_ratio(&mut self, target: PaneId, axis: SplitDirection, delta: f32) -> bool {
158        match self {
159            LayoutNode::Leaf(_) => false,
160            LayoutNode::Split {
161                direction,
162                ratio,
163                first,
164                second,
165            } => {
166                let in_first = first.pane_ids().contains(&target);
167                let in_second = second.pane_ids().contains(&target);
168                if !in_first && !in_second {
169                    return false;
170                }
171                // If this split matches the axis and the target is directly in one branch
172                if *direction == axis {
173                    if in_first {
174                        // Growing the first child means increasing the ratio
175                        let new_ratio = (*ratio + delta).clamp(0.1, 0.9);
176                        *ratio = new_ratio;
177                        return true;
178                    } else {
179                        // Target is in second child; growing second means decreasing ratio
180                        let new_ratio = (*ratio - delta).clamp(0.1, 0.9);
181                        *ratio = new_ratio;
182                        return true;
183                    }
184                }
185                // Otherwise recurse into the branch containing the target
186                if in_first {
187                    first.adjust_ratio(target, axis, delta)
188                } else {
189                    second.adjust_ratio(target, axis, delta)
190                }
191            }
192        }
193    }
194
195    /// Swap two leaf pane IDs in the tree.
196    pub fn swap_leaves(&mut self, a: PaneId, b: PaneId) -> bool {
197        // First replace a with a sentinel, then b with a, then sentinel with b
198        let sentinel = u32::MAX;
199        if !self.replace_leaf(a, sentinel) {
200            return false;
201        }
202        if !self.replace_leaf(b, a) {
203            // Undo
204            self.replace_leaf(sentinel, a);
205            return false;
206        }
207        self.replace_leaf(sentinel, b);
208        true
209    }
210
211    /// Replace a leaf's pane ID with a new ID.
212    pub fn replace_leaf(&mut self, old_id: PaneId, new_id: PaneId) -> bool {
213        match self {
214            LayoutNode::Leaf(id) if *id == old_id => {
215                *id = new_id;
216                true
217            }
218            LayoutNode::Leaf(_) => false,
219            LayoutNode::Split { first, second, .. } => {
220                first.replace_leaf(old_id, new_id) || second.replace_leaf(old_id, new_id)
221            }
222        }
223    }
224
225    /// Find the pane position for a specific pane given bounds.
226    pub fn find_position(
227        &self,
228        target: PaneId,
229        col: usize,
230        row: usize,
231        cols: usize,
232        rows: usize,
233    ) -> Option<PanePosition> {
234        let mut positions = Vec::new();
235        self.compute_positions_inner(col, row, cols, rows, &mut positions);
236        positions
237            .into_iter()
238            .find(|(id, _)| *id == target)
239            .map(|(_, pos)| pos)
240    }
241}
242
243enum RemoveResult {
244    NotFound,
245    Removed,
246    Replaced(LayoutNode),
247}
248
249/// The layout engine manages the binary tree of pane arrangements.
250#[derive(Debug)]
251pub struct LayoutEngine {
252    root: Option<LayoutNode>,
253}
254
255impl LayoutEngine {
256    /// Create an empty layout.
257    pub fn new() -> Self {
258        Self { root: None }
259    }
260
261    /// Add a pane as a leaf. If the layout is empty, this becomes the root.
262    /// If non-empty, splits the last pane vertically (fallback behavior).
263    pub fn add_pane(&mut self, pane_id: PaneId) {
264        match &self.root {
265            None => {
266                self.root = Some(LayoutNode::Leaf(pane_id));
267            }
268            Some(_) => {
269                // Find last pane and split it vertically
270                let ids = self.pane_ids();
271                if let Some(&last) = ids.last() {
272                    self.split(last, pane_id, SplitDirection::Vertical);
273                }
274            }
275        }
276    }
277
278    /// Split a target pane, creating a new split node.
279    pub fn split(
280        &mut self,
281        target_pane_id: PaneId,
282        new_pane_id: PaneId,
283        direction: SplitDirection,
284    ) -> bool {
285        if let Some(ref mut root) = self.root {
286            root.split(target_pane_id, new_pane_id, direction)
287        } else {
288            false
289        }
290    }
291
292    /// Remove a pane from the layout, collapsing the tree.
293    pub fn remove_pane(&mut self, pane_id: PaneId) -> bool {
294        if let Some(ref mut root) = self.root {
295            match root.remove(pane_id) {
296                RemoveResult::Removed => {
297                    self.root = None;
298                    true
299                }
300                RemoveResult::Replaced(new_root) => {
301                    self.root = Some(new_root);
302                    true
303                }
304                RemoveResult::NotFound => {
305                    // Check if it was handled internally (nested removal)
306                    // The pane might have been removed from a deeper level
307                    !self.pane_ids().contains(&pane_id)
308                }
309            }
310        } else {
311            false
312        }
313    }
314
315    /// List all pane IDs in the layout tree.
316    pub fn pane_ids(&self) -> Vec<PaneId> {
317        match &self.root {
318            Some(root) => root.pane_ids(),
319            None => Vec::new(),
320        }
321    }
322
323    /// Number of panes in the layout.
324    pub fn count(&self) -> usize {
325        match &self.root {
326            Some(root) => root.count(),
327            None => 0,
328        }
329    }
330
331    /// Compute the absolute position of every pane given the total tab area.
332    pub fn compute_positions(
333        &self,
334        total_cols: usize,
335        total_rows: usize,
336    ) -> Vec<(PaneId, PanePosition)> {
337        let mut out = Vec::new();
338        if let Some(ref root) = self.root {
339            root.compute_positions_inner(0, 0, total_cols, total_rows, &mut out);
340        }
341        out
342    }
343
344    /// Adjust the split ratio for the nearest ancestor of `target` on the given axis.
345    pub fn adjust_ratio(&mut self, target: PaneId, axis: SplitDirection, delta: f32) -> bool {
346        if let Some(ref mut root) = self.root {
347            root.adjust_ratio(target, axis, delta)
348        } else {
349            false
350        }
351    }
352
353    /// Swap two pane IDs in the layout tree.
354    pub fn swap_leaves(&mut self, a: PaneId, b: PaneId) -> bool {
355        if let Some(ref mut root) = self.root {
356            root.swap_leaves(a, b)
357        } else {
358            false
359        }
360    }
361
362    /// Get the root node (for inspection).
363    pub fn root(&self) -> Option<&LayoutNode> {
364        self.root.as_ref()
365    }
366
367    /// Replace the entire layout tree with a new root.
368    /// This is used by swap layouts to apply a template.
369    pub fn set_root(&mut self, root: LayoutNode) {
370        self.root = Some(root);
371    }
372
373    /// Apply a layout template to the current set of pane IDs.
374    /// The template tree's leaf IDs are replaced with the actual pane IDs
375    /// in left-to-right order. If the template has fewer leaves than panes,
376    /// extra panes are split off the last leaf. If more leaves, extra leaves
377    /// are trimmed.
378    pub fn apply_template(&mut self, template: &LayoutNode, pane_ids: &[PaneId]) {
379        if pane_ids.is_empty() {
380            return;
381        }
382        let mut new_tree = template.clone();
383        let template_ids = new_tree.pane_ids();
384        // Replace template leaf IDs with real pane IDs
385        for (i, &real_id) in pane_ids.iter().enumerate() {
386            if i < template_ids.len() {
387                new_tree.replace_leaf(template_ids[i], real_id);
388            }
389        }
390        // If we have more panes than template slots, split extra onto the last leaf
391        if pane_ids.len() > template_ids.len() {
392            let mut last_id = pane_ids[template_ids.len() - 1];
393            for &extra_id in &pane_ids[template_ids.len()..] {
394                new_tree.split(last_id, extra_id, SplitDirection::Vertical);
395                last_id = extra_id;
396            }
397        }
398        // If template has more slots than panes, prune extra leaves
399        if template_ids.len() > pane_ids.len() {
400            for &extra_template_id in &template_ids[pane_ids.len()..] {
401                new_tree.remove(extra_template_id);
402            }
403        }
404        self.root = Some(new_tree);
405    }
406}
407
408impl Default for LayoutEngine {
409    fn default() -> Self {
410        Self::new()
411    }
412}