1use std::cmp::Ordering;
2
3use serde::{Deserialize, Serialize};
4
5use crate::PaneId;
6
7const MIN_SPLIT_RATIO: u16 = 150;
8const MAX_SPLIT_RATIO: u16 = 850;
9const ROOT_LAYOUT_SIZE: f32 = 1000.0;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum SplitAxis {
14 Horizontal,
15 Vertical,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Direction {
21 Left,
22 Right,
23 Up,
24 Down,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(tag = "kind", rename_all = "snake_case")]
29pub enum LayoutNode {
30 Leaf {
31 pane_id: PaneId,
32 },
33 Split {
34 axis: SplitAxis,
35 ratio: u16,
36 first: Box<LayoutNode>,
37 second: Box<LayoutNode>,
38 },
39}
40
41impl LayoutNode {
42 pub fn is_leaf(&self) -> bool {
43 matches!(self, Self::Leaf { .. })
44 }
45
46 pub fn leaf(pane_id: PaneId) -> Self {
47 Self::Leaf { pane_id }
48 }
49
50 pub fn split_leaf(
51 &mut self,
52 target: PaneId,
53 axis: SplitAxis,
54 new_pane: PaneId,
55 ratio: u16,
56 ) -> bool {
57 let direction = match axis {
58 SplitAxis::Horizontal => Direction::Right,
59 SplitAxis::Vertical => Direction::Down,
60 };
61 self.split_leaf_with_direction(target, direction, new_pane, ratio)
62 }
63
64 pub fn split_leaf_with_direction(
65 &mut self,
66 target: PaneId,
67 direction: Direction,
68 new_pane: PaneId,
69 ratio: u16,
70 ) -> bool {
71 let (axis, new_pane_first) = match direction {
72 Direction::Left => (SplitAxis::Horizontal, true),
73 Direction::Right => (SplitAxis::Horizontal, false),
74 Direction::Up => (SplitAxis::Vertical, true),
75 Direction::Down => (SplitAxis::Vertical, false),
76 };
77 match self {
78 Self::Leaf { pane_id } if *pane_id == target => {
79 let existing = *pane_id;
80 let (first, second) = if new_pane_first {
81 (Self::leaf(new_pane), Self::leaf(existing))
82 } else {
83 (Self::leaf(existing), Self::leaf(new_pane))
84 };
85 *self = Self::Split {
86 axis,
87 ratio: clamp_ratio(ratio),
88 first: Box::new(first),
89 second: Box::new(second),
90 };
91 true
92 }
93 Self::Leaf { .. } => false,
94 Self::Split { first, second, .. } => {
95 first.split_leaf_with_direction(target, direction, new_pane, ratio)
96 || second.split_leaf_with_direction(target, direction, new_pane, ratio)
97 }
98 }
99 }
100
101 pub fn remove_leaf(&mut self, target: PaneId) -> bool {
102 match self {
103 Self::Leaf { pane_id } if *pane_id == target => false,
104 Self::Leaf { .. } => false,
105 Self::Split { first, second, .. } => {
106 if let Self::Leaf { pane_id } = first.as_ref()
107 && *pane_id == target
108 {
109 *self = *second.clone();
110 return true;
111 }
112 if let Self::Leaf { pane_id } = second.as_ref()
113 && *pane_id == target
114 {
115 *self = *first.clone();
116 return true;
117 }
118 first.remove_leaf(target) || second.remove_leaf(target)
119 }
120 }
121 }
122
123 pub fn contains(&self, target: PaneId) -> bool {
124 match self {
125 Self::Leaf { pane_id } => *pane_id == target,
126 Self::Split { first, second, .. } => first.contains(target) || second.contains(target),
127 }
128 }
129
130 pub fn leaves(&self) -> Vec<PaneId> {
131 match self {
132 Self::Leaf { pane_id } => vec![*pane_id],
133 Self::Split { first, second, .. } => {
134 let mut leaves = first.leaves();
135 leaves.extend(second.leaves());
136 leaves
137 }
138 }
139 }
140
141 pub fn focus_neighbor(&self, target: PaneId, direction: Direction) -> Option<PaneId> {
142 let leaves = self.collect_leaf_rects();
143 let (_, target_rect) = leaves
144 .iter()
145 .find(|(pane_id, _)| *pane_id == target)
146 .copied()?;
147
148 leaves
149 .into_iter()
150 .filter(|(pane_id, _)| *pane_id != target)
151 .filter_map(|(pane_id, rect)| {
152 rect.directional_score(target_rect, direction)
153 .map(|score| (pane_id, score))
154 })
155 .min_by(|(_, left), (_, right)| left.partial_cmp(right).unwrap_or(Ordering::Equal))
156 .map(|(pane_id, _)| pane_id)
157 }
158
159 pub fn resize_leaf(&mut self, target: PaneId, direction: Direction, amount: i32) -> bool {
160 self.resize_leaf_inner(target, direction, amount.unsigned_abs() as u16)
161 .is_some()
162 }
163
164 pub fn set_ratio_at_path(&mut self, path: &[bool], ratio: u16) -> bool {
165 let ratio = clamp_ratio(ratio);
166 if path.is_empty() {
167 if let Self::Split {
168 ratio: current_ratio,
169 ..
170 } = self
171 {
172 *current_ratio = ratio;
173 return true;
174 }
175 return false;
176 }
177
178 match self {
179 Self::Split { first, second, .. } => {
180 let (head, tail) = path.split_first().expect("path is not empty");
181 if *head {
182 second.set_ratio_at_path(tail, ratio)
183 } else {
184 first.set_ratio_at_path(tail, ratio)
185 }
186 }
187 Self::Leaf { .. } => false,
188 }
189 }
190
191 fn resize_leaf_inner(
192 &mut self,
193 target: PaneId,
194 direction: Direction,
195 amount: u16,
196 ) -> Option<bool> {
197 match self {
198 Self::Leaf { pane_id } => (*pane_id == target).then_some(false),
199 Self::Split {
200 axis,
201 ratio,
202 first,
203 second,
204 } => {
205 let found_in_first = first.contains(target);
206 let child_result = if found_in_first {
207 first.resize_leaf_inner(target, direction, amount)
208 } else {
209 second.resize_leaf_inner(target, direction, amount)
210 };
211
212 match child_result {
213 Some(true) => Some(true),
214 Some(false) => {
215 let delta = split_resize_delta(*axis, direction, found_in_first, amount)?;
216 *ratio = apply_ratio_delta(*ratio, delta);
217 Some(true)
218 }
219 None => None,
220 }
221 }
222 }
223 }
224
225 fn collect_leaf_rects(&self) -> Vec<(PaneId, LayoutRect)> {
226 let mut leaves = Vec::new();
227 self.collect_leaf_rects_into(
228 LayoutRect {
229 x: 0.0,
230 y: 0.0,
231 width: ROOT_LAYOUT_SIZE,
232 height: ROOT_LAYOUT_SIZE,
233 },
234 &mut leaves,
235 );
236 leaves
237 }
238
239 fn collect_leaf_rects_into(&self, rect: LayoutRect, out: &mut Vec<(PaneId, LayoutRect)>) {
240 match self {
241 Self::Leaf { pane_id } => out.push((*pane_id, rect)),
242 Self::Split {
243 axis,
244 ratio,
245 first,
246 second,
247 } => {
248 let ratio = f32::from(*ratio) / 1000.0;
249 match axis {
250 SplitAxis::Horizontal => {
251 let first_width = rect.width * ratio;
252 first.collect_leaf_rects_into(
253 LayoutRect {
254 width: first_width,
255 ..rect
256 },
257 out,
258 );
259 second.collect_leaf_rects_into(
260 LayoutRect {
261 x: rect.x + first_width,
262 width: rect.width - first_width,
263 ..rect
264 },
265 out,
266 );
267 }
268 SplitAxis::Vertical => {
269 let first_height = rect.height * ratio;
270 first.collect_leaf_rects_into(
271 LayoutRect {
272 height: first_height,
273 ..rect
274 },
275 out,
276 );
277 second.collect_leaf_rects_into(
278 LayoutRect {
279 y: rect.y + first_height,
280 height: rect.height - first_height,
281 ..rect
282 },
283 out,
284 );
285 }
286 }
287 }
288 }
289 }
290}
291
292#[derive(Debug, Clone, Copy)]
293struct LayoutRect {
294 x: f32,
295 y: f32,
296 width: f32,
297 height: f32,
298}
299
300impl LayoutRect {
301 fn center_x(self) -> f32 {
302 self.x + (self.width / 2.0)
303 }
304
305 fn center_y(self) -> f32 {
306 self.y + (self.height / 2.0)
307 }
308
309 fn directional_score(self, target: Self, direction: Direction) -> Option<f32> {
310 let primary = match direction {
311 Direction::Left => target.center_x() - self.center_x(),
312 Direction::Right => self.center_x() - target.center_x(),
313 Direction::Up => target.center_y() - self.center_y(),
314 Direction::Down => self.center_y() - target.center_y(),
315 };
316 if primary <= 0.0 {
317 return None;
318 }
319
320 let secondary = match direction {
321 Direction::Left | Direction::Right => (self.center_y() - target.center_y()).abs(),
322 Direction::Up | Direction::Down => (self.center_x() - target.center_x()).abs(),
323 };
324
325 Some((primary * 10.0) + secondary)
326 }
327}
328
329fn clamp_ratio(ratio: u16) -> u16 {
330 ratio.clamp(MIN_SPLIT_RATIO, MAX_SPLIT_RATIO)
331}
332
333fn split_resize_delta(
334 axis: SplitAxis,
335 direction: Direction,
336 found_in_first: bool,
337 amount: u16,
338) -> Option<i32> {
339 match axis {
340 SplitAxis::Horizontal => match (found_in_first, direction) {
341 (true, Direction::Right) => Some(i32::from(amount)),
342 (true, Direction::Left) => Some(-i32::from(amount)),
343 (false, Direction::Right) => Some(-i32::from(amount)),
344 (false, Direction::Left) => Some(i32::from(amount)),
345 _ => None,
346 },
347 SplitAxis::Vertical => match (found_in_first, direction) {
348 (true, Direction::Down) => Some(i32::from(amount)),
349 (true, Direction::Up) => Some(-i32::from(amount)),
350 (false, Direction::Down) => Some(-i32::from(amount)),
351 (false, Direction::Up) => Some(i32::from(amount)),
352 _ => None,
353 },
354 }
355}
356
357fn apply_ratio_delta(current: u16, delta: i32) -> u16 {
358 (i32::from(current) + delta).clamp(i32::from(MIN_SPLIT_RATIO), i32::from(MAX_SPLIT_RATIO))
359 as u16
360}