Skip to main content

agpu/widget/
tree.rs

1//! TreeView widget with hierarchical expand/collapse nodes.
2
3use crate::core::{Color, Position, Rect, TextStyle};
4use crate::ontology::{
5    AgentAction, AgentCapability, Discoverable, SemanticRole, UiNode, WidgetSchema,
6};
7use crate::paint::Painter;
8use crate::widget::Widget;
9
10/// A node in a tree hierarchy.
11#[derive(Debug, Clone)]
12pub struct TreeNode {
13    pub id: String,
14    pub label: String,
15    pub children: Vec<TreeNode>,
16    pub expanded: bool,
17}
18
19impl TreeNode {
20    #[must_use]
21    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
22        Self {
23            id: id.into(),
24            label: label.into(),
25            children: Vec::new(),
26            expanded: false,
27        }
28    }
29
30    #[must_use]
31    pub fn child(mut self, child: TreeNode) -> Self {
32        self.children.push(child);
33        self
34    }
35
36    #[must_use]
37    pub fn expanded(mut self, expanded: bool) -> Self {
38        self.expanded = expanded;
39        self
40    }
41
42    /// Count the total visible (expanded) nodes starting from this node.
43    fn visible_count(&self) -> usize {
44        let mut count = 1; // self
45        if self.expanded {
46            for child in &self.children {
47                count += child.visible_count();
48            }
49        }
50        count
51    }
52
53    /// Recursively draw this node and its visible children.
54    fn draw_recursive(
55        &self,
56        painter: &mut dyn Painter,
57        x: f32,
58        y: &mut f32,
59        row_height: f32,
60        area: &Rect,
61        indent: f32,
62    ) {
63        if *y + row_height < area.y || *y > area.y + area.height {
64            *y += row_height;
65            if self.expanded {
66                for child in &self.children {
67                    child.draw_recursive(painter, x + indent, y, row_height, area, indent);
68                }
69            }
70            return;
71        }
72
73        let style = TextStyle {
74            font_size: 13.0,
75            color: Color::WHITE,
76            ..TextStyle::default()
77        };
78
79        // Expand/collapse arrow if has children
80        if !self.children.is_empty() {
81            let arrow = if self.expanded { "▾" } else { "▸" };
82            painter.text(
83                Position::new(x, *y + (row_height - style.font_size) * 0.5),
84                arrow,
85                &style,
86            );
87        }
88
89        painter.text(
90            Position::new(x + 16.0, *y + (row_height - style.font_size) * 0.5),
91            &self.label,
92            &style,
93        );
94
95        *y += row_height;
96
97        if self.expanded {
98            for child in &self.children {
99                child.draw_recursive(painter, x + indent, y, row_height, area, indent);
100            }
101        }
102    }
103}
104
105/// A hierarchical tree view widget.
106pub struct TreeView {
107    pub id: String,
108    pub root: TreeNode,
109    bg_color: Option<Color>,
110    fg_color: Option<Color>,
111    corner_radius: Option<f32>,
112    font_size: Option<f32>,
113    is_bold: bool,
114}
115
116impl TreeView {
117    #[must_use]
118    pub fn new(id: impl Into<String>, root: TreeNode) -> Self {
119        Self {
120            id: id.into(),
121            root,
122            bg_color: None,
123            fg_color: None,
124            corner_radius: None,
125            font_size: None,
126            is_bold: false,
127        }
128    }
129
130    #[must_use]
131    pub fn bg(mut self, color: Color) -> Self {
132        self.bg_color = Some(color);
133        self
134    }
135
136    #[must_use]
137    pub fn fg(mut self, color: Color) -> Self {
138        self.fg_color = Some(color);
139        self
140    }
141
142    #[must_use]
143    pub fn rounded(mut self, radius: f32) -> Self {
144        self.corner_radius = Some(radius);
145        self
146    }
147
148    #[must_use]
149    pub fn text_size(mut self, size: f32) -> Self {
150        self.font_size = Some(size);
151        self
152    }
153
154    #[must_use]
155    pub fn bold(mut self) -> Self {
156        self.is_bold = true;
157        self
158    }
159}
160
161impl Widget for TreeView {
162    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
163        let bg = self.bg_color.unwrap_or(Color::rgba(0.1, 0.1, 0.13, 1.0));
164        let radius = self.corner_radius.unwrap_or(3.0);
165        painter.fill_rect(area, bg, radius);
166
167        let row_height = 24.0;
168        let indent = 18.0;
169        let mut y = area.y;
170
171        self.root
172            .draw_recursive(painter, area.x + 4.0, &mut y, row_height, &area, indent);
173    }
174
175    fn ui_node(&self) -> UiNode {
176        UiNode::new("TreeView", SemanticRole::TreeNode).with_id(&self.id)
177    }
178}
179
180impl Discoverable for TreeView {
181    fn schema(&self) -> WidgetSchema {
182        WidgetSchema::new(
183            "TreeView",
184            "A hierarchical tree view",
185            SemanticRole::TreeNode,
186        )
187    }
188
189    fn capabilities(&self) -> Vec<AgentCapability> {
190        vec![
191            AgentCapability::Focusable,
192            AgentCapability::Expandable {
193                expanded: self.root.expanded,
194            },
195        ]
196    }
197
198    fn actions(&self) -> Vec<AgentAction> {
199        vec![
200            AgentAction::simple("expand", "Expand a node", true),
201            AgentAction::simple("collapse", "Collapse a node", true),
202        ]
203    }
204
205    fn semantic_role(&self) -> SemanticRole {
206        SemanticRole::TreeNode
207    }
208
209    fn agent_state(&self) -> serde_json::Value {
210        serde_json::json!({
211            "root_label": self.root.label,
212            "root_expanded": self.root.expanded,
213            "visible_nodes": self.root.visible_count(),
214        })
215    }
216
217    fn execute_action(
218        &mut self,
219        action: &str,
220        _params: &serde_json::Value,
221    ) -> Result<serde_json::Value, String> {
222        match action {
223            "expand" => {
224                self.root.expanded = true;
225                Ok(serde_json::json!({ "expanded": true }))
226            }
227            "collapse" => {
228                self.root.expanded = false;
229                Ok(serde_json::json!({ "expanded": false }))
230            }
231            _ => Err(format!("Unknown action: {action}")),
232        }
233    }
234
235    fn agent_id(&self) -> Option<&str> {
236        Some(&self.id)
237    }
238}