Skip to main content

sequoia_dock/
model.rs

1use sequoia_core::WindowId;
2use sequoia_layout::Axis;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
5pub struct PanelId(pub u64);
6
7impl PanelId {
8    pub const fn new(id: u64) -> Self {
9        Self(id)
10    }
11
12    pub const fn get(self) -> u64 {
13        self.0
14    }
15}
16
17impl std::fmt::Display for PanelId {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(f, "panel#{}", self.0)
20    }
21}
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum SplitAxis {
26    #[default]
27    Horizontal,
28    Vertical,
29}
30
31impl SplitAxis {
32    pub const fn axis(self) -> Axis {
33        match self {
34            Self::Horizontal => Axis::Horizontal,
35            Self::Vertical => Axis::Vertical,
36        }
37    }
38
39    pub const fn from_axis(axis: Axis) -> Self {
40        match axis {
41            Axis::Horizontal => Self::Horizontal,
42            Axis::Vertical => Self::Vertical,
43        }
44    }
45}
46
47#[derive(Clone, Debug, PartialEq)]
48#[non_exhaustive]
49pub enum DockNode {
50    Leaf(PanelId),
51
52    Split {
53        axis: SplitAxis,
54        frac: f32,
55        first: Box<DockNode>,
56        second: Box<DockNode>,
57    },
58
59    Tabs {
60        tabs: Vec<PanelId>,
61        active: usize,
62    },
63}
64
65impl DockNode {
66    pub fn leaf(panel: PanelId) -> Self {
67        Self::Leaf(panel)
68    }
69
70    pub fn tabs(tabs: impl IntoIterator<Item = PanelId>) -> Self {
71        let tabs: Vec<PanelId> = tabs.into_iter().collect();
72        Self::Tabs { tabs, active: 0 }
73    }
74
75    pub fn sanitized_frac(frac: f32) -> f32 {
76        if frac.is_finite() {
77            frac.clamp(0.0, 1.0)
78        } else {
79            0.5
80        }
81    }
82
83    pub fn panels(&self) -> Vec<PanelId> {
84        let mut out = Vec::new();
85        self.collect_panels(&mut out);
86        out
87    }
88
89    fn collect_panels(&self, out: &mut Vec<PanelId>) {
90        match self {
91            Self::Leaf(p) => out.push(*p),
92            Self::Split { first, second, .. } => {
93                first.collect_panels(out);
94                second.collect_panels(out);
95            }
96            Self::Tabs { tabs, .. } => out.extend(tabs.iter().copied()),
97        }
98    }
99
100    pub fn contains(&self, panel: PanelId) -> bool {
101        match self {
102            Self::Leaf(p) => *p == panel,
103            Self::Split { first, second, .. } => first.contains(panel) || second.contains(panel),
104            Self::Tabs { tabs, .. } => tabs.contains(&panel),
105        }
106    }
107
108    pub fn path_to(&self, panel: PanelId) -> Option<Vec<bool>> {
109        match self {
110            Self::Leaf(p) => {
111                if *p == panel {
112                    Some(Vec::new())
113                } else {
114                    None
115                }
116            }
117            Self::Tabs { tabs, .. } => {
118                if tabs.contains(&panel) {
119                    Some(Vec::new())
120                } else {
121                    None
122                }
123            }
124            Self::Split { first, second, .. } => {
125                if let Some(mut p) = first.path_to(panel) {
126                    let mut out = vec![false];
127                    out.append(&mut p);
128                    Some(out)
129                } else if let Some(mut p) = second.path_to(panel) {
130                    let mut out = vec![true];
131                    out.append(&mut p);
132                    Some(out)
133                } else {
134                    None
135                }
136            }
137        }
138    }
139
140    fn sanitize_tabs(&mut self) {
141        if let Self::Tabs { tabs, active } = self {
142            if tabs.is_empty() {
143                return;
144            }
145            if *active >= tabs.len() {
146                *active = tabs.len() - 1;
147            }
148        }
149    }
150}
151
152#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct FloatingPanel {
154    pub panel: PanelId,
155
156    pub x: f32,
157    pub y: f32,
158    pub w: f32,
159    pub h: f32,
160
161    pub window: Option<u64>,
162}
163
164impl FloatingPanel {
165    pub fn overlay(panel: PanelId, x: f32, y: f32, w: f32, h: f32) -> Self {
166        Self {
167            panel,
168            x: sanitize_pos(x),
169            y: sanitize_pos(y),
170            w: sanitize_size(w, 240.0),
171            h: sanitize_size(h, 160.0),
172            window: None,
173        }
174    }
175
176    pub fn window_id(&self) -> Option<WindowId> {
177        self.window.map(WindowId::new)
178    }
179
180    pub fn is_overlay(&self) -> bool {
181        self.window.is_none()
182    }
183}
184
185fn sanitize_pos(v: f32) -> f32 {
186    if v.is_finite() { v } else { 0.0 }
187}
188
189fn sanitize_size(v: f32, fallback: f32) -> f32 {
190    if v.is_finite() && v > 0.0 {
191        v
192    } else {
193        fallback
194    }
195}
196
197#[derive(Clone, Debug, PartialEq)]
198pub struct DockModel {
199    pub root: Option<DockNode>,
200    pub floating: Vec<FloatingPanel>,
201}
202
203impl Default for DockModel {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl DockModel {
210    pub fn new() -> Self {
211        Self {
212            root: None,
213            floating: Vec::new(),
214        }
215    }
216
217    pub fn single(panel: PanelId) -> Self {
218        Self {
219            root: Some(DockNode::Leaf(panel)),
220            floating: Vec::new(),
221        }
222    }
223
224    pub fn is_empty(&self) -> bool {
225        self.root.is_none() && self.floating.is_empty()
226    }
227
228    pub fn panels(&self) -> Vec<PanelId> {
229        let mut out = self.root.as_ref().map(|r| r.panels()).unwrap_or_default();
230        out.extend(self.floating.iter().map(|f| f.panel));
231        out
232    }
233
234    pub fn contains(&self, panel: PanelId) -> bool {
235        self.root.as_ref().is_some_and(|r| r.contains(panel))
236            || self.floating.iter().any(|f| f.panel == panel)
237    }
238
239    pub fn is_floating(&self, panel: PanelId) -> bool {
240        self.floating.iter().any(|f| f.panel == panel)
241    }
242
243    pub fn floating_of(&self, panel: PanelId) -> Option<FloatingPanel> {
244        self.floating.iter().find(|f| f.panel == panel).copied()
245    }
246
247    pub fn active_tab_of(&self, panel: PanelId) -> Option<usize> {
248        fn go(node: &DockNode, panel: PanelId) -> Option<usize> {
249            match node {
250                DockNode::Leaf(_) => None,
251                DockNode::Tabs { tabs, active } => {
252                    if tabs.contains(&panel) {
253                        Some(*active)
254                    } else {
255                        None
256                    }
257                }
258                DockNode::Split { first, second, .. } => {
259                    go(first, panel).or_else(|| go(second, panel))
260                }
261            }
262        }
263        self.root.as_ref().and_then(|r| go(r, panel))
264    }
265
266    pub(crate) fn sanitize(&mut self) {
267        fn go(node: &mut DockNode) {
268            match node {
269                DockNode::Leaf(_) => {}
270                DockNode::Tabs { tabs, active } => {
271                    if *active >= tabs.len() && !tabs.is_empty() {
272                        *active = tabs.len() - 1;
273                    }
274                    let _ = tabs;
275                }
276                DockNode::Split {
277                    frac,
278                    first,
279                    second,
280                    ..
281                } => {
282                    *frac = DockNode::sanitized_frac(*frac);
283                    go(first);
284                    go(second);
285                }
286            }
287        }
288        if let Some(r) = &mut self.root {
289            go(r);
290            r.sanitize_tabs();
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn single_contains_and_lists() {
301        let m = DockModel::single(PanelId::new(7));
302        assert!(m.contains(PanelId::new(7)));
303        assert!(!m.contains(PanelId::new(8)));
304        assert_eq!(m.panels(), vec![PanelId::new(7)]);
305    }
306
307    #[test]
308    fn path_to_finds_nested() {
309        let root = DockNode::Split {
310            axis: SplitAxis::Horizontal,
311            frac: 0.3,
312            first: Box::new(DockNode::Leaf(PanelId::new(1))),
313            second: Box::new(DockNode::Tabs {
314                tabs: vec![PanelId::new(2), PanelId::new(3)],
315                active: 1,
316            }),
317        };
318        assert_eq!(root.path_to(PanelId::new(1)), Some(vec![false]));
319        assert_eq!(root.path_to(PanelId::new(3)), Some(vec![true]));
320        assert_eq!(root.path_to(PanelId::new(9)), None);
321    }
322
323    #[test]
324    fn frac_sanitizes() {
325        assert_eq!(DockNode::sanitized_frac(2.0), 1.0);
326        assert_eq!(DockNode::sanitized_frac(f32::NAN), 0.5);
327        assert_eq!(DockNode::sanitized_frac(-1.0), 0.0);
328    }
329
330    #[test]
331    fn floating_overlay_defaults() {
332        let f = FloatingPanel::overlay(PanelId::new(1), f32::NAN, 10.0, -5.0, 100.0);
333        assert!(f.is_overlay());
334        assert_eq!((f.x, f.y, f.w, f.h), (0.0, 10.0, 240.0, 100.0));
335    }
336}