Skip to main content

proof_engine/editor/
hierarchy.rs

1//! Hierarchy panel — scene-tree view with full drag-drop, multi-select,
2//! prefab support, search/filter, undo, serialisation and keyboard navigation.
3
4use std::collections::HashMap;
5
6// ─────────────────────────────────────────────────────────────────────────────
7// NodeKind
8// ─────────────────────────────────────────────────────────────────────────────
9
10/// What kind of scene object a hierarchy node represents.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum NodeKind {
13    Entity,
14    Glyph,
15    ParticleEmitter,
16    ForceField,
17    Light,
18    Camera,
19    Group,
20    Folder,
21    Prefab,
22    Script,
23    Collider,
24    Path,
25    Custom(u32),
26}
27
28impl NodeKind {
29    /// ASCII icon for display.
30    pub fn icon(self) -> char {
31        match self {
32            NodeKind::Entity         => 'E',
33            NodeKind::Glyph          => 'G',
34            NodeKind::ParticleEmitter => 'P',
35            NodeKind::ForceField     => 'F',
36            NodeKind::Light          => 'L',
37            NodeKind::Camera         => 'C',
38            NodeKind::Group          => '+',
39            NodeKind::Folder         => 'D',
40            NodeKind::Prefab         => '*',
41            NodeKind::Script         => 'S',
42            NodeKind::Collider       => 'X',
43            NodeKind::Path           => '~',
44            NodeKind::Custom(_)      => '?',
45        }
46    }
47
48    pub fn label(self) -> &'static str {
49        match self {
50            NodeKind::Entity         => "Entity",
51            NodeKind::Glyph          => "Glyph",
52            NodeKind::ParticleEmitter => "Particles",
53            NodeKind::ForceField     => "Force Field",
54            NodeKind::Light          => "Light",
55            NodeKind::Camera         => "Camera",
56            NodeKind::Group          => "Group",
57            NodeKind::Folder         => "Folder",
58            NodeKind::Prefab         => "Prefab",
59            NodeKind::Script         => "Script",
60            NodeKind::Collider       => "Collider",
61            NodeKind::Path           => "Path",
62            NodeKind::Custom(_)      => "Custom",
63        }
64    }
65}
66
67// ─────────────────────────────────────────────────────────────────────────────
68// NodeId
69// ─────────────────────────────────────────────────────────────────────────────
70
71/// Unique identifier for a hierarchy node.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
73pub struct NodeId(pub u32);
74
75// ─────────────────────────────────────────────────────────────────────────────
76// HierarchyNode
77// ─────────────────────────────────────────────────────────────────────────────
78
79/// A single node in the scene tree.
80#[derive(Debug, Clone)]
81pub struct HierarchyNode {
82    pub id:           NodeId,
83    pub name:         String,
84    pub kind:         NodeKind,
85    pub parent:       Option<NodeId>,
86    pub children:     Vec<NodeId>,
87    pub expanded:     bool,
88    pub visible:      bool,
89    pub locked:       bool,
90    pub is_prefab_root: bool,
91    pub prefab_name:  Option<String>,
92    /// Depth in the tree (0 = root).
93    pub depth:        u32,
94    /// Order among siblings (lower = higher in list).
95    pub sibling_index: usize,
96}
97
98impl HierarchyNode {
99    pub fn new(id: NodeId, name: impl Into<String>, kind: NodeKind) -> Self {
100        Self {
101            id,
102            name: name.into(),
103            kind,
104            parent: None,
105            children: Vec::new(),
106            expanded: true,
107            visible: true,
108            locked: false,
109            is_prefab_root: false,
110            prefab_name: None,
111            depth: 0,
112            sibling_index: 0,
113        }
114    }
115
116    pub fn visibility_icon(&self) -> char {
117        if self.visible { 'O' } else { '-' }
118    }
119
120    pub fn lock_icon(&self) -> char {
121        if self.locked { '#' } else { ' ' }
122    }
123
124    pub fn expand_icon(&self) -> char {
125        if self.children.is_empty() {
126            ' '
127        } else if self.expanded {
128            'v'
129        } else {
130            '>'
131        }
132    }
133
134    /// One-line ASCII representation.
135    pub fn render_line(&self) -> String {
136        let indent = "  ".repeat(self.depth as usize);
137        format!(
138            "{}{} [{}]{} {} {}",
139            indent,
140            self.expand_icon(),
141            self.kind.icon(),
142            if self.is_prefab_root { "*" } else { "" },
143            self.name,
144            if self.locked { "#" } else { "" },
145        )
146    }
147}
148
149// ─────────────────────────────────────────────────────────────────────────────
150// PrefabNode
151// ─────────────────────────────────────────────────────────────────────────────
152
153/// A named, reusable subtree template.
154#[derive(Debug, Clone)]
155pub struct PrefabNode {
156    pub name:      String,
157    pub root_node: NodeId,
158    pub nodes:     Vec<NodeId>,
159    pub overrides: HashMap<NodeId, HashMap<String, String>>,
160}
161
162impl PrefabNode {
163    pub fn new(name: impl Into<String>, root: NodeId) -> Self {
164        Self {
165            name: name.into(),
166            root_node: root,
167            nodes: vec![root],
168            overrides: HashMap::new(),
169        }
170    }
171    pub fn add_override(&mut self, id: NodeId, prop: impl Into<String>, val: impl Into<String>) {
172        self.overrides.entry(id).or_default().insert(prop.into(), val.into());
173    }
174    pub fn get_override(&self, id: NodeId, prop: &str) -> Option<&str> {
175        self.overrides.get(&id)?.get(prop).map(|s| s.as_str())
176    }
177}
178
179// ─────────────────────────────────────────────────────────────────────────────
180// HierarchyAction — feeds UndoHistory
181// ─────────────────────────────────────────────────────────────────────────────
182
183/// All reversible hierarchy operations.
184#[derive(Debug, Clone)]
185pub enum HierarchyAction {
186    AddNode { id: NodeId, parent: Option<NodeId>, name: String, kind: NodeKind },
187    RemoveNode { id: NodeId, saved_node: Option<HierarchyNode>, saved_children: Vec<NodeId> },
188    RenameNode { id: NodeId, old_name: String, new_name: String },
189    Reparent { child: NodeId, old_parent: Option<NodeId>, new_parent: Option<NodeId>, old_index: usize, new_index: usize },
190    ReorderSibling { parent: Option<NodeId>, from_index: usize, to_index: usize },
191    SetVisible { id: NodeId, old: bool, new: bool },
192    SetLocked { id: NodeId, old: bool, new: bool },
193    MakePrefab { id: NodeId, prefab_name: String },
194    DuplicateSubtree { original: NodeId, new_root: NodeId, all_new_ids: Vec<NodeId> },
195    GroupNodes { group_id: NodeId, members: Vec<NodeId>, old_parents: Vec<Option<NodeId>> },
196    UngroupNodes { group_id: NodeId, members: Vec<NodeId> },
197}
198
199// ─────────────────────────────────────────────────────────────────────────────
200// HierarchySerializer
201// ─────────────────────────────────────────────────────────────────────────────
202
203/// Serialises and deserialises the scene tree as indented text.
204pub struct HierarchySerializer;
205
206impl HierarchySerializer {
207    /// Serialise the entire panel to a text format.
208    pub fn serialize(panel: &HierarchyPanel) -> String {
209        let mut out = String::new();
210        let roots: Vec<NodeId> = panel.nodes.values()
211            .filter(|n| n.parent.is_none())
212            .map(|n| n.id)
213            .collect();
214        let mut sorted_roots: Vec<&HierarchyNode> = roots.iter()
215            .filter_map(|id| panel.nodes.get(id))
216            .collect();
217        sorted_roots.sort_by_key(|n| n.sibling_index);
218        for node in sorted_roots {
219            Self::serialize_node(panel, node.id, &mut out);
220        }
221        out
222    }
223
224    fn serialize_node(panel: &HierarchyPanel, id: NodeId, out: &mut String) {
225        if let Some(node) = panel.nodes.get(&id) {
226            let indent = "  ".repeat(node.depth as usize);
227            let visible = if node.visible { 'V' } else { 'H' };
228            let locked  = if node.locked  { 'L' } else { 'U' };
229            out.push_str(&format!(
230                "{}[{}][{}][{}] {}\n",
231                indent,
232                node.kind.icon(),
233                visible,
234                locked,
235                node.name,
236            ));
237            let mut children: Vec<&HierarchyNode> = node.children.iter()
238                .filter_map(|cid| panel.nodes.get(cid))
239                .collect();
240            children.sort_by_key(|n| n.sibling_index);
241            for child in children {
242                Self::serialize_node(panel, child.id, out);
243            }
244        }
245    }
246
247    /// Parse a serialised tree back into nodes.  Returns (nodes, actions_log).
248    pub fn deserialize(text: &str) -> (Vec<(String, NodeKind, u32)>, Vec<String>) {
249        let mut result = Vec::new();
250        let mut errors = Vec::new();
251        for (line_no, line) in text.lines().enumerate() {
252            if line.trim().is_empty() { continue; }
253            // Count leading spaces to determine depth.
254            let depth = (line.len() - line.trim_start().len()) / 2;
255            let trimmed = line.trim();
256            // Expect format: [kind][V/H][L/U] name
257            if trimmed.starts_with('[') {
258                let parts: Vec<&str> = trimmed.splitn(5, ']').collect();
259                if parts.len() >= 4 {
260                    let kind_char = parts[0].trim_start_matches('[');
261                    let name = parts[3].trim().trim_start_matches(' ').to_string();
262                    let kind = match kind_char.chars().next().unwrap_or(' ') {
263                        'E' => NodeKind::Entity,
264                        'G' => NodeKind::Glyph,
265                        'P' => NodeKind::ParticleEmitter,
266                        'F' => NodeKind::ForceField,
267                        'L' => NodeKind::Light,
268                        'C' => NodeKind::Camera,
269                        '+' => NodeKind::Group,
270                        'D' => NodeKind::Folder,
271                        '*' => NodeKind::Prefab,
272                        _   => NodeKind::Entity,
273                    };
274                    result.push((name, kind, depth as u32));
275                } else {
276                    errors.push(format!("Line {}: malformed node '{}'", line_no + 1, trimmed));
277                }
278            } else {
279                errors.push(format!("Line {}: unexpected format", line_no + 1));
280            }
281        }
282        (result, errors)
283    }
284}
285
286// ─────────────────────────────────────────────────────────────────────────────
287// DragDropState
288// ─────────────────────────────────────────────────────────────────────────────
289
290/// Tracks state for simulated drag-and-drop reordering.
291#[derive(Debug, Clone, Default)]
292pub struct DragDropState {
293    pub dragging: Option<NodeId>,
294    pub hover_target: Option<NodeId>,
295    pub hover_position: DropPosition,
296    pub drag_start_y: f32,
297    pub current_y: f32,
298}
299
300/// Where a dragged node will be dropped relative to the hover target.
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
302pub enum DropPosition {
303    #[default]
304    Before,
305    After,
306    Inside,
307}
308
309impl DragDropState {
310    pub fn start_drag(&mut self, id: NodeId, y: f32) {
311        self.dragging = Some(id);
312        self.drag_start_y = y;
313        self.current_y = y;
314    }
315
316    pub fn update(&mut self, y: f32, target: Option<NodeId>, pos: DropPosition) {
317        self.current_y = y;
318        self.hover_target = target;
319        self.hover_position = pos;
320    }
321
322    pub fn finish(&mut self) -> Option<(NodeId, Option<NodeId>, DropPosition)> {
323        let drag = self.dragging.take()?;
324        let target = self.hover_target.take();
325        let pos = self.hover_position;
326        Some((drag, target, pos))
327    }
328
329    pub fn cancel(&mut self) {
330        self.dragging = None;
331        self.hover_target = None;
332    }
333
334    pub fn is_dragging(&self) -> bool {
335        self.dragging.is_some()
336    }
337}
338
339// ─────────────────────────────────────────────────────────────────────────────
340// KeyboardNavState
341// ─────────────────────────────────────────────────────────────────────────────
342
343/// Keyboard navigation state for the hierarchy panel.
344#[derive(Debug, Clone, Default)]
345pub struct KeyboardNavState {
346    pub focused_index: usize,  // index in the flattened visible list
347    pub rename_mode:   bool,
348    pub rename_buffer: String,
349}
350
351impl KeyboardNavState {
352    pub fn move_up(&mut self, visible_count: usize) {
353        if visible_count == 0 { return; }
354        if self.focused_index > 0 {
355            self.focused_index -= 1;
356        }
357    }
358    pub fn move_down(&mut self, visible_count: usize) {
359        if visible_count == 0 { return; }
360        if self.focused_index + 1 < visible_count {
361            self.focused_index += 1;
362        }
363    }
364    pub fn begin_rename(&mut self, current_name: &str) {
365        self.rename_mode = true;
366        self.rename_buffer = current_name.to_string();
367    }
368    pub fn finish_rename(&mut self) -> Option<String> {
369        if self.rename_mode {
370            self.rename_mode = false;
371            Some(std::mem::take(&mut self.rename_buffer))
372        } else {
373            None
374        }
375    }
376    pub fn cancel_rename(&mut self) {
377        self.rename_mode = false;
378        self.rename_buffer.clear();
379    }
380}
381
382// ─────────────────────────────────────────────────────────────────────────────
383// HierarchyPanel
384// ─────────────────────────────────────────────────────────────────────────────
385
386/// The scene hierarchy tree panel.
387pub struct HierarchyPanel {
388    pub nodes:   HashMap<NodeId, HierarchyNode>,
389    pub roots:   Vec<NodeId>,
390    pub selected: Vec<NodeId>,
391    pub prefabs: HashMap<String, PrefabNode>,
392
393    // Internal
394    next_id:      u32,
395    undo_stack:   Vec<HierarchyAction>,
396    redo_stack:   Vec<HierarchyAction>,
397    pub drag_drop: DragDropState,
398    pub keyboard:  KeyboardNavState,
399    pub search_query: String,
400    pub filter_kind:  Option<NodeKind>,
401    /// Cached flat list of visible nodes for rendering / keyboard nav.
402    flat_list:    Vec<NodeId>,
403    flat_dirty:   bool,
404}
405
406impl HierarchyPanel {
407    pub fn new() -> Self {
408        Self {
409            nodes:     HashMap::new(),
410            roots:     Vec::new(),
411            selected:  Vec::new(),
412            prefabs:   HashMap::new(),
413            next_id:   1,
414            undo_stack: Vec::new(),
415            redo_stack: Vec::new(),
416            drag_drop: DragDropState::default(),
417            keyboard:  KeyboardNavState::default(),
418            search_query: String::new(),
419            filter_kind:  None,
420            flat_list:    Vec::new(),
421            flat_dirty:   true,
422        }
423    }
424
425    // ── ID allocation ─────────────────────────────────────────────────────────
426
427    fn alloc_id(&mut self) -> NodeId {
428        let id = NodeId(self.next_id);
429        self.next_id += 1;
430        id
431    }
432
433    // ── Add / remove ─────────────────────────────────────────────────────────
434
435    /// Add a new node, optionally parented to `parent`.
436    pub fn add_node(
437        &mut self,
438        name: impl Into<String>,
439        kind: NodeKind,
440        parent: Option<NodeId>,
441    ) -> NodeId {
442        let id = self.alloc_id();
443        let name_s: String = name.into();
444        let depth = parent
445            .and_then(|p| self.nodes.get(&p))
446            .map(|p| p.depth + 1)
447            .unwrap_or(0);
448        let sibling_index = parent
449            .and_then(|p| self.nodes.get(&p))
450            .map(|p| p.children.len())
451            .unwrap_or(self.roots.len());
452
453        let mut node = HierarchyNode::new(id, name_s.clone(), kind);
454        node.parent = parent;
455        node.depth = depth;
456        node.sibling_index = sibling_index;
457
458        self.nodes.insert(id, node);
459
460        if let Some(pid) = parent {
461            if let Some(p) = self.nodes.get_mut(&pid) {
462                p.children.push(id);
463            }
464        } else {
465            self.roots.push(id);
466        }
467
468        self.undo_stack.push(HierarchyAction::AddNode {
469            id,
470            parent,
471            name: name_s,
472            kind,
473        });
474        self.redo_stack.clear();
475        self.flat_dirty = true;
476        id
477    }
478
479    /// Remove a node and all its descendants.
480    pub fn delete_recursive(&mut self, id: NodeId) {
481        if !self.nodes.contains_key(&id) { return; }
482        // Collect all descendants depth-first.
483        let mut to_remove = Vec::new();
484        self.collect_descendants(id, &mut to_remove);
485        to_remove.push(id);
486
487        // Detach from parent.
488        let parent = self.nodes.get(&id).and_then(|n| n.parent);
489        if let Some(pid) = parent {
490            if let Some(p) = self.nodes.get_mut(&pid) {
491                p.children.retain(|&c| c != id);
492            }
493        } else {
494            self.roots.retain(|&r| r != id);
495        }
496
497        // Save for undo.
498        let saved = self.nodes.get(&id).cloned();
499        let saved_children = self.nodes.get(&id).map(|n| n.children.clone()).unwrap_or_default();
500
501        for rid in &to_remove {
502            self.nodes.remove(rid);
503            self.selected.retain(|&s| &s != rid);
504        }
505
506        self.undo_stack.push(HierarchyAction::RemoveNode {
507            id,
508            saved_node: saved,
509            saved_children,
510        });
511        self.redo_stack.clear();
512        self.flat_dirty = true;
513    }
514
515    fn collect_descendants(&self, id: NodeId, out: &mut Vec<NodeId>) {
516        if let Some(node) = self.nodes.get(&id) {
517            for &child in &node.children {
518                self.collect_descendants(child, out);
519                out.push(child);
520            }
521        }
522    }
523
524    // ── Rename ────────────────────────────────────────────────────────────────
525
526    pub fn rename(&mut self, id: NodeId, new_name: impl Into<String>) {
527        let new_name: String = new_name.into();
528        if let Some(node) = self.nodes.get_mut(&id) {
529            let old_name = std::mem::replace(&mut node.name, new_name.clone());
530            self.undo_stack.push(HierarchyAction::RenameNode { id, old_name, new_name });
531            self.redo_stack.clear();
532            self.flat_dirty = true;
533        }
534    }
535
536    // ── Reparent ─────────────────────────────────────────────────────────────
537
538    /// Move `child` to be a child of `new_parent` (None = root).
539    pub fn reparent(&mut self, child: NodeId, new_parent: Option<NodeId>) {
540        if !self.nodes.contains_key(&child) { return; }
541        // Guard against parenting to self or a descendant.
542        if let Some(np) = new_parent {
543            if np == child { return; }
544            let mut anc = Some(np);
545            while let Some(a) = anc {
546                if a == child { return; } // would create cycle
547                anc = self.nodes.get(&a).and_then(|n| n.parent);
548            }
549        }
550
551        let old_parent = self.nodes[&child].parent;
552        let old_index  = self.nodes[&child].sibling_index;
553
554        // Detach from old parent.
555        if let Some(op) = old_parent {
556            if let Some(p) = self.nodes.get_mut(&op) {
557                p.children.retain(|&c| c != child);
558            }
559            // Fix sibling indices — separate borrow to avoid double-mut.
560            let siblings: Vec<NodeId> = self.nodes.get(&op)
561                .map(|p| p.children.clone())
562                .unwrap_or_default();
563            for (i, cid) in siblings.into_iter().enumerate() {
564                if let Some(cn) = self.nodes.get_mut(&cid) {
565                    cn.sibling_index = i;
566                }
567            }
568        } else {
569            self.roots.retain(|&r| r != child);
570        }
571
572        // Attach to new parent.
573        let new_index = if let Some(np) = new_parent {
574            let len = self.nodes.get(&np).map(|n| n.children.len()).unwrap_or(0);
575            if let Some(p) = self.nodes.get_mut(&np) {
576                p.children.push(child);
577            }
578            len
579        } else {
580            let len = self.roots.len();
581            self.roots.push(child);
582            len
583        };
584
585        // Update child's parent and depth.
586        let new_depth = new_parent
587            .and_then(|p| self.nodes.get(&p))
588            .map(|p| p.depth + 1)
589            .unwrap_or(0);
590        if let Some(cn) = self.nodes.get_mut(&child) {
591            cn.parent = new_parent;
592            cn.sibling_index = new_index;
593        }
594        // Recursively update depths of subtree.
595        self.update_depths(child, new_depth);
596
597        self.undo_stack.push(HierarchyAction::Reparent {
598            child,
599            old_parent,
600            new_parent,
601            old_index,
602            new_index,
603        });
604        self.redo_stack.clear();
605        self.flat_dirty = true;
606    }
607
608    fn update_depths(&mut self, id: NodeId, depth: u32) {
609        if let Some(node) = self.nodes.get_mut(&id) {
610            node.depth = depth;
611        }
612        let children: Vec<NodeId> = self.nodes.get(&id)
613            .map(|n| n.children.clone())
614            .unwrap_or_default();
615        for child in children {
616            self.update_depths(child, depth + 1);
617        }
618    }
619
620    // ── Duplicate subtree ─────────────────────────────────────────────────────
621
622    /// Deep-clone `id` and all its descendants. Returns the new root.
623    pub fn duplicate_subtree(&mut self, id: NodeId) -> Option<NodeId> {
624        if !self.nodes.contains_key(&id) { return None; }
625        let parent = self.nodes[&id].parent;
626        let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
627        let new_root = self.clone_node_recursive(id, parent, &mut id_map);
628        let all_new: Vec<NodeId> = id_map.values().copied().collect();
629        self.undo_stack.push(HierarchyAction::DuplicateSubtree {
630            original: id,
631            new_root,
632            all_new_ids: all_new,
633        });
634        self.redo_stack.clear();
635        self.flat_dirty = true;
636        Some(new_root)
637    }
638
639    fn clone_node_recursive(
640        &mut self,
641        src: NodeId,
642        parent: Option<NodeId>,
643        id_map: &mut HashMap<NodeId, NodeId>,
644    ) -> NodeId {
645        let new_id = self.alloc_id();
646        id_map.insert(src, new_id);
647
648        let src_node = self.nodes[&src].clone();
649        let depth = parent.and_then(|p| self.nodes.get(&p)).map(|p| p.depth + 1).unwrap_or(0);
650        let sibling_index = parent
651            .and_then(|p| self.nodes.get(&p))
652            .map(|p| p.children.len())
653            .unwrap_or(self.roots.len());
654
655        let mut new_node = HierarchyNode::new(
656            new_id,
657            format!("{} (copy)", src_node.name),
658            src_node.kind,
659        );
660        new_node.parent = parent;
661        new_node.depth = depth;
662        new_node.sibling_index = sibling_index;
663        new_node.visible = src_node.visible;
664        new_node.locked  = src_node.locked;
665
666        self.nodes.insert(new_id, new_node);
667
668        if let Some(pid) = parent {
669            if let Some(p) = self.nodes.get_mut(&pid) {
670                p.children.push(new_id);
671            }
672        } else {
673            self.roots.push(new_id);
674        }
675
676        let children: Vec<NodeId> = src_node.children.clone();
677        for child in children {
678            self.clone_node_recursive(child, Some(new_id), id_map);
679        }
680        new_id
681    }
682
683    // ── Expand / collapse ─────────────────────────────────────────────────────
684
685    pub fn expand_all(&mut self) {
686        for node in self.nodes.values_mut() {
687            node.expanded = true;
688        }
689        self.flat_dirty = true;
690    }
691
692    pub fn collapse_all(&mut self) {
693        for node in self.nodes.values_mut() {
694            node.expanded = false;
695        }
696        self.flat_dirty = true;
697    }
698
699    /// Expand the path from root to `id`.
700    pub fn expand_to(&mut self, id: NodeId) {
701        let mut current = self.nodes.get(&id).and_then(|n| n.parent);
702        while let Some(pid) = current {
703            if let Some(n) = self.nodes.get_mut(&pid) {
704                n.expanded = true;
705                current = n.parent;
706            } else {
707                break;
708            }
709        }
710        self.flat_dirty = true;
711    }
712
713    // ── Visibility & lock ─────────────────────────────────────────────────────
714
715    pub fn set_visible(&mut self, id: NodeId, visible: bool) {
716        if let Some(node) = self.nodes.get_mut(&id) {
717            let old = node.visible;
718            node.visible = visible;
719            self.undo_stack.push(HierarchyAction::SetVisible { id, old, new: visible });
720            self.redo_stack.clear();
721        }
722    }
723
724    pub fn toggle_visible(&mut self, id: NodeId) {
725        let vis = self.nodes.get(&id).map(|n| !n.visible).unwrap_or(false);
726        self.set_visible(id, vis);
727    }
728
729    pub fn set_locked(&mut self, id: NodeId, locked: bool) {
730        if let Some(node) = self.nodes.get_mut(&id) {
731            let old = node.locked;
732            node.locked = locked;
733            self.undo_stack.push(HierarchyAction::SetLocked { id, old, new: locked });
734            self.redo_stack.clear();
735        }
736    }
737
738    pub fn toggle_locked(&mut self, id: NodeId) {
739        let locked = self.nodes.get(&id).map(|n| !n.locked).unwrap_or(false);
740        self.set_locked(id, locked);
741    }
742
743    // ── Search / filter ───────────────────────────────────────────────────────
744
745    pub fn set_search(&mut self, query: impl Into<String>) {
746        self.search_query = query.into();
747        self.flat_dirty = true;
748    }
749
750    pub fn clear_search(&mut self) {
751        self.search_query.clear();
752        self.flat_dirty = true;
753    }
754
755    pub fn set_kind_filter(&mut self, kind: Option<NodeKind>) {
756        self.filter_kind = kind;
757        self.flat_dirty = true;
758    }
759
760    fn node_matches(&self, node: &HierarchyNode) -> bool {
761        let name_ok = self.search_query.is_empty()
762            || node.name.to_lowercase().contains(&self.search_query.to_lowercase());
763        let kind_ok = self.filter_kind.map(|k| k == node.kind).unwrap_or(true);
764        name_ok && kind_ok
765    }
766
767    pub fn filter_by_name(&self, query: &str) -> Vec<NodeId> {
768        self.nodes.values()
769            .filter(|n| n.name.to_lowercase().contains(&query.to_lowercase()))
770            .map(|n| n.id)
771            .collect()
772    }
773
774    pub fn filter_by_kind(&self, kind: NodeKind) -> Vec<NodeId> {
775        self.nodes.values()
776            .filter(|n| n.kind == kind)
777            .map(|n| n.id)
778            .collect()
779    }
780
781    // ── Selection ─────────────────────────────────────────────────────────────
782
783    pub fn select(&mut self, id: NodeId) {
784        self.selected.clear();
785        self.selected.push(id);
786        // Update focused index in flat list.
787        self.rebuild_flat_if_dirty();
788        if let Some(pos) = self.flat_list.iter().position(|&x| x == id) {
789            self.keyboard.focused_index = pos;
790        }
791    }
792
793    pub fn toggle_select(&mut self, id: NodeId) {
794        if let Some(pos) = self.selected.iter().position(|&s| s == id) {
795            self.selected.remove(pos);
796        } else {
797            self.selected.push(id);
798        }
799    }
800
801    /// Shift+click: select a contiguous range in the flat list.
802    pub fn range_select(&mut self, id: NodeId) {
803        self.rebuild_flat_if_dirty();
804        let anchor = self.selected.first().copied();
805        let Some(target_idx) = self.flat_list.iter().position(|&x| x == id) else { return; };
806        let anchor_idx = anchor
807            .and_then(|a| self.flat_list.iter().position(|&x| x == a))
808            .unwrap_or(target_idx);
809        let (lo, hi) = if anchor_idx <= target_idx {
810            (anchor_idx, target_idx)
811        } else {
812            (target_idx, anchor_idx)
813        };
814        self.selected = self.flat_list[lo..=hi].to_vec();
815    }
816
817    pub fn select_all(&mut self) {
818        self.selected = self.nodes.keys().copied().collect();
819    }
820
821    pub fn deselect_all(&mut self) {
822        self.selected.clear();
823    }
824
825    pub fn is_selected(&self, id: NodeId) -> bool {
826        self.selected.contains(&id)
827    }
828
829    // ── Prefab ────────────────────────────────────────────────────────────────
830
831    pub fn make_prefab(&mut self, id: NodeId, prefab_name: impl Into<String>) -> String {
832        let name: String = prefab_name.into();
833        let nodes: Vec<NodeId> = {
834            let mut all = Vec::new();
835            self.collect_descendants(id, &mut all);
836            all.push(id);
837            all
838        };
839        let prefab = PrefabNode { name: name.clone(), root_node: id, nodes, overrides: HashMap::new() };
840        self.prefabs.insert(name.clone(), prefab);
841        if let Some(node) = self.nodes.get_mut(&id) {
842            node.is_prefab_root = true;
843            node.prefab_name = Some(name.clone());
844        }
845        self.undo_stack.push(HierarchyAction::MakePrefab { id, prefab_name: name.clone() });
846        self.redo_stack.clear();
847        name
848    }
849
850    pub fn instantiate_prefab(&mut self, prefab_name: &str, parent: Option<NodeId>) -> Option<NodeId> {
851        let prefab = self.prefabs.get(prefab_name)?.clone();
852        let root = prefab.root_node;
853        Some(self.clone_node_recursive(root, parent, &mut HashMap::new()))
854    }
855
856    // ── Group / Ungroup ───────────────────────────────────────────────────────
857
858    /// Group selected nodes under a new Group node.
859    pub fn group_selected(&mut self, group_name: impl Into<String>) -> Option<NodeId> {
860        if self.selected.is_empty() { return None; }
861        let members: Vec<NodeId> = self.selected.clone();
862
863        // Find common parent (use first member's parent).
864        let common_parent = self.nodes.get(&members[0]).and_then(|n| n.parent);
865        let old_parents: Vec<Option<NodeId>> = members.iter()
866            .map(|&m| self.nodes.get(&m).and_then(|n| n.parent))
867            .collect();
868
869        let group_id = self.add_node(group_name, NodeKind::Group, common_parent);
870
871        // Reparent members under the group.
872        let members_clone = members.clone();
873        for m in members_clone {
874            self.reparent(m, Some(group_id));
875        }
876
877        // Pop the reparent actions so only the group action is at top.
878        // (In a real editor we'd batch these; here we record the compound action.)
879        self.undo_stack.push(HierarchyAction::GroupNodes {
880            group_id,
881            members,
882            old_parents,
883        });
884        self.redo_stack.clear();
885        Some(group_id)
886    }
887
888    // ── Flat list rebuild ─────────────────────────────────────────────────────
889
890    fn rebuild_flat_if_dirty(&mut self) {
891        if !self.flat_dirty { return; }
892        self.flat_list.clear();
893        let roots = self.roots.clone();
894        for root in &roots {
895            self.flatten_node(*root, &mut Vec::new());
896        }
897        self.flat_dirty = false;
898    }
899
900    fn flatten_node(&mut self, id: NodeId, accumulator: &mut Vec<NodeId>) {
901        let (matches, expanded, children) = {
902            let node = match self.nodes.get(&id) { Some(n) => n, None => return };
903            (self.node_matches(node), node.expanded, node.children.clone())
904        };
905        if matches {
906            self.flat_list.push(id);
907        }
908        if expanded {
909            for child in children {
910                self.flatten_node(child, accumulator);
911            }
912        }
913    }
914
915    /// Returns the current flat list (rebuilds if stale).
916    pub fn visible_flat_list(&mut self) -> &[NodeId] {
917        self.rebuild_flat_if_dirty();
918        &self.flat_list
919    }
920
921    // ── Undo / Redo ───────────────────────────────────────────────────────────
922
923    pub fn undo(&mut self) -> bool {
924        let action = match self.undo_stack.pop() {
925            Some(a) => a,
926            None => return false,
927        };
928        match &action {
929            HierarchyAction::AddNode { id, .. } => {
930                self.nodes.remove(id);
931                self.roots.retain(|&r| r != *id);
932                self.flat_dirty = true;
933            }
934            HierarchyAction::RemoveNode { id, saved_node, .. } => {
935                if let Some(saved) = saved_node.clone() {
936                    let parent = saved.parent;
937                    self.nodes.insert(*id, saved);
938                    if let Some(pid) = parent {
939                        if let Some(p) = self.nodes.get_mut(&pid) {
940                            p.children.push(*id);
941                        }
942                    } else {
943                        self.roots.push(*id);
944                    }
945                    self.flat_dirty = true;
946                }
947            }
948            HierarchyAction::RenameNode { id, old_name, .. } => {
949                if let Some(node) = self.nodes.get_mut(id) {
950                    node.name = old_name.clone();
951                    self.flat_dirty = true;
952                }
953            }
954            HierarchyAction::SetVisible { id, old, .. } => {
955                if let Some(node) = self.nodes.get_mut(id) {
956                    node.visible = *old;
957                }
958            }
959            HierarchyAction::SetLocked { id, old, .. } => {
960                if let Some(node) = self.nodes.get_mut(id) {
961                    node.locked = *old;
962                }
963            }
964            HierarchyAction::Reparent { child, old_parent, new_parent, old_index, .. } => {
965                // Reverse: move child back to old_parent at old_index.
966                if let Some(np) = new_parent {
967                    if let Some(p) = self.nodes.get_mut(np) {
968                        p.children.retain(|&c| c != *child);
969                    }
970                } else {
971                    self.roots.retain(|&r| r != *child);
972                }
973                if let Some(op) = old_parent {
974                    if let Some(p) = self.nodes.get_mut(op) {
975                        let idx = (*old_index).min(p.children.len());
976                        p.children.insert(idx, *child);
977                    }
978                } else {
979                    let idx = (*old_index).min(self.roots.len());
980                    self.roots.insert(idx, *child);
981                }
982                if let Some(cn) = self.nodes.get_mut(child) {
983                    cn.parent = *old_parent;
984                    cn.sibling_index = *old_index;
985                }
986                let depth = old_parent
987                    .and_then(|p| self.nodes.get(&p))
988                    .map(|p| p.depth + 1)
989                    .unwrap_or(0);
990                self.update_depths(*child, depth);
991                self.flat_dirty = true;
992            }
993            HierarchyAction::DuplicateSubtree { all_new_ids, .. } => {
994                for &nid in all_new_ids {
995                    if let Some(node) = self.nodes.remove(&nid) {
996                        if let Some(pid) = node.parent {
997                            if let Some(p) = self.nodes.get_mut(&pid) {
998                                p.children.retain(|&c| c != nid);
999                            }
1000                        } else {
1001                            self.roots.retain(|&r| r != nid);
1002                        }
1003                    }
1004                }
1005                self.flat_dirty = true;
1006            }
1007            _ => {}
1008        }
1009        self.redo_stack.push(action);
1010        true
1011    }
1012
1013    pub fn redo(&mut self) -> bool {
1014        let action = match self.redo_stack.pop() {
1015            Some(a) => a,
1016            None => return false,
1017        };
1018        match &action {
1019            HierarchyAction::AddNode { id, parent, name, kind } => {
1020                let depth = parent.and_then(|p| self.nodes.get(&p)).map(|p| p.depth + 1).unwrap_or(0);
1021                let sibling_index = parent
1022                    .and_then(|p| self.nodes.get(&p))
1023                    .map(|p| p.children.len())
1024                    .unwrap_or(self.roots.len());
1025                let mut node = HierarchyNode::new(*id, name.clone(), *kind);
1026                node.parent = *parent;
1027                node.depth = depth;
1028                node.sibling_index = sibling_index;
1029                self.nodes.insert(*id, node);
1030                if let Some(pid) = parent {
1031                    if let Some(p) = self.nodes.get_mut(pid) { p.children.push(*id); }
1032                } else {
1033                    self.roots.push(*id);
1034                }
1035                self.flat_dirty = true;
1036            }
1037            HierarchyAction::RenameNode { id, new_name, .. } => {
1038                if let Some(n) = self.nodes.get_mut(id) {
1039                    n.name = new_name.clone();
1040                    self.flat_dirty = true;
1041                }
1042            }
1043            HierarchyAction::SetVisible { id, new, .. } => {
1044                if let Some(n) = self.nodes.get_mut(id) { n.visible = *new; }
1045            }
1046            HierarchyAction::SetLocked { id, new, .. } => {
1047                if let Some(n) = self.nodes.get_mut(id) { n.locked = *new; }
1048            }
1049            _ => {}
1050        }
1051        self.undo_stack.push(action);
1052        true
1053    }
1054
1055    // ── Keyboard navigation ────────────────────────────────────────────────────
1056
1057    /// Move keyboard focus up; returns the newly focused NodeId if any.
1058    pub fn keyboard_up(&mut self) -> Option<NodeId> {
1059        let count = self.visible_flat_list().len();
1060        self.keyboard.move_up(count);
1061        self.flat_list.get(self.keyboard.focused_index).copied()
1062    }
1063
1064    /// Move keyboard focus down.
1065    pub fn keyboard_down(&mut self) -> Option<NodeId> {
1066        let count = self.visible_flat_list().len();
1067        self.keyboard.move_down(count);
1068        self.flat_list.get(self.keyboard.focused_index).copied()
1069    }
1070
1071    /// Activate rename mode for the focused node.
1072    pub fn keyboard_begin_rename(&mut self) {
1073        if let Some(&id) = self.flat_list.get(self.keyboard.focused_index) {
1074            if let Some(node) = self.nodes.get(&id) {
1075                let name = node.name.clone();
1076                self.keyboard.begin_rename(&name);
1077            }
1078        }
1079    }
1080
1081    /// Finish rename and apply.
1082    pub fn keyboard_finish_rename(&mut self) {
1083        if let Some(new_name) = self.keyboard.finish_rename() {
1084            if let Some(&id) = self.flat_list.get(self.keyboard.focused_index) {
1085                self.rename(id, new_name);
1086            }
1087        }
1088    }
1089
1090    /// Delete the focused node.
1091    pub fn keyboard_delete(&mut self) {
1092        if let Some(&id) = self.flat_list.get(self.keyboard.focused_index).cloned().as_ref() {
1093            self.delete_recursive(id);
1094        }
1095    }
1096
1097    // ── Drag and drop ─────────────────────────────────────────────────────────
1098
1099    pub fn begin_drag(&mut self, id: NodeId, y: f32) {
1100        self.drag_drop.start_drag(id, y);
1101    }
1102
1103    pub fn update_drag(&mut self, y: f32, hover: Option<NodeId>, pos: DropPosition) {
1104        self.drag_drop.update(y, hover, pos);
1105    }
1106
1107    pub fn finish_drag(&mut self) {
1108        if let Some((dragged, Some(target), pos)) = self.drag_drop.finish() {
1109            match pos {
1110                DropPosition::Inside => self.reparent(dragged, Some(target)),
1111                DropPosition::Before | DropPosition::After => {
1112                    let new_parent = self.nodes.get(&target).and_then(|n| n.parent);
1113                    self.reparent(dragged, new_parent);
1114                }
1115            }
1116        } else {
1117            self.drag_drop.cancel();
1118        }
1119    }
1120
1121    // ── Rendering ─────────────────────────────────────────────────────────────
1122
1123    pub fn render_ascii(&mut self) -> String {
1124        self.rebuild_flat_if_dirty();
1125        let flat = self.flat_list.clone();
1126        let mut out = String::new();
1127        out.push_str("┌─ Hierarchy ──────────────────────────\n");
1128        for (i, &id) in flat.iter().enumerate() {
1129            if let Some(node) = self.nodes.get(&id) {
1130                let sel = if self.is_selected(id) { ">" } else { " " };
1131                let focus = if i == self.keyboard.focused_index { "*" } else { " " };
1132                out.push_str(&format!("{}{} {}\n", focus, sel, node.render_line()));
1133            }
1134        }
1135        out.push_str("└──────────────────────────────────────\n");
1136        out
1137    }
1138
1139    pub fn node_count(&self) -> usize {
1140        self.nodes.len()
1141    }
1142}
1143
1144impl Default for HierarchyPanel {
1145    fn default() -> Self { Self::new() }
1146}
1147
1148// ─────────────────────────────────────────────────────────────────────────────
1149// Tests
1150// ─────────────────────────────────────────────────────────────────────────────
1151
1152#[cfg(test)]
1153mod tests {
1154    use super::*;
1155
1156    fn panel_with_tree() -> (HierarchyPanel, NodeId, NodeId, NodeId) {
1157        let mut p = HierarchyPanel::new();
1158        let root  = p.add_node("root",  NodeKind::Entity, None);
1159        let child = p.add_node("child", NodeKind::Glyph,  Some(root));
1160        let leaf  = p.add_node("leaf",  NodeKind::Glyph,  Some(child));
1161        (p, root, child, leaf)
1162    }
1163
1164    #[test]
1165    fn test_add_and_count() {
1166        let (p, _, _, _) = panel_with_tree();
1167        assert_eq!(p.node_count(), 3);
1168    }
1169
1170    #[test]
1171    fn test_delete_recursive() {
1172        let (mut p, root, child, _leaf) = panel_with_tree();
1173        p.delete_recursive(child);
1174        assert!(!p.nodes.contains_key(&child));
1175        assert!(!p.nodes.contains_key(&_leaf));
1176        assert!(p.nodes.contains_key(&root));
1177    }
1178
1179    #[test]
1180    fn test_rename_undo() {
1181        let (mut p, _root, child, _leaf) = panel_with_tree();
1182        p.rename(child, "renamed");
1183        assert_eq!(p.nodes[&child].name, "renamed");
1184        p.undo();
1185        assert_eq!(p.nodes[&child].name, "child");
1186    }
1187
1188    #[test]
1189    fn test_reparent() {
1190        let (mut p, root, _child, leaf) = panel_with_tree();
1191        p.reparent(leaf, Some(root));
1192        assert_eq!(p.nodes[&leaf].parent, Some(root));
1193        assert_eq!(p.nodes[&leaf].depth, 1);
1194    }
1195
1196    #[test]
1197    fn test_reparent_no_cycle() {
1198        let (mut p, root, child, _leaf) = panel_with_tree();
1199        let depth_before = p.nodes[&root].depth;
1200        p.reparent(root, Some(child)); // should be ignored
1201        assert_eq!(p.nodes[&root].depth, depth_before);
1202    }
1203
1204    #[test]
1205    fn test_duplicate_subtree() {
1206        let (mut p, _root, child, _leaf) = panel_with_tree();
1207        let count_before = p.node_count();
1208        p.duplicate_subtree(child);
1209        assert!(p.node_count() > count_before);
1210    }
1211
1212    #[test]
1213    fn test_visibility_toggle() {
1214        let (mut p, root, _, _) = panel_with_tree();
1215        assert!(p.nodes[&root].visible);
1216        p.toggle_visible(root);
1217        assert!(!p.nodes[&root].visible);
1218        p.undo();
1219        assert!(p.nodes[&root].visible);
1220    }
1221
1222    #[test]
1223    fn test_search_filter() {
1224        let (p, _, _, _) = panel_with_tree();
1225        let found = p.filter_by_name("leaf");
1226        assert_eq!(found.len(), 1);
1227        let empty = p.filter_by_name("nonexistent");
1228        assert!(empty.is_empty());
1229    }
1230
1231    #[test]
1232    fn test_kind_filter() {
1233        let (p, _, _, _) = panel_with_tree();
1234        let glyphs = p.filter_by_kind(NodeKind::Glyph);
1235        assert_eq!(glyphs.len(), 2);
1236    }
1237
1238    #[test]
1239    fn test_selection_range() {
1240        let (mut p, root, child, leaf) = panel_with_tree();
1241        p.expand_all();
1242        // Rebuild flat list so range_select works.
1243        p.rebuild_flat_if_dirty();
1244        p.select(root);
1245        p.range_select(leaf);
1246        assert_eq!(p.selected.len(), 3);
1247    }
1248
1249    #[test]
1250    fn test_prefab_creation() {
1251        let (mut p, _root, child, _leaf) = panel_with_tree();
1252        p.make_prefab(child, "my_prefab");
1253        assert!(p.prefabs.contains_key("my_prefab"));
1254        assert!(p.nodes[&child].is_prefab_root);
1255    }
1256
1257    #[test]
1258    fn test_serializer_roundtrip() {
1259        let (p, _, _, _) = panel_with_tree();
1260        let serialized = HierarchySerializer::serialize(&p);
1261        assert!(serialized.contains("root"));
1262        assert!(serialized.contains("child"));
1263        assert!(serialized.contains("leaf"));
1264    }
1265}