Skip to main content

halley_core/
cluster.rs

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/// A cluster is a group of window nodes (members).
36/// When collapsed, a Core node represents the cluster as the handle.
37#[derive(Clone, Debug)]
38pub struct Cluster {
39    pub id: ClusterId,
40    pub(crate) members: Vec<NodeId>,
41
42    /// When collapsed, which Core node represents this cluster.
43    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            // unlimited; no overflow member can exist
245            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        // max_stack 3 means 4 visible (1 master + 3 stack)
307        assert_eq!(cluster.visible_members(3).len(), 4);
308        assert_eq!(cluster.overflow_members(3).len(), 6);
309
310        // max_stack 5 means 6 visible
311        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}