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}