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