Skip to main content

godot_bevy/
node_tree_view.rs

1use thiserror::Error;
2
3#[non_exhaustive]
4#[derive(Debug, Error)]
5pub enum NodeTreeViewError {
6    #[error("Could not find node matching pattern: {0}")]
7    NodeNotFound(String),
8}
9
10/// Trait for objects that can be created from a node reference.
11///
12/// This is implemented by the `#[derive(NodeTreeView)]` macro.
13pub trait NodeTreeView {
14    /// Create a new instance from a node reference.
15    fn from_node<T: godot::obj::Inherits<godot::classes::Node>>(
16        node: godot::obj::Gd<T>,
17    ) -> Result<Self, NodeTreeViewError>
18    where
19        Self: Sized;
20}
21
22/// Find a node by matching a pattern with wildcards.
23///
24/// Supports patterns like:
25/// - `/root/*/HUD/CurrentLevel` - matches any single node name where * appears
26/// - `/root/Level*/HUD/CurrentLevel` - matches node names starting with "Level"
27/// - `*/HUD/CurrentLevel` - matches relative to the base node
28pub fn find_node_by_pattern(
29    base_node: &godot::obj::Gd<godot::classes::Node>,
30    pattern: &str,
31) -> Option<godot::obj::Gd<godot::classes::Node>> {
32    // Handle absolute vs relative paths
33    let (search_root, pattern_parts) = if let Some(stripped) = pattern.strip_prefix('/') {
34        // Absolute path - start from scene tree root
35        let scene_tree = base_node.get_tree()?;
36        let root = scene_tree.get_root()?;
37        let root_as_node = root.upcast::<godot::classes::Node>();
38        let mut parts: Vec<&str> = stripped.split('/').filter(|s| !s.is_empty()).collect();
39
40        // If the first part is "root", skip it since we're already starting from the root
41        if !parts.is_empty() && parts[0] == "root" {
42            parts.remove(0);
43        }
44
45        (root_as_node, parts)
46    } else {
47        // Relative path - start from base node
48        let parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
49        (base_node.clone(), parts)
50    };
51
52    find_node_recursive(&search_root, &pattern_parts, 0)
53}
54
55fn find_node_recursive(
56    current_node: &godot::obj::Gd<godot::classes::Node>,
57    pattern_parts: &[&str],
58    depth: usize,
59) -> Option<godot::obj::Gd<godot::classes::Node>> {
60    // If we've matched all pattern parts, we found our target
61    if depth >= pattern_parts.len() {
62        return Some(current_node.clone());
63    }
64
65    let pattern_part = pattern_parts[depth];
66
67    // If this pattern part is a wildcard
68    if pattern_part == "*" {
69        // Try all children
70        for i in 0..current_node.get_child_count() {
71            if let Some(child) = current_node.get_child(i)
72                && let Some(result) = find_node_recursive(&child, pattern_parts, depth + 1)
73            {
74                return Some(result);
75            }
76        }
77    } else if pattern_part.contains('*') {
78        // Handle prefix/suffix wildcards like "Level*" or "*Button"
79        for i in 0..current_node.get_child_count() {
80            if let Some(child) = current_node.get_child(i) {
81                let child_name = child.get_name().to_string();
82                if matches_wildcard_pattern(&child_name, pattern_part)
83                    && let Some(result) = find_node_recursive(&child, pattern_parts, depth + 1)
84                {
85                    return Some(result);
86                }
87            }
88        }
89    } else {
90        // Exact name match
91        if current_node.has_node(pattern_part) {
92            let child = current_node.get_node_as::<godot::classes::Node>(pattern_part);
93            if let Some(result) = find_node_recursive(&child, pattern_parts, depth + 1) {
94                return Some(result);
95            }
96        }
97    }
98
99    None
100}
101
102fn matches_wildcard_pattern(text: &str, pattern: &str) -> bool {
103    if pattern == "*" {
104        return true;
105    }
106
107    // Handle simple prefix/suffix patterns
108    if pattern.starts_with('*') && pattern.len() > 1 {
109        let suffix = &pattern[1..];
110        return text.ends_with(suffix);
111    }
112
113    if pattern.ends_with('*') && pattern.len() > 1 {
114        let prefix = &pattern[..pattern.len() - 1];
115        return text.starts_with(prefix);
116    }
117
118    // Handle patterns with * in the middle (basic implementation)
119    if let Some(star_pos) = pattern.find('*') {
120        let prefix = &pattern[..star_pos];
121        let suffix = &pattern[star_pos + 1..];
122        return text.starts_with(prefix)
123            && text.ends_with(suffix)
124            && text.len() >= prefix.len() + suffix.len();
125    }
126
127    // No wildcard, exact match
128    text == pattern
129}