1use crate::field::{Node, NodeId};
2use crate::tiling::{MasterStackLayout, Rect, layout_master_stack};
3use std::collections::HashMap;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct ClusterId(u64);
7
8impl ClusterId {
9 pub fn new(raw: u64) -> Self {
10 Self(raw)
11 }
12 pub fn as_u64(self) -> u64 {
13 self.0
14 }
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum ClusterMode {
19 Expanded,
20 Collapsed,
21 Active,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum ClusterRemoveMemberOutcome {
26 Removed,
27 RequiresDissolve,
28}
29
30#[derive(Clone, Debug, Default)]
31pub struct ActiveWorkspace {
32 pub nodes: HashMap<NodeId, Node>,
33}
34
35#[derive(Clone, Debug)]
38pub struct Cluster {
39 pub id: ClusterId,
40 pub(crate) members: Vec<NodeId>,
41
42 pub core: Option<NodeId>,
44
45 pub mode: ClusterMode,
46 pub active_workspace: Option<ActiveWorkspace>,
47}
48
49impl Cluster {
50 pub fn new(id: ClusterId, members: Vec<NodeId>) -> Option<Self> {
51 if members.len() < 2 {
52 return None;
53 }
54 if has_duplicates(&members) {
55 return None;
56 }
57 Some(Self {
58 id,
59 members,
60 core: None,
61 mode: ClusterMode::Expanded,
62 active_workspace: None,
63 })
64 }
65
66 pub fn contains(&self, id: NodeId) -> bool {
67 self.members.contains(&id)
68 }
69
70 pub fn members(&self) -> &[NodeId] {
71 &self.members
72 }
73
74 pub fn master(&self) -> NodeId {
75 self.members[0]
76 }
77
78 pub fn secondaries(&self) -> &[NodeId] {
79 &self.members[1..]
80 }
81
82 pub fn visible_members(&self, max_stack: usize) -> &[NodeId] {
83 if max_stack == 0 {
84 &self.members
85 } else {
86 let limit = max_stack + 1;
87 let end = self.members.len().min(limit);
88 &self.members[..end]
89 }
90 }
91
92 pub fn overflow_members(&self, max_stack: usize) -> &[NodeId] {
93 if max_stack == 0 {
94 &[]
95 } else {
96 let limit = max_stack + 1;
97 if self.members.len() <= limit {
98 &[]
99 } else {
100 &self.members[limit..]
101 }
102 }
103 }
104
105 pub fn core_node(&self) -> Option<NodeId> {
106 self.core
107 }
108
109 pub fn is_collapsed(&self) -> bool {
110 matches!(self.mode, ClusterMode::Collapsed)
111 }
112
113 pub fn is_active(&self) -> bool {
114 matches!(self.mode, ClusterMode::Active)
115 }
116
117 pub fn set_collapsed(&mut self, collapsed: bool) {
118 self.mode = if collapsed {
119 ClusterMode::Collapsed
120 } else {
121 ClusterMode::Expanded
122 };
123 }
124
125 pub fn enter_active(&mut self) {
126 self.mode = ClusterMode::Active;
127 self.active_workspace
128 .get_or_insert_with(ActiveWorkspace::default);
129 }
130
131 pub fn exit_active(&mut self) {
132 self.mode = ClusterMode::Expanded;
133 self.active_workspace = None;
134 }
135
136 pub fn workspace_layout(&self, bounds: Rect, max_stack: usize) -> MasterStackLayout {
137 layout_master_stack(bounds, self.visible_members(max_stack))
138 }
139
140 pub(crate) fn add_member(&mut self, member: NodeId) -> bool {
141 if self.members.contains(&member) {
142 return false;
143 }
144 self.members.push(member);
145 true
146 }
147
148 pub(crate) fn add_member_front(&mut self, member: NodeId) -> bool {
149 if self.members.contains(&member) {
150 return false;
151 }
152 self.members.insert(0, member);
153 true
154 }
155
156 pub fn workspace_member(&self, id: NodeId) -> Option<&Node> {
157 self.active_workspace.as_ref()?.nodes.get(&id)
158 }
159
160 pub fn workspace_member_mut(&mut self, id: NodeId) -> Option<&mut Node> {
161 self.active_workspace.as_mut()?.nodes.get_mut(&id)
162 }
163
164 pub(crate) fn insert_workspace_member(&mut self, node: Node) -> bool {
165 let Some(active_workspace) = self.active_workspace.as_mut() else {
166 return false;
167 };
168 active_workspace.nodes.insert(node.id, node);
169 true
170 }
171
172 pub(crate) fn remove_workspace_member(&mut self, id: NodeId) -> Option<Node> {
173 self.active_workspace.as_mut()?.nodes.remove(&id)
174 }
175
176 pub(crate) fn remove_member(&mut self, member: NodeId) -> Option<ClusterRemoveMemberOutcome> {
177 if !self.members.contains(&member) {
178 return None;
179 }
180 if self.members.len() <= 2 {
181 return Some(ClusterRemoveMemberOutcome::RequiresDissolve);
182 }
183
184 self.members.retain(|&id| id != member);
185 Some(ClusterRemoveMemberOutcome::Removed)
186 }
187
188 pub(crate) fn remove_member_for_node_removal(&mut self, member: NodeId) -> bool {
189 let before = self.members.len();
190 self.members.retain(|&id| id != member);
191 self.members.len() != before
192 }
193
194 pub(crate) fn reorder_members(&mut self, ordered_members: Vec<NodeId>) -> bool {
195 if ordered_members.len() != self.members.len() || has_duplicates(&ordered_members) {
196 return false;
197 }
198
199 let mut current = self.members.clone();
200 let mut reordered = ordered_members.clone();
201 current.sort_by_key(|id| id.as_u64());
202 reordered.sort_by_key(|id| id.as_u64());
203 if current != reordered {
204 return false;
205 }
206
207 self.members = ordered_members;
208 true
209 }
210
211 pub(crate) fn promote_member_to_master(&mut self, member: NodeId) -> bool {
212 let Some(index) = self.members.iter().position(|&id| id == member) else {
213 return false;
214 };
215 if index == 0 {
216 return true;
217 }
218 self.members.remove(index);
219 self.members.insert(0, member);
220 true
221 }
222
223 pub(crate) fn swap_overflow_member_with_visible(
224 &mut self,
225 overflow_member: NodeId,
226 visible_member: NodeId,
227 max_stack: usize,
228 ) -> bool {
229 let Some(overflow_index) = self.members.iter().position(|&id| id == overflow_member) else {
230 return false;
231 };
232 let Some(visible_index) = self.members.iter().position(|&id| id == visible_member) else {
233 return false;
234 };
235 if max_stack > 0 {
236 let limit = max_stack + 1;
237 if overflow_index < limit || visible_index >= limit {
238 return false;
239 }
240 } else {
241 return false;
243 }
244
245 self.members[overflow_index] = visible_member;
246 self.members[visible_index] = overflow_member;
247 true
248 }
249
250 pub(crate) fn reorder_overflow_member(
251 &mut self,
252 member: NodeId,
253 target_overflow_index: usize,
254 max_stack: usize,
255 ) -> bool {
256 let Some(member_index) = self.members.iter().position(|&id| id == member) else {
257 return false;
258 };
259 if max_stack == 0 {
260 return false;
261 }
262 let limit = max_stack + 1;
263 if member_index < limit {
264 return false;
265 }
266
267 let overflow_len = self.members.len().saturating_sub(limit);
268 if overflow_len <= 1 {
269 return true;
270 }
271
272 let member = self.members.remove(member_index);
273 let clamped_index = target_overflow_index.min(overflow_len - 1);
274 let insert_index = (limit + clamped_index).min(self.members.len());
275 self.members.insert(insert_index, member);
276 true
277 }
278}
279
280fn has_duplicates(members: &[NodeId]) -> bool {
281 let mut seen = std::collections::HashSet::new();
282 for member in members {
283 if !seen.insert(*member) {
284 return true;
285 }
286 }
287 false
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn ids(n: u64) -> Vec<NodeId> {
295 (0..n).map(NodeId::new).collect()
296 }
297
298 #[test]
299 fn visible_members_respects_max_stack() {
300 let members = ids(10);
301 let cluster = Cluster::new(ClusterId::new(1), members.clone()).unwrap();
302
303 assert_eq!(cluster.visible_members(3).len(), 4);
305 assert_eq!(cluster.overflow_members(3).len(), 6);
306
307 assert_eq!(cluster.visible_members(5).len(), 6);
309 assert_eq!(cluster.overflow_members(5).len(), 4);
310 }
311
312 #[test]
313 fn zero_max_stack_means_unlimited_visible() {
314 let members = ids(10);
315 let cluster = Cluster::new(ClusterId::new(1), members.clone()).unwrap();
316
317 assert_eq!(cluster.visible_members(0).len(), 10);
318 assert_eq!(cluster.overflow_members(0).len(), 0);
319 }
320
321 #[test]
322 fn visible_members_capped_by_total_members() {
323 let members = ids(3);
324 let cluster = Cluster::new(ClusterId::new(1), members.clone()).unwrap();
325
326 assert_eq!(cluster.visible_members(5).len(), 3);
327 assert_eq!(cluster.overflow_members(5).len(), 0);
328 }
329}