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 cluster = Cluster::new(id, members).ok_or(ClusterCreateError::TooFewMembers)?;
710 self.clusters.insert(id, cluster);
711 Ok(id)
712 }
713
714 pub fn cluster_id_for_core_public(&self, core: NodeId) -> Option<ClusterId> {
715 self.clusters
716 .iter()
717 .find_map(|(&cid, c)| (c.core == Some(core)).then_some(cid))
718 }
719
720 pub fn cluster_id_for_member_public(&self, member: NodeId) -> Option<ClusterId> {
721 self.clusters
722 .iter()
723 .find_map(|(&cid, c)| c.contains(member).then_some(cid))
724 }
725
726 pub fn add_member_to_cluster(
727 &mut self,
728 id: ClusterId,
729 member: NodeId,
730 ) -> Result<(), ClusterAddMemberError> {
731 if self.node(member).is_none() {
732 return Err(ClusterAddMemberError::MissingNode(member));
733 }
734 if self.cluster_id_for_member_public(member).is_some() {
735 return Err(ClusterAddMemberError::AlreadyClustered(member));
736 }
737 let Some(cluster) = self.clusters.get_mut(&id) else {
738 return Err(ClusterAddMemberError::MissingCluster);
739 };
740 if !cluster.add_member(member) {
741 return Err(ClusterAddMemberError::AlreadyClustered(member));
742 }
743 Ok(())
744 }
745
746 pub fn add_member_to_cluster_front(
747 &mut self,
748 id: ClusterId,
749 member: NodeId,
750 ) -> Result<(), ClusterAddMemberError> {
751 if self.node(member).is_none() {
752 return Err(ClusterAddMemberError::MissingNode(member));
753 }
754 if self.cluster_id_for_member_public(member).is_some() {
755 return Err(ClusterAddMemberError::AlreadyClustered(member));
756 }
757 let Some(cluster) = self.clusters.get_mut(&id) else {
758 return Err(ClusterAddMemberError::MissingCluster);
759 };
760 if !cluster.add_member_front(member) {
761 return Err(ClusterAddMemberError::AlreadyClustered(member));
762 }
763 Ok(())
764 }
765
766 pub fn remove_member_from_cluster(
767 &mut self,
768 id: ClusterId,
769 member: NodeId,
770 ) -> Option<ClusterRemoveMemberOutcome> {
771 let Some(cluster) = self.clusters.get_mut(&id) else {
772 return None;
773 };
774 cluster.remove_member(member)
775 }
776
777 pub fn reorder_cluster_members(
778 &mut self,
779 id: ClusterId,
780 ordered_members: Vec<NodeId>,
781 ) -> Result<(), ClusterReorderError> {
782 let Some(cluster) = self.clusters.get_mut(&id) else {
783 return Err(ClusterReorderError::MissingCluster);
784 };
785 for &member in &ordered_members {
786 if !cluster.contains(member) {
787 return Err(ClusterReorderError::UnknownMember(member));
788 }
789 }
790 if !cluster.reorder_members(ordered_members) {
791 return Err(ClusterReorderError::InvalidMembers);
792 }
793 Ok(())
794 }
795
796 pub fn promote_cluster_member_to_master(
797 &mut self,
798 id: ClusterId,
799 member: NodeId,
800 ) -> Result<(), ClusterReorderError> {
801 let Some(cluster) = self.clusters.get_mut(&id) else {
802 return Err(ClusterReorderError::MissingCluster);
803 };
804 if !cluster.contains(member) {
805 return Err(ClusterReorderError::UnknownMember(member));
806 }
807 if !cluster.promote_member_to_master(member) {
808 return Err(ClusterReorderError::InvalidMembers);
809 }
810 Ok(())
811 }
812
813 pub fn swap_cluster_overflow_member_with_visible(
814 &mut self,
815 id: ClusterId,
816 overflow_member: NodeId,
817 visible_member: NodeId,
818 max_stack: usize,
819 ) -> bool {
820 let Some(cluster) = self.clusters.get_mut(&id) else {
821 return false;
822 };
823 cluster.swap_overflow_member_with_visible(overflow_member, visible_member, max_stack)
824 }
825
826 pub fn reorder_cluster_overflow_member(
827 &mut self,
828 id: ClusterId,
829 member: NodeId,
830 target_overflow_index: usize,
831 max_stack: usize,
832 ) -> bool {
833 let Some(cluster) = self.clusters.get_mut(&id) else {
834 return false;
835 };
836 cluster.reorder_overflow_member(member, target_overflow_index, max_stack)
837 }
838
839 pub fn cycle_cluster_stacking_members(
840 &mut self,
841 id: ClusterId,
842 direction: ClusterCycleDirection,
843 ) -> Option<NodeId> {
844 let cluster = self.clusters.get_mut(&id)?;
845 cycle_stacking_members(&mut cluster.members, direction)
846 }
847
848 pub fn dissolve_cluster(&mut self, id: ClusterId) -> bool {
849 self.finish_dissolve_cluster(id)
850 }
851
852 pub fn activate_cluster_workspace(&mut self, id: ClusterId) -> bool {
853 let (members, core_id, already_active) = {
854 let Some(cluster) = self.clusters.get(&id) else {
855 return false;
856 };
857 (
858 cluster.members().to_vec(),
859 cluster.core,
860 cluster.is_active(),
861 )
862 };
863 if already_active {
864 return true;
865 }
866
867 let mut workspace_nodes = HashMap::new();
868 for member in &members {
869 let Some(node) = self.nodes.remove(member) else {
870 return false;
871 };
872 workspace_nodes.insert(*member, node);
873 }
874
875 if let Some(core_id) = core_id {
876 let _ = self.nodes.remove(&core_id);
877 }
878
879 let Some(cluster) = self.clusters.get_mut(&id) else {
880 return false;
881 };
882 cluster.enter_active();
883 for (_, node) in workspace_nodes {
884 let _ = cluster.insert_workspace_member(node);
885 }
886 true
887 }
888
889 pub fn deactivate_cluster_workspace(&mut self, id: ClusterId) -> bool {
890 let workspace_nodes = {
891 let Some(cluster) = self.clusters.get_mut(&id) else {
892 return false;
893 };
894 if !cluster.is_active() {
895 return true;
896 }
897 let Some(active_workspace) = cluster.active_workspace.take() else {
898 cluster.exit_active();
899 return true;
900 };
901 cluster.mode = crate::cluster::ClusterMode::Expanded;
902 active_workspace.nodes
903 };
904
905 for (_, node) in workspace_nodes {
906 self.insert_existing(node);
907 }
908 true
909 }
910
911 pub fn carry_cluster_by_core(&mut self, core: NodeId, to: Vec2) -> bool {
913 if self.cluster_id_for_core_public(core).is_none() {
914 return false;
915 }
916 if self.node(core).is_some_and(|n| n.pinned) {
917 return false;
918 }
919 self.carry(core, to)
920 }
921
922 pub fn collapse_cluster(&mut self, id: ClusterId) -> Option<NodeId> {
924 let (members, already_collapsed, existing_core) = {
925 let c = self.clusters.get(&id)?;
926 (c.members().to_vec(), c.is_collapsed(), c.core)
927 };
928
929 if already_collapsed {
930 return existing_core;
931 }
932
933 if self.cluster(id).is_some_and(|cluster| cluster.is_active()) {
934 let _ = self.deactivate_cluster_workspace(id);
935 }
936
937 for m in &members {
938 self.set_state(*m, NodeState::Node);
939 if let Some(n) = self.node_mut(*m) {
940 n.visibility.set(Visibility::HIDDEN_BY_CLUSTER, true);
941 }
942 }
943
944 let mut sum = Vec2 { x: 0.0, y: 0.0 };
945 for m in &members {
946 let n = self.node(*m)?;
947 sum.x += n.pos.x;
948 sum.y += n.pos.y;
949 }
950 let k = members.len() as f32;
951 let core_pos = Vec2 {
952 x: sum.x / k,
953 y: sum.y / k,
954 };
955
956 let core_id = match existing_core {
957 Some(cid) => {
958 if !self.nodes.contains_key(&cid) {
959 let core = Node {
960 id: cid,
961 kind: NodeKind::Core,
962 state: NodeState::Core,
963 label: format!("Cluster {}", id.as_u64()),
964 pos: core_pos,
965 intrinsic_size: Vec2 { x: 48.0, y: 48.0 },
966 footprint: Vec2 { x: 48.0, y: 48.0 },
967 resize_footprint: None,
968 pinned: false,
969 anchor: false,
970 visibility: Visibility::NONE,
971 last_touch_ms: 0,
972 decay: DecayLevel::Hot,
973 };
974 self.nodes.insert(cid, core);
975 }
976 cid
977 }
978 None => {
979 let cid = NodeId::new(self.next_node);
980 self.next_node += 1;
981
982 let core = Node {
983 id: cid,
984 kind: NodeKind::Core,
985 state: NodeState::Core,
986 label: format!("Cluster {}", id.as_u64()),
987 pos: core_pos,
988 intrinsic_size: Vec2 { x: 48.0, y: 48.0 },
989 footprint: Vec2 { x: 48.0, y: 48.0 },
990 resize_footprint: None,
991 pinned: false,
992 anchor: false,
993 visibility: Visibility::NONE,
994 last_touch_ms: 0,
995 decay: DecayLevel::Hot,
996 };
997 self.nodes.insert(cid, core);
998 cid
999 }
1000 };
1001
1002 if let Some(n) = self.node_mut(core_id) {
1003 n.pos = core_pos;
1004 n.kind = NodeKind::Core;
1005 n.state = NodeState::Core;
1006 n.footprint = Vec2 { x: 48.0, y: 48.0 };
1007 n.intrinsic_size = Vec2 { x: 48.0, y: 48.0 };
1008
1009 n.visibility.clear(Visibility::HIDDEN_BY_CLUSTER);
1010 n.visibility.clear(Visibility::DETACHED);
1011 }
1012
1013 let c = self.clusters.get_mut(&id)?;
1014 c.set_collapsed(true);
1015 c.core = Some(core_id);
1016
1017 Some(core_id)
1018 }
1019
1020 pub fn expand_cluster(&mut self, id: ClusterId) -> bool {
1022 if self.cluster(id).is_some_and(|cluster| cluster.is_active()) {
1023 return true;
1024 }
1025 let members = {
1026 let c = match self.clusters.get(&id) {
1027 Some(c) => c,
1028 None => return false,
1029 };
1030 if !c.is_collapsed() {
1031 return true;
1032 }
1033 c.members().to_vec()
1034 };
1035
1036 for m in members {
1037 self.set_state(m, NodeState::Active);
1038 if let Some(n) = self.node_mut(m) {
1039 n.visibility.set(Visibility::HIDDEN_BY_CLUSTER, false);
1040 }
1041 }
1042
1043 if let Some(c) = self.clusters.get_mut(&id) {
1044 c.set_collapsed(false);
1045 }
1046 true
1047 }
1048
1049 pub fn insert_existing(&mut self, node: Node) {
1050 self.next_node = self.next_node.max(node.id.as_u64() + 1);
1052 self.nodes.insert(node.id, node);
1053 }
1054
1055 pub fn clusters_iter(&self) -> impl Iterator<Item = &Cluster> {
1056 self.clusters.values()
1057 }
1058
1059 fn finish_dissolve_cluster(&mut self, id: ClusterId) -> bool {
1060 let Some(cluster) = self.clusters.remove(&id) else {
1061 return false;
1062 };
1063
1064 if let Some(active_workspace) = cluster.active_workspace {
1065 for (_, mut node) in active_workspace.nodes {
1066 node.visibility.clear(Visibility::HIDDEN_BY_CLUSTER);
1067 node.visibility.clear(Visibility::DETACHED);
1068 node.state = NodeState::Active;
1069 node.footprint = node.resize_footprint.unwrap_or(node.intrinsic_size);
1070 self.insert_existing(node);
1071 }
1072 } else {
1073 for member in cluster.members() {
1074 let _ = self.set_state(*member, NodeState::Active);
1075 if let Some(node) = self.node_mut(*member) {
1076 node.visibility.clear(Visibility::HIDDEN_BY_CLUSTER);
1077 }
1078 }
1079 }
1080
1081 if let Some(core_id) = cluster.core {
1082 let _ = self.nodes.remove(&core_id);
1083 }
1084
1085 true
1086 }
1087}
1088
1089fn find_duplicate_member(members: &[NodeId]) -> Option<NodeId> {
1090 let mut seen = std::collections::HashSet::new();
1091 for member in members {
1092 if !seen.insert(*member) {
1093 return Some(*member);
1094 }
1095 }
1096 None
1097}
1098
1099impl Default for Field {
1100 fn default() -> Self {
1101 Self::new()
1102 }
1103}
1104
1105#[cfg(test)]
1106mod tests {
1107 use super::*;
1108
1109 #[test]
1110 fn cluster_create_rejects_missing_nodes() {
1111 let mut f = Field::new();
1112 let missing = NodeId::new(999);
1113 assert_eq!(
1114 f.create_cluster(vec![missing]),
1115 Err(ClusterCreateError::TooFewMembers)
1116 );
1117 }
1118
1119 #[test]
1120 fn cluster_create_rejects_singletons() {
1121 let mut f = Field::new();
1122 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1123
1124 assert_eq!(
1125 f.create_cluster(vec![a]),
1126 Err(ClusterCreateError::TooFewMembers)
1127 );
1128 }
1129
1130 #[test]
1131 fn cluster_create_rejects_duplicate_members() {
1132 let mut f = Field::new();
1133 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1134 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1135
1136 assert_eq!(
1137 f.create_cluster(vec![a, a, b]),
1138 Err(ClusterCreateError::DuplicateMember)
1139 );
1140 }
1141
1142 #[test]
1143 fn collapse_cluster_creates_core_and_shrinks_members() {
1144 let mut f = Field::new();
1145 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1146 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1147
1148 let cid = f.create_cluster(vec![a, b]).unwrap();
1149 let core = f.collapse_cluster(cid).unwrap();
1150
1151 assert_eq!(f.node(a).unwrap().state, NodeState::Node);
1152 assert_eq!(f.node(b).unwrap().state, NodeState::Node);
1153 assert_eq!(f.node(a).unwrap().footprint, Vec2 { x: 24.0, y: 24.0 });
1154
1155 assert!(
1156 f.node(a)
1157 .unwrap()
1158 .visibility
1159 .has(Visibility::HIDDEN_BY_CLUSTER)
1160 );
1161 assert!(
1162 f.node(b)
1163 .unwrap()
1164 .visibility
1165 .has(Visibility::HIDDEN_BY_CLUSTER)
1166 );
1167 assert!(!f.is_visible(a));
1168 assert!(!f.is_visible(b));
1169
1170 let cn = f.node(core).unwrap();
1171 assert_eq!(cn.kind, NodeKind::Core);
1172 assert_eq!(cn.state, NodeState::Core);
1173 assert_eq!(cn.footprint, Vec2 { x: 48.0, y: 48.0 });
1174 assert!(f.is_visible(core));
1175
1176 let c = f.cluster(cid).unwrap();
1177 assert!(c.is_collapsed());
1178 assert_eq!(c.core, Some(core));
1179 }
1180
1181 #[test]
1182 fn collapsing_active_cluster_restores_visible_core_to_field_queries() {
1183 let mut f = Field::new();
1184 let a = f.spawn_surface("A", Vec2 { x: -20.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1185 let b = f.spawn_surface("B", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1186 let c = f.spawn_surface("C", Vec2 { x: 200.0, y: 0.0 }, Vec2 { x: 80.0, y: 40.0 });
1187
1188 let cid = f.create_cluster(vec![a, b]).unwrap();
1189 let first_core = f.collapse_cluster(cid).unwrap();
1190 assert!(f.activate_cluster_workspace(cid));
1191 assert!(f.node(first_core).is_none());
1192
1193 let core = f.collapse_cluster(cid).unwrap();
1194 assert_eq!(core, first_core);
1195
1196 let core_node = f.node(core).unwrap();
1197 assert_eq!(core_node.kind, NodeKind::Core);
1198 assert_eq!(core_node.state, NodeState::Core);
1199 assert!(f.nodes().contains_key(&core));
1200 assert!(f.participates_in_field_view(core));
1201 assert!(f.is_visible(core));
1202
1203 let view = Rect {
1204 min: Vec2 {
1205 x: -100.0,
1206 y: -100.0,
1207 },
1208 max: Vec2 { x: 100.0, y: 100.0 },
1209 };
1210
1211 assert!(f.in_view(view).contains(&core));
1212 assert!(f.in_view_all(view).contains(&core));
1213 assert!(f.visuals_visible().iter().any(|visual| visual.id == core));
1214 assert!(
1215 f.visuals_in_view(view)
1216 .iter()
1217 .any(|visual| visual.id == core)
1218 );
1219 assert!(!f.in_view(view).contains(&a));
1220 assert!(!f.in_view(view).contains(&b));
1221 assert!(!f.is_visible(a));
1222 assert!(!f.is_visible(b));
1223 assert!(f.is_visible(c));
1224 }
1225
1226 #[test]
1227 fn expand_cluster_restores_members_active_and_visible() {
1228 let mut f = Field::new();
1229 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1230 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1231
1232 let cid = f.create_cluster(vec![a, b]).unwrap();
1233 f.collapse_cluster(cid).unwrap();
1234
1235 assert!(f.expand_cluster(cid));
1236
1237 assert_eq!(f.node(a).unwrap().state, NodeState::Active);
1238 assert_eq!(f.node(b).unwrap().state, NodeState::Active);
1239 assert_eq!(f.node(a).unwrap().footprint, Vec2 { x: 100.0, y: 50.0 });
1240
1241 assert!(
1242 !f.node(a)
1243 .unwrap()
1244 .visibility
1245 .has(Visibility::HIDDEN_BY_CLUSTER)
1246 );
1247 assert!(f.is_visible(a));
1248
1249 let c = f.cluster(cid).unwrap();
1250 assert!(!c.is_collapsed());
1251 }
1252
1253 #[test]
1254 fn carry_respects_pinned() {
1255 let mut f = Field::new();
1256 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1257
1258 assert!(f.carry(id, Vec2 { x: 5.0, y: 5.0 }));
1259 assert_eq!(f.node(id).unwrap().pos, Vec2 { x: 5.0, y: 5.0 });
1260
1261 assert!(f.set_pinned(id, true));
1262 assert!(!f.carry(id, Vec2 { x: 9.0, y: 9.0 }));
1263 assert_eq!(f.node(id).unwrap().pos, Vec2 { x: 5.0, y: 5.0 });
1264 }
1265
1266 #[test]
1267 fn in_view_finds_intersections() {
1268 let mut f = Field::new();
1269 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1270 let _b = f.spawn_surface("B", Vec2 { x: 100.0, y: 100.0 }, Vec2 { x: 10.0, y: 10.0 });
1271
1272 let view = Rect {
1273 min: Vec2 { x: -20.0, y: -20.0 },
1274 max: Vec2 { x: 20.0, y: 20.0 },
1275 };
1276
1277 let ids = f.in_view_all(view);
1278 assert_eq!(ids, vec![a]);
1279 }
1280
1281 #[test]
1282 fn in_view_skips_hidden_nodes() {
1283 let mut f = Field::new();
1284 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1285
1286 assert!(f.set_hidden(a, true));
1287
1288 let view = Rect {
1289 min: Vec2 { x: -20.0, y: -20.0 },
1290 max: Vec2 { x: 20.0, y: 20.0 },
1291 };
1292
1293 let ids = f.in_view(view);
1294 assert!(ids.is_empty());
1295 assert!(!f.is_visible(a));
1296 }
1297
1298 #[test]
1299 fn set_state_changes_footprint() {
1300 let mut f = Field::new();
1301 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
1302
1303 assert_eq!(f.node(id).unwrap().footprint, Vec2 { x: 100.0, y: 50.0 });
1304
1305 assert!(f.set_state(id, NodeState::Node));
1306 assert_eq!(f.node(id).unwrap().footprint, Vec2 { x: 24.0, y: 24.0 });
1307
1308 assert!(f.set_state(id, NodeState::Active));
1309 assert_eq!(f.node(id).unwrap().footprint, Vec2 { x: 100.0, y: 50.0 });
1310 }
1311
1312 #[test]
1313 fn touch_sets_last_touch_and_wakes_node() {
1314 let mut f = Field::new();
1315 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1316
1317 assert!(f.set_decay_level(id, DecayLevel::Cold));
1318 assert_eq!(f.node(id).unwrap().state, NodeState::Node);
1319
1320 assert!(f.touch(id, 1234));
1321 let n = f.node(id).unwrap();
1322 assert_eq!(n.last_touch_ms, 1234);
1323 assert_eq!(n.decay, DecayLevel::Hot);
1324 assert_eq!(n.state, NodeState::Active);
1325 }
1326
1327 #[test]
1328 fn set_decay_level_maps_to_representation_state() {
1329 let mut f = Field::new();
1330 let id = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1331
1332 assert!(f.set_decay_level(id, DecayLevel::Hot));
1333 assert_eq!(f.node(id).unwrap().decay, DecayLevel::Hot);
1334 assert_eq!(f.node(id).unwrap().state, NodeState::Active);
1335
1336 assert!(f.set_decay_level(id, DecayLevel::Cold));
1337 assert_eq!(f.node(id).unwrap().decay, DecayLevel::Cold);
1338 assert_eq!(f.node(id).unwrap().state, NodeState::Node);
1339 }
1340
1341 #[test]
1342 fn core_ignores_set_decay_level() {
1343 let mut f = Field::new();
1344 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1345 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1346
1347 let cid = f.create_cluster(vec![a, b]).unwrap();
1348 let core = f.collapse_cluster(cid).unwrap();
1349
1350 assert!(f.set_decay_level(core, DecayLevel::Cold));
1351 let n = f.node(core).unwrap();
1352 assert_eq!(n.kind, NodeKind::Core);
1353 assert_eq!(n.state, NodeState::Core);
1354 }
1355
1356 #[test]
1357 fn carry_cluster_by_core_moves_only_core_representation() {
1358 let mut f = Field::new();
1359 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1360 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1361
1362 let cid = f.create_cluster(vec![a, b]).unwrap();
1363 let core = f.collapse_cluster(cid).unwrap();
1364
1365 let core_before = f.node(core).unwrap().pos;
1366 let a_before = f.node(a).unwrap().pos;
1367 let b_before = f.node(b).unwrap().pos;
1368
1369 assert!(f.carry_cluster_by_core(core, Vec2 { x: 100.0, y: 50.0 }));
1370
1371 let core_after = f.node(core).unwrap().pos;
1372 let a_after = f.node(a).unwrap().pos;
1373 let b_after = f.node(b).unwrap().pos;
1374
1375 assert_eq!(core_after, Vec2 { x: 100.0, y: 50.0 });
1376 assert_ne!(core_after, core_before);
1377 assert_eq!(a_after, a_before);
1378 assert_eq!(b_after, b_before);
1379 }
1380
1381 #[test]
1382 fn carry_cluster_by_core_respects_pinned() {
1383 let mut f = Field::new();
1384 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1385 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1386
1387 let cid = f.create_cluster(vec![a, b]).unwrap();
1388 let core = f.collapse_cluster(cid).unwrap();
1389
1390 let core_pos = f.node(core).unwrap().pos;
1391 let a_pos = f.node(a).unwrap().pos;
1392 let b_pos = f.node(b).unwrap().pos;
1393
1394 assert!(f.set_pinned(core, true));
1395 assert!(!f.carry_cluster_by_core(core, Vec2 { x: 999.0, y: 999.0 }));
1396
1397 assert_eq!(f.node(core).unwrap().pos, core_pos);
1398 assert_eq!(f.node(a).unwrap().pos, a_pos);
1399 assert_eq!(f.node(b).unwrap().pos, b_pos);
1400 }
1401
1402 #[test]
1403 fn visuals_skip_hidden_nodes() {
1404 let mut f = Field::new();
1405 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1406 let b = f.spawn_surface("B", Vec2 { x: 50.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1407
1408 assert!(f.set_hidden(b, true));
1409
1410 let vis = f.visuals_visible();
1411 assert_eq!(vis.len(), 1);
1412 assert_eq!(vis[0].id, a);
1413 }
1414
1415 #[test]
1416 fn remove_member_requires_explicit_dissolve_for_two_member_cluster() {
1417 let mut f = Field::new();
1418 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1419 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1420
1421 let cid = f.create_cluster(vec![a, b]).unwrap();
1422
1423 assert_eq!(
1424 f.remove_member_from_cluster(cid, a),
1425 Some(ClusterRemoveMemberOutcome::RequiresDissolve)
1426 );
1427 let cluster = f.cluster(cid).unwrap();
1428 assert_eq!(cluster.members(), &[a, b]);
1429 assert_eq!(cluster.master(), a);
1430 }
1431
1432 #[test]
1433 fn raw_member_removal_dissolves_two_member_cluster_without_leaking_singleton() {
1434 let mut f = Field::new();
1435 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1436 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1437
1438 let cid = f.create_cluster(vec![a, b]).unwrap();
1439 let core = f.collapse_cluster(cid).unwrap();
1440
1441 let (_, effect) = f.remove_node_cluster_safe(a).unwrap();
1442
1443 assert_eq!(effect, Some(RemoveNodeClusterEffect::DissolvedCluster(cid)));
1444 assert!(f.cluster(cid).is_none());
1445 assert!(f.node(core).is_none());
1446 assert!(f.node(a).is_none());
1447 assert!(f.node(b).is_some());
1448 assert!(f.is_visible(b));
1449 }
1450
1451 #[test]
1452 fn raw_member_removal_keeps_larger_cluster_valid() {
1453 let mut f = Field::new();
1454 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1455 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1456 let c = f.spawn_surface("C", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1457
1458 let cid = f.create_cluster(vec![a, b, c]).unwrap();
1459
1460 let (_, effect) = f.remove_node_cluster_safe(c).unwrap();
1461
1462 assert_eq!(effect, Some(RemoveNodeClusterEffect::RemovedMember(cid)));
1463 let cluster = f.cluster(cid).unwrap();
1464 assert_eq!(cluster.members(), &[a, b]);
1465 }
1466
1467 #[test]
1468 fn promote_and_reorder_preserve_explicit_master_contract() {
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 let c = f.spawn_surface("C", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1473
1474 let cid = f.create_cluster(vec![a, b, c]).unwrap();
1475 f.promote_cluster_member_to_master(cid, c).unwrap();
1476 assert_eq!(f.cluster(cid).unwrap().members(), &[c, a, b]);
1477 assert_eq!(f.cluster(cid).unwrap().master(), c);
1478
1479 f.reorder_cluster_members(cid, vec![b, c, a]).unwrap();
1480 assert_eq!(f.cluster(cid).unwrap().members(), &[b, c, a]);
1481 assert_eq!(f.cluster(cid).unwrap().master(), b);
1482 assert_eq!(f.cluster(cid).unwrap().secondaries(), &[c, a]);
1483 }
1484
1485 #[test]
1486 fn active_cluster_members_do_not_participate_in_field_dynamics() {
1487 let mut f = Field::new();
1488 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1489 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1490
1491 let cid = f.create_cluster(vec![a, b]).unwrap();
1492 assert!(f.activate_cluster_workspace(cid));
1493
1494 assert!(f.is_active_cluster_member(a));
1495 assert!(!f.participates_in_field_dynamics(a));
1496 assert!(!f.participates_in_field_activity(a));
1497 }
1498
1499 #[test]
1500 fn spawning_into_active_cluster_workspace_bypasses_field_storage() {
1501 let mut f = Field::new();
1502 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1503 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1504
1505 let cid = f.create_cluster(vec![a, b]).unwrap();
1506 assert!(f.activate_cluster_workspace(cid));
1507
1508 let c = f
1509 .spawn_surface_in_active_cluster(cid, "C", Vec2 { x: 30.0, y: 20.0 })
1510 .unwrap();
1511
1512 assert!(f.node(c).is_some());
1513 assert!(!f.nodes().contains_key(&c));
1514 assert!(f.is_active_cluster_member(c));
1515 assert_eq!(f.cluster(cid).unwrap().members(), &[a, b, c]);
1516 }
1517
1518 #[test]
1519 fn spawning_into_active_cluster_workspace_front_inserts_new_member_first() {
1520 let mut f = Field::new();
1521 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1522 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1523
1524 let cid = f.create_cluster(vec![a, b]).unwrap();
1525 assert!(f.activate_cluster_workspace(cid));
1526
1527 let c = f
1528 .spawn_surface_in_active_cluster_front(cid, "C", Vec2 { x: 30.0, y: 20.0 })
1529 .unwrap();
1530
1531 assert_eq!(f.cluster(cid).unwrap().members(), &[c, a, b]);
1532 }
1533
1534 #[test]
1535 fn adding_member_to_cluster_front_inserts_new_member_first() {
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 let c = f.spawn_surface("C", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1540
1541 let cid = f.create_cluster(vec![a, b]).unwrap();
1542 f.add_member_to_cluster_front(cid, c).unwrap();
1543
1544 assert_eq!(f.cluster(cid).unwrap().members(), &[c, a, b]);
1545 }
1546
1547 #[test]
1548 fn active_cluster_workspace_members_support_state_and_position_updates() {
1549 let mut f = Field::new();
1550 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1551 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1552
1553 let cid = f.create_cluster(vec![a, b]).unwrap();
1554 assert!(f.activate_cluster_workspace(cid));
1555
1556 assert!(f.set_state(a, NodeState::Node));
1557 assert!(f.carry(a, Vec2 { x: 400.0, y: 300.0 }));
1558
1559 let node = f.node(a).unwrap();
1560 assert_eq!(node.state, NodeState::Node);
1561 assert_eq!(node.pos, Vec2 { x: 400.0, y: 300.0 });
1562 assert!(f.bounds(a).is_some());
1563 }
1564
1565 #[test]
1566 fn cluster_workspace_layout_only_tiles_first_four_members() {
1567 let mut f = Field::new();
1568 let members = (0..6)
1569 .map(|index| {
1570 f.spawn_surface(
1571 format!("N{}", index),
1572 Vec2 {
1573 x: index as f32 * 10.0,
1574 y: 0.0,
1575 },
1576 Vec2 { x: 10.0, y: 10.0 },
1577 )
1578 })
1579 .collect::<Vec<_>>();
1580
1581 let cid = f.create_cluster(members.clone()).unwrap();
1582 let cluster = f.cluster(cid).unwrap();
1583 let layout = cluster.workspace_layout(
1584 crate::tiling::Rect {
1585 x: 0.0,
1586 y: 0.0,
1587 w: 1000.0,
1588 h: 600.0,
1589 },
1590 3,
1591 );
1592
1593 assert_eq!(cluster.visible_members(3), &members[..4]);
1594 assert_eq!(cluster.overflow_members(3), &members[4..]);
1595 assert_eq!(layout.tiles.len(), 4);
1596 assert!(
1597 layout
1598 .tiles
1599 .iter()
1600 .all(|tile| members[..4].contains(&tile.id))
1601 );
1602 }
1603
1604 #[test]
1605 fn swapping_overflow_member_with_visible_preserves_queue_order() {
1606 let mut f = Field::new();
1607 let members = (0..6)
1608 .map(|index| {
1609 f.spawn_surface(
1610 format!("N{}", index),
1611 Vec2 {
1612 x: index as f32 * 10.0,
1613 y: 0.0,
1614 },
1615 Vec2 { x: 10.0, y: 10.0 },
1616 )
1617 })
1618 .collect::<Vec<_>>();
1619
1620 let cid = f.create_cluster(members.clone()).unwrap();
1621 assert!(f.swap_cluster_overflow_member_with_visible(cid, members[4], members[2], 3));
1622
1623 let cluster = f.cluster(cid).unwrap();
1624 assert_eq!(
1625 cluster.members(),
1626 &[
1627 members[0], members[1], members[4], members[3], members[2], members[5]
1628 ]
1629 );
1630 assert_eq!(
1631 cluster.visible_members(3),
1632 &[members[0], members[1], members[4], members[3]]
1633 );
1634 assert_eq!(cluster.overflow_members(3), &[members[2], members[5]]);
1635 }
1636
1637 #[test]
1638 fn reordering_overflow_members_updates_queue_order_only() {
1639 let mut f = Field::new();
1640 let members = (0..7)
1641 .map(|index| {
1642 f.spawn_surface(
1643 format!("N{}", index),
1644 Vec2 {
1645 x: index as f32 * 10.0,
1646 y: 0.0,
1647 },
1648 Vec2 { x: 10.0, y: 10.0 },
1649 )
1650 })
1651 .collect::<Vec<_>>();
1652
1653 let cid = f.create_cluster(members.clone()).unwrap();
1654 assert!(f.reorder_cluster_overflow_member(cid, members[6], 0, 3));
1655
1656 let cluster = f.cluster(cid).unwrap();
1657 assert_eq!(cluster.visible_members(3), &members[..4]);
1658 assert_eq!(
1659 cluster.overflow_members(3),
1660 &[members[6], members[4], members[5]]
1661 );
1662 }
1663
1664 #[test]
1665 fn touch_is_noop_for_cluster_members() {
1666 let mut f = Field::new();
1667 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1668 let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1669
1670 let cid = f.create_cluster(vec![a, b]).unwrap();
1671 let before = f.node(a).unwrap().last_touch_ms;
1672
1673 assert!(f.touch(a, 9999));
1674 assert_eq!(f.node(a).unwrap().last_touch_ms, before);
1675
1676 assert!(f.activate_cluster_workspace(cid));
1677 assert!(f.touch(a, 12345));
1678 assert_eq!(f.node(a).unwrap().last_touch_ms, before);
1679 }
1680
1681 #[test]
1682 fn active_cluster_members_are_excluded_from_field_view_queries() {
1683 let mut f = Field::new();
1684 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1685 let b = f.spawn_surface("B", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1686 let c = f.spawn_surface("C", Vec2 { x: 200.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
1687
1688 let cid = f.create_cluster(vec![a, b]).unwrap();
1689 assert!(f.activate_cluster_workspace(cid));
1690
1691 let view = Rect {
1692 min: Vec2 { x: -50.0, y: -50.0 },
1693 max: Vec2 { x: 50.0, y: 50.0 },
1694 };
1695
1696 assert!(!f.in_view(view).contains(&a));
1697 assert!(!f.in_view(view).contains(&b));
1698 assert_eq!(f.in_view(view), vec![]);
1699 assert_eq!(f.in_view_all(view), vec![]);
1700 assert_eq!(f.visuals_visible().len(), 1);
1701 assert_eq!(f.visuals_visible()[0].id, c);
1702 }
1703}