1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//! Layout management operations for PaneManager
//!
//! Handles bounds calculation, terminal resizing, divider management,
//! split ratio adjustment, and drag-to-resize functionality.
use super::PaneManager;
use crate::pane::tmux_helpers::DividerUpdateContext;
use crate::pane::types::{DividerRect, PaneBounds, PaneNode, SplitDirection, split_child_bounds};
impl PaneManager {
/// Set the total bounds available for panes and recalculate layout
pub fn set_bounds(&mut self, bounds: PaneBounds) {
self.total_bounds = bounds;
self.recalculate_bounds();
}
/// Recalculate bounds for all panes
pub fn recalculate_bounds(&mut self) {
if let Some(ref mut root) = self.root {
root.calculate_bounds(self.total_bounds, self.divider_width);
}
self.apply_zoom_bounds();
}
/// Resize all pane terminals to match their current bounds
///
/// This should be called after bounds are updated (split, resize, window resize)
/// to ensure each PTY is sized correctly for its pane area.
pub fn resize_all_terminals(&self, cell_width: f32, cell_height: f32) {
self.resize_all_terminals_with_padding(cell_width, cell_height, 0.0, 0.0);
}
/// Resize all terminal PTYs to match their pane bounds, accounting for padding.
///
/// The padding reduces the content area where text is rendered, so terminals
/// should be sized for the padded (smaller) area to avoid content being cut off.
///
/// `height_offset` is an additional height reduction (e.g., pane title bar height)
/// subtracted once from each pane's content height.
pub fn resize_all_terminals_with_padding(
&self,
cell_width: f32,
cell_height: f32,
padding: f32,
height_offset: f32,
) {
if let Some(ref root) = self.root {
for pane in root.all_panes() {
// Calculate content size (bounds minus padding on each side, minus title bar)
let content_width = (pane.bounds.width - padding * 2.0).max(cell_width);
let content_height =
(pane.bounds.height - padding * 2.0 - height_offset).max(cell_height);
let cols = (content_width / cell_width).floor() as usize;
let rows = (content_height / cell_height).floor() as usize;
pane.resize_terminal_with_cell_dims(
cols.max(1),
rows.max(1),
cell_width as u32,
cell_height as u32,
);
}
}
}
/// Set the divider width
pub fn set_divider_width(&mut self, width: f32) {
self.divider_width = width;
self.recalculate_bounds();
}
/// Get the divider width
pub fn divider_width(&self) -> f32 {
self.divider_width
}
/// Get the hit detection padding (extra area around divider for easier grabbing)
pub fn divider_hit_padding(&self) -> f32 {
(self.divider_hit_width - self.divider_width).max(0.0) / 2.0
}
/// Get all divider rectangles in the pane tree
pub fn get_dividers(&self) -> Vec<DividerRect> {
// A zoomed pane covers every divider: none is drawn or draggable.
if self.zoomed_pane_id.is_some() {
return Vec::new();
}
self.root
.as_ref()
.map(|r| r.collect_dividers(self.total_bounds, self.divider_width))
.unwrap_or_default()
}
/// Find a divider at the given position
///
/// Returns the index of the divider if found, with optional padding for easier grabbing
pub fn find_divider_at(&self, x: f32, y: f32, padding: f32) -> Option<usize> {
let dividers = self.get_dividers();
for (i, divider) in dividers.iter().enumerate() {
if divider.contains(x, y, padding) {
return Some(i);
}
}
None
}
/// Check if a position is on a divider
pub fn is_on_divider(&self, x: f32, y: f32) -> bool {
let padding = (self.divider_hit_width - self.divider_width).max(0.0) / 2.0;
self.find_divider_at(x, y, padding).is_some()
}
/// Set the divider hit width
pub fn set_divider_hit_width(&mut self, width: f32) {
self.divider_hit_width = width;
}
/// Get the divider at an index
pub fn get_divider(&self, index: usize) -> Option<DividerRect> {
self.get_dividers().get(index).copied()
}
/// Resize by dragging a divider to a new position, never past the
/// minimum pane size (PN4).
///
/// `divider_index`: Which divider is being dragged
/// `new_position`: New mouse position (x for vertical, y for horizontal dividers)
pub fn drag_divider(&mut self, divider_index: usize, new_x: f32, new_y: f32) {
if self.zoomed_pane_id.is_some() {
return;
}
let Some(mut root) = self.root.take() else {
return;
};
let mut divider_count = 0;
let ctx = DividerUpdateContext {
target_index: divider_index,
new_x,
new_y,
bounds: self.total_bounds,
divider_width: self.divider_width,
};
self.update_divider_ratio(&mut root, &mut divider_count, &ctx);
self.root = Some(root);
self.recalculate_bounds();
}
/// Recursively find and update the split ratio for a divider
fn update_divider_ratio(
&self,
node: &mut PaneNode,
current_index: &mut usize,
ctx: &DividerUpdateContext,
) -> bool {
match node {
PaneNode::Leaf(_) => false,
PaneNode::Split {
direction,
ratio,
first,
second,
} => {
if *current_index == ctx.target_index {
let wanted = match direction {
SplitDirection::Horizontal => {
(ctx.new_y - ctx.bounds.y) / ctx.bounds.height
}
SplitDirection::Vertical => (ctx.new_x - ctx.bounds.x) / ctx.bounds.width,
};
*ratio = self
.clamp_drag_ratio(*direction, ctx.bounds, first, second, wanted, *ratio);
return true;
}
*current_index += 1;
let (first_bounds, second_bounds) =
split_child_bounds(*direction, *ratio, ctx.bounds, ctx.divider_width);
let first_ctx = DividerUpdateContext {
bounds: first_bounds,
..*ctx
};
if self.update_divider_ratio(first, current_index, &first_ctx) {
return true;
}
let second_ctx = DividerUpdateContext {
bounds: second_bounds,
..*ctx
};
self.update_divider_ratio(second, current_index, &second_ctx)
}
}
}
}