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

commit90392b92aedb1e8b56da03c70edf80200ebc0e43
parente42a26c436
authorLucas Galante <[email protected]>
date2026-08-04 20:59
Wire copy button and treelist chevrons to the cce-icons set

Button::new_copy_icon now attaches the 'copy' icon face via
upload_icon (the ramp x-button pattern), keeping the clipboard-emoji
label as the fallback when the icon set is missing.

TreeList section rows draw chevron-right/chevron-down images in
place of the text triangles: own_labels leaves the icon slot open
and shifts the name when the icons resolve, and paint() draws them
clipped to the list viewport. Text triangles remain the fallback.

Verified live in cce-data-editor (both collapsed and expanded rows).

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/widget/container/treelist.rs | 57 +++++++++++++++++++++++++++++++++++++---
 src/widget/input/button.rs       |  8 +++++-
 2 files changed, 61 insertions(+), 4 deletions(-)

diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index d679622..ab5cf7f 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -952,6 +952,43 @@ impl Paint for TreeList {
             pc.text_with(l.text, l.x, l.y, l.font_size, l.color, font.clone(), b);
         }
 
+        // Section chevrons: image icons in the slot own_labels leaves open,
+        // clipped to the list viewport like the row text.
+        {
+            let (_, tree_font_size) = crate::layout::tree_font_parsed();
+            let list_left = self.scroll_box.base.x;
+            let list_top = self.scroll_box.viewport_y;
+            let list_bottom = list_top + self.scroll_box.viewport_h;
+            let viewport = Rect {
+                x: list_left,
+                y: list_top,
+                width: self.scroll_box.base.w,
+                height: self.scroll_box.viewport_h,
+            };
+            pc.clip(viewport, |pc| {
+                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;
+                    }
+                    if let TreeElement::Section { indent, collapsed, .. } = item {
+                        if self.editing_key_idx == Some(i) {
+                            continue;
+                        }
+                        if let Some((id, _, _)) = Self::chevron_icon(*collapsed) {
+                            let s = tree_font_size;
+                            pc.image(id, Rect {
+                                x: list_left + 8.0 + *indent as f32 * 12.0,
+                                y: row_y + 7.0,
+                                width: s,
+                                height: s,
+                            }, 1.0);
+                        }
+                    }
+                }
+            });
+        }
+
         // Field children, in the legacy children() order.
         let dummy = UiContext::new();
         self.search_box.paint_self(&dummy, pc);
@@ -1511,6 +1548,12 @@ impl TreeList {
     /// column ends (None = only the shared list/header bounds apply). Key and
     /// Type cells clip at their separators so text can't bleed into the next
     /// column; sections span the whole row.
+    /// The section-row chevron (cce-icons), cached per size by `upload_icon`;
+    /// `None` when the icon set is missing (rows fall back to text triangles).
+    fn chevron_icon(collapsed: bool) -> Option<(u32, u32, u32)> {
+        crate::upload_icon(if collapsed { "chevron-right" } else { "chevron-down" }, 32)
+    }
+
     pub(crate) fn own_labels(&self) -> Vec<(TextLabel, Option<f32>)> {
         let f32_to_rgb = |c: [f32; 4]| -> [u8; 3] {
             [
@@ -1563,10 +1606,18 @@ impl TreeList {
             match item {
                 TreeElement::Section { name, indent, collapsed, .. } => {
                     if self.editing_key_idx != Some(i) {
-                        let display_text = format!("{} {}", if *collapsed { "▶" } else { "▼" }, name);
+                        let sx = list_left + 8.0 + *indent as f32 * 12.0;
+                        // Chevron icons (cce-icons) replace the text triangles
+                        // when available — paint() draws the image in the slot
+                        // this leaves open. Text triangles are the fallback.
+                        let (text, tx) = if Self::chevron_icon(*collapsed).is_some() {
+                            (name.clone(), sx + tree_font_size + 6.0)
+                        } else {
+                            (format!("{} {}", if *collapsed { "▶" } else { "▼" }, name), sx)
+                        };
                         labels.push((TextLabel {
-                            text: display_text,
-                            x: list_left + 8.0 + *indent as f32 * 12.0,
+                            text,
+                            x: tx,
                             y: row_y + 6.0,
                             font_size: tree_font_size,
                             color: f32_to_rgb(crate::color::tree_section_text_color()),
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index d1338c2..19b6edf 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -93,7 +93,13 @@ impl Button {
     }
 
     pub fn new_copy_icon(x: f32, y: f32, w: f32, h: f32) -> Adapted<Button> {
-        Button::adapted(ButtonKind::CopyIcon, x, y, w, h)
+        let b = Button::adapted(ButtonKind::CopyIcon, x, y, w, h);
+        // Copy icon face (cce-icons); label fallback if the icon set is
+        // missing on this machine.
+        match crate::upload_icon("copy", 32) {
+            Some((id, iw, ih)) => b.with_icon(id, iw as f32, ih as f32),
+            None => b.with_label("📋"),
+        }
     }
 
     /// Whether an icon face is set (hosts size icon buttons square).