git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/container/treelist.rs (83.5K)

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