1use omp_core::{Str, StrMut};
2use smallvec::SmallVec;
3
4use crate::{
5 component::{Component, EventCtx, Flow, Hit, HitTag, PaintCtx, Slot, next_slot},
6 context::UiContext,
7 frame::{Rect, Style},
8 input::{Key, Mouse},
9 props::{Prop, PropValue, Props},
10 rich::cell_width,
11};
12
13pub struct TreeNode {
15 props: Props,
16 slot: Slot,
17 label: Str,
18 children: Vec<Self>,
19}
20
21impl TreeNode {
22 pub fn new() -> Self {
24 Self {
25 props: Props::new(),
26 slot: next_slot(),
27 label: Str::default(),
28 children: Vec::new(),
29 }
30 }
31
32 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
34 self.props.set(prop, value);
35 self
36 }
37
38 pub fn label(mut self, label: impl Into<Str>) -> Self {
40 append(&mut self.label, label.into());
41 self
42 }
43
44 pub fn node(mut self, node: Self) -> Self {
46 self.children.push(node);
47 self
48 }
49
50 fn effective_label(&self) -> &str {
51 if self.label.is_empty() {
52 self.props.str_of(Prop::Label).map_or("", Str::as_str)
53 } else {
54 &self.label
55 }
56 }
57}
58
59impl Default for TreeNode {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65#[derive(Clone, Debug, Default)]
66struct TreeState {
67 cursor: u16,
68 chosen: Option<Str>,
69 open: SmallVec<Slot, 8>,
70}
71
72#[derive(Clone, Debug)]
73struct TreeRow {
74 node: Slot,
75 depth: u16,
76 path: Str,
77 label: Str,
78 has_children: bool,
79 gutters: SmallVec<bool, 8>,
83 last: bool,
85}
86
87pub struct Tree {
89 props: Props,
90 slot: Slot,
91 nodes: Vec<TreeNode>,
92 state: TreeState,
93 rows: Vec<TreeRow>,
94 rows_dirty: bool,
95}
96
97impl Tree {
98 pub fn new() -> Self {
100 Self {
101 props: Props::new(),
102 slot: next_slot(),
103 nodes: Vec::new(),
104 state: TreeState::default(),
105 rows: Vec::new(),
106 rows_dirty: true,
107 }
108 }
109
110 #[allow(dead_code, reason = "acceptance-suite probe")]
111 pub(crate) const fn visible_rows_len(&self) -> usize {
112 self.rows.len()
113 }
114
115 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
117 self.props.set(prop, value);
118 self
119 }
120
121 pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
123 self.props.set(prop, value);
124 self
125 }
126
127 pub fn node(mut self, node: TreeNode) -> Self {
129 collect_open(std::slice::from_ref(&node), &mut self.state.open);
130 self.nodes.push(node);
131 self.rows_dirty = true;
132 self
133 }
134
135 fn rebuild_rows(&mut self) {
136 if !self.rows_dirty {
137 return;
138 }
139 self.rows.clear();
140 let mut trail = SmallVec::new();
141 walk_rows(&self.nodes, 0, "", &self.state.open, &mut trail, &mut self.rows);
142 if self.rows.is_empty() {
143 self.state.cursor = 0;
144 } else {
145 self.state.cursor = self.state.cursor.min(self.rows.len() as u16 - 1);
146 }
147 self.rows_dirty = false;
148 }
149
150 fn toggle(&mut self, slot: Slot) {
151 if self.state.open.contains(&slot) {
152 self.state.open.retain(|open| *open != slot);
153 } else {
154 self.state.open.push(slot);
155 }
156 self.rows_dirty = true;
157 }
158}
159
160impl Default for Tree {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166impl Component for Tree {
167 fn props(&self) -> &Props {
168 &self.props
169 }
170
171 fn props_mut(&mut self) -> &mut Props {
172 &mut self.props
173 }
174
175 fn slot(&self) -> Slot {
176 self.slot
177 }
178
179 fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
180 self.rebuild_rows();
181 let nat = self
182 .rows
183 .iter()
184 .map(|row| {
185 cell_width(&row.label)
186 .saturating_add(row.depth.saturating_mul(2))
187 .saturating_add(6)
188 })
189 .max()
190 .unwrap_or(16);
191 (16, nat)
192 }
193
194 fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
195 self.rebuild_rows();
196 u16::try_from(self.rows.len()).unwrap_or(u16::MAX)
197 }
198
199 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
200 self.rebuild_rows();
201 let focused = pc.focus == Some(self.slot);
202 let hover_row = match pc.hover {
203 Some((slot, HitTag::Row(index))) if slot == self.slot => Some(index),
204 _ => None,
205 };
206 let bottom = rect.y.saturating_add(rect.height).min(pc.clip);
207 for (index, row) in self.rows.iter().enumerate() {
208 let index = index as u16;
209 let y = rect.y.saturating_add(index);
210 if y >= bottom {
211 break;
212 }
213 let hovered = hover_row == Some(index);
214 if hovered {
215 pc.frame
216 .fill(Rect::new(rect.x, y, rect.width, 1), Style::new().bg(pc.ctx.theme.hover));
217 }
218 let tint = |style: Style| {
219 if hovered {
220 style.bg(pc.ctx.theme.hover)
221 } else {
222 style
223 }
224 };
225 let here = focused && index == self.state.cursor;
226 let mut x = pc.frame.put(
227 rect.x,
228 y,
229 if here { pc.ctx.charset.cursor() } else { " " },
230 tint(Style::new().fg(pc.ctx.theme.accent)),
231 );
232 if let Some(family) = self.props.guides() {
233 let (branch, last, cont) = pc.ctx.charset.guides(family);
234 let guide = tint(Style::new().fg(pc.ctx.theme.muted));
235 for &more in &row.gutters {
236 x = pc.frame.put(x, y, if more { cont } else { " " }, guide);
237 }
238 if row.depth > 0 {
239 x = pc
240 .frame
241 .put(x, y, if row.last { last } else { branch }, guide);
242 x = pc.frame.put(x, y, " ", guide);
243 }
244 } else {
245 for _ in 0..row.depth {
246 x = pc
247 .frame
248 .put(x, y, " ", tint(Style::new().fg(pc.ctx.theme.fg)));
249 }
250 }
251 let expander = if row.has_children {
252 pc.ctx.charset.expander(self.state.open.contains(&row.node))
253 } else if self.props.guides().is_some() {
254 ""
256 } else {
257 " "
258 };
259 x = pc
260 .frame
261 .put(x, y, expander, tint(Style::new().fg(pc.ctx.theme.muted)));
262 let style = if here {
263 tint(Style::new().fg(pc.ctx.theme.accent).bold())
264 } else {
265 tint(Style::new().fg(pc.ctx.theme.fg))
266 };
267 x = pc.frame.put(x, y, &row.label, style);
268 if self.state.chosen.as_ref() == Some(&row.path) {
269 pc.frame.put(
270 x.saturating_add(1),
271 y,
272 pc.ctx.charset.check(),
273 tint(Style::new().fg(pc.ctx.theme.ok)),
274 );
275 }
276 pc.hits.push(Hit {
277 rect: Rect::new(rect.x, y, rect.width, 1),
278 slot: self.slot,
279 tag: HitTag::Row(index),
280 });
281 }
282 }
283
284 fn focusable(&self) -> bool {
285 true
286 }
287
288 fn enter(&mut self, forward: bool) {
289 self.rebuild_rows();
290 if self.rows.is_empty() {
291 self.state.cursor = 0;
292 } else {
293 self.state.cursor = if forward {
294 0
295 } else {
296 self.rows.len() as u16 - 1
297 };
298 }
299 }
300
301 fn key(&mut self, _ec: &mut EventCtx<'_>, key: Key) -> Flow {
302 self.rebuild_rows();
303 if self.rows.is_empty() {
304 return Flow::Skip;
305 }
306 self.state.cursor = self.state.cursor.min(self.rows.len() as u16 - 1);
307 let current = usize::from(self.state.cursor);
308 let row = self.rows[current].clone();
309 let is_open = self.state.open.contains(&row.node);
310 match key {
311 Key::Up => {
312 if self.state.cursor == 0 {
313 return Flow::Skip;
314 }
315 self.state.cursor -= 1;
316 },
317 Key::Down => {
318 if self.state.cursor + 1 >= self.rows.len() as u16 {
319 return Flow::Skip;
320 }
321 self.state.cursor += 1;
322 },
323 Key::Right if row.has_children && !is_open => self.toggle(row.node),
324 Key::Left => {
325 if row.has_children && is_open {
326 self.toggle(row.node);
327 } else if let Some(parent) = self.rows[..current]
328 .iter()
329 .rposition(|candidate| candidate.depth + 1 == row.depth)
330 {
331 self.state.cursor = parent as u16;
332 } else {
333 return Flow::Skip;
334 }
335 },
336 Key::Enter | Key::Space => {
337 if row.has_children {
338 self.toggle(row.node);
339 } else {
340 self.state.chosen = Some(row.path);
341 }
342 },
343 _ => return Flow::Skip,
344 }
345 Flow::Consumed
346 }
347
348 fn mouse(
349 &mut self,
350 _ec: &mut EventCtx<'_>,
351 tag: HitTag,
352 _at: (u16, u16),
353 _rect: Rect,
354 mouse: Mouse,
355 ) -> Flow {
356 self.rebuild_rows();
357 match mouse {
358 Mouse::WheelUp | Mouse::WheelDown => {
359 if self.rows.is_empty() {
360 return Flow::Skip;
361 }
362 let delta = if mouse == Mouse::WheelUp { -1 } else { 1 };
363 self.state.cursor =
364 (i64::from(self.state.cursor) + delta).clamp(0, self.rows.len() as i64 - 1) as u16;
365 Flow::Consumed
366 },
367 Mouse::Click => {
368 let HitTag::Row(index) = tag else {
369 return Flow::Skip;
370 };
371 let Some(row) = self.rows.get(usize::from(index)).cloned() else {
372 return Flow::Skip;
373 };
374 self.state.cursor = index;
375 if row.has_children {
376 self.toggle(row.node);
377 } else {
378 self.state.chosen = Some(row.path);
379 }
380 Flow::Consumed
381 },
382 Mouse::RightClick
383 | Mouse::MiddleClick
384 | Mouse::Move
385 | Mouse::Drag
386 | Mouse::Release
387 | Mouse::WheelLeft
388 | Mouse::WheelRight => Flow::Skip,
389 }
390 }
391
392 fn value(&self, out: &mut serde_json::Map<String, serde_json::Value>) {
393 let Some(id) = self.props.id() else {
394 return;
395 };
396 let value = self
397 .state
398 .chosen
399 .as_ref()
400 .map_or(serde_json::Value::Null, |path| serde_json::Value::String(path.to_string()));
401 out.insert(id.to_string(), value);
402 }
403}
404
405fn collect_open(nodes: &[TreeNode], open: &mut SmallVec<Slot, 8>) {
406 for node in nodes {
407 if node.props.flag(Prop::Open) {
408 open.push(node.slot);
409 }
410 collect_open(&node.children, open);
411 }
412}
413
414fn walk_rows(
415 nodes: &[TreeNode],
416 depth: u16,
417 prefix: &str,
418 open: &[Slot],
419 trail: &mut SmallVec<bool, 8>,
420 rows: &mut Vec<TreeRow>,
421) {
422 let count = nodes.len();
423 for (index, node) in nodes.iter().enumerate() {
424 let label = node.effective_label();
425 let path = if prefix.is_empty() {
426 Str::new(label)
427 } else {
428 let mut path =
429 StrMut::with_capacity(prefix.len().saturating_add(label.len()).saturating_add(1));
430 path.push_str(prefix);
431 path.push('/');
432 path.push_str(label);
433 path.freeze()
434 };
435 let has_children = !node.children.is_empty();
436 let last = index + 1 == count;
437 let gutters = if depth > 1 {
441 trail[1..].into()
442 } else {
443 SmallVec::new()
444 };
445 rows.push(TreeRow {
446 node: node.slot,
447 depth,
448 path: path.clone(),
449 label: Str::new(label),
450 has_children,
451 gutters,
452 last,
453 });
454 if open.contains(&node.slot) {
455 trail.push(!last);
456 walk_rows(&node.children, depth.saturating_add(1), &path, open, trail, rows);
457 trail.pop();
458 }
459 }
460}
461
462fn append(target: &mut Str, suffix: Str) {
463 if target.is_empty() {
464 *target = suffix;
465 return;
466 }
467 let mut joined = StrMut::with_capacity(target.len().saturating_add(suffix.len()));
468 joined.push_str(target);
469 joined.push_str(&suffix);
470 *target = joined.freeze();
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn expand_collapse_rebuilds_rows_and_values_use_paths() {
479 let ctx = UiContext::default();
480 let mut tree = Tree::new().with(Prop::Id, "tree-id").node(
481 TreeNode::new()
482 .label("root")
483 .node(TreeNode::new().label("leaf")),
484 );
485 let mut ec = EventCtx::new(&ctx, 30, 4);
486 assert_eq!(tree.height(&ctx, 30), 1);
487 assert!(!tree.rows_dirty);
488 assert_eq!(tree.key(&mut ec, Key::Right), Flow::Consumed);
489 assert!(tree.rows_dirty);
490 assert_eq!(tree.height(&ctx, 30), 2);
491 assert_eq!(tree.rows[1].path, "root/leaf");
492 assert_eq!(tree.key(&mut ec, Key::Down), Flow::Consumed);
493 assert_eq!(tree.key(&mut ec, Key::Enter), Flow::Consumed);
494 let mut values = serde_json::Map::new();
495 tree.value(&mut values);
496 assert_eq!(values["tree-id"], serde_json::json!("root/leaf"));
497 assert_eq!(tree.key(&mut ec, Key::Left), Flow::Consumed);
498 assert_eq!(tree.state.cursor, 0);
499 assert_eq!(tree.key(&mut ec, Key::Left), Flow::Consumed);
500 assert_eq!(tree.height(&ctx, 30), 1);
501 }
502
503 #[test]
504 fn open_property_seeds_visible_rows() {
505 let ctx = UiContext::default();
506 let mut tree = Tree::new().node(
507 TreeNode::new()
508 .with(Prop::Label, "root")
509 .with(Prop::Open, true)
510 .node(TreeNode::new().label("leaf")),
511 );
512 assert_eq!(tree.height(&ctx, 20), 2);
513 assert_eq!(tree.rows[0].label, "root");
514 }
515}