Skip to main content

taskers_domain/
layout.rs

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}