git.lucas.co / cce-ui
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(&current_prefix) {
 144                     seen_prefixes.insert(current_prefix.clone());
 145                     let collapsed = collapsed_sections.contains(&current_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(&current_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(&current) {
 341                     self.collapsed_sections.remove(&current);
 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 }