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 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            // unlimited; no overflow member can exist
242            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        // max_stack 3 means 4 visible (1 master + 3 stack)
304        assert_eq!(cluster.visible_members(3).len(), 4);
305        assert_eq!(cluster.overflow_members(3).len(), 6);
306
307        // max_stack 5 means 6 visible
308        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}