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