kano_tui/
component.rs

1use std::rc::Rc;
2
3use kano::{platform::Platform, Children};
4use ratatui::{
5    style::{Color, Modifier},
6    text::{Line, Span, Text},
7    widgets::{Block, Borders, Padding, Paragraph, Wrap},
8    Frame,
9};
10
11use crate::{
12    node::{NodeKind, NodeRef},
13    Tui,
14};
15
16#[derive(Clone)]
17pub struct Component<C> {
18    pub data: Rc<ComponentData>,
19    pub children: C,
20}
21
22impl<C: Children<Tui>> kano::Diff<Tui> for Component<C> {
23    type State = (Rc<ComponentData>, C::State);
24
25    fn init(self, cursor: &mut <Tui as Platform>::Cursor) -> Self::State {
26        cursor.set_component(self.data.clone());
27        let children_state = self.children.init(cursor);
28
29        (self.data, children_state)
30    }
31
32    fn diff(self, state: &mut Self::State, cursor: &mut <Tui as Platform>::Cursor) {
33        self.children.diff(&mut state.1, cursor);
34    }
35}
36
37impl<C: Children<Tui>> kano::View<Tui> for Component<C> {}
38
39#[derive(Clone, Debug)]
40pub struct ComponentData {
41    pub layout: Layout,
42    pub style: Style,
43}
44
45#[derive(Clone, Debug)]
46pub enum Layout {
47    Block,
48    Paragraph,
49    Inline,
50}
51
52#[derive(Clone, Default, Debug)]
53pub struct Style {
54    pub modifier: Option<Modifier>,
55    pub fg: Option<Color>,
56    pub bg: Option<Color>,
57    pub prefix: Option<(&'static str, Box<Style>)>,
58    pub postfix: Option<(&'static str, Box<Style>)>,
59}
60
61impl ComponentData {
62    pub fn render(&self, node: NodeRef, frame: &mut Frame, area: ratatui::prelude::Rect) {
63        let mut spans = vec![];
64        let mut lines = vec![];
65
66        collect_lines(node, &mut spans, &mut lines, Default::default());
67
68        if !spans.is_empty() {
69            lines.push(Line::from(spans));
70        }
71
72        frame.render_widget(
73            Paragraph::new(Text::from(lines))
74                .wrap(Wrap { trim: true })
75                .block(
76                    Block::default()
77                        .title("Kano TUI. Press q to quit.")
78                        .borders(Borders::ALL)
79                        .border_set(ratatui::symbols::border::DOUBLE)
80                        .padding(Padding::uniform(1)),
81                ),
82            area,
83        );
84    }
85}
86
87pub fn all_children(node: NodeRef) -> Vec<NodeRef> {
88    let mut output = vec![];
89    let mut next_child = node.first_child();
90
91    while let Some(child) = next_child {
92        output.push(child.clone());
93        next_child = child.next_sibling();
94    }
95
96    output
97}
98
99pub fn text_children(node: NodeRef) -> String {
100    let mut buf = String::new();
101
102    let mut next_child = node.first_child();
103
104    while let Some(child) = next_child {
105        match &child.0.borrow().kind {
106            NodeKind::Text(text) => {
107                buf.push_str(&text);
108            }
109            _ => {}
110        }
111
112        next_child = child.next_sibling();
113    }
114
115    buf
116}
117
118fn collect_lines<'a>(
119    node: NodeRef,
120    spans: &mut Vec<Span<'a>>,
121    lines: &mut Vec<Line<'a>>,
122    tui_style: ratatui::style::Style,
123) {
124    match &node.0.borrow().kind {
125        NodeKind::Empty => {}
126        NodeKind::Text(text) => {
127            spans.push(Span::styled(text.clone(), tui_style));
128        }
129        NodeKind::Component(data) => {
130            match &data.layout {
131                Layout::Block | Layout::Paragraph => {
132                    if !spans.is_empty() {
133                        lines.push(Line::from(std::mem::take(spans)));
134                    }
135                }
136                Layout::Inline => {}
137            }
138
139            if let Some((prefix, style)) = &data.style.prefix {
140                let mut tui_style = tui_style.clone();
141                apply_style(&mut tui_style, &style);
142                spans.push(Span::styled(*prefix, tui_style));
143            }
144
145            let mut sub_style = tui_style.clone();
146            apply_style(&mut sub_style, &data.style);
147
148            let mut next_child = node.first_child();
149
150            while let Some(child) = next_child {
151                collect_lines(child.clone(), spans, lines, sub_style);
152                next_child = child.next_sibling();
153            }
154
155            if let Some((postfix, style)) = &data.style.postfix {
156                let mut tui_style = tui_style.clone();
157                apply_style(&mut tui_style, &style);
158                spans.push(Span::styled(*postfix, tui_style));
159            }
160        }
161    }
162}
163
164fn apply_style(tui_style: &mut ratatui::style::Style, style: &Style) {
165    if let Some(modifier) = &style.modifier {
166        *tui_style = tui_style.add_modifier(*modifier);
167    }
168    if let Some(fg) = &style.fg {
169        *tui_style = tui_style.fg(*fg);
170    }
171    if let Some(bg) = &style.bg {
172        *tui_style = tui_style.bg(*bg);
173    }
174}