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