Skip to main content

lv_tui/
node.rs

1use std::cell::Cell;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use crate::buffer::Buffer;
5use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
6use crate::dirty::Dirty;
7use crate::event::{Event, EventPhase};
8use crate::geom::{Pos, Rect, Size};
9use crate::layout::Constraint;
10use crate::render::RenderCx;
11use crate::style::{Style, TextAlign, TextTruncate, TextWrap};
12
13/// A globally unique identifier for a node in the component tree.
14///
15/// `NodeId::ROOT` (value 0) is reserved for the root node. All other nodes
16/// receive a monotonically increasing id from [`NodeId::new`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct NodeId(u64);
19
20impl NodeId {
21    /// Returns a new unique `NodeId`.
22    pub fn new() -> Self {
23        static NEXT: AtomicU64 = AtomicU64::new(1);
24        Self(NEXT.fetch_add(1, Ordering::Relaxed))
25    }
26
27    /// The reserved root node id (value 0).
28    pub const ROOT: NodeId = NodeId(0);
29}
30
31/// A node in the component tree.
32///
33/// Each `Node` owns a boxed [`Component`], tracks its layout [`Rect`],
34/// dirty flags, parent link, and child nodes. The tree is mutable —
35/// `Node::add_child` and the builder methods on container widgets grow
36/// the tree at construction time.
37pub struct Node {
38    /// Unique identifier for this node.
39    pub id: NodeId,
40    /// The layout rectangle assigned to this node (interior-mutable for
41    /// borrow-splitting during layout and render).
42    pub rect: Cell<Rect>,
43    /// Per-node dirty flags.
44    pub dirty: Dirty,
45    /// Parent node id, if any.
46    pub parent: Option<NodeId>,
47    /// The boxed component that owns the logic for this node.
48    pub component: Box<dyn Component>,
49    /// Child nodes in display order.
50    pub children: Vec<Node>,
51}
52
53impl Node {
54    /// Returns the current layout rectangle of this node.
55    pub fn rect(&self) -> Rect {
56        self.rect.get()
57    }
58
59    /// Updates the layout rectangle of this node.
60    pub fn set_rect(&self, r: Rect) {
61        self.rect.set(r);
62    }
63
64    /// Creates a new leaf node wrapping the given component.
65    ///
66    /// The node receives a fresh [`NodeId`] via [`NodeId::new`].
67    pub fn new(component: impl Component + 'static) -> Self {
68        Self {
69            id: NodeId::new(),
70            rect: Cell::new(Rect::default()),
71            dirty: Dirty::NONE,
72            parent: None,
73            component: Box::new(component),
74            children: Vec::new(),
75        }
76    }
77
78    /// Creates the root node with [`NodeId::ROOT`].
79    pub fn root(component: impl Component + 'static) -> Self {
80        Self {
81            id: NodeId::ROOT,
82            rect: Cell::new(Rect::default()),
83            dirty: Dirty::NONE,
84            parent: None,
85            component: Box::new(component),
86            children: Vec::new(),
87        }
88    }
89
90    /// Appends a child node at the end of the children list.
91    pub fn add_child(&mut self, child: Node) {
92        self.children.push(child);
93    }
94
95    /// Renders this subtree into `buffer`, clipping to this node's rect.
96    pub fn render(&self, buffer: &mut Buffer, focused_id: Option<NodeId>) {
97        self.render_with_clip(buffer, focused_id, None);
98    }
99
100    /// Renders this subtree with an optional clip rectangle.
101    pub fn render_with_clip(
102        &self,
103        buffer: &mut Buffer,
104        focused_id: Option<NodeId>,
105        clip_rect: Option<Rect>,
106    ) {
107        self.render_with_clip_and_wrap(
108            buffer,
109            focused_id,
110            clip_rect,
111            TextWrap::None,
112            TextTruncate::None,
113            TextAlign::Left,
114        );
115    }
116
117    /// Renders this subtree with full text-flow control.
118    pub fn render_with_clip_and_wrap(
119        &self,
120        buffer: &mut Buffer,
121        focused_id: Option<NodeId>,
122        clip_rect: Option<Rect>,
123        wrap: crate::style::TextWrap,
124        truncate: crate::style::TextTruncate,
125        align: crate::style::TextAlign,
126    ) {
127        self.render_inner(buffer, focused_id, clip_rect, wrap, truncate, align, None, None);
128    }
129
130    /// Renders this subtree, merging style values inherited from a parent.
131    pub fn render_with_parent(
132        &self,
133        buffer: &mut Buffer,
134        focused_id: Option<NodeId>,
135        clip_rect: Option<Rect>,
136        wrap: crate::style::TextWrap,
137        truncate: crate::style::TextTruncate,
138        align: crate::style::TextAlign,
139        parent_style: Option<&crate::style::Style>,
140    ) {
141        self.render_inner(
142            buffer,
143            focused_id,
144            clip_rect,
145            wrap,
146            truncate,
147            align,
148            None,
149            parent_style,
150        );
151    }
152
153    /// Core render entry-point.
154    ///
155    /// Resolves the effective style from the stylesheet, parent inheritance,
156    /// and the component's own style, then calls `Component::render`.
157    pub fn render_inner(
158        &self,
159        buffer: &mut Buffer,
160        focused_id: Option<NodeId>,
161        clip_rect: Option<Rect>,
162        wrap: crate::style::TextWrap,
163        truncate: crate::style::TextTruncate,
164        align: crate::style::TextAlign,
165        sheet: Option<&crate::style_parser::StyleSheet>,
166        parent_style: Option<&Style>,
167    ) {
168        let mut style = self.component.style();
169        if let Some(p) = parent_style {
170            style = crate::style_parser::inherit_style(p, &style);
171        }
172        if let Some(s) = sheet {
173            use crate::style_parser::WidgetState;
174            // Build widget state for pseudo-class matching
175            let focus_within = focused_id.map_or(false, |fid| {
176                if fid == self.id { true }
177                else {
178                    let mut ids = Vec::new();
179                    self.collect_focusable(&mut ids);
180                    ids.contains(&fid)
181                }
182            });
183            let state = WidgetState {
184                focused: focused_id == Some(self.id),
185                focus_within,
186                ..WidgetState::default()
187            };
188            let r = s.resolve(
189                self.component.type_name(),
190                self.component.id(),
191                self.component.class(),
192                &state,
193            );
194            style = crate::style_parser::merge_styles(r, &style);
195        }
196        let mut cx = RenderCx::new(self.rect(), buffer, style);
197        cx.focused_id = focused_id;
198        cx.clip_rect = clip_rect;
199        cx.wrap = wrap;
200        cx.truncate = truncate;
201        cx.align = align;
202        self.component.render(&mut cx);
203    }
204
205    /// Dispatches an event to this node and (if not stopped) its children.
206    ///
207    /// Calls `Component::update` before `Component::event`.
208    pub fn event(
209        &mut self,
210        event: &Event,
211        global_dirty: &mut Dirty,
212        quit: &mut bool,
213        task_tx: Option<std::sync::mpsc::Sender<String>>,
214    ) {
215        let mut stopped = false;
216        let mut cx = EventCx::with_task_sender(
217            &mut self.dirty,
218            global_dirty,
219            quit,
220            EventPhase::Target,
221            &mut stopped,
222            task_tx.clone(),
223        );
224        cx.current_node_id = self.id;
225        self.component.update(&mut cx);
226        self.component.event(event, &mut cx);
227        if !stopped {
228            for child in &mut self.children {
229                child.event(event, global_dirty, quit, task_tx.clone());
230            }
231        }
232    }
233
234    /// Measures the intrinsic size of this node's subtree given `constraint`.
235    pub fn measure(&self, constraint: Constraint) -> Size {
236        let mut cx = MeasureCx { constraint };
237        self.component.measure(constraint, &mut cx)
238    }
239
240    /// Recursively calls [`Component::mount`] on this node and its children.
241    pub fn mount(
242        &mut self,
243        global_dirty: &mut Dirty,
244        quit: &mut bool,
245        task_tx: Option<std::sync::mpsc::Sender<String>>,
246    ) {
247        let mut stopped = false;
248        let mut cx = EventCx::with_task_sender(
249            &mut self.dirty,
250            global_dirty,
251            quit,
252            EventPhase::Target,
253            &mut stopped,
254            task_tx.clone(),
255        );
256        cx.current_node_id = self.id;
257        self.component.mount(&mut cx);
258        for child in &mut self.children {
259            child.mount(global_dirty, quit, task_tx.clone());
260        }
261    }
262
263    /// Recursively draws border outlines and type-name labels for every node.
264    ///
265    /// The currently focused node is drawn with a double border; others use
266    /// rounded borders. This is toggled by the `d` key in debug mode.
267    pub fn debug_render(&self, buffer: &mut Buffer, focused_id: Option<NodeId>) {
268        let type_name = self.component.type_name();
269        let short = type_name.rsplit("::").next().unwrap_or(type_name);
270
271        let border = if focused_id == Some(self.id) {
272            crate::style::Border::Double
273        } else {
274            crate::style::Border::Rounded
275        };
276        let style = crate::style::Style::default().fg(crate::style::Color::Yellow);
277        buffer.draw_border(self.rect(), border, &style);
278
279        buffer.write_text(
280            Pos {
281                x: self.rect().x.saturating_add(2),
282                y: self.rect().y,
283            },
284            self.rect(),
285            short,
286            &style,
287        );
288
289        self.component.for_each_child(&mut |child: &Node| {
290            child.debug_render(buffer, focused_id);
291        });
292    }
293
294    /// Recursively calls [`Component::unmount`] on children (reverse order),
295    /// then on this node.
296    pub fn unmount(
297        &mut self,
298        global_dirty: &mut Dirty,
299        quit: &mut bool,
300        task_tx: Option<std::sync::mpsc::Sender<String>>,
301    ) {
302        for child in &mut self.children {
303            child.unmount(global_dirty, quit, task_tx.clone());
304        }
305        let mut stopped = false;
306        let mut cx = EventCx::with_task_sender(
307            &mut self.dirty,
308            global_dirty,
309            quit,
310            EventPhase::Target,
311            &mut stopped,
312            task_tx,
313        );
314        self.component.unmount(&mut cx);
315    }
316
317    /// Runs layout for this subtree.
318    ///
319    /// Sets this node's rect, assigns parent links to direct children, then
320    /// calls `Component::layout` so the component can size and position its
321    /// children.
322    pub fn layout(&mut self, rect: Rect) {
323        self.set_rect(rect);
324        for child in &mut self.children {
325            child.parent = Some(self.id);
326        }
327        let mut cx = LayoutCx::new(&mut self.children);
328        self.component.layout(rect, &mut cx);
329    }
330
331    /// Returns the path of node ids from `self` down to `target_id`, inclusive.
332    ///
333    /// Returns `None` if `target_id` is not in this subtree.
334    pub fn find_path_to(&self, target_id: NodeId) -> Option<Vec<NodeId>> {
335        if self.id == target_id {
336            return Some(vec![self.id]);
337        }
338        let mut result: Option<Vec<NodeId>> = None;
339        self.component.for_each_child(&mut |child: &Node| {
340            if result.is_none() {
341                if let Some(child_path) = child.find_path_to(target_id) {
342                    let mut full = vec![self.id];
343                    full.extend(child_path);
344                    result = Some(full);
345                }
346            }
347        });
348        result
349    }
350
351    /// Returns `true` if this node or any descendant has [`Dirty::PAINT`] set.
352    pub fn any_needs_paint(&self) -> bool {
353        self.dirty.contains(Dirty::PAINT)
354            || self.children.iter().any(|c| c.any_needs_paint())
355    }
356
357    /// Returns `true` if this node or any descendant has [`Dirty::LAYOUT`] set.
358    pub fn any_needs_layout(&self) -> bool {
359        self.dirty.contains(Dirty::LAYOUT)
360            || self.children.iter().any(|c| c.any_needs_layout())
361    }
362
363    /// Resets all dirty flags on this node to [`Dirty::NONE`].
364    ///
365    /// Does not recurse into children — the caller is responsible for clearing
366    /// the full tree if needed.
367    pub fn clear_dirty(&mut self) {
368        self.dirty = Dirty::NONE;
369    }
370
371    /// Collects ids of all focusable nodes in this subtree into `ids`.
372    pub fn collect_focusable(&self, ids: &mut Vec<NodeId>) {
373        if self.component.focusable() {
374            ids.push(self.id);
375        }
376        self.component
377            .for_each_child(&mut |child: &Node| child.collect_focusable(ids));
378    }
379
380    /// Performs a hit-test against the layout rects in this subtree.
381    ///
382    /// Returns the deepest node whose rect contains `pos`, or `None` if no
383    /// node matches.
384    pub fn hit_test(&self, pos: Pos) -> Option<NodeId> {
385        let mut hit: Option<NodeId> = None;
386        self.component.for_each_child(&mut |child: &Node| {
387            if let Some(id) = child.hit_test(pos) {
388                hit = Some(id);
389            }
390        });
391        if hit.is_none() && self.rect().contains(pos) {
392            hit = Some(self.id);
393        }
394        hit
395    }
396
397    /// Dispatches an event to a specific target node in this subtree.
398    ///
399    /// Used by the runtime for capture and bubble phases. Returns `true` if
400    /// the target was found, `false` otherwise.
401    pub fn send_event(
402        &mut self,
403        target_id: NodeId,
404        event: &Event,
405        global_dirty: &mut Dirty,
406        quit: &mut bool,
407        phase: EventPhase,
408        stopped: &mut bool,
409        task_tx: Option<std::sync::mpsc::Sender<String>>,
410    ) -> bool {
411        if *stopped {
412            return true;
413        }
414        if self.id == target_id {
415            let mut cx = EventCx::with_task_sender(
416                &mut self.dirty,
417                global_dirty,
418                quit,
419                phase,
420                stopped,
421                task_tx.clone(),
422            );
423            cx.current_node_id = self.id;
424            self.component.event(event, &mut cx);
425            return true;
426        }
427        let mut found = false;
428        self.component.for_each_child_mut(&mut |child: &mut Node| {
429            if !found && !*stopped {
430                found = child.send_event(
431                    target_id, event, global_dirty, quit, phase, stopped, task_tx.clone(),
432                );
433            }
434        });
435        found
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::widgets::Label;
443
444    #[test]
445    fn test_collect_focusable_simple() {
446        let label = Node::new(Label::new("test"));
447        let mut ids = Vec::new();
448        label.collect_focusable(&mut ids);
449        // Label.focusable() returns false
450        assert!(ids.is_empty());
451    }
452
453    #[test]
454    fn test_collect_focusable_tree() {
455        // Column → [Label, Checkbox(opt)] → Checkbox is focusable, Label is not
456        use crate::widgets::{Checkbox, Column};
457        let column = Node::new(
458            Column::new()
459                .child(Label::new("not focusable"))
460                .child(Checkbox::new("focusable"))
461        );
462        let mut ids = Vec::new();
463        column.collect_focusable(&mut ids);
464        assert_eq!(ids.len(), 1);
465    }
466
467    #[test]
468    fn test_collect_focusable_block() {
469        // Column → Block(Checkbox("inner"))
470        use crate::widgets::{Block, Checkbox, Column};
471        let column = Node::new(
472            Column::new()
473                .child(Block::new(Checkbox::new("inner")))
474        );
475        let mut ids = Vec::new();
476        column.collect_focusable(&mut ids);
477        assert_eq!(ids.len(), 1, "focusable inside Block should be found");
478    }
479}