Skip to main content

ansiq_runtime/
routing.rs

1use ansiq_core::{ElementKind, Node, WidgetKey, WidgetRouteContext};
2use ansiq_surface::Key;
3use unicode_width::UnicodeWidthChar;
4
5use crate::FocusState;
6
7#[derive(Debug, PartialEq, Eq)]
8pub struct RouteEffect<Message> {
9    pub handled: bool,
10    pub dirty: bool,
11    pub message: Option<Message>,
12    pub quit: bool,
13}
14
15impl<Message> Default for RouteEffect<Message> {
16    fn default() -> Self {
17        Self {
18            handled: false,
19            dirty: false,
20            message: None,
21            quit: false,
22        }
23    }
24}
25
26pub fn handle_key<Message>(
27    tree: &mut Node<Message>,
28    focus: &mut FocusState,
29    key: Key,
30) -> RouteEffect<Message> {
31    if matches!(key, Key::CtrlC) {
32        return RouteEffect {
33            handled: true,
34            quit: true,
35            ..RouteEffect::default()
36        };
37    }
38
39    if let Some(id) = focus.current() {
40        if let Some(node) = find_node_mut(tree, id) {
41            let effect = route_to_node(node, key);
42            if effect.handled || effect.dirty || effect.message.is_some() || effect.quit {
43                return effect;
44            }
45        }
46    }
47
48    match key {
49        Key::Tab | Key::Down | Key::Char('j') => {
50            focus.next();
51            return RouteEffect {
52                handled: true,
53                ..RouteEffect::default()
54            };
55        }
56        Key::BackTab | Key::Up | Key::Char('k') => {
57            focus.prev();
58            return RouteEffect {
59                handled: true,
60                ..RouteEffect::default()
61            };
62        }
63        _ => {}
64    }
65
66    RouteEffect::default()
67}
68
69fn route_to_node<Message>(node: &mut Node<Message>, key: Key) -> RouteEffect<Message> {
70    let Some(widget_key) = map_widget_key(key) else {
71        return RouteEffect::default();
72    };
73
74    node.element
75        .kind
76        .route_widget_key(
77            widget_key,
78            WidgetRouteContext {
79                viewport_height: node.rect.height as usize,
80                scroll_view_max_offset: scroll_view_max_offset(node),
81            },
82        )
83        .map(|effect| RouteEffect {
84            handled: true,
85            dirty: effect.dirty,
86            message: effect.message,
87            quit: false,
88        })
89        .unwrap_or_default()
90}
91
92fn find_node_mut<Message>(node: &mut Node<Message>, id: usize) -> Option<&mut Node<Message>> {
93    if node.id == id {
94        return Some(node);
95    }
96
97    for child in &mut node.children {
98        if let Some(found) = find_node_mut(child, id) {
99            return Some(found);
100        }
101    }
102
103    None
104}
105
106fn scroll_view_max_offset<Message>(node: &Node<Message>) -> Option<usize> {
107    let child = node.children.first()?;
108    let (content, _) = scroll_text_content(child)?;
109    let lines = wrap_lines(&content, node.rect.width);
110    Some(lines.len().saturating_sub(node.rect.height as usize))
111}
112
113fn map_widget_key(key: Key) -> Option<WidgetKey> {
114    match key {
115        Key::Up => Some(WidgetKey::Up),
116        Key::Down => Some(WidgetKey::Down),
117        Key::Left => Some(WidgetKey::Left),
118        Key::Right => Some(WidgetKey::Right),
119        Key::Esc => Some(WidgetKey::Escape),
120        Key::Enter => Some(WidgetKey::Enter),
121        Key::Backspace => Some(WidgetKey::Backspace),
122        Key::Char(ch) => Some(WidgetKey::Char(ch)),
123        _ => None,
124    }
125}
126
127fn scroll_text_content<Message>(node: &Node<Message>) -> Option<(String, ())> {
128    match &node.element.kind {
129        ElementKind::StreamingText(props) => Some((props.content.clone(), ())),
130        ElementKind::Text(props) => Some((props.content.clone(), ())),
131        ElementKind::Paragraph(props) => Some((props.content.plain(), ())),
132        _ => None,
133    }
134}
135
136fn wrap_lines(content: &str, width: u16) -> Vec<String> {
137    if width == 0 {
138        return Vec::new();
139    }
140
141    let mut lines = Vec::new();
142
143    for raw_line in content.split('\n') {
144        if raw_line.is_empty() {
145            lines.push(String::new());
146            continue;
147        }
148
149        let mut current = String::new();
150        let mut current_width = 0u16;
151
152        for ch in raw_line.chars() {
153            let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
154            if current_width.saturating_add(char_width) > width && !current.is_empty() {
155                lines.push(current);
156                current = String::new();
157                current_width = 0;
158            }
159
160            current.push(ch);
161            current_width = current_width.saturating_add(char_width.max(1));
162        }
163
164        lines.push(current);
165    }
166
167    if lines.is_empty() {
168        lines.push(String::new());
169    }
170
171    lines
172}