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            let r = s.resolve(
174                self.component.type_name(),
175                self.component.id(),
176                self.component.class(),
177            );
178            style = crate::style_parser::merge_styles(r, &style);
179        }
180        let mut cx = RenderCx::new(self.rect(), buffer, style);
181        cx.focused_id = focused_id;
182        cx.clip_rect = clip_rect;
183        cx.wrap = wrap;
184        cx.truncate = truncate;
185        cx.align = align;
186        self.component.render(&mut cx);
187    }
188
189    /// Dispatches an event to this node and (if not stopped) its children.
190    ///
191    /// Calls `Component::update` before `Component::event`.
192    pub fn event(
193        &mut self,
194        event: &Event,
195        global_dirty: &mut Dirty,
196        quit: &mut bool,
197        task_tx: Option<std::sync::mpsc::Sender<String>>,
198    ) {
199        let mut stopped = false;
200        let mut cx = EventCx::with_task_sender(
201            &mut self.dirty,
202            global_dirty,
203            quit,
204            EventPhase::Target,
205            &mut stopped,
206            task_tx.clone(),
207        );
208        self.component.update(&mut cx);
209        self.component.event(event, &mut cx);
210        if !stopped {
211            for child in &mut self.children {
212                child.event(event, global_dirty, quit, task_tx.clone());
213            }
214        }
215    }
216
217    /// Measures the intrinsic size of this node's subtree given `constraint`.
218    pub fn measure(&self, constraint: Constraint) -> Size {
219        let mut cx = MeasureCx { constraint };
220        self.component.measure(constraint, &mut cx)
221    }
222
223    /// Recursively calls [`Component::mount`] on this node and its children.
224    pub fn mount(
225        &mut self,
226        global_dirty: &mut Dirty,
227        quit: &mut bool,
228        task_tx: Option<std::sync::mpsc::Sender<String>>,
229    ) {
230        let mut stopped = false;
231        let mut cx = EventCx::with_task_sender(
232            &mut self.dirty,
233            global_dirty,
234            quit,
235            EventPhase::Target,
236            &mut stopped,
237            task_tx.clone(),
238        );
239        self.component.mount(&mut cx);
240        for child in &mut self.children {
241            child.mount(global_dirty, quit, task_tx.clone());
242        }
243    }
244
245    /// Recursively draws border outlines and type-name labels for every node.
246    ///
247    /// The currently focused node is drawn with a double border; others use
248    /// rounded borders. This is toggled by the `d` key in debug mode.
249    pub fn debug_render(&self, buffer: &mut Buffer, focused_id: Option<NodeId>) {
250        let type_name = self.component.type_name();
251        let short = type_name.rsplit("::").next().unwrap_or(type_name);
252
253        let border = if focused_id == Some(self.id) {
254            crate::style::Border::Double
255        } else {
256            crate::style::Border::Rounded
257        };
258        let style = crate::style::Style::default().fg(crate::style::Color::Yellow);
259        buffer.draw_border(self.rect(), border, &style);
260
261        buffer.write_text(
262            Pos {
263                x: self.rect().x.saturating_add(2),
264                y: self.rect().y,
265            },
266            self.rect(),
267            short,
268            &style,
269        );
270
271        self.component.for_each_child(&mut |child: &Node| {
272            child.debug_render(buffer, focused_id);
273        });
274    }
275
276    /// Recursively calls [`Component::unmount`] on children (reverse order),
277    /// then on this node.
278    pub fn unmount(
279        &mut self,
280        global_dirty: &mut Dirty,
281        quit: &mut bool,
282        task_tx: Option<std::sync::mpsc::Sender<String>>,
283    ) {
284        for child in &mut self.children {
285            child.unmount(global_dirty, quit, task_tx.clone());
286        }
287        let mut stopped = false;
288        let mut cx = EventCx::with_task_sender(
289            &mut self.dirty,
290            global_dirty,
291            quit,
292            EventPhase::Target,
293            &mut stopped,
294            task_tx,
295        );
296        self.component.unmount(&mut cx);
297    }
298
299    /// Runs layout for this subtree.
300    ///
301    /// Sets this node's rect, assigns parent links to direct children, then
302    /// calls `Component::layout` so the component can size and position its
303    /// children.
304    pub fn layout(&mut self, rect: Rect) {
305        self.set_rect(rect);
306        for child in &mut self.children {
307            child.parent = Some(self.id);
308        }
309        let mut cx = LayoutCx::new(&mut self.children);
310        self.component.layout(rect, &mut cx);
311    }
312
313    /// Returns the path of node ids from `self` down to `target_id`, inclusive.
314    ///
315    /// Returns `None` if `target_id` is not in this subtree.
316    pub fn find_path_to(&self, target_id: NodeId) -> Option<Vec<NodeId>> {
317        if self.id == target_id {
318            return Some(vec![self.id]);
319        }
320        let mut result: Option<Vec<NodeId>> = None;
321        self.component.for_each_child(&mut |child: &Node| {
322            if result.is_none() {
323                if let Some(child_path) = child.find_path_to(target_id) {
324                    let mut full = vec![self.id];
325                    full.extend(child_path);
326                    result = Some(full);
327                }
328            }
329        });
330        result
331    }
332
333    /// Returns `true` if this node or any descendant has [`Dirty::PAINT`] set.
334    pub fn any_needs_paint(&self) -> bool {
335        self.dirty.contains(Dirty::PAINT)
336            || self.children.iter().any(|c| c.any_needs_paint())
337    }
338
339    /// Returns `true` if this node or any descendant has [`Dirty::LAYOUT`] set.
340    pub fn any_needs_layout(&self) -> bool {
341        self.dirty.contains(Dirty::LAYOUT)
342            || self.children.iter().any(|c| c.any_needs_layout())
343    }
344
345    /// Resets all dirty flags on this node to [`Dirty::NONE`].
346    ///
347    /// Does not recurse into children — the caller is responsible for clearing
348    /// the full tree if needed.
349    pub fn clear_dirty(&mut self) {
350        self.dirty = Dirty::NONE;
351    }
352
353    /// Collects ids of all focusable nodes in this subtree into `ids`.
354    pub fn collect_focusable(&self, ids: &mut Vec<NodeId>) {
355        if self.component.focusable() {
356            ids.push(self.id);
357        }
358        self.component
359            .for_each_child(&mut |child: &Node| child.collect_focusable(ids));
360    }
361
362    /// Performs a hit-test against the layout rects in this subtree.
363    ///
364    /// Returns the deepest node whose rect contains `pos`, or `None` if no
365    /// node matches.
366    pub fn hit_test(&self, pos: Pos) -> Option<NodeId> {
367        let mut hit: Option<NodeId> = None;
368        self.component.for_each_child(&mut |child: &Node| {
369            if let Some(id) = child.hit_test(pos) {
370                hit = Some(id);
371            }
372        });
373        if hit.is_none() && self.rect().contains(pos) {
374            hit = Some(self.id);
375        }
376        hit
377    }
378
379    /// Dispatches an event to a specific target node in this subtree.
380    ///
381    /// Used by the runtime for capture and bubble phases. Returns `true` if
382    /// the target was found, `false` otherwise.
383    pub fn send_event(
384        &mut self,
385        target_id: NodeId,
386        event: &Event,
387        global_dirty: &mut Dirty,
388        quit: &mut bool,
389        phase: EventPhase,
390        stopped: &mut bool,
391        task_tx: Option<std::sync::mpsc::Sender<String>>,
392    ) -> bool {
393        if *stopped {
394            return true;
395        }
396        if self.id == target_id {
397            let mut cx = EventCx::with_task_sender(
398                &mut self.dirty,
399                global_dirty,
400                quit,
401                phase,
402                stopped,
403                task_tx.clone(),
404            );
405            self.component.event(event, &mut cx);
406            return true;
407        }
408        let mut found = false;
409        self.component.for_each_child_mut(&mut |child: &mut Node| {
410            if !found && !*stopped {
411                found = child.send_event(
412                    target_id, event, global_dirty, quit, phase, stopped, task_tx.clone(),
413                );
414            }
415        });
416        found
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::widgets::Label;
424
425    #[test]
426    fn test_collect_focusable_simple() {
427        let label = Node::new(Label::new("test"));
428        let mut ids = Vec::new();
429        label.collect_focusable(&mut ids);
430        // Label.focusable() returns false
431        assert!(ids.is_empty());
432    }
433
434    #[test]
435    fn test_collect_focusable_tree() {
436        // Column → [Label, Checkbox(opt)] → Checkbox is focusable, Label is not
437        use crate::widgets::{Checkbox, Column};
438        let column = Node::new(
439            Column::new()
440                .child(Label::new("not focusable"))
441                .child(Checkbox::new("focusable"))
442        );
443        let mut ids = Vec::new();
444        column.collect_focusable(&mut ids);
445        assert_eq!(ids.len(), 1);
446    }
447
448    #[test]
449    fn test_collect_focusable_block() {
450        // Column → Block(Checkbox("inner"))
451        use crate::widgets::{Block, Checkbox, Column};
452        let column = Node::new(
453            Column::new()
454                .child(Block::new(Checkbox::new("inner")))
455        );
456        let mut ids = Vec::new();
457        column.collect_focusable(&mut ids);
458        assert_eq!(ids.len(), 1, "focusable inside Block should be found");
459    }
460}