GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(treelist)!: TreeList onto the narrow traits — the last widget is across (Phase 6as)
TreeList::new() returns Adapted<TreeList>; the ~40-method Element impl
becomes Layout/Paint/Input:
- Layout: rect_assigned carries the old set_rect body (base cache + field
arrangement); register_embedded_children keeps the search box / add-key
button / popover box (+ the inline rename editor while editing)
registered and linked under the adapter every tick.
- Paint: paint() emits the whole subtree (the legacy all_rounded_quads
rounded branch verbatim + the row/header labels with their header/list
clamps + the field children through their own adapters); the popover
rides Paint::popover/draw_popover. New Paint::paints_own_subtree hook →
Element::renders_own_subtree on the adapter: the field widgets stay
ctx-linked for event propagation, but the walk must not also descend
(that double-painted them and drew the CLOSED add-key popover box).
legacy_focus_highlight opted in — the focused tree's teal wash is the
shared Element overlay (the 5q trap, again caught by A/B).
- Input: the mouse/cursor/wheel/key bodies move to on_event arms
(set_focused(self) → set_focused_ptr(adapter); the rename editor links
under the adapter's tree id); drags forward to the internal ScrollBox;
the seven tree context-menu actions live on the Input hooks. New
Input::tick_ctx hook (EventCtx-carrying tick) runs the legacy tick body:
rename commit + focus re-target, popover/search drains, scrollbar fade.
EventCtx grows a pub(crate) host_ptr() accessor.
Verified live (data-editor, sole consumer): loaded tree A/B AE=0
byte-identical (incl. focus wash); row click → selection + inline editor;
right-click menu → Copy Key lands "input.accel_speed" in the clipboard;
search click-to-focus. Wheel-over-tree and the Add-Key popover toggle
behave IDENTICALLY to the pre-conversion baseline (both are latent
routed-events issues since 6ac: children-first propagation short-circuits
before the tree's toggle logic) — parity, documented in the RFC.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/widget/container/treelist.rs | 814 ++++++++++++++++-----------------------
src/widget/model.rs | 35 ++
2 files changed, 376 insertions(+), 473 deletions(-)
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index f342a2d..bbc066b 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -1,6 +1,9 @@
use crate::widget::*;
use crate::widget::container::scroll_box::ScrollBox;
use crate::widget::display::TextLabel;
+use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
@@ -174,8 +177,6 @@ pub struct TreeList {
pub item_height: f32,
pub clicked_item: Option<TreeElement>,
pub right_clicked_section: Option<String>,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
pub last_scroll_y: f32,
pub scrollbar_activity_timer: f32,
pub deleted_key_path: Option<String>,
@@ -186,10 +187,10 @@ pub struct TreeList {
}
impl TreeList {
- pub fn new() -> Self {
+ pub fn new() -> Adapted<TreeList> {
let mut scroll_box = ScrollBox::new();
scroll_box.show_background = false;
- Self {
+ Adapted::new(TreeList {
base: Widget::new(),
scroll_box,
search_box: TextBox::new(String::new()).with_placeholder("Search...").with_update_on_type(true),
@@ -206,8 +207,6 @@ impl TreeList {
item_height: 28.0,
clicked_item: None,
right_clicked_section: None,
- parent: None,
- children: Vec::new(),
last_scroll_y: 0.0,
scrollbar_activity_timer: 0.0,
deleted_key_path: None,
@@ -215,7 +214,7 @@ impl TreeList {
editing_key_idx: None,
double_click_timer: None,
rename_request: None,
- }
+ })
}
pub fn take_new_key_path_request(&mut self) -> Option<String> {
@@ -245,7 +244,7 @@ impl TreeList {
let query = self.search_box.text.clone();
self.items = build_tree(&self.flat_keys, &self.annotations, &self.collapsed_sections, &query);
let content_h = self.items.len() as f32 * self.item_height;
- let (_, _, _, h) = self.rect();
+ let h = self.base.h;
let search_margin_y = 6.0;
let search_h = 26.0;
let offset_y = search_h + 2.0 * search_margin_y;
@@ -284,14 +283,6 @@ impl TreeList {
self.rename_request.take()
}
- pub fn check_scroll_activity(&mut self, ctx: &mut UiContext) {
- if (self.scroll_box.scroll_y - self.last_scroll_y).abs() > 0.01 {
- self.scrollbar_activity_timer = 1.0;
- ctx.register_tick_receiver(self.base.id());
- self.last_scroll_y = self.scroll_box.scroll_y;
- self.mark_dirty(ctx);
- }
- }
pub fn scroll_to_selected_key(&mut self) {
if let Some(selected_idx) = self.selected_key_idx {
@@ -349,59 +340,16 @@ impl TreeList {
}
}
-impl Element for TreeList {
- crate::impl_widget_base!(TreeList);
-
- fn blocks_backplate_drag(&self) -> bool {
- true
- }
-
- fn widget_font(&self) -> Option<String> {
- Some(crate::layout::tree_font())
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.base.x = x;
- self.base.y = y;
- self.base.w = w;
- self.base.h = h;
-
- let search_margin_x = 8.0;
- let search_margin_y = 6.0;
- let search_h = 26.0;
- let offset_y = search_h + 2.0 * search_margin_y;
-
- let button_width = 80.0;
- let button_height = search_h;
- let button_x = x + w - search_margin_x - button_width;
-
- self.search_box.set_rect(x + search_margin_x, y + search_margin_y, w - 2.0 * search_margin_x - button_width - 6.0, search_h);
- self.add_key_btn.set_rect(button_x, y + search_margin_y, button_width, button_height);
-
- let (px, py, pw, ph) = self.popover_rect_geom();
- self.add_key_popover_box.set_rect(px + 8.0, py + 5.0, pw - 16.0, ph - 10.0);
-
- let header_h = 26.0;
- self.scroll_box.set_rect(x, y + offset_y + header_h, w, h - offset_y - header_h);
-
- let content_h = self.items.len() as f32 * self.item_height;
- self.scroll_box.update_bounds(content_h, y + offset_y + header_h, h - offset_y - header_h);
- self.last_scroll_y = self.scroll_box.scroll_y;
- }
-
- fn color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
+impl TreeList {
+ fn tree_radius(&self) -> f32 {
+ crate::layout::tree_corner_radius()
}
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+ fn tree_corners(&self) -> (bool, bool, bool, bool) {
(true, true, true, true)
}
- fn corner_radius(&self) -> f32 {
- crate::layout::tree_corner_radius()
- }
-
- fn solid_border(&self) -> Option<([f32; 4], f32)> {
+ fn tree_border(&self) -> Option<([f32; 4], f32)> {
if self.scroll_box.show_border {
Some((crate::color::tree_border_color(), 1.0))
} else {
@@ -409,36 +357,17 @@ impl Element for TreeList {
}
}
- fn focus(&mut self) {
- focus::set_focused(self);
- }
- fn unfocus(&mut self) {
- self.add_key_popover_open = false;
- self.add_key_popover_box.unfocus();
- }
-
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- self.search_box.prepare_text(fs);
- self.add_key_btn.prepare_text(fs);
- if self.add_key_popover_open {
- self.add_key_popover_box.prepare_text(fs);
- }
- self.scroll_box.prepare_text(fs);
- if self.editing_key_idx.is_some() {
- self.edit_box.prepare_text(fs);
- }
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ fn mouse_body(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ui: &mut UiContext, host: Option<*mut (dyn Element + 'static)>, host_id: WidgetId) -> bool {
+ let _ = host_id;
if self.editing_key_idx.is_some() {
if button == MouseButton::Left && state == ElementState::Pressed {
let (ex, ey, ew, eh) = self.edit_box.rect();
if px >= ex && px <= ex + ew && py >= ey && py <= ey + eh {
- if self.edit_box.mouse_input(button, state, px, py, ctx) {
+ if self.edit_box.mouse_input(button, state, px, py, ui) {
return true;
}
} else {
- ctx.clear_focus();
+ ui.clear_focus();
return true;
}
}
@@ -452,11 +381,11 @@ impl Element for TreeList {
if button == MouseButton::Left && state == ElementState::Pressed {
if px < px_rect || px > px_rect + pw || py < py_rect || py > py_rect + ph {
self.add_key_popover_open = false;
- ctx.clear_focus();
+ ui.clear_focus();
changed = true;
} else {
- if self.add_key_popover_box.mouse_input(button, state, px, py, ctx) {
- ctx.set_focused(&mut self.add_key_popover_box);
+ if self.add_key_popover_box.mouse_input(button, state, px, py, ui) {
+ ui.set_focused(&mut self.add_key_popover_box);
changed = true;
}
}
@@ -467,14 +396,14 @@ impl Element for TreeList {
}
}
- if self.scroll_box.mouse_input(button, state, px, py, ctx) {
+ if self.scroll_box.mouse_input(button, state, px, py, ui) {
changed = true;
}
- if self.search_box.mouse_input(button, state, px, py, ctx) {
- ctx.set_focused(&mut self.search_box);
+ if self.search_box.mouse_input(button, state, px, py, ui) {
+ ui.set_focused(&mut self.search_box);
changed = true;
}
- if self.add_key_btn.mouse_input(button, state, px, py, ctx) {
+ if self.add_key_btn.mouse_input(button, state, px, py, ui) {
if self.add_key_btn.take_click() {
self.add_key_popover_open = !self.add_key_popover_open;
if self.add_key_popover_open {
@@ -484,16 +413,15 @@ impl Element for TreeList {
self.add_key_popover_box.select_anchor = None;
self.add_key_popover_box.all_selected = false;
self.add_key_popover_box.editing = true;
- ctx.set_focused(&mut self.add_key_popover_box);
+ ui.set_focused(&mut self.add_key_popover_box);
self.add_key_popover_box.focus();
} else {
- ctx.clear_focus();
+ ui.clear_focus();
}
}
changed = true;
}
- self.check_scroll_activity(ctx);
-
+
let list_left = self.scroll_box.base.x;
let list_width = self.scroll_box.base.w;
let list_top = self.scroll_box.viewport_y;
@@ -502,8 +430,7 @@ impl Element for TreeList {
if button == MouseButton::Left && state == ElementState::Pressed {
let on_scrollbar = self.scroll_box.hit_test_scrollbar(px, py) || self.scroll_box.scrollbar_dragging;
if !on_scrollbar && px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
- ctx.set_focused(self);
- self.focus();
+ if let Some(h) = host { ui.set_focused_ptr(h); }
let relative_y = py - list_top + self.scroll_box.scroll_y;
let row_idx = (relative_y / self.item_height) as usize;
if row_idx < self.items.len() {
@@ -537,17 +464,12 @@ impl Element for TreeList {
self.edit_box.cursor_idx = self.edit_box.text.chars().count();
self.edit_box.select_anchor = Some(0);
- let self_ptr = self as *mut Self;
- let self_id = self.base.id();
- unsafe {
- let eb_ptr = (*self_ptr).edit_box.as_ptr_mut();
- let eb_id = (*self_ptr).edit_box.base().unwrap().id();
- ctx.register_widget(eb_id, eb_ptr);
- ctx.link_ids(self_id, eb_id);
- (*eb_ptr).set_parent(Some(self_ptr), ctx);
- }
+ let eb_ptr = self.edit_box.as_ptr_mut();
+ let eb_id = self.edit_box.base().unwrap().id();
+ ui.register_widget(eb_id, eb_ptr);
+ ui.link_ids(host_id, eb_id);
- ctx.set_focused(&mut self.edit_box);
+ ui.set_focused(&mut self.edit_box);
return true;
}
@@ -585,8 +507,7 @@ impl Element for TreeList {
if button == MouseButton::Right && state == ElementState::Pressed {
if px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
- ctx.set_focused(self);
- self.focus();
+ if let Some(h) = host { ui.set_focused_ptr(h); }
let relative_y = py - list_top + self.scroll_box.scroll_y;
let row_idx = (relative_y / self.item_height) as usize;
if row_idx < self.items.len() {
@@ -605,7 +526,7 @@ impl Element for TreeList {
options.push("Collapse All".to_string());
let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
- ctx.show_context_menu(px, py - scroll_offset, options, 1, self.as_ptr_mut());
+ if let Some(h) = host { ui.show_context_menu(px, py - scroll_offset, options, 1, h); }
changed = true;
}
TreeElement::Leaf { original_idx, ref path, ref name, indent, ref val } => {
@@ -625,7 +546,7 @@ impl Element for TreeList {
"Delete".to_string(),
];
let scroll_offset = crate::widget::hover_animation::get_scroll_offset();
- ctx.show_context_menu(px, py - scroll_offset, options, 1, self.as_ptr_mut());
+ if let Some(h) = host { ui.show_context_menu(px, py - scroll_offset, options, 1, h); }
changed = true;
}
}
@@ -633,18 +554,19 @@ impl Element for TreeList {
}
}
changed
+
}
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- let mut changed = self.scroll_box.on_cursor_moved(px, py, ctx);
- if self.search_box.on_cursor_moved(px, py, ctx) {
+ fn move_body(&mut self, px: f32, py: f32, ui: &mut UiContext) -> bool {
+ let mut changed = self.scroll_box.on_cursor_moved(px, py, ui);
+ if self.search_box.on_cursor_moved(px, py, ui) {
changed = true;
}
- if self.add_key_btn.on_cursor_moved(px, py, ctx) {
+ if self.add_key_btn.on_cursor_moved(px, py, ui) {
changed = true;
}
if self.add_key_popover_open {
- if self.add_key_popover_box.on_cursor_moved(px, py, ctx) {
+ if self.add_key_popover_box.on_cursor_moved(px, py, ui) {
changed = true;
}
}
@@ -670,338 +592,135 @@ impl Element for TreeList {
changed = true;
}
changed
+
}
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- let changed = self.scroll_box.mouse_wheel(delta, px, py, ctx);
- self.check_scroll_activity(ctx);
- changed
- }
-
- fn draggable(&self) -> bool {
- self.scroll_box.draggable()
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- self.scroll_box.drag_begin(px, py);
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- self.scroll_box.drag_update(px, py)
- }
-
- fn drag_end(&mut self) {
- self.scroll_box.drag_end();
- }
-
- fn wants_tick(&self) -> bool {
- true
- }
-
- fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
- let mut changed = false;
- if self.search_box.tick(dt, ctx) {
- changed = true;
- }
- if self.add_key_btn.tick(dt, ctx) {
- changed = true;
- }
- if self.add_key_popover_open {
- if self.add_key_popover_box.tick(dt, ctx) {
- changed = true;
- }
- if !self.add_key_popover_box.editing {
- let path = self.add_key_popover_box.text.trim().to_string();
- if !path.is_empty() {
- self.new_key_path_request = Some(path);
- }
- self.add_key_popover_open = false;
- ctx.clear_focus();
- changed = true;
- }
- }
- if self.search_box.take_change() {
- self.rebuild_tree();
- self.mark_dirty(ctx);
- changed = true;
- }
-
+ fn key_body(&mut self, event: &KeyEvent, ui: &mut UiContext) -> bool {
if self.editing_key_idx.is_some() {
- if self.edit_box.tick(dt, ctx) {
- changed = true;
- }
- if let Some(row_idx) = self.editing_key_idx {
- if row_idx < self.items.len() {
- let list_left = self.scroll_box.base.x;
- let list_top = self.scroll_box.viewport_y;
- let row_y = list_top + row_idx as f32 * self.item_height - self.scroll_box.scroll_y;
- let box_x = list_left + 5.0;
- let box_y = row_y + 2.0;
- self.edit_box.set_rect(box_x, box_y, 170.0, 24.0);
- }
- }
- if !self.edit_box.editing {
- let row_idx = self.editing_key_idx.unwrap();
- if row_idx < self.items.len() {
- let (old_path, relative_name) = match &self.items[row_idx] {
- TreeElement::Section { path, name, .. } => (path.clone(), name.clone()),
- TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
- };
- let new_name = self.edit_box.text.trim().to_string();
- if !new_name.is_empty() && new_name != relative_name {
- let new_path = if let Some(pos) = old_path.rfind('.') {
- format!("{}.{}", &old_path[..pos], new_name)
- } else {
- new_name
- };
- self.rename_request = Some((old_path, new_path));
- }
- }
- self.editing_key_idx = None;
- ctx.set_focused(self);
- self.mark_dirty(ctx);
- changed = true;
- }
- }
-
- if (self.scroll_box.scroll_y - self.last_scroll_y).abs() > 0.01 {
- self.last_scroll_y = self.scroll_box.scroll_y;
- self.scrollbar_activity_timer = 1.0;
- self.mark_dirty(ctx);
- changed = true;
- }
- if self.scrollbar_activity_timer > 0.0 {
- self.scrollbar_activity_timer = (self.scrollbar_activity_timer - dt).max(0.0);
- changed = true;
- }
- changed
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- if self.editing_key_idx.is_some() {
- if self.edit_box.keyboard_input(event, ctx) {
+ if self.edit_box.keyboard_input(event, ui) {
return true;
}
}
if self.add_key_popover_open {
- if self.add_key_popover_box.keyboard_input(event, ctx) {
+ if self.add_key_popover_box.keyboard_input(event, ui) {
return true;
}
}
- if self.search_box.keyboard_input(event, ctx) {
+ if self.search_box.keyboard_input(event, ui) {
return true;
}
false
+
}
+}
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let (r1, r2, r3, r4) = self.rounded_corners();
- let has_rounded = r1 || r2 || r3 || r4;
- if has_rounded {
- return Vec::new();
- }
-
- let mut quads = self.scroll_box.extra_quads();
-
- let list_left = self.scroll_box.base.x;
- let list_width = self.scroll_box.base.w;
-
+impl Layout for TreeList {
+ /// The legacy `set_rect` body: cache the rect on the internal base and arrange the
+ /// field widgets (search box, add-key button, popover box, scroll box).
+ fn rect_assigned(&mut self, rect: Rect) {
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
+ self.base.x = x;
+ self.base.y = y;
+ self.base.w = w;
+ self.base.h = h;
+
+ let search_margin_x = 8.0;
let search_margin_y = 6.0;
let search_h = 26.0;
let offset_y = search_h + 2.0 * search_margin_y;
-
- // Draw Header background and border
- let header_h = 26.0;
- let header_bg_color = [0.12, 0.12, 0.16, 1.0]; // Dark header color
- let header_border_color = [0.18, 0.18, 0.22, 1.0];
- // Header background
- quads.push((list_left, self.base.y + offset_y, list_width, header_h, header_bg_color));
+ let button_width = 80.0;
+ let button_height = search_h;
+ let button_x = x + w - search_margin_x - button_width;
+
+ self.search_box.set_rect(x + search_margin_x, y + search_margin_y, w - 2.0 * search_margin_x - button_width - 6.0, search_h);
+ self.add_key_btn.set_rect(button_x, y + search_margin_y, button_width, button_height);
- // Separator line below header
- quads.push((list_left, self.base.y + offset_y + header_h - 1.0, list_width, 1.0, header_border_color));
+ let (px, py, pw, ph) = self.popover_rect_geom();
+ self.add_key_popover_box.set_rect(px + 8.0, py + 5.0, pw - 16.0, ph - 10.0);
- // Vertical separators inside header
- quads.push((list_left + 180.0, self.base.y + offset_y, 1.0, header_h, header_border_color));
- quads.push((list_left + 235.0, self.base.y + offset_y, 1.0, header_h, header_border_color));
+ let header_h = 26.0;
+ self.scroll_box.set_rect(x, y + offset_y + header_h, w, h - offset_y - header_h);
+
+ let content_h = self.items.len() as f32 * self.item_height;
+ self.scroll_box.update_bounds(content_h, y + offset_y + header_h, h - offset_y - header_h);
+ self.last_scroll_y = self.scroll_box.scroll_y;
+
+ }
- let list_top = self.scroll_box.viewport_y;
- let list_bottom = self.scroll_box.viewport_y + self.scroll_box.viewport_h;
+ /// Keep the field widgets registered/linked under the adapter every tick (the legacy
+ /// `set_parent` side effect; also heals the inline rename editor's registry entry).
+ fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
+ let sb_ptr = self.search_box.as_ptr_mut();
+ let sb_id = self.search_box.base().unwrap().id();
+ ctx.register_widget(sb_id, sb_ptr);
+ ctx.link_ids(host_id, sb_id);
- for (i, item) in self.items.iter().enumerate() {
- let row_y = list_top + i as f32 * self.item_height - self.scroll_box.scroll_y;
- if row_y + self.item_height < list_top || row_y > list_bottom {
- continue;
- }
-
- let draw_y = row_y.max(list_top);
- let draw_bottom = (row_y + self.item_height).min(list_bottom);
- let draw_h = draw_bottom - draw_y;
- if draw_h <= 0.0 { continue; }
-
- let bg_color = match item {
- TreeElement::Section { .. } => {
- if Some(i) == self.hovered_row_idx {
- crate::color::tree_section_bg_hover_color()
- } else {
- crate::color::tree_section_bg_color()
- }
- }
- TreeElement::Leaf { original_idx, .. } => {
- if Some(*original_idx) == self.selected_key_idx {
- crate::color::tree_leaf_bg_selected_color()
- } else if Some(i) == self.hovered_row_idx {
- crate::color::tree_leaf_bg_hover_color()
- } else if i % 2 == 0 {
- crate::color::tree_leaf_bg_even_color()
- } else {
- crate::color::tree_leaf_bg_odd_color()
- }
- }
- };
-
- quads.push((list_left + 1.0, draw_y, list_width - 2.0, draw_h, bg_color));
-
- // Draw column separator lines and color preview for Leaf rows
- if let TreeElement::Leaf { ref val, original_idx, .. } = item {
- let separator_color = crate::color::tree_separator_color();
- quads.push((list_left + 180.0, draw_y, 1.0, draw_h, separator_color));
- quads.push((list_left + 235.0, draw_y, 1.0, draw_h, separator_color));
+ let btn_ptr = self.add_key_btn.as_ptr_mut();
+ let btn_id = self.add_key_btn.base().unwrap().id();
+ ctx.register_widget(btn_id, btn_ptr);
+ ctx.link_ids(host_id, btn_id);
- // Color preview in 3rd column next to value string (only when not selected)
- if Some(*original_idx) != self.selected_key_idx {
- if let serde_json::Value::String(s) = val {
- if s.starts_with('#') {
- if let Some(rgba) = parse_hex_f32(s) {
- let preview_x = list_left + 245.0;
- let preview_y = row_y + 4.0;
- let preview_bottom = (row_y + 20.0).min(list_bottom);
- let preview_draw_y = preview_y.max(list_top);
- let preview_draw_h = preview_bottom - preview_draw_y;
- if preview_draw_h > 0.0 {
- // Checkerboard pattern
- let grid_size = 8.0;
- quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, [1.0, 1.0, 1.0, 1.0]));
- let cols = (16.0f32 / grid_size).ceil() as i32;
- let rows = (preview_draw_h as f32 / grid_size).ceil() as i32;
- for r in 0..rows {
- for c in 0..cols {
- if (r + c) % 2 == 1 {
- let qx = preview_x + c as f32 * grid_size;
- let qy = preview_draw_y + r as f32 * grid_size;
- let qw = grid_size.min(preview_x + 16.0 - qx);
- let qh = grid_size.min(preview_draw_y + preview_draw_h - qy);
- if qw > 0.0 && qh > 0.0 {
- quads.push((qx, qy, qw, qh, [0.8, 0.8, 0.8, 1.0]));
- }
- }
- }
- }
- quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, rgba));
- }
- }
- }
- }
- }
- }
+ let pop_ptr = self.add_key_popover_box.as_ptr_mut();
+ let pop_id = self.add_key_popover_box.base().unwrap().id();
+ ctx.register_widget(pop_id, pop_ptr);
+ ctx.link_ids(host_id, pop_id);
- if row_y + self.item_height <= list_bottom {
- quads.push((list_left + 1.0, row_y + self.item_height - 1.0, list_width - 2.0, 1.0, [0.13, 0.13, 0.17, 1.0]));
- }
+ if self.editing_key_idx.is_some() {
+ let eb_ptr = self.edit_box.as_ptr_mut();
+ let eb_id = self.edit_box.base().unwrap().id();
+ ctx.register_widget(eb_id, eb_ptr);
+ ctx.link_ids(host_id, eb_id);
}
-
- quads
}
+}
- fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- self.parent
+impl Paint for TreeList {
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
}
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
- self.parent = parent;
- if parent.is_some() {
- let self_ptr = self as *mut Self;
- let self_id = self.base.id();
- unsafe {
- let sb_ptr = (*self_ptr).search_box.as_ptr_mut();
- let sb_id = (*self_ptr).search_box.base().unwrap().id();
- ctx.register_widget(sb_id, sb_ptr);
- ctx.link_ids(self_id, sb_id);
- (*sb_ptr).set_parent(Some(self_ptr), ctx);
-
- let btn_ptr = (*self_ptr).add_key_btn.as_ptr_mut();
- let btn_id = (*self_ptr).add_key_btn.base().unwrap().id();
- ctx.register_widget(btn_id, btn_ptr);
- ctx.link_ids(self_id, btn_id);
- (*btn_ptr).set_parent(Some(self_ptr), ctx);
-
- let pop_ptr = (*self_ptr).add_key_popover_box.as_ptr_mut();
- let pop_id = (*self_ptr).add_key_popover_box.base().unwrap().id();
- ctx.register_widget(pop_id, pop_ptr);
- ctx.link_ids(self_id, pop_id);
- (*pop_ptr).set_parent(Some(self_ptr), ctx);
- }
- }
+ fn widget_font(&self) -> Option<String> {
+ Some(crate::layout::tree_font())
}
- fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
- let mut list = self.children.clone();
- let self_ptr = self as *const Self as *mut Self;
- unsafe {
- list.push((*self_ptr).search_box.as_ptr_mut());
- list.push((*self_ptr).add_key_btn.as_ptr_mut());
- if (*self_ptr).add_key_popover_open {
- list.push((*self_ptr).add_key_popover_box.as_ptr_mut());
- }
- if (*self_ptr).editing_key_idx.is_some() {
- list.push((*self_ptr).edit_box.as_ptr_mut());
- }
- }
- list
+ // The field widgets stay ctx-linked (event propagation descends them), but their
+ // pixels come from `paint`'s child pass — the walk must not also descend.
+ fn paints_own_subtree(&self) -> bool {
+ true
}
- // TreeList draws its rows, backgrounds and separators in the recursive all_rounded_quads below,
- // so the Phase 3 paint walk emits that directly instead of descending (see Element docs).
- fn renders_own_subtree(&self) -> bool {
+ // Legacy TreeList kept the default Element focus/hover highlight overlay (the teal
+ // wash over the focused tree, drawn by the old all_quads default) — opt back in
+ // (the 5q TextBox trap).
+ fn legacy_focus_highlight(&self) -> bool {
true
}
- /// The whole subtree — geometry via the recursive `all_*` aggregates, text via the
- /// recursive bounded getter — emitted here so the paint walk's `renders_own_subtree`
- /// branch is just `paint_self` + no descent, with no trait-getter use left in the walk.
- fn paint_self(&self, ui: &UiContext, pc: &mut crate::scene::paint::PaintCtx) {
- use crate::scene::layout::Rect;
- for (x, y, w, h, r, c, corners) in self.all_rounded_quads(ui) {
- pc.rounded_rect(Rect { x, y, width: w, height: h }, r, corners, c);
- }
- for (x, y, w, h, c) in self.all_quads(ui) {
- pc.quad(Rect { x, y, width: w, height: h }, c);
- }
- for (cx, cy, r, t, s, e, c) in self.extra_arcs() {
- pc.arc(cx, cy, r, t, s, e, c);
- }
- for (cx, cy, r, c) in self.extra_circles() {
- pc.circle(cx, cy, r, c);
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
+ self.search_box.prepare_text(fs);
+ self.add_key_btn.prepare_text(fs);
+ if self.add_key_popover_open {
+ self.add_key_popover_box.prepare_text(fs);
}
- for (tl, font, bounds) in self.subtree_fonted_labels(ui) {
- pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
+ self.scroll_box.prepare_text(fs);
+ if self.editing_key_idx.is_some() {
+ self.edit_box.prepare_text(fs);
}
+
}
- fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
- let mut quads = Vec::new();
- let (r1, r2, r3, r4) = self.rounded_corners();
- let has_rounded = r1 || r2 || r3 || r4;
- if !has_rounded {
- for &child_ptr in &self.children(ctx) {
- let widget = unsafe { &*child_ptr };
- quads.extend(widget.all_rounded_quads(ctx));
- }
- return quads;
- }
-
- let radius = self.corner_radius();
- let (x, y, w, h) = self.rect();
+ /// The whole tree — container border/background, search/header chrome, virtualized
+ /// rows (backgrounds, separators, color previews, button pills), the scrollbar, the
+ /// row/header labels, and the field children (search box, add-key button, the add-key
+ /// popover box while open, the inline rename editor while editing). Ported verbatim
+ /// from the legacy `all_rounded_quads` rounded branch + `subtree_fonted_labels`;
+ /// children paint through their own adapters (dummy ctx — none of their paint reads it).
+ fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+ let (r1, r2, r3, r4) = self.tree_corners();
+ let mut quads: Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> = Vec::new();
+ let radius = self.tree_radius();
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
let opacity = crate::layout::tree_opacity();
let apply_opacity = |mut c: [f32; 4]| -> [f32; 4] {
@@ -1010,7 +729,7 @@ impl Element for TreeList {
};
// 1. Draw container border and background
- if let Some((border_color, thickness)) = self.solid_border() {
+ if let Some((border_color, thickness)) = self.tree_border() {
quads.push((x, y, w, h, radius, apply_opacity(border_color), (r1, r2, r3, r4)));
quads.push((x + thickness, y + thickness, w - 2.0 * thickness, h - 2.0 * thickness, radius - thickness, apply_opacity(crate::color::tree_background_color()), (r1, r2, r3, r4)));
} else {
@@ -1182,22 +901,225 @@ impl Element for TreeList {
if show_on_top {
quads.extend(get_scrollbar_quads());
}
+ for (qx, qy, qw, qh, qr, qc, qcorners) in quads {
+ if qr > 0.1 {
+ pc.rounded_rect(Rect { x: qx, y: qy, width: qw, height: qh }, qr, qcorners, qc);
+ } else {
+ pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+ }
+ }
+
+ // Row/header labels with the legacy header/list viewport bounds.
+ let font = Some(crate::layout::tree_font());
+ let (x, y, w, _h) = (rect.x, rect.y, rect.width, rect.height);
+ let search_margin_y = 6.0;
+ let search_h = 26.0;
+ let offset_y = search_h + 2.0 * search_margin_y;
+ let header_h = 26.0;
+ let list_bounds = Some([self.scroll_box.base.x, self.scroll_box.viewport_y, self.scroll_box.base.x + self.scroll_box.base.w, self.scroll_box.viewport_y + self.scroll_box.viewport_h]);
+ let header_bounds = Some([x, y + offset_y, x + w, y + offset_y + header_h]);
+ for (idx, l) in self.own_labels().into_iter().enumerate() {
+ let b = if idx < 3 { header_bounds } else { list_bounds };
+ pc.text_with(l.text, l.x, l.y, l.font_size, l.color, font.clone(), b);
+ }
- for &child_ptr in &self.children(ctx) {
- let widget = unsafe { &*child_ptr };
- quads.extend(widget.all_rounded_quads(ctx));
+ // Field children, in the legacy children() order.
+ let dummy = UiContext::new();
+ self.search_box.paint_self(&dummy, pc);
+ self.add_key_btn.paint_self(&dummy, pc);
+ if self.add_key_popover_open {
+ self.add_key_popover_box.paint_self(&dummy, pc);
+ }
+ if self.editing_key_idx.is_some() {
+ self.edit_box.paint_self(&dummy, pc);
}
- quads
}
- fn add_child(&mut self, child: *mut (dyn Element + 'static), _ctx: &mut UiContext) {
- self.children.push(child);
+ fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
+ if self.add_key_popover_open {
+ Some(self.popover_rect_geom())
+ } else {
+ None
+ }
+
+ }
+
+ fn draw_popover(&self, _rect: Rect, pc: &mut dyn crate::layout::RenderTarget) {
+ if !self.add_key_popover_open { return; }
+
+ let (rx, ry, rw, rh) = self.popover_rect_geom();
+
+ // 1. Soft layered drop shadows
+ pc.rect([0.02, 0.02, 0.05, 0.15], rx + 1.0, ry + 1.0, rw, rh);
+ pc.rect([0.02, 0.02, 0.05, 0.08], rx + 3.0, ry + 3.0, rw, rh);
+ pc.rect([0.02, 0.02, 0.05, 0.04], rx + 5.0, ry + 5.0, rw, rh);
+
+ let theme = crate::color::active_theme();
+
+ // 2. High-contrast premium outer border
+ pc.rect(theme.surface_border, rx, ry, rw, rh);
+
+ // 3. Frosted glass background
+ pc.rect(theme.surface_bg, rx + 1.0, ry + 1.0, rw - 2.0, rh - 2.0); // bg
+
+ }
+}
+
+impl Input for TreeList {
+ fn blocks_backplate_drag(&self) -> bool {
+ true
+ }
+
+ /// The tree must see every press: outside presses dismiss the add-key popover and
+ /// commit/cancel the inline rename editor (the legacy ungated `mouse_input` contract).
+ fn gates_presses(&self) -> bool {
+ false
+ }
+
+ fn wants_tick(&self) -> bool {
+ true
+ }
+
+ fn draggable(&self, _rect: Rect) -> bool {
+ self.scroll_box.draggable()
+ }
+
+ fn is_dragging(&self) -> bool {
+ self.scroll_box.is_dragging()
+ }
+
+ fn drag_begin(&mut self, px: f32, py: f32, _rect: Rect) {
+ self.scroll_box.drag_begin(px, py);
+ }
+
+ fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
+ self.scroll_box.drag_update(px, py)
+ }
+
+ fn drag_end(&mut self) {
+ self.scroll_box.drag_end();
+ }
+
+ /// The legacy `tick` body: advances the field widgets, drains the add-key popover and
+ /// search box, positions/commits the inline rename editor (re-targeting focus to the
+ /// adapter on commit), and runs the scrollbar activity fade.
+ fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
+ let host = ectx.host_ptr();
+ let Some(ui) = ectx.ui.as_deref_mut() else {
+ return false;
+ };
+ let mut changed = false;
+ if self.search_box.tick(dt, ui) {
+ changed = true;
+ }
+ if self.add_key_btn.tick(dt, ui) {
+ changed = true;
+ }
+ if self.add_key_popover_open {
+ if self.add_key_popover_box.tick(dt, ui) {
+ changed = true;
+ }
+ if !self.add_key_popover_box.editing {
+ let path = self.add_key_popover_box.text.trim().to_string();
+ if !path.is_empty() {
+ self.new_key_path_request = Some(path);
+ }
+ self.add_key_popover_open = false;
+ ui.clear_focus();
+ changed = true;
+ }
+ }
+ if self.search_box.take_change() {
+ self.rebuild_tree();
+ changed = true;
+ }
+
+ if self.editing_key_idx.is_some() {
+ if self.edit_box.tick(dt, ui) {
+ changed = true;
+ }
+ if let Some(row_idx) = self.editing_key_idx {
+ if row_idx < self.items.len() {
+ let list_left = self.scroll_box.base.x;
+ let list_top = self.scroll_box.viewport_y;
+ let row_y = list_top + row_idx as f32 * self.item_height - self.scroll_box.scroll_y;
+ let box_x = list_left + 5.0;
+ let box_y = row_y + 2.0;
+ self.edit_box.set_rect(box_x, box_y, 170.0, 24.0);
+ }
+ }
+ if !self.edit_box.editing {
+ let row_idx = self.editing_key_idx.unwrap();
+ if row_idx < self.items.len() {
+ let (old_path, relative_name) = match &self.items[row_idx] {
+ TreeElement::Section { path, name, .. } => (path.clone(), name.clone()),
+ TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
+ };
+ let new_name = self.edit_box.text.trim().to_string();
+ if !new_name.is_empty() && new_name != relative_name {
+ let new_path = if let Some(pos) = old_path.rfind('.') {
+ format!("{}.{}", &old_path[..pos], new_name)
+ } else {
+ new_name
+ };
+ self.rename_request = Some((old_path, new_path));
+ }
+ }
+ self.editing_key_idx = None;
+ if let Some(h) = host { ui.set_focused_ptr(h); }
+ changed = true;
+ }
+ }
+
+ if (self.scroll_box.scroll_y - self.last_scroll_y).abs() > 0.01 {
+ self.last_scroll_y = self.scroll_box.scroll_y;
+ self.scrollbar_activity_timer = 1.0;
+ changed = true;
+ }
+ if self.scrollbar_activity_timer > 0.0 {
+ self.scrollbar_activity_timer = (self.scrollbar_activity_timer - dt).max(0.0);
+ changed = true;
+ }
+ changed
+
}
- fn clear_children(&mut self, _ctx: &mut UiContext) {
- self.children.clear();
+ fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+ let host = ectx.host_ptr();
+ let host_id = ectx.id;
+ match event {
+ Event::MouseButton { button, state, x, y, .. } => {
+ let (button, state, px, py) = (*button, *state, *x, *y);
+ let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
+ self.mouse_body(button, state, px, py, ui, host, host_id)
+ }
+ Event::PointerMove { x, y, .. } => {
+ let (px, py) = (*x, *y);
+ let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
+ self.move_body(px, py, ui)
+ }
+ Event::MouseWheel { delta, x, y, .. } => {
+ let (delta, px, py) = (delta.clone(), *x, *y);
+ let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
+ self.scroll_box.mouse_wheel(&delta, px, py, ui)
+ }
+ Event::KeyInput(ev) => {
+ let ev = ev.clone();
+ let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
+ self.key_body(&ev, ui)
+ }
+ Event::FocusOut => {
+ self.add_key_popover_open = false;
+ self.add_key_popover_box.unfocus();
+
+ false
+ }
+ _ => false,
+ }
}
+ // The tree context-menu actions, dispatched by the global context menu through the
+ // adapter's Element forwards.
fn copy_key(&self) {
if let Some(idx) = self.selected_key_idx {
if idx < self.flat_keys.len() {
@@ -1301,32 +1223,6 @@ impl Element for TreeList {
self.right_clicked_section = None;
}
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- if self.add_key_popover_open {
- Some(self.popover_rect_geom())
- } else {
- None
- }
- }
-
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- if !self.add_key_popover_open { return; }
-
- let (rx, ry, rw, rh) = self.popover_rect_geom();
-
- // 1. Soft layered drop shadows
- pc.rect([0.02, 0.02, 0.05, 0.15], rx + 1.0, ry + 1.0, rw, rh);
- pc.rect([0.02, 0.02, 0.05, 0.08], rx + 3.0, ry + 3.0, rw, rh);
- pc.rect([0.02, 0.02, 0.05, 0.04], rx + 5.0, ry + 5.0, rw, rh);
-
- let theme = crate::color::active_theme();
-
- // 2. High-contrast premium outer border
- pc.rect(theme.surface_border, rx, ry, rw, rh);
-
- // 3. Frosted glass background
- pc.rect(theme.surface_bg, rx + 1.0, ry + 1.0, rw - 2.0, rh - 2.0); // bg
- }
}
impl TreeList {
@@ -1757,32 +1653,4 @@ impl TreeList {
labels
}
- pub(crate) fn subtree_fonted_labels(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- let font = self.widget_font();
- let (x, y, w, _h) = self.rect();
-
- let search_margin_y = 6.0;
- let search_h = 26.0;
- let offset_y = search_h + 2.0 * search_margin_y;
- let header_h = 26.0;
-
- let list_bounds = Some([self.scroll_box.base.x, self.scroll_box.viewport_y, self.scroll_box.base.x + self.scroll_box.base.w, self.scroll_box.viewport_y + self.scroll_box.viewport_h]);
- let header_bounds = Some([x, y + offset_y, x + w, y + offset_y + header_h]);
-
- let mut labels = self.own_labels().into_iter().enumerate().map(|(idx, l)| {
- let b = if idx < 3 {
- header_bounds
- } else {
- list_bounds
- };
- (l, font.clone(), b)
- }).collect::<Vec<_>>();
-
- labels.extend(self.search_box.own_labels_with_font_and_bounds(ctx));
- labels.extend(self.add_key_btn.own_labels_with_font_and_bounds(ctx));
- if self.add_key_popover_open {
- labels.extend(self.add_key_popover_box.own_labels_with_font_and_bounds(ctx));
- }
- labels
- }
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 747ff0b..e9b91ef 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -300,6 +300,16 @@ pub trait Paint {
/// to shape.
fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem, _rect: Rect) {}
+ /// Whether [`paint`](Paint::paint) emits the widget's ENTIRE subtree, so the paint walk
+ /// must not also descend into its (ctx-linked) children — the legacy
+ /// `Element::renders_own_subtree` contract. TreeList: its field widgets stay ctx-linked
+ /// for event propagation, but their pixels come from `paint`'s own child pass (which
+ /// gates the add-key popover box on the popover actually being open).
+ fn paints_own_subtree(&self) -> bool {
+ false
+ }
+
+
/// Whether the adapter re-enables the legacy shared focus/hover highlight overlay
/// (`Element::highlight_quad`'s default) for this widget. The adapter suppresses it for
/// migrated widgets — matching the `None` overrides most legacy controls carried — but
@@ -405,6 +415,14 @@ impl EventCtx<'_> {
pub fn widget_addr(&self) -> usize {
self.self_ptr.map(|p| p as *const () as usize).unwrap_or(0)
}
+
+ /// The adapter's pointer, for legacy sites that must hand it onward — TreeList makes
+ /// itself the focus target (`set_focused_ptr`) and the context-menu target
+ /// (`show_context_menu`) with the pointer hosts registered. Transitional; dies with
+ /// `Element`. None outside a routed path.
+ pub(crate) fn host_ptr(&self) -> Option<*mut (dyn Element + 'static)> {
+ self.self_ptr
+ }
}
/// The input concern — hit-testing and event handling against the laid-out rect. Mirrors the
@@ -563,6 +581,14 @@ pub trait Input {
false
}
+ /// Context-carrying tick for legacy stateful containers whose per-frame work needs the
+ /// routing context — TreeList commits its inline rename editor, re-targets focus, and
+ /// drains its search box on tick. Runs right after [`tick`](Input::tick) with a routed
+ /// [`EventCtx`] (ui + the adapter's id/pointer). Transitional, like the capability hooks.
+ fn tick_ctx(&mut self, _dt: f32, _ectx: &mut EventCtx) -> bool {
+ false
+ }
+
/// Whether this widget wants `tick` calls from tick-gating hosts (legacy
/// `Element::wants_tick`; the designer ticks unconditionally and ignores this).
fn wants_tick(&self) -> bool {
@@ -1232,6 +1258,10 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
/// (`own_labels_with_font_and_bounds`, or the per-label hatch), so a display list built by
/// the paint walk carries per-widget fonts and clip rects (Phase 6 — text ordering
/// relative to geometry is immaterial: glyphs always render in the later text pass).
+ fn renders_own_subtree(&self) -> bool {
+ Paint::paints_own_subtree(&self.inner)
+ }
+
fn paint_self(&self, ui: &UiContext, ctx: &mut PaintCtx) {
let mut tmp = PaintCtx::new();
Paint::paint(&self.inner, self.content_rect(), &mut tmp);
@@ -1457,6 +1487,11 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
Layout::register_embedded_children(&mut self.inner, host_id, ctx);
let rect = self.content_rect();
let mut changed = Input::tick(&mut self.inner, dt, rect);
+ {
+ let self_ptr = self.as_ptr_mut();
+ let mut ectx = EventCtx { rect, id: host_id, ui: Some(&mut *ctx), self_ptr: Some(self_ptr) };
+ changed |= Input::tick_ctx(&mut self.inner, dt, &mut ectx);
+ }
if self.visible {
for child in self.visible_children() {
changed |= unsafe { &mut *child }.tick(dt, ctx);