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