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}