Skip to main content

halley_core/
field.rs

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/// A stable identity for anything that exists in the Field.
11#[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/// 2D point / vector in Field coordinates.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Vec2 {
33    pub x: f32,
34    pub y: f32,
35}
36
37/// Axis-aligned rectangle.
38#[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/// Semantic visibility flags.
66/// This is NOT rendering; it's "experience-layer existence":
67/// - hidden nodes should be skipped by focus/nav/bearings/in_view.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct Visibility(u8);
70
71impl Visibility {
72    pub const NONE: Self = Self(0);
73
74    /// Hidden because user/system explicitly hid it.
75    pub const HIDDEN_EXPLICIT: Self = Self(1 << 0);
76
77    /// Hidden because its cluster is collapsed.
78    pub const HIDDEN_BY_CLUSTER: Self = Self(1 << 1);
79
80    /// Node exists in storage, but is currently detached from the experience layer.
81    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/// What kind of thing a node represents.
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum NodeKind {
107    Surface,
108    Core, // collapsed cluster handle
109}
110
111/// Representation state.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub enum NodeState {
114    Active,
115    Drifting,
116    Node, // dot with label
117    Core, // only meaningful for Core kind
118}
119
120/// A Node is the universal "thing" that exists in the Field.
121#[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    /// Center position in Field coordinates.
130    pub pos: Vec2,
131
132    pub intrinsic_size: Vec2, // "real" size for Active
133    pub footprint: Vec2,      // spatial occupancy right now
134    pub resize_footprint: Option<Vec2>,
135
136    /// Pinned in place (movement constraint). This was previously called `anchored`.
137    pub pinned: bool,
138
139    /// Routing marker: important node that should always be surfaced in navigation
140    /// (Bearings/Lens). Does NOT bypass visibility rules.
141    pub anchor: bool,
142
143    /// Semantic visibility / participation flags.
144    pub visibility: Visibility,
145
146    pub last_touch_ms: u64,
147    pub decay: DecayLevel,
148}
149
150/// The infinite 2D space containing all Nodes.
151pub 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    /// Spawn a basic Surface node.
248    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    /// Remove a node from the Field.
313    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    /// Set/unset movement pinning.
397    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    /// Back-compat alias: previously `anchor()` meant "pinned in place".
406    /// Prefer `set_pinned()`. (We keep this to avoid churn in other modules.)
407    pub fn anchor(&mut self, id: NodeId, on: bool) -> bool {
408        self.set_pinned(id, on)
409    }
410
411    /// Set/unset routing anchor marker.
412    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    /// Return all experience-visible anchors (stable order).
425    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    /// Carry a node to a new position (respects pinning).
439    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    /// Axis-aligned bounds in Field space.
451    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    /// Return nodes that intersect the view rect AND are experience-visible.
474    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    /// Return all nodes that intersect the view rect (includes hidden nodes).
485    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    /// True iff the node exists and is not hidden by any visibility reason.
495    pub fn is_visible(&self, id: NodeId) -> bool {
496        self.node(id).is_some_and(|n| !n.visibility.is_hidden())
497    }
498
499    /// Explicit hide/show (does not touch cluster-hidden).
500    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    /// Detach/attach.
509    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    /// Record interaction with a node.
518    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        // Core is a handle; it doesn't switch representation via touch.
529        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    /// Apply a decay level to a node by mapping it to representation state.
538    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        // Core is a handle; it doesn't decay away.
547        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    /// Canonical visuals feed: for full behavior, use `build_visuals()` directly.
606    /// These helpers delegate to the same implementation to avoid drift.
607    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    /// Remove a cluster record (needed for cross-space transfer).
674    pub fn remove_cluster(&mut self, id: ClusterId) -> Option<Cluster> {
675        self.clusters.remove(&id)
676    }
677
678    /// Insert an existing cluster record (needed for cross-space transfer).
679    pub fn insert_cluster(&mut self, cluster: Cluster) {
680        // keep ids stable; bump next_cluster so future creates don’t collide
681        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    /// Drag the cluster by its core handle.
912    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    /// Collapse the cluster into a Core node.
923    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    /// Expand the cluster.
1021    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        // keep ids stable; bump next_node if needed so future spawns don’t collide
1051        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}