Skip to main content

taskers_domain/
layout.rs

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}