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}