1use serde::{Deserialize, Serialize};
2
3use crate::model::HarnessId;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub enum Axis {
10 Horizontal,
11 Vertical,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DropZone {
16 Left,
17 Right,
18 Top,
19 Bottom,
20 Center,
21}
22
23impl DropZone {
24 pub fn is_center(self) -> bool {
25 matches!(self, DropZone::Center)
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33pub enum LayoutNode {
34 Leaf(HarnessId),
35 Split {
36 axis: Axis,
37 ratio: f32,
39 first: Box<LayoutNode>,
40 second: Box<LayoutNode>,
41 },
42}
43
44impl LayoutNode {
45 pub fn leaf(id: HarnessId) -> Self {
46 Self::Leaf(id)
47 }
48
49 pub fn split(axis: Axis, ratio: f32, first: LayoutNode, second: LayoutNode) -> Self {
50 Self::Split {
51 axis,
52 ratio: ratio.clamp(0.1, 0.9),
53 first: Box::new(first),
54 second: Box::new(second),
55 }
56 }
57
58 pub fn leaves(&self) -> Vec<HarnessId> {
59 let mut out = Vec::new();
60 self.collect_leaves(&mut out);
61 out
62 }
63
64 fn collect_leaves(&self, out: &mut Vec<HarnessId>) {
65 match self {
66 LayoutNode::Leaf(id) => out.push(*id),
67 LayoutNode::Split { first, second, .. } => {
68 first.collect_leaves(out);
69 second.collect_leaves(out);
70 }
71 }
72 }
73
74 pub fn len(&self) -> usize {
75 match self {
76 LayoutNode::Leaf(_) => 1,
77 LayoutNode::Split { first, second, .. } => first.len() + second.len(),
78 }
79 }
80
81 pub fn is_empty(&self) -> bool {
82 false
83 }
84
85 pub fn contains(&self, id: HarnessId) -> bool {
86 match self {
87 LayoutNode::Leaf(leaf) => *leaf == id,
88 LayoutNode::Split { first, second, .. } => first.contains(id) || second.contains(id),
89 }
90 }
91
92 pub fn first_leaf(&self) -> Option<HarnessId> {
93 match self {
94 LayoutNode::Leaf(id) => Some(*id),
95 LayoutNode::Split { first, .. } => first.first_leaf(),
96 }
97 }
98
99 pub fn split_at(&mut self, target: HarnessId, new: HarnessId, zone: DropZone) -> bool {
101 if zone.is_center() {
102 return false;
103 }
104
105 match self {
106 LayoutNode::Leaf(id) if *id == target => {
107 let old = self.clone();
108 *self = match zone {
109 DropZone::Left => Self::split(Axis::Horizontal, 0.5, Self::leaf(new), old),
110 DropZone::Right => Self::split(Axis::Horizontal, 0.5, old, Self::leaf(new)),
111 DropZone::Top => Self::split(Axis::Vertical, 0.5, Self::leaf(new), old),
112 DropZone::Bottom => Self::split(Axis::Vertical, 0.5, old, Self::leaf(new)),
113 DropZone::Center => unreachable!(),
114 };
115 true
116 }
117 LayoutNode::Leaf(_) => false,
118 LayoutNode::Split { first, second, .. } => {
119 first.split_at(target, new, zone) || second.split_at(target, new, zone)
120 }
121 }
122 }
123
124 pub fn swap(&mut self, a: HarnessId, b: HarnessId) -> bool {
126 if a == b {
127 return false;
128 }
129
130 let mut found_a = false;
131 let mut found_b = false;
132 self.for_each_leaf_mut(&mut |id| {
133 if *id == a {
134 *id = b;
135 found_a = true;
136 } else if *id == b {
137 *id = a;
138 found_b = true;
139 }
140 });
141 found_a && found_b
142 }
143
144 fn for_each_leaf_mut(&mut self, f: &mut impl FnMut(&mut HarnessId)) {
145 match self {
146 LayoutNode::Leaf(id) => f(id),
147 LayoutNode::Split { first, second, .. } => {
148 first.for_each_leaf_mut(f);
149 second.for_each_leaf_mut(f);
150 }
151 }
152 }
153
154 pub fn remove(&mut self, id: HarnessId) -> bool {
157 let replacement = match self {
158 LayoutNode::Leaf(_) => return false,
159 LayoutNode::Split { first, second, .. } => {
160 if matches!(first.as_ref(), LayoutNode::Leaf(leaf) if *leaf == id) {
161 Some((**second).clone())
162 } else if matches!(second.as_ref(), LayoutNode::Leaf(leaf) if *leaf == id) {
163 Some((**first).clone())
164 } else if first.remove(id) || second.remove(id) {
165 return true;
166 } else {
167 None
168 }
169 }
170 };
171
172 match replacement {
173 Some(node) => {
174 *self = node;
175 true
176 }
177 None => false,
178 }
179 }
180
181 pub fn set_ratio(&mut self, path: &[bool], value: f32) -> bool {
184 match (self, path) {
185 (LayoutNode::Split { ratio, .. }, []) => {
186 *ratio = value.clamp(0.1, 0.9);
187 true
188 }
189 (LayoutNode::Split { first, second, .. }, [side, rest @ ..]) => {
190 if *side { second } else { first }.set_ratio(rest, value)
191 }
192 (LayoutNode::Leaf(_), _) => false,
193 }
194 }
195
196 pub fn move_leaf(&mut self, dragged: HarnessId, target: HarnessId, zone: DropZone) -> bool {
198 if dragged == target {
199 return false;
200 }
201 if self.len() < 2 || !self.contains(target) {
202 return false;
203 }
204
205 if zone.is_center() {
206 return self.swap(dragged, target);
207 }
208
209 self.remove(dragged);
210 if !self.contains(target) {
211 return false;
212 }
213 self.split_at(target, dragged, zone)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn set_ratio_follows_path() {
223 let id = HarnessId::new;
224 let inner = LayoutNode::split(
225 Axis::Vertical,
226 0.5,
227 LayoutNode::leaf(id(2)),
228 LayoutNode::leaf(id(3)),
229 );
230 let mut root = LayoutNode::split(Axis::Horizontal, 0.5, LayoutNode::leaf(id(1)), inner);
231 assert!(root.set_ratio(&[true], 0.3));
232 assert!(root.set_ratio(&[], 2.0));
233 assert!(!root.set_ratio(&[false], 0.4));
234 let LayoutNode::Split { ratio, second, .. } = &root else {
235 panic!()
236 };
237 assert_eq!(*ratio, 0.9);
238 assert!(matches!(**second, LayoutNode::Split { ratio, .. } if ratio == 0.3));
239 }
240}