1use crate::cluster::{Cluster, ClusterId, ClusterRemoveMemberOutcome};
2use crate::cluster_layout::ClusterCycleDirection;
3use crate::decay::DecayLevel;
4use crate::stacking::cycle_stacking_members;
5use crate::viewport::Viewport;
6use crate::visual::{NodeVisual, VisualParams, build_visuals, build_visuals_in_view};
7
8use std::collections::HashMap;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct NodeId(u64);
13
14impl NodeId {
15 pub fn new(raw: u64) -> Self {
16 Self(raw)
17 }
18
19 pub fn as_u64(self) -> u64 {
20 self.0
21 }
22}
23
24impl std::fmt::Display for NodeId {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{}", self.0)
27 }
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Vec2 {
33 pub x: f32,
34 pub y: f32,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct Rect {
40 pub min: Vec2,
41 pub max: Vec2,
42}
43
44impl Rect {
45 pub fn width(self) -> f32 {
46 self.max.x - self.min.x
47 }
48
49 pub fn height(self) -> f32 {
50 self.max.y - self.min.y
51 }
52
53 pub fn contains(self, p: Vec2) -> bool {
54 p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
55 }
56
57 pub fn intersects(self, other: Rect) -> bool {
58 self.min.x <= other.max.x
59 && self.max.x >= other.min.x
60 && self.min.y <= other.max.y
61 && self.max.y >= other.min.y
62 }
63}
64
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct Visibility(u8);
70
71impl Visibility {
72 pub const NONE: Self = Self(0);
73
74 pub const HIDDEN_EXPLICIT: Self = Self(1 << 0);
76
77 pub const HIDDEN_BY_CLUSTER: Self = Self(1 << 1);
79
80 pub const DETACHED: Self = Self(1 << 2);
82
83 pub fn is_hidden(self) -> bool {
84 (self.0 & (Self::HIDDEN_EXPLICIT.0 | Self::HIDDEN_BY_CLUSTER.0 | Self::DETACHED.0)) != 0
85 }
86
87 pub fn has(self, flag: Self) -> bool {
88 (self.0 & flag.0) != 0
89 }
90
91 pub fn set(&mut self, flag: Self, on: bool) {
92 if on {
93 self.0 |= flag.0;
94 } else {
95 self.0 &= !flag.0;
96 }
97 }
98
99 pub fn clear(&mut self, flag: Self) {
100 self.0 &= !flag.0;
101 }
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum NodeKind {
107 Surface,
108 Core, }
110
111#[derive(Clone, Debug, PartialEq, Eq)]
113pub enum NodeState {
114 Active,
115 Drifting,
116 Node, Core, }
119
120#[derive(Clone, Debug)]
122pub struct Node {
123 pub id: NodeId,
124 pub kind: NodeKind,
125 pub state: NodeState,
126
127 pub label: String,
128
129 pub pos: Vec2,
131
132 pub intrinsic_size: Vec2, pub footprint: Vec2, pub resize_footprint: Option<Vec2>,
135
136 pub pinned: bool,
138
139 pub anchor: bool,
142
143 pub visibility: Visibility,
145
146 pub last_touch_ms: u64,
147 pub decay: DecayLevel,
148}
149
150pub struct Field {
152 next_node: u64,
153 nodes: HashMap<NodeId, Node>,
154
155 next_cluster: u64,
156 clusters: HashMap<ClusterId, Cluster>,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub enum ClusterCreateError {
161 TooFewMembers,
162 DuplicateMember,
163 MissingNode(NodeId),
164 AlreadyClustered(NodeId),
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum ClusterAddMemberError {
169 MissingCluster,
170 MissingNode(NodeId),
171 AlreadyClustered(NodeId),
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175pub enum ClusterWorkspaceSpawnError {
176 MissingCluster,
177 ClusterNotActive,
178}
179
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub enum ClusterReorderError {
182 MissingCluster,
183 InvalidMembers,
184 UnknownMember(NodeId),
185}
186
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub enum RemoveNodeClusterEffect {
189 RemovedMember(ClusterId),
190 DissolvedCluster(ClusterId),
191 RemovedCore(ClusterId),
192}
193
194impl Field {
195 fn make_surface_node(id: NodeId, label: String, pos: Vec2, size: Vec2) -> Node {
196 Node {
197 id,
198 kind: NodeKind::Surface,
199 state: NodeState::Active,
200 label,
201 pos,
202 intrinsic_size: size,
203 footprint: size,
204 resize_footprint: None,
205 pinned: false,
206 anchor: false,
207 visibility: Visibility::NONE,
208 last_touch_ms: 0,
209 decay: DecayLevel::Hot,
210 }
211 }
212
213 pub fn new() -> Self {
214 Self {
215 next_node: 1,
216 nodes: HashMap::new(),
217 next_cluster: 1,
218 clusters: HashMap::new(),
219 }
220 }
221
222 pub fn nodes(&self) -> &HashMap<NodeId, Node> {
223 &self.nodes
224 }
225
226 pub fn node(&self, id: NodeId) -> Option<&Node> {
227 if let Some(node) = self.nodes.get(&id) {
228 return Some(node);
229 }
230 self.clusters
231 .values()
232 .find_map(|cluster| cluster.workspace_member(id))
233 }
234
235 pub fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
236 if self.nodes.contains_key(&id) {
237 return self.nodes.get_mut(&id);
238 }
239 for cluster in self.clusters.values_mut() {
240 if let Some(node) = cluster.workspace_member_mut(id) {
241 return Some(node);
242 }
243 }
244 None
245 }
246
247 pub fn spawn_surface(&mut self, label: impl Into<String>, pos: Vec2, size: Vec2) -> NodeId {
249 let id = NodeId(self.next_node);
250 self.next_node += 1;
251
252 let node = Self::make_surface_node(id, label.into(), pos, size);
253
254 self.nodes.insert(id, node);
255 id
256 }
257
258 pub fn spawn_surface_in_active_cluster(
259 &mut self,
260 id: ClusterId,
261 label: impl Into<String>,
262 size: Vec2,
263 ) -> Result<NodeId, ClusterWorkspaceSpawnError> {
264 let label = label.into();
265 let Some(cluster) = self.clusters.get_mut(&id) else {
266 return Err(ClusterWorkspaceSpawnError::MissingCluster);
267 };
268 if !cluster.is_active() {
269 return Err(ClusterWorkspaceSpawnError::ClusterNotActive);
270 }
271
272 let node_id = NodeId(self.next_node);
273 self.next_node += 1;
274 if !cluster.add_member(node_id) {
275 return Err(ClusterWorkspaceSpawnError::ClusterNotActive);
276 }
277
278 let node = Self::make_surface_node(node_id, label, Vec2 { x: 0.0, y: 0.0 }, size);
279 if !cluster.insert_workspace_member(node) {
280 return Err(ClusterWorkspaceSpawnError::ClusterNotActive);
281 }
282 Ok(node_id)
283 }
284
285 pub fn spawn_surface_in_active_cluster_front(
286 &mut self,
287 id: ClusterId,
288 label: impl Into<String>,
289 size: Vec2,
290 ) -> Result<NodeId, ClusterWorkspaceSpawnError> {
291 let label = label.into();
292 let Some(cluster) = self.clusters.get_mut(&id) else {
293 return Err(ClusterWorkspaceSpawnError::MissingCluster);
294 };
295 if !cluster.is_active() {
296 return Err(ClusterWorkspaceSpawnError::ClusterNotActive);
297 }
298
299 let node_id = NodeId(self.next_node);
300 self.next_node += 1;
301 if !cluster.add_member_front(node_id) {
302 return Err(ClusterWorkspaceSpawnError::ClusterNotActive);
303 }
304
305 let node = Self::make_surface_node(node_id, label, Vec2 { x: 0.0, y: 0.0 }, size);
306 if !cluster.insert_workspace_member(node) {
307 return Err(ClusterWorkspaceSpawnError::ClusterNotActive);
308 }
309 Ok(node_id)
310 }
311
312 pub fn remove(&mut self, id: NodeId) -> Option<Node> {
314 self.remove_node_cluster_safe(id).map(|(node, _)| node)
315 }
316
317 pub fn remove_node_cluster_safe(
318 &mut self,
319 id: NodeId,
320 ) -> Option<(Node, Option<RemoveNodeClusterEffect>)> {
321 if let Some(cid) = self.cluster_id_for_member_public(id) {
322 let cluster_len = self.cluster(cid)?.members().len();
323 let removed = if self.cluster(cid).is_some_and(|cluster| cluster.is_active()) {
324 self.clusters
325 .get_mut(&cid)?
326 .active_workspace
327 .as_mut()?
328 .nodes
329 .remove(&id)?
330 } else {
331 self.nodes.remove(&id)?
332 };
333 if cluster_len <= 2 {
334 self.finish_dissolve_cluster(cid);
335 return Some((
336 removed,
337 Some(RemoveNodeClusterEffect::DissolvedCluster(cid)),
338 ));
339 }
340
341 let cluster = self.clusters.get_mut(&cid)?;
342 cluster.remove_member_for_node_removal(id);
343 return Some((removed, Some(RemoveNodeClusterEffect::RemovedMember(cid))));
344 }
345
346 if let Some(cid) = self.cluster_id_for_core_public(id) {
347 let removed = self.nodes.remove(&id)?;
348 let was_collapsed = self
349 .cluster(cid)
350 .is_some_and(|cluster| cluster.is_collapsed());
351 if was_collapsed {
352 let _ = self.expand_cluster(cid);
353 }
354 if let Some(cluster) = self.clusters.get_mut(&cid) {
355 cluster.core = None;
356 cluster.set_collapsed(false);
357 }
358 return Some((removed, Some(RemoveNodeClusterEffect::RemovedCore(cid))));
359 }
360
361 self.nodes.remove(&id).map(|node| (node, None))
362 }
363
364 pub fn is_cluster_member(&self, id: NodeId) -> bool {
365 self.cluster_id_for_member_public(id).is_some()
366 }
367
368 pub fn is_active_cluster_member(&self, id: NodeId) -> bool {
369 self.clusters
370 .values()
371 .any(|cluster| cluster.is_active() && cluster.contains(id))
372 }
373
374 pub fn participates_in_field_dynamics(&self, id: NodeId) -> bool {
375 self.node(id).is_some() && !self.is_active_cluster_member(id)
376 }
377
378 pub fn participates_in_field_activity(&self, id: NodeId) -> bool {
379 self.node(id).is_some() && !self.is_cluster_member(id)
380 }
381
382 pub fn participates_in_field_view(&self, id: NodeId) -> bool {
383 self.node(id).is_some() && !self.is_active_cluster_member(id)
384 }
385
386 pub fn node_ids_all(&self) -> Vec<NodeId> {
387 let mut ids: Vec<NodeId> = self.nodes.keys().copied().collect();
388 for cluster in self.clusters.values() {
389 if let Some(active_workspace) = cluster.active_workspace.as_ref() {
390 ids.extend(active_workspace.nodes.keys().copied());
391 }
392 }
393 ids
394 }
395
396 pub fn set_pinned(&mut self, id: NodeId, on: bool) -> bool {
398 let Some(n) = self.node_mut(id) else {
399 return false;
400 };
401 n.pinned = on;
402 true
403 }
404
405 pub fn anchor(&mut self, id: NodeId, on: bool) -> bool {
408 self.set_pinned(id, on)
409 }
410
411 pub fn set_anchor(&mut self, id: NodeId, on: bool) -> bool {
413 let Some(n) = self.node_mut(id) else {
414 return false;
415 };
416 n.anchor = on;
417 true
418 }
419
420 pub fn is_anchor(&self, id: NodeId) -> bool {
421 self.node(id).is_some_and(|n| n.anchor)
422 }
423
424 pub fn anchors(&self) -> Vec<NodeId> {
426 let mut out: Vec<NodeId> = self
427 .nodes
428 .iter()
429 .filter_map(|(&id, n)| {
430 (self.participates_in_field_view(id) && self.is_visible(id) && n.anchor)
431 .then_some(id)
432 })
433 .collect();
434 out.sort_by_key(|id| id.as_u64());
435 out
436 }
437
438 pub fn carry(&mut self, id: NodeId, to: Vec2) -> bool {
440 let Some(n) = self.node_mut(id) else {
441 return false;
442 };
443 if n.pinned {
444 return false;
445 }
446 n.pos = to;
447 true
448 }
449
450 pub fn bounds(&self, id: NodeId) -> Option<Rect> {
452 let n = self.node(id)?;
453 Some(Self::bounds_for_node(n))
454 }
455
456 fn bounds_for_node(n: &Node) -> Rect {
457 let half = Vec2 {
458 x: n.footprint.x * 0.5,
459 y: n.footprint.y * 0.5,
460 };
461 Rect {
462 min: Vec2 {
463 x: n.pos.x - half.x,
464 y: n.pos.y - half.y,
465 },
466 max: Vec2 {
467 x: n.pos.x + half.x,
468 y: n.pos.y + half.y,
469 },
470 }
471 }
472
473 pub fn in_view(&self, view: Rect) -> Vec<NodeId> {
475 self.nodes
476 .keys()
477 .copied()
478 .filter(|&id| self.participates_in_field_view(id))
479 .filter(|&id| self.is_visible(id))
480 .filter(|&id| self.bounds(id).is_some_and(|b| b.intersects(view)))
481 .collect()
482 }
483
484 pub fn in_view_all(&self, view: Rect) -> Vec<NodeId> {
486 self.nodes
487 .keys()
488 .copied()
489 .filter(|&id| self.participates_in_field_view(id))
490 .filter(|&id| self.bounds(id).is_some_and(|b| b.intersects(view)))
491 .collect()
492 }
493
494 pub fn is_visible(&self, id: NodeId) -> bool {
496 self.node(id).is_some_and(|n| !n.visibility.is_hidden())
497 }
498
499 pub fn set_hidden(&mut self, id: NodeId, on: bool) -> bool {
501 let Some(n) = self.node_mut(id) else {
502 return false;
503 };
504 n.visibility.set(Visibility::HIDDEN_EXPLICIT, on);
505 true
506 }
507
508 pub fn set_detached(&mut self, id: NodeId, on: bool) -> bool {
510 let Some(n) = self.node_mut(id) else {
511 return false;
512 };
513 n.visibility.set(Visibility::DETACHED, on);
514 true
515 }
516
517 pub fn touch(&mut self, id: NodeId, now_ms: u64) -> bool {
519 if self.is_cluster_member(id) {
520 return self.node(id).is_some();
521 }
522 let Some(n) = self.node_mut(id) else {
523 return false;
524 };
525 n.last_touch_ms = now_ms;
526 n.decay = DecayLevel::Hot;
527
528 if n.kind != NodeKind::Core {
530 n.state = NodeState::Active;
531 n.footprint = n.resize_footprint.unwrap_or(n.intrinsic_size);
532 }
533
534 true
535 }
536
537 pub fn set_decay_level(&mut self, id: NodeId, level: DecayLevel) -> bool {
539 if self.cluster_id_for_member_public(id).is_some() {
540 return self.node(id).is_some();
541 }
542 let Some(n) = self.node(id) else {
543 return false;
544 };
545
546 if n.kind == NodeKind::Core {
548 return true;
549 }
550
551 let state = match level {
552 DecayLevel::Hot => NodeState::Active,
553 DecayLevel::Cold => NodeState::Node,
554 };
555
556 if let Some(nm) = self.node_mut(id) {
557 nm.decay = level;
558 }
559 self.set_state(id, state)
560 }
561
562 pub fn set_state(&mut self, id: NodeId, state: NodeState) -> bool {
563 const DOT: Vec2 = Vec2 { x: 24.0, y: 24.0 };
564 const CORE: Vec2 = Vec2 { x: 48.0, y: 48.0 };
565
566 let Some(n) = self.node_mut(id) else {
567 return false;
568 };
569
570 n.state = state.clone();
571 n.footprint = match state {
572 NodeState::Active => n.resize_footprint.unwrap_or(n.intrinsic_size),
573 NodeState::Drifting => n.footprint,
574 NodeState::Node => DOT,
575 NodeState::Core => CORE,
576 };
577
578 true
579 }
580
581 pub fn set_resize_footprint(&mut self, id: NodeId, size: Option<Vec2>) -> bool {
582 let Some(n) = self.nodes.get_mut(&id) else {
583 return false;
584 };
585
586 n.resize_footprint = size;
587 if matches!(n.state, NodeState::Active) {
588 n.footprint = n.resize_footprint.unwrap_or(n.intrinsic_size);
589 }
590
591 true
592 }
593
594 pub fn sync_active_footprint_to_intrinsic(&mut self, id: NodeId) -> bool {
595 let Some(n) = self.nodes.get_mut(&id) else {
596 return false;
597 };
598 n.resize_footprint = None;
599 if matches!(n.state, NodeState::Active) {
600 n.footprint = n.intrinsic_size;
601 }
602 true
603 }
604
605 pub fn visuals_visible(&self) -> Vec<NodeVisual> {
608 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 0.0, y: 0.0 });
609 build_visuals(self, &vp, VisualParams::default())
610 }
611
612 pub fn visuals_in_view(&self, view: Rect) -> Vec<NodeVisual> {
613 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 0.0, y: 0.0 });
614 build_visuals_in_view(self, &vp, view, VisualParams::default())
615 }
616
617 pub fn cluster(&self, id: ClusterId) -> Option<&Cluster> {
618 self.clusters.get(&id)
619 }
620
621 pub fn cluster_ids(&self) -> Vec<ClusterId> {
622 let mut ids: Vec<_> = self.clusters.keys().copied().collect();
623 ids.sort_by_key(|id| id.as_u64());
624 ids
625 }
626
627 pub fn cluster_mut(&mut self, id: ClusterId) -> Option<&mut Cluster> {
628 self.clusters.get_mut(&id)
629 }
630
631 pub fn move_member_into_active_cluster_workspace(
632 &mut self,
633 id: ClusterId,
634 member: NodeId,
635 ) -> bool {
636 let Some(node) = self.nodes.remove(&member) else {
637 return false;
638 };
639 let Some(cluster) = self.clusters.get_mut(&id) else {
640 self.nodes.insert(member, node);
641 return false;
642 };
643 if !cluster.is_active()
644 || !cluster.contains(member)
645 || !cluster.insert_workspace_member(node)
646 {
647 if let Some(node) = cluster.remove_workspace_member(member) {
648 self.nodes.insert(member, node);
649 }
650 return false;
651 }
652 true
653 }
654
655 pub fn move_member_out_of_active_cluster_workspace(
656 &mut self,
657 id: ClusterId,
658 member: NodeId,
659 ) -> bool {
660 let Some(cluster) = self.clusters.get_mut(&id) else {
661 return false;
662 };
663 if !cluster.is_active() {
664 return false;
665 }
666 let Some(node) = cluster.remove_workspace_member(member) else {
667 return false;
668 };
669 self.insert_existing(node);
670 true
671 }
672
673 pub fn remove_cluster(&mut self, id: ClusterId) -> Option<Cluster> {
675 self.clusters.remove(&id)
676 }
677
678 pub fn insert_cluster(&mut self, cluster: Cluster) {
680 self.next_cluster = self.next_cluster.max(cluster.id.as_u64() + 1);
682 self.clusters.insert(cluster.id, cluster);
683 }
684
685 pub fn create_cluster(
686 &mut self,
687 members: Vec<NodeId>,
688 ) -> Result<ClusterId, ClusterCreateError> {
689 if members.len() < 2 {
690 return Err(ClusterCreateError::TooFewMembers);
691 }
692
693 if find_duplicate_member(&members).is_some() {
694 return Err(ClusterCreateError::DuplicateMember);
695 }
696
697 for &member in &members {
698 if self.node(member).is_none() {
699 return Err(ClusterCreateError::MissingNode(member));
700 }
701 if self.cluster_id_for_member_public(member).is_some() {
702 return Err(ClusterCreateError::AlreadyClustered(member));
703 }
704 }
705
706 let id = ClusterId::new(self.next_cluster);
707 self.next_cluster += 1;
708
709 let mut any_member_pinned = false;
710 for &member in &members {
711 if self.node(member).is_some_and(|n| n.pinned) {
712 any_member_pinned = true;
713 let _ = self.set_pinned(member, false);
714 }
715 }
716
717 let mut cluster = Cluster::new(id, members).ok_or(ClusterCreateError::TooFewMembers)?;
718 cluster.pinned = any_member_pinned;
719 self.clusters.insert(id, cluster);
720 Ok(id)
721 }
722
723 pub fn cluster_id_for_core_public(&self, core: NodeId) -> Option<ClusterId> {
724 self.clusters
725 .iter()
726 .find_map(|(&cid, c)| (c.core == Some(core)).then_some(cid))
727 }
728
729 pub fn cluster_id_for_member_public(&self, member: NodeId) -> Option<ClusterId> {
730 self.clusters
731 .iter()
732 .find_map(|(&cid, c)| c.contains(member).then_some(cid))
733 }
734
735 pub fn add_member_to_cluster(
736 &mut self,
737 id: ClusterId,
738 member: NodeId,
739 ) -> Result<(), ClusterAddMemberError> {
740 if self.node(member).is_none() {
741 return Err(ClusterAddMemberError::MissingNode(member));
742 }
743 if self.cluster_id_for_member_public(member).is_some() {
744 return Err(ClusterAddMemberError::AlreadyClustered(member));
745 }
746 let is_pinned = self.node(member).is_some_and(|n| n.pinned);
747 if is_pinned {
748 let _ = self.set_pinned(member, false);
749 }
750
751 let Some(cluster) = self.clusters.get_mut(&id) else {
752 return Err(ClusterAddMemberError::MissingCluster);
753 };
754 if !cluster.add_member(member) {
755 return Err(ClusterAddMemberError::AlreadyClustered(member));
756 }
757
758 if is_pinned {
759 cluster.pinned = true;
760 }
761
762 Ok(())
763 }
764
765 pub fn add_member_to_cluster_front(
766 &mut self,
767 id: ClusterId,
768 member: NodeId,
769 ) -> Result<(), ClusterAddMemberError> {
770 if self.node(member).is_none() {
771 return Err(ClusterAddMemberError::MissingNode(member));
772 }
773 if self.cluster_id_for_member_public(member).is_some() {
774 return Err(ClusterAddMemberError::AlreadyClustered(member));
775 }
776
777 let is_pinned = self.node(member).is_some_and(|n| n.pinned);
778 if is_pinned {
779 let _ = self.set_pinned(member, false);
780 }
781
782 let Some(cluster) = self.clusters.get_mut(&id) else {
783 return Err(ClusterAddMemberError::MissingCluster);
784 };
785 if !cluster.add_member_front(member) {
786 return Err(ClusterAddMemberError::AlreadyClustered(member));
787 }
788
789 if is_pinned {
790 cluster.pinned = true;
791 }
792
793 Ok(())
794 }
795
796 pub fn remove_member_from_cluster(
797 &mut self,
798 id: ClusterId,
799 member: NodeId,
800 ) -> Option<ClusterRemoveMemberOutcome> {
801 let Some(cluster) = self.clusters.get_mut(&id) else {
802 return None;
803 };
804 cluster.remove_member(member)
805 }
806
807 pub fn reorder_cluster_members(
808 &mut self,
809 id: ClusterId,
810 ordered_members: Vec<NodeId>,
811 ) -> Result<(), ClusterReorderError> {
812 let Some(cluster) = self.clusters.get_mut(&id) else {
813 return Err(ClusterReorderError::MissingCluster);
814 };
815 for &member in &ordered_members {
816 if !cluster.contains(member) {
817 return Err(ClusterReorderError::UnknownMember(member));
818 }
819 }
820 if !cluster.reorder_members(ordered_members) {
821 return Err(ClusterReorderError::InvalidMembers);
822 }
823 Ok(())
824 }
825
826 pub fn promote_cluster_member_to_master(
827 &mut self,
828 id: ClusterId,
829 member: NodeId,
830 ) -> Result<(), ClusterReorderError> {
831 let Some(cluster) = self.clusters.get_mut(&id) else {
832 return Err(ClusterReorderError::MissingCluster);
833 };
834 if !cluster.contains(member) {
835 return Err(ClusterReorderError::UnknownMember(member));
836 }
837 if !cluster.promote_member_to_master(member) {
838 return Err(ClusterReorderError::InvalidMembers);
839 }
840 Ok(())
841 }
842
843 pub fn swap_cluster_overflow_member_with_visible(
844 &mut self,
845 id: ClusterId,
846 overflow_member: NodeId,
847 visible_member: NodeId,
848 max_stack: usize,
849 ) -> bool {
850 let Some(cluster) = self.clusters.get_mut(&id) else {
851 return false;
852 };
853 cluster.swap_overflow_member_with_visible(overflow_member, visible_member, max_stack)
854 }
855
856 pub fn reorder_cluster_overflow_member(
857 &mut self,
858 id: ClusterId,
859 member: NodeId,
860 target_overflow_index: usize,
861 max_stack: usize,
862 ) -> bool {
863 let Some(cluster) = self.clusters.get_mut(&id) else {
864 return false;
865 };
866 cluster.reorder_overflow_member(member, target_overflow_index, max_stack)
867 }
868
869 pub fn cycle_cluster_stacking_members(
870 &mut self,
871 id: ClusterId,
872 direction: ClusterCycleDirection,
873 ) -> Option<NodeId> {
874 let cluster = self.clusters.get_mut(&id)?;
875 cycle_stacking_members(&mut cluster.members, direction)
876 }
877
878 pub fn dissolve_cluster(&mut self, id: ClusterId) -> bool {
879 self.finish_dissolve_cluster(id)
880 }
881
882 pub fn activate_cluster_workspace(&mut self, id: ClusterId) -> bool {
883 let (members, core_id, already_active) = {
884 let Some(cluster) = self.clusters.get(&id) else {
885 return false;
886 };
887 (
888 cluster.members().to_vec(),
889 cluster.core,
890 cluster.is_active(),
891 )
892 };
893 if already_active {
894 return true;
895 }
896
897 let mut workspace_nodes = HashMap::new();
898 for member in &members {
899 let Some(node) = self.nodes.remove(member) else {
900 return false;
901 };
902 workspace_nodes.insert(*member, node);
903 }
904
905 if let Some(core_id) = core_id {
906 let _ = self.nodes.remove(&core_id);
907 }
908
909 let Some(cluster) = self.clusters.get_mut(&id) else {
910 return false;
911 };
912 cluster.enter_active();
913 for (_, node) in workspace_nodes {
914 let _ = cluster.insert_workspace_member(node);
915 }
916 true
917 }
918
919 pub fn deactivate_cluster_workspace(&mut self, id: ClusterId) -> bool {
920 let workspace_nodes = {
921 let Some(cluster) = self.clusters.get_mut(&id) else {
922 return false;
923 };
924 if !cluster.is_active() {
925 return true;
926 }
927 let Some(active_workspace) = cluster.active_workspace.take() else {
928 cluster.exit_active();
929 return true;
930 };
931 cluster.mode = crate::cluster::ClusterMode::Expanded;
932 active_workspace.nodes
933 };
934
935 for (_, node) in workspace_nodes {
936 self.insert_existing(node);
937 }
938 true
939 }
940
941 pub fn carry_cluster_by_core(&mut self, core: NodeId, to: Vec2) -> bool {
943 if self.cluster_id_for_core_public(core).is_none() {
944 return false;
945 }
946 if self.node(core).is_some_and(|n| n.pinned) {
947 return false;
948 }
949 self.carry(core, to)
950 }
951
952 pub fn collapse_cluster(&mut self, id: ClusterId) -> Option<NodeId> {
954 let (members, already_collapsed, existing_core) = {
955 let c = self.clusters.get(&id)?;
956 (c.members().to_vec(), c.is_collapsed(), c.core)
957 };
958
959 if already_collapsed {
960 return existing_core;
961 }
962
963 if self.cluster(id).is_some_and(|cluster| cluster.is_active()) {
964 let _ = self.deactivate_cluster_workspace(id);
965 }
966
967 for m in &members {
968 self.set_state(*m, NodeState::Node);
969 if let Some(n) = self.node_mut(*m) {
970 n.visibility.set(Visibility::HIDDEN_BY_CLUSTER, true);
971 }
972 }
973
974 let mut sum = Vec2 { x: 0.0, y: 0.0 };
975 for m in &members {
976 let n = self.node(*m)?;
977 sum.x += n.pos.x;
978 sum.y += n.pos.y;
979 }
980 let k = members.len() as f32;
981 let core_pos = Vec2 {
982 x: sum.x / k,
983 y: sum.y / k,
984 };
985
986 let core_id = match existing_core {
987 Some(cid) => {
988 if !self.nodes.contains_key(&cid) {
989 let core = Node {
990 id: cid,
991 kind: NodeKind::Core,
992 state: NodeState::Core,
993 label: format!("Cluster {}", id.as_u64()),
994 pos: core_pos,
995 intrinsic_size: Vec2 { x: 48.0, y: 48.0 },
996 footprint: Vec2 { x: 48.0, y: 48.0 },
997 resize_footprint: None,
998 pinned: false,
999 anchor: false,
1000 visibility: Visibility::NONE,
1001 last_touch_ms: 0,
1002 decay: DecayLevel::Hot,
1003 };
1004 self.nodes.insert(cid, core);
1005 }
1006 cid
1007 }
1008 None => {
1009 let cid = NodeId::new(self.next_node);
1010 self.next_node += 1;
1011
1012 let core = Node {
1013 id: cid,
1014 kind: NodeKind::Core,
1015 state: NodeState::Core,
1016 label: format!("Cluster {}", id.as_u64()),
1017 pos: core_pos,
1018 intrinsic_size: Vec2 { x: 48.0, y: 48.0 },
1019 footprint: Vec2 { x: 48.0, y: 48.0 },
1020 resize_footprint: None,
1021 pinned: false,
1022 anchor: false,
1023 visibility: Visibility::NONE,
1024 last_touch_ms: 0,
1025 decay: DecayLevel::Hot,
1026 };
1027 self.nodes.insert(cid, core);
1028 cid
1029 }
1030 };
1031
1032 if let Some(n) = self.node_mut(core_id) {
1033 n.pos = core_pos;
1034 n.kind = NodeKind::Core;
1035 n.state = NodeState::Core;
1036 n.footprint = Vec2 { x: 48.0, y: 48.0 };
1037 n.intrinsic_size = Vec2 { x: 48.0, y: 48.0 };
1038
1039 n.visibility.clear(Visibility::HIDDEN_BY_CLUSTER);
1040 n.visibility.clear(Visibility::DETACHED);
1041 }
1042
1043 let c = self.clusters.get_mut(&id)?;
1044 let pinned = c.pinned;
1045 c.set_collapsed(true);
1046 c.core = Some(core_id);
1047
1048 if let Some(n) = self.node_mut(core_id) {
1049 n.pinned = pinned;
1050 }
1051
1052 Some(core_id)
1053 }
1054
1055 pub fn expand_cluster(&mut self, id: ClusterId) -> bool {
1057 if self.cluster(id).is_some_and(|cluster| cluster.is_active()) {
1058 return true;
1059 }
1060 let members = {
1061 let c = match self.clusters.get(&id) {
1062 Some(c) => c,
1063 None => return false,
1064 };
1065 if !c.is_collapsed() {
1066 return true;
1067 }
1068 c.members().to_vec()
1069 };
1070
1071 for m in members {
1072 self.set_state(m, NodeState::Active);
1073 if let Some(n) = self.node_mut(m) {
1074 n.visibility.set(Visibility::HIDDEN_BY_CLUSTER, false);
1075 }
1076 }
1077
1078 if let Some(c) = self.clusters.get_mut(&id) {
1079 c.set_collapsed(false);
1080 }
1081 true
1082 }
1083
1084 pub fn insert_existing(&mut self, node: Node) {
1085 self.next_node = self.next_node.max(node.id.as_u64() + 1);
1087 self.nodes.insert(node.id, node);
1088 }
1089
1090 pub fn clusters_iter(&self) -> impl Iterator<Item = &Cluster> {
1091 self.clusters.values()
1092 }
1093
1094 fn finish_dissolve_cluster(&mut self, id: ClusterId) -> bool {
1095 let Some(cluster) = self.clusters.remove(&id) else {
1096 return false;
1097 };
1098
1099 if let Some(active_workspace) = cluster.active_workspace {
1100 for (_, mut node) in active_workspace.nodes {
1101 node.visibility.clear(Visibility::HIDDEN_BY_CLUSTER);
1102 node.visibility.clear(Visibility::DETACHED);
1103 node.state = NodeState::Active;
1104 node.footprint = node.resize_footprint.unwrap_or(node.intrinsic_size);
1105 self.insert_existing(node);
1106 }
1107 } else {
1108 for member in cluster.members() {
1109 let _ = self.set_state(*member, NodeState::Active);
1110 if let Some(node) = self.node_mut(*member) {
1111 node.visibility.clear(Visibility::HIDDEN_BY_CLUSTER);
1112 }
1113 }
1114 }
1115
1116 if let Some(core_id) = cluster.core {
1117 let _ = self.nodes.remove(&core_id);
1118 }
1119
1120 true
1121 }
1122}
1123
1124fn find_duplicate_member(members: &[NodeId]) -> Option<NodeId> {
1125 let mut seen = std::collections::HashSet::new();
1126 for member in members {
1127 if !seen.insert(*member) {
1128 return Some(*member);
1129 }
1130 }
1131 None
1132}
1133
1134impl Default for Field {
1135 fn default() -> Self {
1136 Self::new()
1137 }
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142 use super::*;
1143
1144 #[test]
1145 fn cluster_create_rejects_missing_nodes() {
1146 let mut f = Field::new();
1147 let missing = NodeId::new(999);
1148 assert_eq!(
1149 f.create_cluster(vec![missing]),
1150 Err(ClusterCreateError::TooFewMembers)
1151 );
1152 }
1153
1154 #[test]
1155 fn cluster_create_rejects_singletons() {
1156 let mut f = Field::new();
1157 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1158
1159 assert_eq!(
1160 f.create_cluster(vec![a]),
1161 Err(ClusterCreateError::TooFewMembers)
1162 );
1163 }
1164
1165 #[test]
1166 fn cluster_create_rejects_duplicate_members() {
1167 let mut f = Field::new();
1168 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1169 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1170
1171 assert_eq!(
1172 f.create_cluster(vec![a, a, b]),
1173 Err(ClusterCreateError::DuplicateMember)
1174 );
1175 }
1176
1177 #[test]
1178 fn collapse_cluster_creates_core_and_shrinks_members() {
1179 let mut f = Field::new();
1180 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1181 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1182
1183 let cid = f.create_cluster(vec![a, b]).unwrap();
1184 let core = f.collapse_cluster(cid).unwrap();
1185
1186 assert_eq!(f.node(a).unwrap().state, NodeState::Node);
1187 assert_eq!(f.node(b).unwrap().state, NodeState::Node);
1188 assert_eq!(f.node(a).unwrap().footprint, Vec2 { x: 24.0, y: 24.0 });
1189
1190 assert!(
1191 f.node(a)
1192 .unwrap()
1193 .visibility
1194 .has(Visibility::HIDDEN_BY_CLUSTER)
1195 );
1196 assert!(
1197 f.node(b)
1198 .unwrap()
1199 .visibility
1200 .has(Visibility::HIDDEN_BY_CLUSTER)
1201 );
1202 assert!(!f.is_visible(a));
1203 assert!(!f.is_visible(b));
1204
1205 let cn = f.node(core).unwrap();
1206 assert_eq!(cn.kind, NodeKind::Core);
1207 assert_eq!(cn.state, NodeState::Core);
1208 assert_eq!(cn.footprint, Vec2 { x: 48.0, y: 48.0 });
1209 assert!(f.is_visible(core));
1210
1211 let c = f.cluster(cid).unwrap();
1212 assert!(c.is_collapsed());
1213 assert_eq!(c.core, Some(core));
1214 }
1215
1216 #[test]
1217 fn collapsing_active_cluster_restores_visible_core_to_field_queries() {
1218 let mut f = Field::new();
1219 let a = f.spawn_surface("A", Vec2 { x: -20.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1220 let b = f.spawn_surface("B", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1221 let c = f.spawn_surface("C", Vec2 { x: 200.0, y: 0.0 }, Vec2 { x: 80.0, y: 40.0 });
1222
1223 let cid = f.create_cluster(vec![a, b]).unwrap();
1224 let first_core = f.collapse_cluster(cid).unwrap();
1225 assert!(f.activate_cluster_workspace(cid));
1226 assert!(f.node(first_core).is_none());
1227
1228 let core = f.collapse_cluster(cid).unwrap();
1229 assert_eq!(core, first_core);
1230
1231 let core_node = f.node(core).unwrap();
1232 assert_eq!(core_node.kind, NodeKind::Core);
1233 assert_eq!(core_node.state, NodeState::Core);
1234 assert!(f.nodes().contains_key(&core));
1235 assert!(f.participates_in_field_view(core));
1236 assert!(f.is_visible(core));
1237
1238 let view = Rect {
1239 min: Vec2 {
1240 x: -100.0,
1241 y: -100.0,
1242 },
1243 max: Vec2 { x: 100.0, y: 100.0 },
1244 };
1245
1246 assert!(f.in_view(view).contains(&core));
1247 assert!(f.in_view_all(view).contains(&core));
1248 assert!(f.visuals_visible().iter().any(|visual| visual.id == core));
1249 assert!(
1250 f.visuals_in_view(view)
1251 .iter()
1252 .any(|visual| visual.id == core)
1253 );
1254 assert!(!f.in_view(view).contains(&a));
1255 assert!(!f.in_view(view).contains(&b));
1256 assert!(!f.is_visible(a));
1257 assert!(!f.is_visible(b));
1258 assert!(f.is_visible(c));
1259 }
1260
1261 #[test]
1262 fn expand_cluster_restores_members_active_and_visible() {
1263 let mut f = Field::new();
1264 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1265 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1266
1267 let cid = f.create_cluster(vec![a, b]).unwrap();
1268 f.collapse_cluster(cid).unwrap();
1269
1270 assert!(f.expand_cluster(cid));
1271
1272 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
1273 assert_eq!(f.node(b).unwrap().state, NodeState::Active);
1274 assert_eq!(f.node(a).unwrap().footprint, Vec2 { x: 100.0, y: 50.0 });
1275
1276 assert!(
1277 !f.node(a)
1278 .unwrap()
1279 .visibility
1280 .has(Visibility::HIDDEN_BY_CLUSTER)
1281 );
1282 assert!(f.is_visible(a));
1283
1284 let c = f.cluster(cid).unwrap();
1285 assert!(!c.is_collapsed());
1286 }
1287
1288 #[test]
1289 fn carry_respects_pinned() {
1290 let mut f = Field::new();
1291 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1292
1293 assert!(f.carry(id, Vec2 { x: 5.0, y: 5.0 }));
1294 assert_eq!(f.node(id).unwrap().pos, Vec2 { x: 5.0, y: 5.0 });
1295
1296 assert!(f.set_pinned(id, true));
1297 assert!(!f.carry(id, Vec2 { x: 9.0, y: 9.0 }));
1298 assert_eq!(f.node(id).unwrap().pos, Vec2 { x: 5.0, y: 5.0 });
1299 }
1300
1301 #[test]
1302 fn in_view_finds_intersections() {
1303 let mut f = Field::new();
1304 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1305 let _b = f.spawn_surface("B", Vec2 { x: 100.0, y: 100.0 }, Vec2 { x: 10.0, y: 10.0 });
1306
1307 let view = Rect {
1308 min: Vec2 { x: -20.0, y: -20.0 },
1309 max: Vec2 { x: 20.0, y: 20.0 },
1310 };
1311
1312 let ids = f.in_view_all(view);
1313 assert_eq!(ids, vec![a]);
1314 }
1315
1316 #[test]
1317 fn in_view_skips_hidden_nodes() {
1318 let mut f = Field::new();
1319 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1320
1321 assert!(f.set_hidden(a, true));
1322
1323 let view = Rect {
1324 min: Vec2 { x: -20.0, y: -20.0 },
1325 max: Vec2 { x: 20.0, y: 20.0 },
1326 };
1327
1328 let ids = f.in_view(view);
1329 assert!(ids.is_empty());
1330 assert!(!f.is_visible(a));
1331 }
1332
1333 #[test]
1334 fn set_state_changes_footprint() {
1335 let mut f = Field::new();
1336 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1337
1338 assert_eq!(f.node(id).unwrap().footprint, Vec2 { x: 100.0, y: 50.0 });
1339
1340 assert!(f.set_state(id, NodeState::Node));
1341 assert_eq!(f.node(id).unwrap().footprint, Vec2 { x: 24.0, y: 24.0 });
1342
1343 assert!(f.set_state(id, NodeState::Active));
1344 assert_eq!(f.node(id).unwrap().footprint, Vec2 { x: 100.0, y: 50.0 });
1345 }
1346
1347 #[test]
1348 fn touch_sets_last_touch_and_wakes_node() {
1349 let mut f = Field::new();
1350 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1351
1352 assert!(f.set_decay_level(id, DecayLevel::Cold));
1353 assert_eq!(f.node(id).unwrap().state, NodeState::Node);
1354
1355 assert!(f.touch(id, 1234));
1356 let n = f.node(id).unwrap();
1357 assert_eq!(n.last_touch_ms, 1234);
1358 assert_eq!(n.decay, DecayLevel::Hot);
1359 assert_eq!(n.state, NodeState::Active);
1360 }
1361
1362 #[test]
1363 fn set_decay_level_maps_to_representation_state() {
1364 let mut f = Field::new();
1365 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1366
1367 assert!(f.set_decay_level(id, DecayLevel::Hot));
1368 assert_eq!(f.node(id).unwrap().decay, DecayLevel::Hot);
1369 assert_eq!(f.node(id).unwrap().state, NodeState::Active);
1370
1371 assert!(f.set_decay_level(id, DecayLevel::Cold));
1372 assert_eq!(f.node(id).unwrap().decay, DecayLevel::Cold);
1373 assert_eq!(f.node(id).unwrap().state, NodeState::Node);
1374 }
1375
1376 #[test]
1377 fn core_ignores_set_decay_level() {
1378 let mut f = Field::new();
1379 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1380 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1381
1382 let cid = f.create_cluster(vec![a, b]).unwrap();
1383 let core = f.collapse_cluster(cid).unwrap();
1384
1385 assert!(f.set_decay_level(core, DecayLevel::Cold));
1386 let n = f.node(core).unwrap();
1387 assert_eq!(n.kind, NodeKind::Core);
1388 assert_eq!(n.state, NodeState::Core);
1389 }
1390
1391 #[test]
1392 fn carry_cluster_by_core_moves_only_core_representation() {
1393 let mut f = Field::new();
1394 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1395 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1396
1397 let cid = f.create_cluster(vec![a, b]).unwrap();
1398 let core = f.collapse_cluster(cid).unwrap();
1399
1400 let core_before = f.node(core).unwrap().pos;
1401 let a_before = f.node(a).unwrap().pos;
1402 let b_before = f.node(b).unwrap().pos;
1403
1404 assert!(f.carry_cluster_by_core(core, Vec2 { x: 100.0, y: 50.0 }));
1405
1406 let core_after = f.node(core).unwrap().pos;
1407 let a_after = f.node(a).unwrap().pos;
1408 let b_after = f.node(b).unwrap().pos;
1409
1410 assert_eq!(core_after, Vec2 { x: 100.0, y: 50.0 });
1411 assert_ne!(core_after, core_before);
1412 assert_eq!(a_after, a_before);
1413 assert_eq!(b_after, b_before);
1414 }
1415
1416 #[test]
1417 fn carry_cluster_by_core_respects_pinned() {
1418 let mut f = Field::new();
1419 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1420 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1421
1422 let cid = f.create_cluster(vec![a, b]).unwrap();
1423 let core = f.collapse_cluster(cid).unwrap();
1424
1425 let core_pos = f.node(core).unwrap().pos;
1426 let a_pos = f.node(a).unwrap().pos;
1427 let b_pos = f.node(b).unwrap().pos;
1428
1429 assert!(f.set_pinned(core, true));
1430 assert!(!f.carry_cluster_by_core(core, Vec2 { x: 999.0, y: 999.0 }));
1431
1432 assert_eq!(f.node(core).unwrap().pos, core_pos);
1433 assert_eq!(f.node(a).unwrap().pos, a_pos);
1434 assert_eq!(f.node(b).unwrap().pos, b_pos);
1435 }
1436
1437 #[test]
1438 fn visuals_skip_hidden_nodes() {
1439 let mut f = Field::new();
1440 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1441 let b = f.spawn_surface("B", Vec2 { x: 50.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1442
1443 assert!(f.set_hidden(b, true));
1444
1445 let vis = f.visuals_visible();
1446 assert_eq!(vis.len(), 1);
1447 assert_eq!(vis[0].id, a);
1448 }
1449
1450 #[test]
1451 fn remove_member_requires_explicit_dissolve_for_two_member_cluster() {
1452 let mut f = Field::new();
1453 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1454 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1455
1456 let cid = f.create_cluster(vec![a, b]).unwrap();
1457
1458 assert_eq!(
1459 f.remove_member_from_cluster(cid, a),
1460 Some(ClusterRemoveMemberOutcome::RequiresDissolve)
1461 );
1462 let cluster = f.cluster(cid).unwrap();
1463 assert_eq!(cluster.members(), &[a, b]);
1464 assert_eq!(cluster.master(), a);
1465 }
1466
1467 #[test]
1468 fn raw_member_removal_dissolves_two_member_cluster_without_leaking_singleton() {
1469 let mut f = Field::new();
1470 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1471 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1472
1473 let cid = f.create_cluster(vec![a, b]).unwrap();
1474 let core = f.collapse_cluster(cid).unwrap();
1475
1476 let (_, effect) = f.remove_node_cluster_safe(a).unwrap();
1477
1478 assert_eq!(effect, Some(RemoveNodeClusterEffect::DissolvedCluster(cid)));
1479 assert!(f.cluster(cid).is_none());
1480 assert!(f.node(core).is_none());
1481 assert!(f.node(a).is_none());
1482 assert!(f.node(b).is_some());
1483 assert!(f.is_visible(b));
1484 }
1485
1486 #[test]
1487 fn raw_member_removal_keeps_larger_cluster_valid() {
1488 let mut f = Field::new();
1489 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1490 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1491 let c = f.spawn_surface("C", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1492
1493 let cid = f.create_cluster(vec![a, b, c]).unwrap();
1494
1495 let (_, effect) = f.remove_node_cluster_safe(c).unwrap();
1496
1497 assert_eq!(effect, Some(RemoveNodeClusterEffect::RemovedMember(cid)));
1498 let cluster = f.cluster(cid).unwrap();
1499 assert_eq!(cluster.members(), &[a, b]);
1500 }
1501
1502 #[test]
1503 fn promote_and_reorder_preserve_explicit_master_contract() {
1504 let mut f = Field::new();
1505 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1506 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1507 let c = f.spawn_surface("C", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1508
1509 let cid = f.create_cluster(vec![a, b, c]).unwrap();
1510 f.promote_cluster_member_to_master(cid, c).unwrap();
1511 assert_eq!(f.cluster(cid).unwrap().members(), &[c, a, b]);
1512 assert_eq!(f.cluster(cid).unwrap().master(), c);
1513
1514 f.reorder_cluster_members(cid, vec![b, c, a]).unwrap();
1515 assert_eq!(f.cluster(cid).unwrap().members(), &[b, c, a]);
1516 assert_eq!(f.cluster(cid).unwrap().master(), b);
1517 assert_eq!(f.cluster(cid).unwrap().secondaries(), &[c, a]);
1518 }
1519
1520 #[test]
1521 fn active_cluster_members_do_not_participate_in_field_dynamics() {
1522 let mut f = Field::new();
1523 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1524 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1525
1526 let cid = f.create_cluster(vec![a, b]).unwrap();
1527 assert!(f.activate_cluster_workspace(cid));
1528
1529 assert!(f.is_active_cluster_member(a));
1530 assert!(!f.participates_in_field_dynamics(a));
1531 assert!(!f.participates_in_field_activity(a));
1532 }
1533
1534 #[test]
1535 fn spawning_into_active_cluster_workspace_bypasses_field_storage() {
1536 let mut f = Field::new();
1537 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1538 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1539
1540 let cid = f.create_cluster(vec![a, b]).unwrap();
1541 assert!(f.activate_cluster_workspace(cid));
1542
1543 let c = f
1544 .spawn_surface_in_active_cluster(cid, "C", Vec2 { x: 30.0, y: 20.0 })
1545 .unwrap();
1546
1547 assert!(f.node(c).is_some());
1548 assert!(!f.nodes().contains_key(&c));
1549 assert!(f.is_active_cluster_member(c));
1550 assert_eq!(f.cluster(cid).unwrap().members(), &[a, b, c]);
1551 }
1552
1553 #[test]
1554 fn spawning_into_active_cluster_workspace_front_inserts_new_member_first() {
1555 let mut f = Field::new();
1556 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1557 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1558
1559 let cid = f.create_cluster(vec![a, b]).unwrap();
1560 assert!(f.activate_cluster_workspace(cid));
1561
1562 let c = f
1563 .spawn_surface_in_active_cluster_front(cid, "C", Vec2 { x: 30.0, y: 20.0 })
1564 .unwrap();
1565
1566 assert_eq!(f.cluster(cid).unwrap().members(), &[c, a, b]);
1567 }
1568
1569 #[test]
1570 fn adding_member_to_cluster_front_inserts_new_member_first() {
1571 let mut f = Field::new();
1572 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1573 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1574 let c = f.spawn_surface("C", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1575
1576 let cid = f.create_cluster(vec![a, b]).unwrap();
1577 f.add_member_to_cluster_front(cid, c).unwrap();
1578
1579 assert_eq!(f.cluster(cid).unwrap().members(), &[c, a, b]);
1580 }
1581
1582 #[test]
1583 fn active_cluster_workspace_members_support_state_and_position_updates() {
1584 let mut f = Field::new();
1585 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1586 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1587
1588 let cid = f.create_cluster(vec![a, b]).unwrap();
1589 assert!(f.activate_cluster_workspace(cid));
1590
1591 assert!(f.set_state(a, NodeState::Node));
1592 assert!(f.carry(a, Vec2 { x: 400.0, y: 300.0 }));
1593
1594 let node = f.node(a).unwrap();
1595 assert_eq!(node.state, NodeState::Node);
1596 assert_eq!(node.pos, Vec2 { x: 400.0, y: 300.0 });
1597 assert!(f.bounds(a).is_some());
1598 }
1599
1600 #[test]
1601 fn cluster_workspace_layout_only_tiles_first_four_members() {
1602 let mut f = Field::new();
1603 let members = (0..6)
1604 .map(|index| {
1605 f.spawn_surface(
1606 format!("N{}", index),
1607 Vec2 {
1608 x: index as f32 * 10.0,
1609 y: 0.0,
1610 },
1611 Vec2 { x: 10.0, y: 10.0 },
1612 )
1613 })
1614 .collect::<Vec<_>>();
1615
1616 let cid = f.create_cluster(members.clone()).unwrap();
1617 let cluster = f.cluster(cid).unwrap();
1618 let layout = cluster.workspace_layout(
1619 crate::tiling::Rect {
1620 x: 0.0,
1621 y: 0.0,
1622 w: 1000.0,
1623 h: 600.0,
1624 },
1625 3,
1626 );
1627
1628 assert_eq!(cluster.visible_members(3), &members[..4]);
1629 assert_eq!(cluster.overflow_members(3), &members[4..]);
1630 assert_eq!(layout.tiles.len(), 4);
1631 assert!(
1632 layout
1633 .tiles
1634 .iter()
1635 .all(|tile| members[..4].contains(&tile.id))
1636 );
1637 }
1638
1639 #[test]
1640 fn swapping_overflow_member_with_visible_preserves_queue_order() {
1641 let mut f = Field::new();
1642 let members = (0..6)
1643 .map(|index| {
1644 f.spawn_surface(
1645 format!("N{}", index),
1646 Vec2 {
1647 x: index as f32 * 10.0,
1648 y: 0.0,
1649 },
1650 Vec2 { x: 10.0, y: 10.0 },
1651 )
1652 })
1653 .collect::<Vec<_>>();
1654
1655 let cid = f.create_cluster(members.clone()).unwrap();
1656 assert!(f.swap_cluster_overflow_member_with_visible(cid, members[4], members[2], 3));
1657
1658 let cluster = f.cluster(cid).unwrap();
1659 assert_eq!(
1660 cluster.members(),
1661 &[
1662 members[0], members[1], members[4], members[3], members[2], members[5]
1663 ]
1664 );
1665 assert_eq!(
1666 cluster.visible_members(3),
1667 &[members[0], members[1], members[4], members[3]]
1668 );
1669 assert_eq!(cluster.overflow_members(3), &[members[2], members[5]]);
1670 }
1671
1672 #[test]
1673 fn reordering_overflow_members_updates_queue_order_only() {
1674 let mut f = Field::new();
1675 let members = (0..7)
1676 .map(|index| {
1677 f.spawn_surface(
1678 format!("N{}", index),
1679 Vec2 {
1680 x: index as f32 * 10.0,
1681 y: 0.0,
1682 },
1683 Vec2 { x: 10.0, y: 10.0 },
1684 )
1685 })
1686 .collect::<Vec<_>>();
1687
1688 let cid = f.create_cluster(members.clone()).unwrap();
1689 assert!(f.reorder_cluster_overflow_member(cid, members[6], 0, 3));
1690
1691 let cluster = f.cluster(cid).unwrap();
1692 assert_eq!(cluster.visible_members(3), &members[..4]);
1693 assert_eq!(
1694 cluster.overflow_members(3),
1695 &[members[6], members[4], members[5]]
1696 );
1697 }
1698
1699 #[test]
1700 fn touch_is_noop_for_cluster_members() {
1701 let mut f = Field::new();
1702 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1703 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1704
1705 let cid = f.create_cluster(vec![a, b]).unwrap();
1706 let before = f.node(a).unwrap().last_touch_ms;
1707
1708 assert!(f.touch(a, 9999));
1709 assert_eq!(f.node(a).unwrap().last_touch_ms, before);
1710
1711 assert!(f.activate_cluster_workspace(cid));
1712 assert!(f.touch(a, 12345));
1713 assert_eq!(f.node(a).unwrap().last_touch_ms, before);
1714 }
1715
1716 #[test]
1717 fn active_cluster_members_are_excluded_from_field_view_queries() {
1718 let mut f = Field::new();
1719 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1720 let b = f.spawn_surface("B", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1721 let c = f.spawn_surface("C", Vec2 { x: 200.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1722
1723 let cid = f.create_cluster(vec![a, b]).unwrap();
1724 assert!(f.activate_cluster_workspace(cid));
1725
1726 let view = Rect {
1727 min: Vec2 { x: -50.0, y: -50.0 },
1728 max: Vec2 { x: 50.0, y: 50.0 },
1729 };
1730
1731 assert!(!f.in_view(view).contains(&a));
1732 assert!(!f.in_view(view).contains(&b));
1733 assert_eq!(f.in_view(view), vec![]);
1734 assert_eq!(f.in_view_all(view), vec![]);
1735 assert_eq!(f.visuals_visible().len(), 1);
1736 assert_eq!(f.visuals_visible()[0].id, c);
1737 }
1738}