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 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}