GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/container/treelist.rs (77.8K)
1 use crate::widget::*;
2 use crate::widget::container::scroll_box::ScrollBox;
3 use crate::widget::display::TextLabel;
4 use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
5 use crate::scene::layout::Rect;
6 use crate::scene::paint::PaintCtx;
7 use std::collections::HashSet;
8
9 #[derive(Debug, Clone, PartialEq)]
10 pub enum TreeElement {
11 Section {
12 path: String,
13 name: String,
14 indent: usize,
15 collapsed: bool,
16 },
17 Leaf {
18 path: String,
19 name: String,
20 indent: usize,
21 val: serde_json::Value,
22 original_idx: usize,
23 }
24 }
25
26 #[derive(Debug, Clone)]
27 enum PathToken {
28 Key(String),
29 Index(usize),
30 }
31
32 fn parse_path(path: &str) -> Vec<PathToken> {
33 let mut tokens = Vec::new();
34 for part in path.split('.') {
35 if part.is_empty() { continue; }
36 if let Some(bracket_idx) = part.find('[') {
37 let name = &part[..bracket_idx];
38 if !name.is_empty() {
39 tokens.push(PathToken::Key(name.to_string()));
40 }
41 let mut rest = &part[bracket_idx..];
42 while let Some(start) = rest.find('[') {
43 if let Some(end) = rest.find(']') {
44 let idx_str = &rest[start + 1..end];
45 if let Ok(idx) = idx_str.parse::<usize>() {
46 tokens.push(PathToken::Index(idx));
47 }
48 rest = &rest[end + 1..];
49 } else {
50 break;
51 }
52 }
53 } else {
54 tokens.push(PathToken::Key(part.to_string()));
55 }
56 }
57 tokens
58 }
59
60 fn matches_query(key_path: &str, val: &serde_json::Value, annotation: Option<&str>, query: &str) -> bool {
61 if query.is_empty() {
62 return true;
63 }
64 let query_lower = query.to_lowercase();
65 if key_path.to_lowercase().contains(&query_lower) {
66 return true;
67 }
68 let val_str = match val {
69 serde_json::Value::String(s) => s.clone(),
70 serde_json::Value::Bool(b) => b.to_string(),
71 serde_json::Value::Number(n) => n.to_string(),
72 other => serde_json::to_string(other).unwrap_or_default(),
73 };
74 if val_str.to_lowercase().contains(&query_lower) {
75 return true;
76 }
77 if let Some(anno) = annotation {
78 if anno.to_lowercase().contains(&query_lower) {
79 return true;
80 }
81 }
82 false
83 }
84
85 fn build_tree(
86 flat_keys: &[(String, serde_json::Value)],
87 annotations: &[Option<String>],
88 collapsed_sections: &HashSet<String>,
89 query: &str,
90 ) -> Vec<TreeElement> {
91 let mut items = Vec::new();
92 let mut seen_prefixes = HashSet::new();
93
94 let mut matching_indices = HashSet::new();
95 for (idx, (key_path, val)) in flat_keys.iter().enumerate() {
96 let annotation = annotations.get(idx).and_then(|opt| opt.as_deref());
97 if query.is_empty() || matches_query(key_path, val, annotation, query) {
98 matching_indices.insert(idx);
99 }
100 }
101
102 for (original_idx, (key_path, val)) in flat_keys.iter().enumerate() {
103 if !matching_indices.contains(&original_idx) {
104 continue;
105 }
106 let tokens = parse_path(key_path);
107 let mut current_prefix = String::new();
108 let mut is_hidden = false;
109
110 for i in 0..tokens.len() {
111 let token = &tokens[i];
112 let part_name = match token {
113 PathToken::Key(k) => {
114 if current_prefix.is_empty() {
115 current_prefix = k.clone();
116 } else {
117 current_prefix = format!("{}.{}", current_prefix, k);
118 }
119 k.clone()
120 }
121 PathToken::Index(idx) => {
122 let s = format!("[{}]", idx);
123 current_prefix = format!("{}{}", current_prefix, s);
124 s
125 }
126 };
127
128 let is_last = i == tokens.len() - 1;
129
130 if is_hidden {
131 continue;
132 }
133
134 if is_last {
135 items.push(TreeElement::Leaf {
136 path: key_path.clone(),
137 name: part_name,
138 indent: i,
139 val: val.clone(),
140 original_idx,
141 });
142 } else {
143 if !seen_prefixes.contains(¤t_prefix) {
144 seen_prefixes.insert(current_prefix.clone());
145 let collapsed = collapsed_sections.contains(¤t_prefix);
146 items.push(TreeElement::Section {
147 path: current_prefix.clone(),
148 name: part_name,
149 indent: i,
150 collapsed,
151 });
152 }
153 if collapsed_sections.contains(¤t_prefix) {
154 is_hidden = true;
155 }
156 }
157 }
158 }
159 items
160 }
161
162 #[derive(Debug, Clone)]
163 pub struct TreeList {
164 pub base: Widget,
165 pub scroll_box: ScrollBox,
166 pub search_box: crate::widget::Adapted<TextBox>,
167 pub add_key_btn: crate::widget::Adapted<Button>,
168 pub add_key_popover_open: bool,
169 pub add_key_popover_box: crate::widget::Adapted<TextBox>,
170 pub new_key_path_request: Option<String>,
171 pub flat_keys: Vec<(String, serde_json::Value)>,
172 pub annotations: Vec<Option<String>>,
173 pub collapsed_sections: HashSet<String>,
174 pub items: Vec<TreeElement>,
175 pub selected_key_idx: Option<usize>,
176 pub hovered_row_idx: Option<usize>,
177 pub item_height: f32,
178 pub clicked_item: Option<TreeElement>,
179 pub right_clicked_section: Option<String>,
180 pub last_scroll_y: f32,
181 pub scrollbar_activity_timer: f32,
182 /// Lights the recess rim (`paint` has no UiContext, so this mirrors focus).
183 /// Driven by FocusIn/FocusOut by default; an app whose inline editors float
184 /// over the tree (cce-data-editor) overrides it per input event with its own
185 /// focus-within computation — those editors take ctx focus away from the
186 /// tree while still being, visually, part of the tree pane.
187 pub focused: bool,
188 pub deleted_key_path: Option<String>,
189 pub edit_box: crate::widget::Adapted<TextBox>,
190 pub editing_key_idx: Option<usize>,
191 pub double_click_timer: Option<(std::time::Instant, usize)>,
192 pub rename_request: Option<(String, String)>,
193 }
194
195 impl TreeList {
196 pub fn new() -> Adapted<TreeList> {
197 let mut scroll_box = ScrollBox::new();
198 scroll_box.show_background = false;
199 Adapted::new(TreeList {
200 base: Widget::new(),
201 scroll_box,
202 search_box: TextBox::new(String::new()).with_placeholder("Search...").with_update_on_type(true),
203 add_key_btn: Button::new(0.0, 0.0, 80.0, 26.0).with_label("+ Add Key"),
204 add_key_popover_open: false,
205 add_key_popover_box: TextBox::new(String::new()).with_placeholder("new.key.path").with_multiline(false),
206 new_key_path_request: None,
207 flat_keys: Vec::new(),
208 annotations: Vec::new(),
209 collapsed_sections: HashSet::new(),
210 items: Vec::new(),
211 selected_key_idx: None,
212 hovered_row_idx: None,
213 item_height: 28.0,
214 clicked_item: None,
215 right_clicked_section: None,
216 last_scroll_y: 0.0,
217 scrollbar_activity_timer: 0.0,
218 focused: false,
219 deleted_key_path: None,
220 edit_box: TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true),
221 editing_key_idx: None,
222 double_click_timer: None,
223 rename_request: None,
224 })
225 }
226
227 pub fn take_new_key_path_request(&mut self) -> Option<String> {
228 self.new_key_path_request.take()
229 }
230
231 pub fn popover_rect_geom(&self) -> (f32, f32, f32, f32) {
232 let (bx, by, bw, bh) = self.add_key_btn.rect();
233 let popover_w = 220.0;
234 let popover_h = 36.0;
235 let popover_x = bx + bw - popover_w;
236 let popover_y = by + bh + 4.0;
237 (popover_x, popover_y, popover_w, popover_h)
238 }
239
240 pub fn focus_search(&mut self, ctx: &mut UiContext) {
241 ctx.set_focused(&mut self.search_box);
242 self.search_box.focus();
243 }
244
245 pub fn set_flat_keys(&mut self, flat_keys: Vec<(String, serde_json::Value)>) {
246 self.flat_keys = flat_keys;
247 self.rebuild_tree();
248 }
249
250 pub fn rebuild_tree(&mut self) {
251 let query = self.search_box.text.clone();
252 self.items = build_tree(&self.flat_keys, &self.annotations, &self.collapsed_sections, &query);
253 let content_h = self.items.len() as f32 * self.item_height;
254 let h = self.base.h;
255 let search_margin_y = 6.0;
256 let search_h = 26.0;
257 let offset_y = search_h + 2.0 * search_margin_y;
258 let header_h = 26.0;
259 self.scroll_box.update_bounds(content_h, self.scroll_box.viewport_y, h - offset_y - header_h);
260 }
261
262 /// The on-screen rect of a leaf row, or `None` unless the row is FULLY
263 /// visible. Deliberately full-containment, unlike the draw-side
264 /// virtualization (which returns partial rows to be drawn cut by the
265 /// clip): this positions a floating overlay (cce-data-editor's inline
266 /// value editors) that draws OVER the well unclipped, and an editor
267 /// hanging half off the list edge is worse than one that waits for its
268 /// row to scroll fully into view.
269 pub fn get_row_rect(&self, original_idx: usize) -> Option<(f32, f32, f32, f32)> {
270 let list_top = self.scroll_box.viewport_y;
271 let list_bottom = self.scroll_box.viewport_y + self.scroll_box.viewport_h;
272
273 let row_idx = self.items.iter().position(|item| {
274 match item {
275 TreeElement::Leaf { original_idx: idx, .. } => *idx == original_idx,
276 _ => false,
277 }
278 })?;
279
280 let row_y = list_top + row_idx as f32 * self.item_height - self.scroll_box.scroll_y;
281 if row_y >= list_top && row_y + self.item_height <= list_bottom {
282 Some((self.scroll_box.base.x, row_y, self.scroll_box.base.w, self.item_height))
283 } else {
284 None
285 }
286 }
287
288 pub fn take_clicked_item(&mut self) -> Option<TreeElement> {
289 self.clicked_item.take()
290 }
291
292 pub fn take_deleted_key_path(&mut self) -> Option<String> {
293 self.deleted_key_path.take()
294 }
295
296 pub fn take_rename_request(&mut self) -> Option<(String, String)> {
297 self.rename_request.take()
298 }
299
300
301 pub fn scroll_to_selected_key(&mut self) {
302 if let Some(selected_idx) = self.selected_key_idx {
303 let visible_row_idx = self.items.iter().position(|item| {
304 if let TreeElement::Leaf { original_idx, .. } = item {
305 *original_idx == selected_idx
306 } else {
307 false
308 }
309 });
310 if let Some(row_idx) = visible_row_idx {
311 let row_top = row_idx as f32 * self.item_height;
312 let row_bottom = row_top + self.item_height;
313 let viewport_h = self.scroll_box.viewport_h;
314
315 if row_top < self.scroll_box.scroll_y {
316 self.scroll_box.scroll_y = row_top;
317 } else if row_bottom > self.scroll_box.scroll_y + viewport_h {
318 self.scroll_box.scroll_y = (row_bottom - viewport_h).max(0.0);
319 }
320
321 let max_scroll = (self.scroll_box.content_h - viewport_h).max(0.0);
322 self.scroll_box.scroll_y = self.scroll_box.scroll_y.clamp(0.0, max_scroll);
323 }
324 }
325 }
326
327 pub fn select_and_show_key(&mut self, key_path: &str) -> bool {
328 let found_idx = self.flat_keys.iter().position(|(k, _)| k == key_path);
329 if let Some(idx) = found_idx {
330 self.selected_key_idx = Some(idx);
331
332 let parts: Vec<&str> = key_path.split('.').collect();
333 let mut current = String::new();
334 let mut expanded_any = false;
335 for i in 0..parts.len() - 1 {
336 if !current.is_empty() {
337 current.push('.');
338 }
339 current.push_str(parts[i]);
340 if self.collapsed_sections.contains(¤t) {
341 self.collapsed_sections.remove(¤t);
342 expanded_any = true;
343 }
344 }
345 if expanded_any {
346 self.rebuild_tree();
347 }
348
349 self.scroll_to_selected_key();
350 true
351 } else {
352 false
353 }
354 }
355 }
356
357 impl TreeList {
358 fn tree_radius(&self) -> f32 {
359 crate::layout::tree_corner_radius()
360 }
361
362 fn tree_corners(&self) -> (bool, bool, bool, bool) {
363 (true, true, true, true)
364 }
365
366 fn tree_border(&self) -> Option<([f32; 4], f32)> {
367 if self.scroll_box.show_border {
368 Some((crate::color::tree_border_color(), 1.0))
369 } else {
370 None
371 }
372 }
373
374 fn mouse_body(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ui: &mut UiContext, host: Option<*mut (dyn WidgetHost + 'static)>, host_id: WidgetId) -> bool {
375 let _ = host_id;
376 if self.editing_key_idx.is_some() {
377 if button == MouseButton::Left && state == ElementState::Pressed {
378 let (ex, ey, ew, eh) = self.edit_box.rect();
379 if px >= ex && px <= ex + ew && py >= ey && py <= ey + eh {
380 if self.edit_box.mouse_input(button, state, px, py, ui) {
381 return true;
382 }
383 } else {
384 ui.clear_focus();
385 return true;
386 }
387 }
388 return false;
389 }
390
391 let mut changed = false;
392
393 if self.add_key_popover_open {
394 let (px_rect, py_rect, pw, ph) = self.popover_rect_geom();
395 if button == MouseButton::Left && state == ElementState::Pressed {
396 if px < px_rect || px > px_rect + pw || py < py_rect || py > py_rect + ph {
397 self.add_key_popover_open = false;
398 ui.clear_focus();
399 changed = true;
400 } else {
401 if self.add_key_popover_box.mouse_input(button, state, px, py, ui) {
402 ui.set_focused(&mut self.add_key_popover_box);
403 changed = true;
404 }
405 }
406 }
407 let (px_rect, py_rect, pw, ph) = self.popover_rect_geom();
408 if px >= px_rect && px <= px_rect + pw && py >= py_rect && py <= py_rect + ph {
409 return true;
410 }
411 }
412
413 if self.scroll_box.mouse_input(button, state, px, py, ui) {
414 changed = true;
415 }
416 if self.search_box.mouse_input(button, state, px, py, ui) {
417 ui.set_focused(&mut self.search_box);
418 changed = true;
419 }
420 if self.add_key_btn.mouse_input(button, state, px, py, ui) {
421 if self.add_key_btn.take_click() {
422 self.add_key_popover_open = !self.add_key_popover_open;
423 if self.add_key_popover_open {
424 self.add_key_popover_box.text.clear();
425 self.add_key_popover_box.edit_buffer.clear();
426 self.add_key_popover_box.cursor_idx = 0;
427 self.add_key_popover_box.select_anchor = None;
428 self.add_key_popover_box.all_selected = false;
429 self.add_key_popover_box.editing = true;
430 ui.set_focused(&mut self.add_key_popover_box);
431 self.add_key_popover_box.focus();
432 } else {
433 ui.clear_focus();
434 }
435 }
436 changed = true;
437 }
438
439 let list_left = self.scroll_box.base.x;
440 let list_width = self.scroll_box.base.w;
441 let list_top = self.scroll_box.viewport_y;
442 let list_bottom = self.scroll_box.viewport_y + self.scroll_box.viewport_h;
443
444 // The tree receives every press UNGATED (see gates_presses) for its
445 // dismiss/commit semantics, which skips the router's popover-coverage
446 // check — honor it here for row interactions, or a click on a menu
447 // floating over the tree (the File dropdown) also selects the row
448 // beneath it. The dismiss paths above deliberately stay: a covered
449 // press IS an outside press for the rename editor and add-key popover.
450 let covered = ui.is_coordinate_covered(host_id, px, py);
451
452 if button == MouseButton::Left && state == ElementState::Pressed && !covered {
453 let on_scrollbar = self.scroll_box.hit_test_scrollbar(px, py) || self.scroll_box.scrollbar_dragging;
454 if !on_scrollbar && px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
455 if let Some(h) = host { ui.set_focused_ptr(h); }
456 let relative_y = py - list_top + self.scroll_box.scroll_y;
457 let row_idx = (relative_y / self.item_height) as usize;
458 if row_idx < self.items.len() {
459 let item = self.items[row_idx].clone();
460
461 let mut is_double = false;
462 let now = std::time::Instant::now();
463 if let Some((prev_time, prev_row)) = self.double_click_timer {
464 if prev_row == row_idx && now.duration_since(prev_time).as_millis() < 300 {
465 is_double = true;
466 }
467 }
468 self.double_click_timer = Some((now, row_idx));
469
470 if is_double {
471 let (_path_to_edit, relative_name) = match &item {
472 TreeElement::Section { path, name, .. } => {
473 if self.collapsed_sections.contains(path) {
474 self.collapsed_sections.remove(path);
475 } else {
476 self.collapsed_sections.insert(path.clone());
477 }
478 self.rebuild_tree();
479 (path.clone(), name.clone())
480 }
481 TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
482 };
483 self.editing_key_idx = Some(row_idx);
484 self.edit_box = TextBox::new(relative_name).with_multiline(false).with_draw_bg_border(true);
485 self.edit_box.editing = true;
486 self.edit_box.cursor_idx = self.edit_box.text.chars().count();
487 self.edit_box.select_anchor = Some(0);
488
489 let eb_ptr = self.edit_box.as_ptr_mut();
490 let eb_id = self.edit_box.base().id();
491 ui.register_widget(eb_id, eb_ptr);
492 ui.link_ids(host_id, eb_id);
493
494 ui.set_focused(&mut self.edit_box);
495 return true;
496 }
497
498 match item {
499 TreeElement::Section { ref path, .. } => {
500 if self.collapsed_sections.contains(path) {
501 self.collapsed_sections.remove(path);
502 } else {
503 self.collapsed_sections.insert(path.clone());
504 }
505 self.rebuild_tree();
506 self.clicked_item = Some(TreeElement::Section {
507 path: path.clone(),
508 name: String::new(),
509 indent: 0,
510 collapsed: self.collapsed_sections.contains(path),
511 });
512 changed = true;
513 }
514 TreeElement::Leaf { original_idx, ref path, ref name, indent, ref val } => {
515 self.selected_key_idx = Some(original_idx);
516 self.clicked_item = Some(TreeElement::Leaf {
517 path: path.clone(),
518 name: name.clone(),
519 indent,
520 val: val.clone(),
521 original_idx,
522 });
523 changed = true;
524 }
525 }
526 }
527 }
528 }
529
530 if button == MouseButton::Right && state == ElementState::Pressed && !covered {
531 if px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
532 if let Some(h) = host { ui.set_focused_ptr(h); }
533 let relative_y = py - list_top + self.scroll_box.scroll_y;
534 let row_idx = (relative_y / self.item_height) as usize;
535 if row_idx < self.items.len() {
536 let item = self.items[row_idx].clone();
537 match item {
538 TreeElement::Section { ref path, collapsed, .. } => {
539 self.right_clicked_section = Some(path.clone());
540
541 let mut options = vec![path.clone()];
542 if collapsed {
543 options.push("Expand".to_string());
544 } else {
545 options.push("Collapse".to_string());
546 }
547 options.push("Expand All".to_string());
548 options.push("Collapse All".to_string());
549
550 let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
551 if let Some(h) = host { ui.show_context_menu(px, py - scroll_offset, options, 1, h); }
552 changed = true;
553 }
554 TreeElement::Leaf { original_idx, ref path, ref name, indent, ref val } => {
555 self.selected_key_idx = Some(original_idx);
556 self.clicked_item = Some(TreeElement::Leaf {
557 path: path.clone(),
558 name: name.clone(),
559 indent,
560 val: val.clone(),
561 original_idx,
562 });
563
564 let options = vec![
565 path.clone(),
566 "Copy Key".to_string(),
567 "Copy Value".to_string(),
568 "Delete".to_string(),
569 ];
570 let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
571 if let Some(h) = host { ui.show_context_menu(px, py - scroll_offset, options, 1, h); }
572 changed = true;
573 }
574 }
575 }
576 }
577 }
578 changed
579
580 }
581
582 fn move_body(&mut self, px: f32, py: f32, ui: &mut UiContext) -> bool {
583 let mut changed = self.scroll_box.on_cursor_moved(px, py, ui);
584 if self.search_box.on_cursor_moved(px, py, ui) {
585 changed = true;
586 }
587 if self.add_key_btn.on_cursor_moved(px, py, ui) {
588 changed = true;
589 }
590 if self.add_key_popover_open {
591 if self.add_key_popover_box.on_cursor_moved(px, py, ui) {
592 changed = true;
593 }
594 }
595
596 let list_left = self.scroll_box.base.x;
597 let list_width = self.scroll_box.base.w;
598 let list_top = self.scroll_box.viewport_y;
599 let list_bottom = self.scroll_box.viewport_y + self.scroll_box.viewport_h;
600
601 let old_hovered = self.hovered_row_idx;
602 self.hovered_row_idx = None;
603
604 let on_scrollbar = self.scroll_box.hit_test_scrollbar(px, py) || self.scroll_box.scrollbar_dragging;
605 if !on_scrollbar && px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
606 let relative_y = py - list_top + self.scroll_box.scroll_y;
607 let row_idx = (relative_y / self.item_height) as usize;
608 if row_idx < self.items.len() {
609 self.hovered_row_idx = Some(row_idx);
610 }
611 }
612
613 if old_hovered != self.hovered_row_idx {
614 changed = true;
615 }
616 changed
617
618 }
619
620 fn key_body(&mut self, event: &KeyEvent, ui: &mut UiContext) -> bool {
621 if self.editing_key_idx.is_some() {
622 if self.edit_box.keyboard_input(event, ui) {
623 return true;
624 }
625 }
626 if self.add_key_popover_open {
627 if self.add_key_popover_box.keyboard_input(event, ui) {
628 return true;
629 }
630 }
631 if self.search_box.keyboard_input(event, ui) {
632 return true;
633 }
634 false
635
636 }
637 }
638
639 impl Layout for TreeList {
640 /// The legacy `set_rect` body: cache the CONTENT rect on the internal base and
641 /// arrange the field widgets (search box, add-key button, popover box, scroll box)
642 /// in it. The content rect, not `rect_assigned`'s block: the adapter's block holds
643 /// the detached label strip above the content, and `paint` draws the well at the
644 /// content rect — fields placed from the block sat one strip above the well, the
645 /// search box straddling its top edge over the label and the header row's text
646 /// clipped away outside its bounds (the gallery's labelled tree).
647 fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {
648 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
649 self.base.x = x;
650 self.base.y = y;
651 self.base.w = w;
652 self.base.h = h;
653
654 let search_margin_x = 8.0;
655 let search_margin_y = 6.0;
656 let search_h = 26.0;
657 let offset_y = search_h + 2.0 * search_margin_y;
658
659 let (btn_family, btn_size) = crate::layout::parse_font_string(&crate::layout::button_font());
660 let label_w = crate::widget::display::measure_text_width(
661 self.add_key_btn.base().label.as_deref().unwrap_or(""),
662 &btn_family,
663 btn_size.unwrap_or(12.0),
664 );
665 let button_width = label_w + 2.0 * crate::layout::button_padding();
666 let button_height = search_h;
667 let button_x = x + w - search_margin_x - button_width;
668
669 self.search_box.set_rect(x + search_margin_x, y + search_margin_y, w - 2.0 * search_margin_x - button_width - 6.0, search_h);
670 self.add_key_btn.set_rect(button_x, y + search_margin_y, button_width, button_height);
671
672 let (px, py, pw, ph) = self.popover_rect_geom();
673 self.add_key_popover_box.set_rect(px + 8.0, py + 5.0, pw - 16.0, ph - 10.0);
674
675 let header_h = 26.0;
676 self.scroll_box.set_rect(x, y + offset_y + header_h, w, h - offset_y - header_h);
677
678 let content_h = self.items.len() as f32 * self.item_height;
679 self.scroll_box.update_bounds(content_h, y + offset_y + header_h, h - offset_y - header_h);
680 self.last_scroll_y = self.scroll_box.scroll_y;
681
682 }
683
684 /// Keep the field widgets registered/linked under the adapter every tick (the legacy
685 /// `set_parent` side effect; also heals the inline rename editor's registry entry).
686 fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
687 // Registered but deliberately NOT tree-linked (6bd): the tree is a SELF-ROUTING
688 // composite — mouse_body/move_body/key_body forward to every field widget
689 // internally, so the router's children-first descent double-delivered AND starved
690 // the tree-level logic (the recorded 6as latents: the hit add-key button consumed
691 // the press before mouse_body's take_click toggle ran, so the popover never
692 // opened, and the wheel died the same way). Registration alone keeps the ids
693 // resolvable for focus, coverage, and the spatial grid.
694 let _ = host_id;
695 let sb_ptr = self.search_box.as_ptr_mut();
696 let sb_id = self.search_box.base().id();
697 ctx.register_widget(sb_id, sb_ptr);
698
699 let btn_ptr = self.add_key_btn.as_ptr_mut();
700 let btn_id = self.add_key_btn.base().id();
701 ctx.register_widget(btn_id, btn_ptr);
702
703 let pop_ptr = self.add_key_popover_box.as_ptr_mut();
704 let pop_id = self.add_key_popover_box.base().id();
705 ctx.register_widget(pop_id, pop_ptr);
706
707 if self.editing_key_idx.is_some() {
708 let eb_ptr = self.edit_box.as_ptr_mut();
709 let eb_id = self.edit_box.base().id();
710 ctx.register_widget(eb_id, eb_ptr);
711 }
712 }
713 }
714
715 impl Paint for TreeList {
716 fn color(&self) -> [f32; 4] {
717 [0.0, 0.0, 0.0, 0.0]
718 }
719
720 fn widget_font(&self) -> Option<String> {
721 Some(crate::layout::tree_font())
722 }
723
724 // The field widgets are registered but not tree-linked (self-routing, see
725 // register_embedded_children); their pixels come from `paint`'s child pass — the
726 // walk must not descend either.
727 fn paints_own_subtree(&self) -> bool {
728 true
729 }
730
731 fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
732 self.search_box.prepare_text(fs);
733 self.add_key_btn.prepare_text(fs);
734 if self.add_key_popover_open {
735 self.add_key_popover_box.prepare_text(fs);
736 }
737 if self.editing_key_idx.is_some() {
738 self.edit_box.prepare_text(fs);
739 }
740
741 }
742
743 /// The whole tree — container border/background, search/header chrome, virtualized
744 /// rows (backgrounds, separators, color previews, button pills), the scrollbar, the
745 /// row/header labels, and the field children (search box, add-key button, the add-key
746 /// popover box while open, the inline rename editor while editing). Ported verbatim
747 /// from the legacy `all_rounded_quads` rounded branch + `subtree_fonted_labels`;
748 /// children paint through their own adapters (dummy ctx — none of their paint reads it).
749 fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
750 let (r1, r2, r3, r4) = self.tree_corners();
751 let mut quads: Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> = Vec::new();
752 let radius = self.tree_radius();
753 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
754
755 let opacity = crate::layout::tree_opacity();
756 let apply_opacity = |mut c: [f32; 4]| -> [f32; 4] {
757 c[3] *= opacity;
758 c
759 };
760
761 // 1. Draw container border and background. A true fill + border ring
762 // (not the legacy full-rect border punched out by the background quad,
763 // which read as a whole-pane border_color wash once the background
764 // went transparent).
765 if let Some((border_color, thickness)) = self.tree_border() {
766 let rr = |on: bool| if on { radius } else { 0.0 };
767 pc.border(
768 Rect { x, y, width: w, height: h },
769 (rr(r1), rr(r2), rr(r3), rr(r4)),
770 apply_opacity(crate::color::tree_background_color()),
771 apply_opacity(border_color),
772 thickness,
773 );
774 } else {
775 quads.push((x, y, w, h, radius, apply_opacity(crate::color::tree_background_color()), (r1, r2, r3, r4)));
776 }
777
778 let search_margin_y = 6.0;
779 let search_h = 26.0;
780 let offset_y = search_h + 2.0 * search_margin_y;
781
782 // Draw Header border (no background fill — the header sits directly on
783 // the pane surface, like the rows).
784 let header_h = 26.0;
785 let header_border_color = [0.18, 0.18, 0.22, 1.0];
786
787 let list_left = self.scroll_box.base.x;
788 let list_width = self.scroll_box.base.w;
789
790 // Separator line below header
791 quads.push((list_left + 1.0, y + offset_y + header_h - 1.0, list_width - 2.0, 1.0, 0.0, apply_opacity(header_border_color), (false, false, false, false)));
792
793 // Vertical separators inside header
794 quads.push((list_left + 180.0, y + offset_y + 1.0, 1.0, header_h - 2.0, 0.0, apply_opacity(header_border_color), (false, false, false, false)));
795 quads.push((list_left + 235.0, y + offset_y + 1.0, 1.0, header_h - 2.0, 0.0, apply_opacity(header_border_color), (false, false, false, false)));
796
797 // Helper to collect scrollbar quads
798 let get_scrollbar_quads = || {
799 let mut sb_quads = Vec::new();
800 let scroll_quads = self.scroll_box.extra_quads();
801 if scroll_quads.len() > 1 {
802 for q in &scroll_quads[1..] {
803 sb_quads.push((q.0, q.1, q.2, q.3, 0.0, q.4, (false, false, false, false)));
804 }
805 }
806 sb_quads
807 };
808
809 let show_on_top = self.scrollbar_activity_timer > 0.0;
810 let relief_scrollbar = crate::layout::control_relief();
811
812 // If NOT on top, draw scrollbar first (behind items). Relief style
813 // emits prims directly — they land before the quads flush below, so
814 // the ordering matches the flat path.
815 if !show_on_top {
816 if relief_scrollbar {
817 self.scroll_box.paint_scrollbar_relief(pc);
818 } else {
819 quads.extend(get_scrollbar_quads());
820 }
821 }
822
823 // 2. Draw items (row backgrounds, separator lines, color previews)
824 let list_left = self.scroll_box.base.x;
825 let list_width = self.scroll_box.base.w;
826 let list_top = self.scroll_box.viewport_y;
827 let list_bottom = self.scroll_box.viewport_y + self.scroll_box.viewport_h;
828
829 for (i, item) in self.items.iter().enumerate() {
830 let row_y = list_top + i as f32 * self.item_height - self.scroll_box.scroll_y;
831 if row_y + self.item_height < list_top || row_y > list_bottom {
832 continue;
833 }
834
835 let draw_y = row_y.max(list_top);
836 let draw_bottom = (row_y + self.item_height).min(list_bottom);
837 let draw_h = draw_bottom - draw_y;
838 if draw_h <= 0.0 { continue; }
839
840 let bg_color = match item {
841 TreeElement::Section { .. } => {
842 if Some(i) == self.hovered_row_idx {
843 crate::color::tree_section_bg_hover_color()
844 } else {
845 crate::color::tree_section_bg_color()
846 }
847 }
848 TreeElement::Leaf { original_idx, .. } => {
849 if Some(*original_idx) == self.selected_key_idx {
850 crate::color::tree_leaf_bg_selected_color()
851 } else if Some(i) == self.hovered_row_idx {
852 crate::color::tree_leaf_bg_hover_color()
853 } else if i % 2 == 0 {
854 crate::color::tree_leaf_bg_even_color()
855 } else {
856 crate::color::tree_leaf_bg_odd_color()
857 }
858 }
859 };
860
861 let row_r1 = false;
862 let row_r2 = false;
863 let mut row_r3 = false;
864 let mut row_r4 = false;
865 let row_radius = radius - 1.0;
866
867 if draw_bottom >= list_bottom - radius {
868 row_r3 = r3;
869 row_r4 = r4;
870 }
871
872 quads.push((list_left + 1.0, draw_y, list_width - 2.0, draw_h, row_radius, apply_opacity(bg_color), (row_r1, row_r2, row_r3, row_r4)));
873
874 if let TreeElement::Leaf { ref val, original_idx, .. } = item {
875 let separator_color = crate::color::tree_separator_color();
876 quads.push((list_left + 180.0, draw_y, 1.0, draw_h, 0.0, apply_opacity(separator_color), (false, false, false, false)));
877 quads.push((list_left + 235.0, draw_y, 1.0, draw_h, 0.0, apply_opacity(separator_color), (false, false, false, false)));
878
879 let mut is_button = false;
880 if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
881 if anno == "button" || anno.starts_with("button:") {
882 is_button = true;
883 }
884 }
885
886 if Some(*original_idx) != self.selected_key_idx {
887 if is_button {
888 let btn_x = list_left + 245.0;
889 let btn_y = row_y + 1.0;
890 let btn_bottom = (row_y + 27.0).min(list_bottom);
891 let btn_draw_y = btn_y.max(list_top);
892 let btn_draw_h = btn_bottom - btn_draw_y;
893 if btn_draw_h > 0.0 {
894 let btn_bg = [0.10, 0.29, 0.33, 0.65]; // theme button color
895 quads.push((btn_x, btn_draw_y, 125.0, btn_draw_h, 4.0, apply_opacity(btn_bg), (true, true, true, true)));
896 }
897 } else if let serde_json::Value::String(s) = val {
898 if s.starts_with('#') {
899 if let Some(rgba) = parse_hex_f32(s) {
900 let preview_x = list_left + 245.0;
901 let preview_y = row_y + 4.0;
902 let preview_bottom = (row_y + 20.0).min(list_bottom);
903 let preview_draw_y = preview_y.max(list_top);
904 let preview_draw_h = preview_bottom - preview_draw_y;
905 if preview_draw_h > 0.0 {
906 // Checkerboard pattern
907 let grid_size = 8.0;
908 quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, 0.0, [1.0, 1.0, 1.0, 1.0], (false, false, false, false)));
909 let cols = (16.0f32 / grid_size).ceil() as i32;
910 let rows = (preview_draw_h as f32 / grid_size).ceil() as i32;
911 for r in 0..rows {
912 for c in 0..cols {
913 if (r + c) % 2 == 1 {
914 let qx = preview_x + c as f32 * grid_size;
915 let qy = preview_draw_y + r as f32 * grid_size;
916 let qw = grid_size.min(preview_x + 16.0 - qx);
917 let qh = grid_size.min(preview_draw_y + preview_draw_h - qy);
918 if qw > 0.0 && qh > 0.0 {
919 quads.push((qx, qy, qw, qh, 0.0, [0.8, 0.8, 0.8, 1.0], (false, false, false, false)));
920 }
921 }
922 }
923 }
924 quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, 0.0, rgba, (false, false, false, false)));
925 }
926 }
927 }
928 }
929 }
930 }
931
932 if row_y + self.item_height <= list_bottom {
933 let mut sep_r3 = false;
934 let mut sep_r4 = false;
935 if row_y + self.item_height >= list_bottom - radius {
936 sep_r3 = r3;
937 sep_r4 = r4;
938 }
939 quads.push((list_left + 1.0, row_y + self.item_height - 1.0, list_width - 2.0, 1.0, row_radius, apply_opacity([0.13, 0.13, 0.17, 1.0]), (false, false, sep_r3, sep_r4)));
940 }
941 }
942
943 // If on top (scroll activity), the scrollbar draws after the rows —
944 // the relief prims are emitted after the quads flush below.
945 if show_on_top && !relief_scrollbar {
946 quads.extend(get_scrollbar_quads());
947 }
948 for (qx, qy, qw, qh, qr, qc, qcorners) in quads {
949 if qr > 0.1 {
950 pc.rounded_rect(Rect { x: qx, y: qy, width: qw, height: qh }, qr, qcorners, qc);
951 } else {
952 pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
953 }
954 }
955 if show_on_top && relief_scrollbar {
956 self.scroll_box.paint_scrollbar_relief(pc);
957 }
958
959 // Recessed well like a text box or list: the tree floor sits below the
960 // pane surface, its wall carved over the bg and row quads above. Focus
961 // lights the rim in the highlight accent instead of washing the tree
962 // (the retired legacy_focus_highlight overlay).
963 if crate::layout::control_relief() {
964 let depth = crate::layout::bevel_width().min(h * 0.2);
965 let (well, radii) = crate::layout::carve_inside(Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), depth);
966 if self.focused {
967 let hc = crate::color::highlight_primary_color();
968 pc.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
969 } else {
970 pc.recess(well, radii, depth);
971 }
972 }
973
974 // Row/header labels with the legacy header/list viewport bounds.
975 let font = Some(crate::layout::tree_font());
976 let (x, y, w, _h) = (rect.x, rect.y, rect.width, rect.height);
977 let search_margin_y = 6.0;
978 let search_h = 26.0;
979 let offset_y = search_h + 2.0 * search_margin_y;
980 let header_h = 26.0;
981 // Labels and chevrons paint over the recessed well's wall (the recess
982 // is shading-only and already drawn), so cut them at the wall's inner
983 // edge — content slides under the bevel instead of sitting on it. The
984 // top stays at the viewport: the wall there is behind the search/header
985 // strip, outside the scroll area.
986 let wall = if crate::layout::control_relief() {
987 crate::layout::bevel_width().min(rect.height * 0.2)
988 } else {
989 0.0
990 };
991 let list_bounds = Some([
992 self.scroll_box.base.x + wall,
993 self.scroll_box.viewport_y,
994 self.scroll_box.base.x + self.scroll_box.base.w - wall,
995 self.scroll_box.viewport_y + self.scroll_box.viewport_h - wall,
996 ]);
997 let header_bounds = Some([x, y + offset_y, x + w, y + offset_y + header_h]);
998 for (idx, (l, col_max_x)) in self.own_labels().into_iter().enumerate() {
999 let mut b = if idx < 3 { header_bounds } else { list_bounds };
1000 if let (Some(bb), Some(mx)) = (b.as_mut(), col_max_x) {
1001 bb[2] = bb[2].min(mx);
1002 }
1003 pc.text_with(l.text, l.x, l.y, l.font_size, l.color, font.clone(), b);
1004 }
1005
1006 // Section chevrons: image icons in the slot own_labels leaves open,
1007 // clipped like the row text — to the list viewport shrunk by the
1008 // well wall, so arrows are cut off by the bevel, never drawn on it.
1009 {
1010 let (_, tree_font_size) = crate::layout::tree_font_parsed();
1011 let list_left = self.scroll_box.base.x;
1012 let list_top = self.scroll_box.viewport_y;
1013 let list_bottom = list_top + self.scroll_box.viewport_h;
1014 let viewport = Rect {
1015 x: list_left + wall,
1016 y: list_top,
1017 width: self.scroll_box.base.w - 2.0 * wall,
1018 height: self.scroll_box.viewport_h - wall,
1019 };
1020 pc.clip(viewport, |pc| {
1021 for (i, item) in self.items.iter().enumerate() {
1022 let row_y = list_top + i as f32 * self.item_height - self.scroll_box.scroll_y;
1023 if row_y + self.item_height < list_top || row_y > list_bottom {
1024 continue;
1025 }
1026 if let TreeElement::Section { indent, collapsed, .. } = item {
1027 if self.editing_key_idx == Some(i) {
1028 continue;
1029 }
1030 if let Some((id, _, _)) = Self::chevron_icon(*collapsed) {
1031 let s = tree_font_size;
1032 pc.image(id, Rect {
1033 x: list_left + 8.0 + *indent as f32 * 12.0,
1034 y: row_y + 7.0,
1035 width: s,
1036 height: s,
1037 }, 1.0);
1038 }
1039 }
1040 }
1041 });
1042 }
1043
1044 // Field children, in the legacy children() order.
1045 let dummy = UiContext::new();
1046 self.search_box.paint_self(&dummy, pc);
1047 self.add_key_btn.paint_self(&dummy, pc);
1048 if self.add_key_popover_open {
1049 self.add_key_popover_box.paint_self(&dummy, pc);
1050 }
1051 if self.editing_key_idx.is_some() {
1052 self.edit_box.paint_self(&dummy, pc);
1053 }
1054 }
1055
1056 fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
1057 if self.add_key_popover_open {
1058 Some(self.popover_rect_geom())
1059 } else {
1060 None
1061 }
1062
1063 }
1064
1065 fn draw_popover(&self, _rect: Rect, pc: &mut dyn crate::layout::RenderTarget) {
1066 if !self.add_key_popover_open { return; }
1067
1068 let (rx, ry, rw, rh) = self.popover_rect_geom();
1069
1070 // 1. Soft layered drop shadows
1071 pc.rect([0.02, 0.02, 0.05, 0.15], rx + 1.0, ry + 1.0, rw, rh);
1072 pc.rect([0.02, 0.02, 0.05, 0.08], rx + 3.0, ry + 3.0, rw, rh);
1073 pc.rect([0.02, 0.02, 0.05, 0.04], rx + 5.0, ry + 5.0, rw, rh);
1074
1075 let theme = crate::color::active_theme();
1076
1077 // 2. High-contrast premium outer border
1078 pc.rect(theme.surface_border, rx, ry, rw, rh);
1079
1080 // 3. Frosted glass background
1081 pc.rect(theme.surface_bg, rx + 1.0, ry + 1.0, rw - 2.0, rh - 2.0); // bg
1082
1083 }
1084 }
1085
1086 impl Input for TreeList {
1087 fn focus_role(&self) -> crate::widget::FocusRole {
1088 crate::widget::FocusRole::Well
1089 }
1090 fn blocks_root_plate_drag(&self) -> bool {
1091 true
1092 }
1093
1094 /// The tree must see every press: outside presses dismiss the add-key popover and
1095 /// commit/cancel the inline rename editor (the legacy ungated `mouse_input` contract).
1096 fn gates_presses(&self) -> bool {
1097 false
1098 }
1099
1100 fn wants_tick(&self) -> bool {
1101 true
1102 }
1103
1104 fn draggable(&self, _rect: Rect) -> bool {
1105 self.scroll_box.draggable()
1106 }
1107
1108 fn is_dragging(&self) -> bool {
1109 self.scroll_box.is_dragging()
1110 }
1111
1112 fn drag_begin(&mut self, px: f32, py: f32, _rect: Rect) {
1113 self.scroll_box.drag_begin(px, py);
1114 }
1115
1116 fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
1117 self.scroll_box.drag_update(px, py)
1118 }
1119
1120 fn drag_end(&mut self) {
1121 self.scroll_box.drag_end();
1122 }
1123
1124 /// The legacy `tick` body: advances the field widgets, drains the add-key popover and
1125 /// search box, positions/commits the inline rename editor (re-targeting focus to the
1126 /// adapter on commit), and runs the scrollbar activity fade.
1127 fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
1128 let host = ectx.host_ptr();
1129 let host_id = ectx.id;
1130 let Some(ui) = ectx.ui.as_deref_mut() else {
1131 return false;
1132 };
1133 let mut changed = false;
1134 if self.search_box.tick(dt, ui) {
1135 changed = true;
1136 }
1137 if self.add_key_btn.tick(dt, ui) {
1138 changed = true;
1139 }
1140 if self.add_key_popover_open {
1141 if self.add_key_popover_box.tick(dt, ui) {
1142 changed = true;
1143 }
1144 if !self.add_key_popover_box.editing {
1145 let path = self.add_key_popover_box.text.trim().to_string();
1146 if !path.is_empty() {
1147 self.new_key_path_request = Some(path);
1148 }
1149 self.add_key_popover_open = false;
1150 ui.clear_focus();
1151 changed = true;
1152 }
1153 }
1154 if self.search_box.take_change() {
1155 self.rebuild_tree();
1156 changed = true;
1157 }
1158
1159 if self.editing_key_idx.is_some() {
1160 if self.edit_box.tick(dt, ui) {
1161 changed = true;
1162 }
1163 if let Some(row_idx) = self.editing_key_idx {
1164 if row_idx < self.items.len() {
1165 let list_left = self.scroll_box.base.x;
1166 let list_top = self.scroll_box.viewport_y;
1167 let row_y = list_top + row_idx as f32 * self.item_height - self.scroll_box.scroll_y;
1168 let box_x = list_left + 5.0;
1169 let box_y = row_y + 2.0;
1170 self.edit_box.set_rect(box_x, box_y, 170.0, 24.0);
1171 }
1172 }
1173 if !self.edit_box.editing {
1174 let row_idx = self.editing_key_idx.unwrap();
1175 if row_idx < self.items.len() {
1176 let (old_path, relative_name) = match &self.items[row_idx] {
1177 TreeElement::Section { path, name, .. } => (path.clone(), name.clone()),
1178 TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
1179 };
1180 let new_name = self.edit_box.text.trim().to_string();
1181 if !new_name.is_empty() && new_name != relative_name {
1182 let new_path = if let Some(pos) = old_path.rfind('.') {
1183 format!("{}.{}", &old_path[..pos], new_name)
1184 } else {
1185 new_name
1186 };
1187 self.rename_request = Some((old_path, new_path));
1188 }
1189 }
1190 self.editing_key_idx = None;
1191 // Undo the register+link done when editing began (see `mouse_body`). The paint
1192 // (`if self.editing_key_idx.is_some()`) and the `set_rect` beside it are both
1193 // gated on editing, but the tree link was not — so leaving it attached parked a
1194 // 170x24 child at the last-edited row's screen coordinates that kept its stale
1195 // rect, kept hit-testing (visible, gates_presses default true) and swallowed the
1196 // press before `mouse_body` ever ran: an invisible dead zone that ate row clicks
1197 // and silently re-entered editing on an unpainted box. Each rename also minted a
1198 // fresh TextBox id into the same field, so the child list grew monotonically.
1199 let eb_id = self.edit_box.base().id();
1200 ui.unlink_child(host_id, eb_id);
1201 ui.unregister_widget(eb_id);
1202 if let Some(h) = host { ui.set_focused_ptr(h); }
1203 changed = true;
1204 }
1205 }
1206
1207 // The list's own glide/coast (wheel notches and trackpad flicks land
1208 // in the ScrollBox; only its tick moves the drawn offset).
1209 if self.scroll_box.tick(dt, ui) {
1210 changed = true;
1211 }
1212 if (self.scroll_box.scroll_y - self.last_scroll_y).abs() > 0.01 {
1213 self.last_scroll_y = self.scroll_box.scroll_y;
1214 self.scrollbar_activity_timer = 1.0;
1215 changed = true;
1216 }
1217 if self.scrollbar_activity_timer > 0.0 {
1218 self.scrollbar_activity_timer = (self.scrollbar_activity_timer - dt).max(0.0);
1219 changed = true;
1220 }
1221 changed
1222
1223 }
1224
1225 fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
1226 let host = ectx.host_ptr();
1227 let host_id = ectx.id;
1228 match event {
1229 Event::MouseButton { button, state, x, y, .. } => {
1230 let (button, state, px, py) = (*button, *state, *x, *y);
1231 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1232 self.mouse_body(button, state, px, py, ui, host, host_id)
1233 }
1234 Event::PointerMove { x, y, .. } => {
1235 let (px, py) = (*x, *y);
1236 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1237 self.move_body(px, py, ui)
1238 }
1239 Event::MouseWheel { delta, x, y, .. } => {
1240 let (delta, px, py) = (delta.clone(), *x, *y);
1241 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1242 self.scroll_box.mouse_wheel(&delta, px, py, ui)
1243 }
1244 Event::KeyInput(ev) => {
1245 let ev = ev.clone();
1246 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
1247 self.key_body(&ev, ui)
1248 }
1249 Event::FocusIn => {
1250 self.focused = true;
1251 false
1252 }
1253 Event::FocusOut => {
1254 self.focused = false;
1255 self.add_key_popover_open = false;
1256 self.add_key_popover_box.unfocus();
1257 false
1258 }
1259 _ => false,
1260 }
1261 }
1262
1263 fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
1264 use crate::widget::ContextAction as CA;
1265 match action {
1266 CA::CopyKey => self.copy_key(),
1267 CA::CopyValue => self.copy_value(),
1268 CA::DeleteKey => self.delete_key(),
1269 CA::ExpandNode => self.expand_node(),
1270 CA::CollapseNode => self.collapse_node(),
1271 CA::ExpandAll => self.expand_all_nodes(),
1272 CA::CollapseAll => self.collapse_all_nodes(),
1273 _ => return false,
1274 }
1275 true
1276 }
1277 }
1278
1279 impl TreeList {
1280 // The tree context-menu actions, dispatched by the global context menu through
1281 // `Input::context_action`.
1282 fn copy_key(&self) {
1283 if let Some(idx) = self.selected_key_idx {
1284 if idx < self.flat_keys.len() {
1285 let key_path = &self.flat_keys[idx].0;
1286 clipboard::copy_to_clipboard(key_path);
1287 }
1288 }
1289 }
1290
1291 fn copy_value(&self) {
1292 if let Some(idx) = self.selected_key_idx {
1293 if idx < self.flat_keys.len() {
1294 let val = &self.flat_keys[idx].1;
1295 let val_str = match val {
1296 serde_json::Value::String(s) => s.clone(),
1297 serde_json::Value::Bool(b) => b.to_string(),
1298 serde_json::Value::Number(n) => n.to_string(),
1299 other => serde_json::to_string(other).unwrap_or_default(),
1300 };
1301 clipboard::copy_to_clipboard(&val_str);
1302 }
1303 }
1304 }
1305
1306 fn delete_key(&mut self) {
1307 if let Some(idx) = self.selected_key_idx {
1308 if idx < self.flat_keys.len() {
1309 let key_path = self.flat_keys[idx].0.clone();
1310 self.deleted_key_path = Some(key_path);
1311 }
1312 }
1313 }
1314
1315 fn expand_node(&mut self) {
1316 if let Some(path) = self.right_clicked_section.clone() {
1317 self.collapsed_sections.remove(&path);
1318 self.rebuild_tree();
1319 self.clicked_item = Some(TreeElement::Section {
1320 path,
1321 name: String::new(),
1322 indent: 0,
1323 collapsed: false,
1324 });
1325 }
1326 self.right_clicked_section = None;
1327 }
1328
1329 fn collapse_node(&mut self) {
1330 if let Some(path) = self.right_clicked_section.clone() {
1331 self.collapsed_sections.insert(path.clone());
1332 self.rebuild_tree();
1333 self.clicked_item = Some(TreeElement::Section {
1334 path,
1335 name: String::new(),
1336 indent: 0,
1337 collapsed: true,
1338 });
1339 }
1340 self.right_clicked_section = None;
1341 }
1342
1343 fn expand_all_nodes(&mut self) {
1344 self.collapsed_sections.clear();
1345 self.rebuild_tree();
1346 if let Some(path) = self.right_clicked_section.clone() {
1347 self.clicked_item = Some(TreeElement::Section {
1348 path,
1349 name: String::new(),
1350 indent: 0,
1351 collapsed: false,
1352 });
1353 } else {
1354 self.clicked_item = Some(TreeElement::Section {
1355 path: String::new(),
1356 name: String::new(),
1357 indent: 0,
1358 collapsed: false,
1359 });
1360 }
1361 self.right_clicked_section = None;
1362 }
1363
1364 fn collapse_all_nodes(&mut self) {
1365 self.collapsed_sections = self.get_all_section_paths();
1366 self.rebuild_tree();
1367 if let Some(path) = self.right_clicked_section.clone() {
1368 self.clicked_item = Some(TreeElement::Section {
1369 path,
1370 name: String::new(),
1371 indent: 0,
1372 collapsed: true,
1373 });
1374 } else {
1375 self.clicked_item = Some(TreeElement::Section {
1376 path: String::new(),
1377 name: String::new(),
1378 indent: 0,
1379 collapsed: true,
1380 });
1381 }
1382 self.right_clicked_section = None;
1383 }
1384
1385 }
1386
1387 impl TreeList {
1388 fn get_all_section_paths(&self) -> HashSet<String> {
1389 let mut sections = HashSet::new();
1390 for (key_path, _) in &self.flat_keys {
1391 let tokens = parse_path(key_path);
1392 let mut current_prefix = String::new();
1393 for i in 0..(tokens.len().saturating_sub(1)) {
1394 let token = &tokens[i];
1395 match token {
1396 PathToken::Key(k) => {
1397 if current_prefix.is_empty() {
1398 current_prefix = k.clone();
1399 } else {
1400 current_prefix = format!("{}.{}", current_prefix, k);
1401 }
1402 }
1403 PathToken::Index(idx) => {
1404 let s = format!("[{}]", idx);
1405 current_prefix = format!("{}{}", current_prefix, s);
1406 }
1407 };
1408 sections.insert(current_prefix.clone());
1409 }
1410 }
1411 sections
1412 }
1413 }
1414
1415 fn parse_hex_f32(s: &str) -> Option<[f32; 4]> {
1416 crate::color::parse_hex_rgba_linear(s)
1417 }
1418
1419 unsafe impl Send for TreeList {}
1420 unsafe impl Sync for TreeList {}
1421
1422 #[cfg(test)]
1423 mod tests {
1424 use super::*;
1425 use crate::context::UiContext;
1426
1427 /// A labelled tree's fields sit in its content rect, below the label strip — where
1428 /// `paint` draws the well — not at the top of the block the adapter is given.
1429 #[test]
1430 fn a_labelled_trees_fields_are_in_its_content_rect() {
1431 let mut tree_list = TreeList::new().with_label("TreeList");
1432 let strip = tree_list.label_strip();
1433 assert!(strip > 0.0, "a detached label has a strip");
1434 tree_list.set_rect(10.0, 52.0, 380.0, 200.0 + strip);
1435 let content_y = 52.0 + strip;
1436 let (_, sy, _, sh) = tree_list.search_box.rect();
1437 assert_eq!(sy, content_y + 6.0, "the search box is inside the well, one margin down");
1438 let (_, by, _, _) = tree_list.add_key_btn.rect();
1439 assert_eq!(by, sy, "the add-key button shares the search row");
1440 let header_y = content_y + sh + 12.0;
1441 assert_eq!(tree_list.scroll_box.base.y, header_y + 26.0, "the rows start under the header");
1442 let labels = tree_list.own_labels();
1443 let key = labels.iter().find(|(l, _)| l.text == "Key").expect("a Key header");
1444 assert_eq!(key.0.y, header_y + 6.0, "the header text is in the header band");
1445 assert_eq!(tree_list.scroll_box.base.y + tree_list.scroll_box.base.h, 52.0 + strip + 200.0, "the rows end at the block's bottom");
1446 }
1447
1448 #[test]
1449 fn test_treelist_blocks_window_drag() {
1450 // root plate container is DELETED: dissolved windows ask `drag_allowed_at` instead — same
1451 // walk, minus the registered-movable-root plate container requirement.
1452 let mut ctx = UiContext::new();
1453 let mut tree_list = TreeList::new();
1454 tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
1455
1456 ctx.register_widget(tree_list.base().id(), tree_list.as_ptr_mut());
1457 ctx.tick(0.016);
1458 ctx.clear_dirty();
1459
1460 assert!(!ctx.drag_allowed_at(100.0, 200.0), "clicking the TreeList must block the window drag");
1461 assert!(ctx.drag_allowed_at(600.0, 300.0), "empty surface stays draggable");
1462 }
1463
1464 #[test]
1465 fn test_exact_app_layout_blocks_drag() {
1466 // The data-editor shape: a parentless tree registered directly (dissolved root).
1467 let mut ctx = UiContext::new();
1468 let mut tree_list = TreeList::new();
1469
1470 ctx.register_widget(tree_list.base().id(), tree_list.as_ptr_mut());
1471 ctx.rebuild_spatial_grid();
1472
1473 let list_top = 52.0;
1474 let list_bottom = 600.0 - 180.0;
1475 tree_list.set_rect(10.0, list_top, 380.0, list_bottom - list_top);
1476 ctx.rebuild_spatial_grid();
1477
1478 assert!(!ctx.drag_allowed_at(100.0, 200.0), "clicking the TreeList under the app layout must block the drag");
1479 }
1480
1481 #[test]
1482 fn test_treelist_separators() {
1483 let ctx = UiContext::new();
1484 let mut tree_list = TreeList::new();
1485 tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
1486 tree_list.set_flat_keys(vec![
1487 ("style.data.tree.corner_radius".to_string(), serde_json::Value::Number(serde_json::Number::from(8)))
1488 ]);
1489
1490 println!("Flat keys size: {}", tree_list.flat_keys.len());
1491 println!("Items count: {}", tree_list.items.len());
1492 for (i, item) in tree_list.items.iter().enumerate() {
1493 println!("Item {}: {:?}", i, item);
1494 }
1495 println!("scroll_box base.x: {}", tree_list.scroll_box.base.x);
1496 println!("scroll_box viewport_y: {}", tree_list.scroll_box.viewport_y);
1497 println!("scroll_box viewport_h: {}", tree_list.scroll_box.viewport_h);
1498 println!("scroll_box scroll_y: {}", tree_list.scroll_box.scroll_y);
1499 println!("item_height: {}", tree_list.item_height);
1500
1501 let quads = tree_list.all_rounded_quads(&ctx);
1502 println!("Rounded quads count: {}", quads.len());
1503 for (i, q) in quads.iter().enumerate() {
1504 println!("Quad {}: {:?}", i, q);
1505 }
1506 assert!(quads.len() > 1, "Should have more than 1 quad!");
1507 }
1508
1509 #[test]
1510 fn test_keybind_label() {
1511 let mut tree_list = TreeList::new();
1512 tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
1513 tree_list.annotations = vec![Some("menu:flat,adaptive".to_string())];
1514 tree_list.set_flat_keys(vec![
1515 ("input.accel_profile".to_string(), serde_json::Value::String("flat".to_string()))
1516 ]);
1517
1518 let labels = tree_list.own_labels();
1519 for label in &labels {
1520 println!("TEST LABEL: {:?}", label);
1521 }
1522
1523 let has_menu_label = labels.iter().any(|(l, _)| l.text == "(menu)");
1524 assert!(has_menu_label, "Should have (menu) label!");
1525 }
1526
1527 #[test]
1528 fn test_treelist_headers() {
1529 let tree_list = TreeList::new();
1530 let labels = tree_list.own_labels();
1531 assert!(labels.iter().any(|(l, _)| l.text == "Key"), "Should have Key header!");
1532 assert!(labels.iter().any(|(l, _)| l.text == "Type"), "Should have Type header!");
1533 assert!(labels.iter().any(|(l, _)| l.text == "Value"), "Should have Value header!");
1534 }
1535
1536 #[test]
1537 fn test_treelist_search_filtering() {
1538 let mut tree_list = TreeList::new();
1539 tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
1540 tree_list.set_flat_keys(vec![
1541 ("style.data.tree.corner_radius".to_string(), serde_json::Value::Number(serde_json::Number::from(8))),
1542 ("style.data.tree.border_color".to_string(), serde_json::Value::String("#ff0000".to_string())),
1543 ("input.accel_profile".to_string(), serde_json::Value::String("flat".to_string())),
1544 ]);
1545
1546 // Match none
1547 tree_list.search_box.text = "nonexistent".to_string();
1548 tree_list.rebuild_tree();
1549 assert!(tree_list.items.is_empty(), "Tree should be empty for nonexistent search query!");
1550
1551 // Match partially on key path
1552 tree_list.search_box.text = "corner".to_string();
1553 tree_list.rebuild_tree();
1554 assert!(!tree_list.items.is_empty(), "Tree should have items matching 'corner'!");
1555 let has_corner = tree_list.items.iter().any(|item| match item {
1556 TreeElement::Leaf { name, .. } => name == "corner_radius",
1557 _ => false,
1558 });
1559 assert!(has_corner, "Tree should contain 'corner_radius' item!");
1560 let has_accel = tree_list.items.iter().any(|item| match item {
1561 TreeElement::Leaf { name, .. } => name == "accel_profile",
1562 _ => false,
1563 });
1564 assert!(!has_accel, "Tree should not contain 'accel_profile' item!");
1565
1566 // Match on value
1567 tree_list.search_box.text = "flat".to_string();
1568 tree_list.rebuild_tree();
1569 let has_accel = tree_list.items.iter().any(|item| match item {
1570 TreeElement::Leaf { name, .. } => name == "accel_profile",
1571 _ => false,
1572 });
1573 assert!(has_accel, "Tree should contain 'accel_profile' when matching on value 'flat'!");
1574
1575 // Collapse matching section when filtered
1576 tree_list.search_box.text = "corner".to_string();
1577 tree_list.collapsed_sections.insert("style.data.tree".to_string());
1578 tree_list.rebuild_tree();
1579 let has_corner = tree_list.items.iter().any(|item| match item {
1580 TreeElement::Leaf { name, .. } => name == "corner_radius",
1581 _ => false,
1582 });
1583 assert!(!has_corner, "Tree should NOT contain 'corner_radius' item when its parent section 'style.data.tree' is collapsed!");
1584
1585 let has_collapsed_section = tree_list.items.iter().any(|item| match item {
1586 TreeElement::Section { path, collapsed, .. } => path == "style.data.tree" && *collapsed,
1587 _ => false,
1588 });
1589 assert!(has_collapsed_section, "Tree should contain 'style.data.tree' collapsed section!");
1590 }
1591
1592 #[test]
1593 fn test_treelist_double_click_rename() {
1594 let mut ctx = UiContext::new();
1595 let mut tree_list = TreeList::new();
1596 tree_list.set_rect(0.0, 0.0, 380.0, 500.0);
1597 tree_list.set_flat_keys(vec![
1598 ("style.control.dropdown.color".to_string(), serde_json::Value::String("#ff00ff".to_string()))
1599 ]);
1600
1601 // 1. Test renaming a section (row 0)
1602 let list_top = tree_list.scroll_box.viewport_y;
1603 let py0 = list_top + 10.0;
1604 tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py0, &mut ctx);
1605 std::thread::sleep(std::time::Duration::from_millis(10));
1606 tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py0, &mut ctx);
1607
1608 assert!(tree_list.editing_key_idx.is_some());
1609 assert_eq!(tree_list.edit_box.text, "style"); // Pre-populated with relative name!
1610
1611 tree_list.edit_box.text = "theme".to_string();
1612 tree_list.edit_box.edit_buffer = "theme".to_string();
1613 tree_list.edit_box.editing = false;
1614 tree_list.tick(0.016, &mut ctx);
1615
1616 let req = tree_list.take_rename_request();
1617 assert_eq!(req, Some(("style".to_string(), "theme".to_string())));
1618
1619 // 2. Test renaming a leaf (row 3)
1620 tree_list.rebuild_tree();
1621 let py3 = list_top + 3.0 * tree_list.item_height + 10.0; // Click row 3 (Leaf "color")
1622 tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py3, &mut ctx);
1623 std::thread::sleep(std::time::Duration::from_millis(10));
1624 tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py3, &mut ctx);
1625
1626 assert!(tree_list.editing_key_idx.is_some());
1627 assert_eq!(tree_list.edit_box.text, "color"); // Pre-populated with relative name "color"!
1628
1629 tree_list.edit_box.text = "bg_color".to_string();
1630 tree_list.edit_box.edit_buffer = "bg_color".to_string();
1631 tree_list.edit_box.editing = false;
1632 tree_list.tick(0.016, &mut ctx);
1633
1634 let req = tree_list.take_rename_request();
1635 assert_eq!(req, Some(("style.control.dropdown.color".to_string(), "style.control.dropdown.bg_color".to_string())));
1636 }
1637 }
1638
1639 impl TreeList {
1640 /// Row/header labels plus each label's column clip: the x where its
1641 /// column ends (None = only the shared list/header bounds apply). Key and
1642 /// Type cells clip at their separators so text can't bleed into the next
1643 /// column; sections span the whole row.
1644 /// The section-row chevron (cce-icons), cached per size by `upload_icon`;
1645 /// `None` when the icon set is missing (rows fall back to text triangles).
1646 fn chevron_icon(collapsed: bool) -> Option<(u32, u32, u32)> {
1647 crate::upload_icon(if collapsed { "chevron-right" } else { "chevron-down" }, 32)
1648 }
1649
1650 pub(crate) fn own_labels(&self) -> Vec<(TextLabel, Option<f32>)> {
1651 let f32_to_rgb = |c: [f32; 4]| -> [u8; 3] {
1652 [
1653 (crate::color::linear_to_srgb(c[0]) * 255.0).round() as u8,
1654 (crate::color::linear_to_srgb(c[1]) * 255.0).round() as u8,
1655 (crate::color::linear_to_srgb(c[2]) * 255.0).round() as u8,
1656 ]
1657 };
1658
1659 let (_, tree_font_size) = crate::layout::tree_font_parsed();
1660 let header_font_size = (tree_font_size - 1.0).max(8.0);
1661
1662 let mut labels = Vec::new();
1663 let list_left = self.scroll_box.base.x;
1664 let list_top = self.scroll_box.viewport_y;
1665 let list_bottom = self.scroll_box.viewport_y + self.scroll_box.viewport_h;
1666
1667 let search_margin_y = 6.0;
1668 let search_h = 26.0;
1669 let offset_y = search_h + 2.0 * search_margin_y;
1670
1671 labels.push((TextLabel {
1672 text: "Key".to_string(),
1673 x: list_left + 8.0,
1674 y: self.base.y + offset_y + 6.0,
1675 font_size: header_font_size,
1676 color: [200, 200, 210],
1677 }, None));
1678 labels.push((TextLabel {
1679 text: "Type".to_string(),
1680 x: list_left + 180.0 + 8.0,
1681 y: self.base.y + offset_y + 6.0,
1682 font_size: header_font_size,
1683 color: [200, 200, 210],
1684 }, None));
1685 labels.push((TextLabel {
1686 text: "Value".to_string(),
1687 x: list_left + 235.0 + 8.0,
1688 y: self.base.y + offset_y + 6.0,
1689 font_size: header_font_size,
1690 color: [200, 200, 210],
1691 }, None));
1692
1693 for (i, item) in self.items.iter().enumerate() {
1694 let row_y = list_top + i as f32 * self.item_height - self.scroll_box.scroll_y;
1695 if row_y + self.item_height < list_top || row_y > list_bottom {
1696 continue;
1697 }
1698
1699 match item {
1700 TreeElement::Section { name, indent, collapsed, .. } => {
1701 if self.editing_key_idx != Some(i) {
1702 let sx = list_left + 8.0 + *indent as f32 * 12.0;
1703 // Chevron icons (cce-icons) replace the text triangles
1704 // when available — paint() draws the image in the slot
1705 // this leaves open. Text triangles are the fallback.
1706 let (text, tx) = if Self::chevron_icon(*collapsed).is_some() {
1707 (name.clone(), sx + tree_font_size + 6.0)
1708 } else {
1709 (format!("{} {}", if *collapsed { "▶" } else { "▼" }, name), sx)
1710 };
1711 labels.push((TextLabel {
1712 text,
1713 x: tx,
1714 y: row_y + 6.0,
1715 font_size: tree_font_size,
1716 color: f32_to_rgb(crate::color::tree_section_text_color()),
1717 }, None));
1718 }
1719 }
1720 TreeElement::Leaf { name, indent, val, original_idx, .. } => {
1721 // A unit-suffixed string (`"2mm"`, what `(mm)2.0` reads
1722 // as) shows as the length it is — `2 mm` — not a quoted
1723 // string; its unit is its type below.
1724 let len = val.as_str().and_then(crate::units::Len::parse);
1725 let val_str = match len {
1726 Some(l) => format!("{} {}", crate::units::fmt_num(l.value), l.unit.suffix()),
1727 None => serde_json::to_string(val).unwrap_or_default(),
1728 };
1729 // Generous shaping cap only — the column bounds clip the
1730 // visible text at the list edge. (char-based: the old
1731 // byte slice could panic on multibyte text.)
1732 let display_val = if val_str.chars().count() > 120 {
1733 let cut: String = val_str.chars().take(117).collect();
1734 format!("{}...", cut)
1735 } else {
1736 val_str
1737 };
1738
1739 let color = if Some(*original_idx) == self.selected_key_idx {
1740 f32_to_rgb(crate::color::tree_leaf_text_selected_color())
1741 } else {
1742 f32_to_rgb(crate::color::tree_leaf_text_color())
1743 };
1744
1745 if self.editing_key_idx != Some(i) {
1746 labels.push((TextLabel {
1747 text: name.clone(),
1748 x: list_left + 8.0 + *indent as f32 * 12.0,
1749 y: row_y + 6.0,
1750 font_size: tree_font_size,
1751 color,
1752 }, Some(list_left + 178.0)));
1753 }
1754
1755 let val_ty = match val {
1756 serde_json::Value::Bool(_) => Some("bool"),
1757 serde_json::Value::Number(num) => {
1758 if num.is_f64() {
1759 Some("f64")
1760 } else {
1761 Some("i64")
1762 }
1763 }
1764 serde_json::Value::String(s) => {
1765 if s.starts_with('#') {
1766 let s_clean = s.trim_start_matches('#');
1767 if s_clean.len() == 8 {
1768 Some("rgba")
1769 } else {
1770 Some("rgb")
1771 }
1772 } else if name == "key" || name == "keybind" || name == "shortcut" || name == "open_search" || name == "close_search" || name == "delete" || name.ends_with("_key") || name.ends_with(".key") || name.ends_with(".keybind") || name.ends_with(".shortcut") || name.ends_with(".open_search") || name.ends_with(".close_search") || name.ends_with("_delete") || name.ends_with(".delete") {
1773 Some("keybind")
1774 } else if name == "font" || name.ends_with("_font") || name.ends_with(".font") {
1775 Some("font")
1776 } else if let Some(l) = len {
1777 Some(l.unit.suffix())
1778 } else {
1779 None
1780 }
1781 }
1782 _ => None,
1783 };
1784 let mut display_ty = val_ty.map(|s| s.to_string());
1785 if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
1786 if anno.starts_with("menu:") {
1787 display_ty = Some("menu".to_string());
1788 } else if anno == "button" || anno.starts_with("button:") {
1789 display_ty = Some("button".to_string());
1790 } else {
1791 display_ty = Some(anno.clone());
1792 }
1793 } else if display_ty.is_none() {
1794 if let serde_json::Value::String(_) = val {
1795 display_ty = Some("string".to_string());
1796 }
1797 }
1798
1799 if let Some(ty) = display_ty {
1800 let ty_text = format!("({})", ty);
1801 labels.push((TextLabel {
1802 text: ty_text,
1803 x: list_left + 190.0,
1804 y: row_y + 6.0,
1805 font_size: tree_font_size,
1806 color: f32_to_rgb(crate::color::tree_type_text_color()),
1807 }, Some(list_left + 233.0)));
1808 }
1809
1810 if Some(*original_idx) != self.selected_key_idx {
1811 let mut is_button = false;
1812 if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
1813 if anno == "button" || anno.starts_with("button:") {
1814 is_button = true;
1815 }
1816 }
1817
1818 let is_color = if let serde_json::Value::String(s) = val {
1819 s.starts_with('#')
1820 } else {
1821 false
1822 };
1823
1824 let label_x = if is_color {
1825 list_left + 267.0
1826 } else {
1827 list_left + 245.0
1828 };
1829
1830 if is_button {
1831 labels.push((TextLabel {
1832 text: display_val,
1833 x: list_left + 245.0 + 8.0,
1834 y: row_y + 6.0,
1835 font_size: tree_font_size,
1836 color: [240, 240, 245],
1837 }, None));
1838 } else {
1839 labels.push((TextLabel {
1840 text: display_val,
1841 x: label_x,
1842 y: row_y + 6.0,
1843 font_size: tree_font_size,
1844 color: f32_to_rgb(crate::color::tree_value_text_color()),
1845 }, None));
1846 }
1847 }
1848 }
1849 }
1850 }
1851 labels
1852 }
1853
1854 }