Skip to main content

par_term/pane/manager/
layout.rs

1//! Layout management operations for PaneManager
2//!
3//! Handles bounds calculation, terminal resizing, divider management,
4//! split ratio adjustment, and drag-to-resize functionality.
5
6use super::PaneManager;
7use crate::pane::tmux_helpers::DividerUpdateContext;
8use crate::pane::types::{DividerRect, PaneBounds, PaneId, PaneNode, SplitDirection};
9
10impl PaneManager {
11    /// Set the total bounds available for panes and recalculate layout
12    pub fn set_bounds(&mut self, bounds: PaneBounds) {
13        self.total_bounds = bounds;
14        self.recalculate_bounds();
15    }
16
17    /// Recalculate bounds for all panes
18    pub fn recalculate_bounds(&mut self) {
19        if let Some(ref mut root) = self.root {
20            root.calculate_bounds(self.total_bounds, self.divider_width);
21        }
22    }
23
24    /// Resize all pane terminals to match their current bounds
25    ///
26    /// This should be called after bounds are updated (split, resize, window resize)
27    /// to ensure each PTY is sized correctly for its pane area.
28    pub fn resize_all_terminals(&self, cell_width: f32, cell_height: f32) {
29        self.resize_all_terminals_with_padding(cell_width, cell_height, 0.0, 0.0);
30    }
31
32    /// Resize all terminal PTYs to match their pane bounds, accounting for padding.
33    ///
34    /// The padding reduces the content area where text is rendered, so terminals
35    /// should be sized for the padded (smaller) area to avoid content being cut off.
36    ///
37    /// `height_offset` is an additional height reduction (e.g., pane title bar height)
38    /// subtracted once from each pane's content height.
39    pub fn resize_all_terminals_with_padding(
40        &self,
41        cell_width: f32,
42        cell_height: f32,
43        padding: f32,
44        height_offset: f32,
45    ) {
46        if let Some(ref root) = self.root {
47            for pane in root.all_panes() {
48                // Calculate content size (bounds minus padding on each side, minus title bar)
49                let content_width = (pane.bounds.width - padding * 2.0).max(cell_width);
50                let content_height =
51                    (pane.bounds.height - padding * 2.0 - height_offset).max(cell_height);
52
53                let cols = (content_width / cell_width).floor() as usize;
54                let rows = (content_height / cell_height).floor() as usize;
55
56                pane.resize_terminal_with_cell_dims(
57                    cols.max(1),
58                    rows.max(1),
59                    cell_width as u32,
60                    cell_height as u32,
61                );
62            }
63        }
64    }
65
66    /// Set the divider width
67    pub fn set_divider_width(&mut self, width: f32) {
68        self.divider_width = width;
69        self.recalculate_bounds();
70    }
71
72    /// Get the divider width
73    pub fn divider_width(&self) -> f32 {
74        self.divider_width
75    }
76
77    /// Get the hit detection padding (extra area around divider for easier grabbing)
78    pub fn divider_hit_padding(&self) -> f32 {
79        (self.divider_hit_width - self.divider_width).max(0.0) / 2.0
80    }
81
82    /// Resize a split by adjusting its ratio
83    ///
84    /// `pane_id`: The pane whose adjacent split should be resized
85    /// `delta`: Amount to adjust the ratio (-1.0 to 1.0)
86    pub fn resize_split(&mut self, pane_id: PaneId, delta: f32) {
87        if let Some(ref mut root) = self.root {
88            Self::adjust_split_ratio(root, pane_id, delta);
89            self.recalculate_bounds();
90        }
91    }
92
93    /// Recursively find and adjust the split ratio for a pane
94    pub(super) fn adjust_split_ratio(node: &mut PaneNode, target_id: PaneId, delta: f32) -> bool {
95        match node {
96            PaneNode::Leaf(_) => false,
97            PaneNode::Split {
98                ratio,
99                first,
100                second,
101                ..
102            } => {
103                // Check if target is in first child
104                if first.all_pane_ids().contains(&target_id) {
105                    // Try to find in nested splits first
106                    if Self::adjust_split_ratio(first, target_id, delta) {
107                        return true;
108                    }
109                    // Adjust this split's ratio (making first child larger/smaller)
110                    *ratio = (*ratio + delta).clamp(0.1, 0.9);
111                    return true;
112                }
113
114                // Check if target is in second child
115                if second.all_pane_ids().contains(&target_id) {
116                    // Try to find in nested splits first
117                    if Self::adjust_split_ratio(second, target_id, delta) {
118                        return true;
119                    }
120                    // Adjust this split's ratio (making second child larger/smaller)
121                    *ratio = (*ratio - delta).clamp(0.1, 0.9);
122                    return true;
123                }
124
125                false
126            }
127        }
128    }
129
130    /// Get all divider rectangles in the pane tree
131    pub fn get_dividers(&self) -> Vec<DividerRect> {
132        self.root
133            .as_ref()
134            .map(|r| r.collect_dividers(self.total_bounds, self.divider_width))
135            .unwrap_or_default()
136    }
137
138    /// Find a divider at the given position
139    ///
140    /// Returns the index of the divider if found, with optional padding for easier grabbing
141    pub fn find_divider_at(&self, x: f32, y: f32, padding: f32) -> Option<usize> {
142        let dividers = self.get_dividers();
143        for (i, divider) in dividers.iter().enumerate() {
144            if divider.contains(x, y, padding) {
145                return Some(i);
146            }
147        }
148        None
149    }
150
151    /// Check if a position is on a divider
152    pub fn is_on_divider(&self, x: f32, y: f32) -> bool {
153        let padding = (self.divider_hit_width - self.divider_width).max(0.0) / 2.0;
154        self.find_divider_at(x, y, padding).is_some()
155    }
156
157    /// Set the divider hit width
158    pub fn set_divider_hit_width(&mut self, width: f32) {
159        self.divider_hit_width = width;
160    }
161
162    /// Get the divider at an index
163    pub fn get_divider(&self, index: usize) -> Option<DividerRect> {
164        self.get_dividers().get(index).copied()
165    }
166
167    /// Resize by dragging a divider to a new position
168    ///
169    /// `divider_index`: Which divider is being dragged
170    /// `new_position`: New mouse position (x for vertical, y for horizontal dividers)
171    pub fn drag_divider(&mut self, divider_index: usize, new_x: f32, new_y: f32) {
172        // Get the divider info first
173        let dividers = self.get_dividers();
174        if dividers.get(divider_index).is_some() {
175            // Find the split node that owns this divider and update its ratio
176            if let Some(ref mut root) = self.root {
177                let mut divider_count = 0;
178                let ctx = DividerUpdateContext {
179                    target_index: divider_index,
180                    new_x,
181                    new_y,
182                    bounds: self.total_bounds,
183                    divider_width: self.divider_width,
184                };
185                Self::update_divider_ratio(root, &mut divider_count, &ctx);
186                self.recalculate_bounds();
187            }
188        }
189    }
190
191    /// Recursively find and update the split ratio for a divider
192    pub(super) fn update_divider_ratio(
193        node: &mut PaneNode,
194        current_index: &mut usize,
195        ctx: &DividerUpdateContext,
196    ) -> bool {
197        match node {
198            PaneNode::Leaf(_) => false,
199            PaneNode::Split {
200                direction,
201                ratio,
202                first,
203                second,
204            } => {
205                // Check if this is the target divider
206                if *current_index == ctx.target_index {
207                    // Calculate new ratio based on mouse position
208                    let new_ratio = match direction {
209                        SplitDirection::Horizontal => {
210                            // Horizontal split: mouse Y position determines ratio
211                            ((ctx.new_y - ctx.bounds.y) / ctx.bounds.height).clamp(0.1, 0.9)
212                        }
213                        SplitDirection::Vertical => {
214                            // Vertical split: mouse X position determines ratio
215                            ((ctx.new_x - ctx.bounds.x) / ctx.bounds.width).clamp(0.1, 0.9)
216                        }
217                    };
218                    *ratio = new_ratio;
219                    return true;
220                }
221                *current_index += 1;
222
223                // Calculate child bounds to recurse
224                let (first_bounds, second_bounds) = match direction {
225                    SplitDirection::Horizontal => {
226                        let first_height = (ctx.bounds.height - ctx.divider_width) * *ratio;
227                        let second_height = ctx.bounds.height - first_height - ctx.divider_width;
228                        (
229                            PaneBounds::new(
230                                ctx.bounds.x,
231                                ctx.bounds.y,
232                                ctx.bounds.width,
233                                first_height,
234                            ),
235                            PaneBounds::new(
236                                ctx.bounds.x,
237                                ctx.bounds.y + first_height + ctx.divider_width,
238                                ctx.bounds.width,
239                                second_height,
240                            ),
241                        )
242                    }
243                    SplitDirection::Vertical => {
244                        let first_width = (ctx.bounds.width - ctx.divider_width) * *ratio;
245                        let second_width = ctx.bounds.width - first_width - ctx.divider_width;
246                        (
247                            PaneBounds::new(
248                                ctx.bounds.x,
249                                ctx.bounds.y,
250                                first_width,
251                                ctx.bounds.height,
252                            ),
253                            PaneBounds::new(
254                                ctx.bounds.x + first_width + ctx.divider_width,
255                                ctx.bounds.y,
256                                second_width,
257                                ctx.bounds.height,
258                            ),
259                        )
260                    }
261                };
262
263                // Try children
264                let first_ctx = DividerUpdateContext {
265                    bounds: first_bounds,
266                    ..*ctx
267                };
268                if Self::update_divider_ratio(first, current_index, &first_ctx) {
269                    return true;
270                }
271                let second_ctx = DividerUpdateContext {
272                    bounds: second_bounds,
273                    ..*ctx
274                };
275                Self::update_divider_ratio(second, current_index, &second_ctx)
276            }
277        }
278    }
279}