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
//! Pane layout actions: arrow resize (PN3), equalize (A3), layout presets
//! (A4), and split left / up (A5), for local and par-mux tabs.
//!
//! Local tabs change their own tree. Attached tabs send the daemon command
//! and let its `%layout-change` drive the mirror, the pattern split, swap
//! and close already use: a local edit would be overwritten by the next
//! layout push, and a second client would never see it (M7).
use crate::app::window_state::WindowState;
use crate::pane::{LayoutPreset, NavigationDirection, ResizeStep, SplitDirection};
impl WindowState {
/// Move the divider nearest the focused pane in the arrow's direction
/// by `pane_resize_step` percent of its split (PN3), never past
/// `pane_min_size` (PN4).
pub fn resize_pane(&mut self, direction: NavigationDirection) {
let step = self.config.load().panes.pane_resize_step.clamp(0.5, 50.0) / 100.0;
self.resize_pane_by(direction, ResizeStep::Fraction(step));
}
/// [`Self::resize_pane`] with an explicit step (resize mode's
/// Shift+arrow moves one cell, A6).
///
/// Attached tabs run the same resize on the mirror and send the result
/// as absolute pane sizes (the divider-drag path), which the daemon
/// echoes back as `%layout-change` (M7). The daemon's relative
/// `resize-pane -L/-R` grows the TARGET pane whichever side of the
/// divider it sits on and reaches only its direct parent split, so it
/// cannot express "move the divider in the arrow's direction" for a
/// right-hand pane or a nested layout.
pub(crate) fn resize_pane_by(&mut self, direction: NavigationDirection, step: ResizeStep) {
let moved = self
.tab_manager
.active_tab_mut()
.and_then(|tab| tab.pane_manager_mut())
.and_then(|pm| {
let focused = pm.focused_pane_id()?;
Some(pm.resize_toward_by(focused, direction, step))
});
if moved == Some(true) {
let horizontal_divider = matches!(
direction,
NavigationDirection::Up | NavigationDirection::Down
);
self.sync_pane_resize_to_tmux(horizontal_divider);
self.after_pane_layout_change();
}
}
/// Give every pane in the tab an equal area (A3).
pub(crate) fn equalize_panes(&mut self) {
if self.mux_relayout(LayoutPreset::EvenHorizontal, true) {
return;
}
if self.refuse_in_tmux_gateway("Equalize") {
return;
}
let kept = self
.tab_manager
.active_tab_mut()
.and_then(|tab| tab.pane_manager_mut())
.map(|pm| pm.equalize());
if kept == Some(false) {
self.show_toast("Equalize refused: a pane would be below the minimum size");
}
self.after_pane_layout_change();
}
/// Equalize the split a double-clicked divider belongs to (PN8).
pub(crate) fn equalize_divider(&mut self, divider_index: usize) {
if let Some(pm) = self
.tab_manager
.active_tab_mut()
.and_then(|tab| tab.pane_manager_mut())
{
pm.equalize_divider(divider_index);
}
// The divider-drag sync: a no-op for local tabs, absolute sizes to
// the tmux gateway or par-mux daemon otherwise.
let horizontal = self
.tab_manager
.active_tab()
.and_then(|t| t.get_divider(divider_index))
.map(|d| d.is_horizontal);
if let Some(horizontal) = horizontal {
self.sync_pane_resize_to_tmux(horizontal);
}
self.after_pane_layout_change();
}
/// Rearrange the tab's panes into a preset (A4).
pub(crate) fn apply_layout_preset(&mut self, preset: LayoutPreset) {
if self.mux_relayout(preset, false) {
return;
}
if self.refuse_in_tmux_gateway("Layout presets") {
return;
}
let kept = self
.tab_manager
.active_tab_mut()
.and_then(|tab| tab.pane_manager_mut())
.map(|pm| pm.apply_preset(preset));
if kept == Some(false) {
self.show_toast(format!(
"Layout {} refused: a pane would be below the minimum size",
preset.name()
));
}
self.after_pane_layout_change();
}
/// Step to the next preset in [`LayoutPreset::ALL`] (A4 `cycle_layout`).
pub(crate) fn cycle_layout_preset(&mut self) {
let current = self
.tab_manager
.active_tab()
.and_then(|t| t.pane_manager())
.and_then(|pm| pm.layout_preset());
let next = match current {
Some(p) => {
let i = LayoutPreset::ALL.iter().position(|q| *q == p).unwrap_or(0);
LayoutPreset::ALL[(i + 1) % LayoutPreset::ALL.len()]
}
None => LayoutPreset::ALL[0],
};
self.apply_layout_preset(next);
}
/// Split with the new pane left of (`Vertical`) or above
/// (`Horizontal`) the focused one (A5).
pub(crate) fn split_pane_before(&mut self, direction: SplitDirection) {
#[cfg(feature = "mux")]
if self.split_pane_via_mux_placed(direction == SplitDirection::Vertical, true) {
return;
}
if self.refuse_in_tmux_gateway("Split left/up") {
return;
}
self.user_split(direction, true);
}
/// Attached tabs: the daemon has no `select-layout`, so a preset or
/// equalize is expressed as absolute pane sizes computed from the local
/// mirror, sent with `resize-pane -x/-y`. The structural presets
/// (`main-*`, `tiled`, a different chain axis) would need panes moved
/// daemon-side; those are refused with a toast. Returns whether the
/// request was consumed (an attached tab).
fn mux_relayout(&mut self, preset: LayoutPreset, equalize_only: bool) -> bool {
#[cfg(feature = "mux")]
if self.focused_mux_pane_from_native().is_some() {
if !equalize_only {
self.show_toast(format!(
"Layout {}: not available in par-mux tabs yet (equalize is)",
preset.name()
));
return true;
}
let equalized = self
.tab_manager
.active_tab_mut()
.and_then(|tab| tab.pane_manager_mut())
.is_some_and(|pm| pm.equalize());
if equalized {
// Both axes: equalize can change widths and heights.
self.sync_pane_resize_to_tmux(false);
self.sync_pane_resize_to_tmux(true);
}
self.after_pane_layout_change();
return true;
}
let _ = (preset, equalize_only);
false
}
}