Skip to main content

tui_lipan/widgets/graph/
mod.rs

1//! Node-edge graph widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_graph;
8pub use node::GraphRenderNode;
9pub(crate) use node::graph_local_content_point;
10pub use reconcile::reconcile_graph;
11
12use std::sync::Arc;
13
14use crate::callback::Callback;
15use crate::core::element::{Element, ElementKind};
16use crate::style::{BorderStyle, Length, Padding, Style, StyleSlot};
17
18/// Direction used to lay out a [`Graph`] tree.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
20pub enum GraphDirection {
21    /// Parents are above children.
22    #[default]
23    TopDown,
24    /// Parents are left of children.
25    LeftRight,
26}
27
28/// Graph layout algorithm.
29#[non_exhaustive]
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
31pub enum GraphLayout {
32    /// Tidy layered tree layout.
33    #[default]
34    Tree,
35}
36
37/// Stable path identifying a node within a [`Graph`] tree.
38#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39pub struct GraphNodePath(Arc<[usize]>);
40
41impl GraphNodePath {
42    /// Return the root node path.
43    pub fn root() -> Self {
44        Self(Arc::new([]))
45    }
46
47    /// Build a node path from child-index segments.
48    pub fn from_segments(segments: impl IntoIterator<Item = usize>) -> Self {
49        Self(segments.into_iter().collect::<Vec<_>>().into())
50    }
51
52    /// Return the child-index segments from root to node.
53    pub fn segments(&self) -> &[usize] {
54        &self.0
55    }
56}
57
58impl From<Vec<usize>> for GraphNodePath {
59    fn from(value: Vec<usize>) -> Self {
60        Self(value.into())
61    }
62}
63
64impl AsRef<[usize]> for GraphNodePath {
65    fn as_ref(&self) -> &[usize] {
66        self.segments()
67    }
68}
69
70/// Event payload for pointer and keyboard interactions on graph nodes.
71#[derive(Clone, Debug, PartialEq, Eq, Hash)]
72pub struct GraphNodeEvent {
73    /// Path of the target node in the graph tree.
74    pub path: GraphNodePath,
75    /// Label of the target node.
76    pub label: Arc<str>,
77}
78
79/// A labeled node in a [`Graph`] tree.
80#[derive(Clone, Debug)]
81pub struct GraphNode {
82    pub(crate) label: Arc<str>,
83    pub(crate) children: Arc<[GraphNode]>,
84    pub(crate) style: Style,
85    pub(crate) hover_style: Style,
86    pub(crate) focus_style: StyleSlot,
87    pub(crate) border: Option<bool>,
88}
89
90impl GraphNode {
91    /// Create a graph node with a label.
92    pub fn new(label: impl Into<Arc<str>>) -> Self {
93        Self {
94            label: label.into(),
95            children: Arc::new([]),
96            style: Style::default(),
97            hover_style: Style::default(),
98            focus_style: StyleSlot::Replace(Style::default()),
99            border: None,
100        }
101    }
102
103    /// Replace all children, discarding anything already added with
104    /// [`child`](Self::child). Call `child` repeatedly to append instead.
105    pub fn children(mut self, children: impl IntoIterator<Item = GraphNode>) -> Self {
106        self.children = children.into_iter().collect::<Vec<_>>().into();
107        self
108    }
109
110    /// Add one child node.
111    pub fn child(mut self, child: GraphNode) -> Self {
112        let mut children = self.children.to_vec();
113        children.push(child);
114        self.children = children.into();
115        self
116    }
117
118    /// Set this node's style.
119    pub fn style(mut self, style: Style) -> Self {
120        self.style = style;
121        self
122    }
123
124    /// Set the style patched onto this node when hovered.
125    pub fn hover_style(mut self, style: Style) -> Self {
126        self.hover_style = style;
127        self
128    }
129
130    /// Set the style patched onto this node when it has internal graph focus.
131    pub fn focus_style(mut self, style: Style) -> Self {
132        self.focus_style = StyleSlot::Replace(style);
133        self
134    }
135
136    /// Extend the active theme focus style when this node has internal graph focus.
137    pub fn extend_focus_style(mut self, style: Style) -> Self {
138        self.focus_style = StyleSlot::Extend(style);
139        self
140    }
141
142    /// Inherit the active theme focus style when this node has internal graph focus.
143    pub fn inherit_focus_style(mut self) -> Self {
144        self.focus_style = StyleSlot::Inherit;
145        self
146    }
147
148    /// Set the focused node style slot directly for composite forwarding.
149    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
150        self.focus_style = slot;
151        self
152    }
153
154    /// Override whether this node renders with a border.
155    pub fn border(mut self, border: bool) -> Self {
156        self.border = Some(border);
157        self
158    }
159}
160
161/// A direct-paint node-edge visualization for trees.
162#[derive(Clone)]
163pub struct Graph {
164    pub(crate) root: Option<GraphNode>,
165    pub(crate) direction: GraphDirection,
166    pub(crate) layout: GraphLayout,
167    pub(crate) gap_x: u16,
168    pub(crate) gap_y: u16,
169    pub(crate) max_node_width: u16,
170    pub(crate) node_padding: Padding,
171    pub(crate) node_border: bool,
172    pub(crate) node_border_style: BorderStyle,
173    pub(crate) style: Style,
174    pub(crate) node_style: Style,
175    pub(crate) node_hover_style: Style,
176    pub(crate) focusable: bool,
177    pub(crate) focused_path: Option<GraphNodePath>,
178    pub(crate) node_focus_style: StyleSlot,
179    pub(crate) edge_style: Style,
180    pub(crate) edge_border_style: BorderStyle,
181    pub(crate) on_node_click: Option<Callback<GraphNodeEvent>>,
182    pub(crate) on_node_hover: Option<Callback<GraphNodeEvent>>,
183    pub(crate) on_node_focus: Option<Callback<GraphNodeEvent>>,
184    pub(crate) on_node_activate: Option<Callback<GraphNodeEvent>>,
185    pub(crate) padding: Padding,
186    pub(crate) border: bool,
187    pub(crate) border_style: BorderStyle,
188    /// Requested width.
189    /// Default: [`Length::Auto`].
190    pub(crate) width: Length,
191    /// Requested height.
192    /// Default: [`Length::Auto`].
193    pub(crate) height: Length,
194}
195
196impl Default for Graph {
197    fn default() -> Self {
198        Self {
199            root: None,
200            direction: GraphDirection::TopDown,
201            layout: GraphLayout::Tree,
202            gap_x: 2,
203            gap_y: 1,
204            max_node_width: 24,
205            node_padding: (0, 1).into(),
206            node_border: true,
207            node_border_style: BorderStyle::Plain,
208            style: Style::default(),
209            node_style: Style::default(),
210            node_hover_style: Style::default(),
211            focusable: false,
212            focused_path: None,
213            node_focus_style: StyleSlot::Inherit,
214            edge_style: Style::default(),
215            edge_border_style: BorderStyle::Plain,
216            on_node_click: None,
217            on_node_hover: None,
218            on_node_focus: None,
219            on_node_activate: None,
220            padding: Padding::default(),
221            border: false,
222            border_style: BorderStyle::Plain,
223            width: Length::Auto,
224            height: Length::Auto,
225        }
226    }
227}
228
229impl Graph {
230    /// Create an empty graph.
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Set the root tree node.
236    pub fn root(mut self, root: GraphNode) -> Self {
237        self.root = Some(root);
238        self
239    }
240
241    /// Clear the root node.
242    pub fn empty(mut self) -> Self {
243        self.root = None;
244        self
245    }
246
247    /// Set the layout direction.
248    pub fn direction(mut self, direction: GraphDirection) -> Self {
249        self.direction = direction;
250        self
251    }
252
253    /// Set the graph layout mode.
254    pub fn layout(mut self, layout: GraphLayout) -> Self {
255        self.layout = layout;
256        self
257    }
258
259    /// Set the horizontal gap in cells.
260    pub fn gap_x(mut self, gap_x: u16) -> Self {
261        self.gap_x = gap_x;
262        self
263    }
264
265    /// Set the vertical gap in cells.
266    pub fn gap_y(mut self, gap_y: u16) -> Self {
267        self.gap_y = gap_y;
268        self
269    }
270
271    /// Set maximum node label width before wrapping.
272    pub fn max_node_width(mut self, width: u16) -> Self {
273        self.max_node_width = width.max(1);
274        self
275    }
276
277    /// Set padding inside each graph node.
278    pub fn node_padding(mut self, padding: impl Into<Padding>) -> Self {
279        self.node_padding = padding.into();
280        self
281    }
282
283    /// Enable or disable borders around graph nodes by default.
284    pub fn node_border(mut self, node_border: bool) -> Self {
285        self.node_border = node_border;
286        self
287    }
288
289    /// Set the border style used for graph node boxes.
290    pub fn node_border_style(mut self, border_style: BorderStyle) -> Self {
291        self.node_border_style = border_style;
292        self
293    }
294
295    /// Set the base graph style.
296    pub fn style(mut self, style: Style) -> Self {
297        self.style = style;
298        self
299    }
300
301    /// Set the default node style.
302    pub fn node_style(mut self, style: Style) -> Self {
303        self.node_style = style;
304        self
305    }
306
307    /// Set the style applied to hovered graph nodes.
308    pub fn node_hover_style(mut self, style: Style) -> Self {
309        self.node_hover_style = style;
310        self
311    }
312
313    /// Enable or disable keyboard focus for graph nodes.
314    pub fn focusable(mut self, focusable: bool) -> Self {
315        self.focusable = focusable;
316        self
317    }
318
319    /// Set the focused graph node path.
320    pub fn focused_path(mut self, path: GraphNodePath) -> Self {
321        self.focused_path = Some(path);
322        self
323    }
324
325    /// Set the style applied to the focused graph node.
326    pub fn node_focus_style(mut self, style: Style) -> Self {
327        self.node_focus_style = StyleSlot::Replace(style);
328        self
329    }
330
331    /// Extend the active theme focus style for the focused graph node.
332    pub fn extend_node_focus_style(mut self, style: Style) -> Self {
333        self.node_focus_style = StyleSlot::Extend(style);
334        self
335    }
336
337    /// Inherit the active theme focus style for the focused graph node.
338    pub fn inherit_node_focus_style(mut self) -> Self {
339        self.node_focus_style = StyleSlot::Inherit;
340        self
341    }
342
343    /// Set the focused graph node style slot.
344    pub fn node_focus_style_slot(mut self, slot: StyleSlot) -> Self {
345        self.node_focus_style = slot;
346        self
347    }
348
349    /// Set a callback for clicks on graph nodes.
350    pub fn on_node_click(mut self, cb: Callback<GraphNodeEvent>) -> Self {
351        self.on_node_click = Some(cb);
352        self
353    }
354
355    /// Set a callback for hover events on graph nodes.
356    pub fn on_node_hover(mut self, cb: Callback<GraphNodeEvent>) -> Self {
357        self.on_node_hover = Some(cb);
358        self
359    }
360
361    /// Set a callback for focus changes on graph nodes.
362    pub fn on_node_focus(mut self, cb: Callback<GraphNodeEvent>) -> Self {
363        self.on_node_focus = Some(cb);
364        self
365    }
366
367    /// Set a callback for keyboard activation of graph nodes.
368    pub fn on_node_activate(mut self, cb: Callback<GraphNodeEvent>) -> Self {
369        self.on_node_activate = Some(cb);
370        self
371    }
372
373    /// Set the edge style.
374    pub fn edge_style(mut self, style: Style) -> Self {
375        self.edge_style = style;
376        self
377    }
378
379    /// Set the box-drawing style used for graph edge elbows.
380    pub fn edge_border_style(mut self, border_style: BorderStyle) -> Self {
381        self.edge_border_style = border_style;
382        self
383    }
384
385    /// Set graph padding inside the optional outer border.
386    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
387        self.padding = padding.into();
388        self
389    }
390
391    /// Enable or disable the graph border.
392    pub fn border(mut self, border: bool) -> Self {
393        self.border = border;
394        self
395    }
396
397    /// Set graph border style.
398    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
399        self.border_style = border_style;
400        self
401    }
402
403    /// Set requested graph width.
404    pub fn width(mut self, width: Length) -> Self {
405        self.width = width;
406        self
407    }
408
409    /// Set requested graph height.
410    pub fn height(mut self, height: Length) -> Self {
411        self.height = height;
412        self
413    }
414
415    /// Pan offset that centers the node identified by `path` within a viewport
416    /// of `viewport_w` x `viewport_h` cells.
417    ///
418    /// The returned `(x, y)` is in the same coordinate space as
419    /// [`crate::widgets::PanView::offset`], so it can be fed straight to a
420    /// `PanView` wrapping this graph to bring the node into the middle of the
421    /// view. The node layout is computed from the graph definition alone, so
422    /// this works before the graph is mounted. Returns `None` when `path` does
423    /// not resolve to a laid-out node (e.g. an empty graph).
424    ///
425    /// Prefer [`Self::focus_offset_for`] when the graph sits in a bounded
426    /// pan viewport and edge nodes should only move far enough to stay
427    /// visible without scrolling empty space past the content bounds.
428    pub fn center_offset_for(
429        &self,
430        path: &GraphNodePath,
431        viewport_w: u16,
432        viewport_h: u16,
433    ) -> Option<(i32, i32)> {
434        let output = layout::build_graph_output(self);
435        let node = output.nodes.iter().find(|node| &node.path == path)?;
436        let border = i32::from(self.border);
437        let node_center_x = border
438            + i32::from(self.padding.left)
439            + i32::from(node.rect.x)
440            + i32::from(node.rect.w) / 2;
441        let node_center_y = border
442            + i32::from(self.padding.top)
443            + i32::from(node.rect.y)
444            + i32::from(node.rect.h) / 2;
445        Some((
446            node_center_x - i32::from(viewport_w) / 2,
447            node_center_y - i32::from(viewport_h) / 2,
448        ))
449    }
450
451    /// Pan offset that brings `path` into focus: centers when the node has
452    /// surrounding content room, otherwise clamps so the node stays visible
453    /// without scrolling empty space past the graph bounds.
454    ///
455    /// Same coordinate space and pre-mount behavior as
456    /// [`Self::center_offset_for`].
457    pub fn focus_offset_for(
458        &self,
459        path: &GraphNodePath,
460        viewport_w: u16,
461        viewport_h: u16,
462    ) -> Option<(i32, i32)> {
463        let (x, y) = self.center_offset_for(path, viewport_w, viewport_h)?;
464        let (content_w, content_h) = layout::measure_graph(self);
465        Some((
466            clamp_focus_axis(x, content_w, viewport_w),
467            clamp_focus_axis(y, content_h, viewport_h),
468        ))
469    }
470}
471
472/// Prefer centering, but keep the content filling the viewport when possible:
473/// clamp into `[0, content - viewport]` when content is larger, or into
474/// `[content - viewport, 0]` when content fits inside the viewport.
475fn clamp_focus_axis(desired: i32, content: u16, viewport: u16) -> i32 {
476    let diff = i32::from(content) - i32::from(viewport);
477    if diff >= 0 {
478        desired.clamp(0, diff)
479    } else {
480        desired.clamp(diff, 0)
481    }
482}
483
484impl From<Graph> for Element {
485    fn from(value: Graph) -> Self {
486        Element::new(ElementKind::Graph(Box::new(value)))
487    }
488}