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

commitc1d56a90808b806145cf9018a73a7d7a600a1349
parent60600f65c2
authorLucas Galante <[email protected]>
date2026-07-11 19:18
refactor(font-selector): FontSelector onto the narrow traits (Phase 6as leaf sweep)

Field + fade-truncated family label + picker glyph paint from the laid-out
rect (per-label clip bounds preserved); click spawns cce-fonts --select on
the release path (release gated on rect containment, not the adapter's
press gate); the child reaps in Input::tick under a permanent wants_tick
(replaces the dynamic tick-receiver registration). Detached label rides
the adapter. Constructor returns Adapted<FontSelector>. DE loaded A/B
AE=0; layout-interface default A/B cursor-only.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk

 src/widget/input/font_selector.rs | 362 ++++++++++++++++++--------------------
 1 file changed, 168 insertions(+), 194 deletions(-)

diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index b7a4d7d..28d68ee 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -1,39 +1,31 @@
+use crate::scene::layout::{Rect, Size};
+use crate::scene::paint::PaintCtx;
+use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
 use crate::widget::*;
 use std::sync::{Arc, Mutex};
 
+/// Font-family picker field (narrow-trait model, Phase 6as leaf sweep): click spawns
+/// `cce-fonts --select` and the tick reaps the child, committing its stdout as the new
+/// family. The detached control label rides the adapter; the model paints the field,
+/// the (possibly fade-truncated) family name, and the picker glyph.
 #[derive(Debug, Clone)]
 pub struct FontSelector {
-    base: Widget,
     pub font_family: String,
     just_changed: bool,
-    pub parent: Option<*mut (dyn Element + 'static)>,
-    pub children: Vec<*mut (dyn Element + 'static)>,
     pressed: bool,
+    hovered: bool,
     child: Arc<Mutex<Option<std::process::Child>>>,
 }
 
 impl FontSelector {
-    pub fn new(font_family: String) -> Self {
-        Self {
-            base: Widget::new(),
+    pub fn new(font_family: String) -> Adapted<FontSelector> {
+        Adapted::new(FontSelector {
             font_family,
             just_changed: false,
-            parent: None,
-            children: Vec::new(),
             pressed: false,
+            hovered: false,
             child: Arc::new(Mutex::new(None)),
-        }
-    }
-
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
-    }
-
-    pub fn with_config(mut self, file: &str, key: &str) -> Self {
-        self.base.config_file = Some(file.to_string());
-        self.base.config_key = Some(key.to_string());
-        self
+        })
     }
 
     pub fn take_change(&mut self) -> bool {
@@ -41,165 +33,25 @@ impl FontSelector {
         self.just_changed = false;
         changed
     }
-}
-
-impl Element for FontSelector {
-    crate::impl_widget_base!(FontSelector);
-
-    // Leaf legacy widget: own fonted labels via paint_self (the default no longer
-    // drains the text getters).
-    fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
-        crate::scene::painter::paint_legacy_leaf(self, ui, ctx, self.own_fonted_labels());
-    }
-
-    fn preferred_height(&self) -> Option<f32> {
-        Some(crate::layout::font_selector_height())
-    }
-
-    fn widget_font(&self) -> Option<String> {
-        Some(crate::layout::font_selector_font())
-    }
-
-    fn color(&self) -> [f32; 4] {
-        [0.08, 0.08, 0.12, 1.0]
-    }
-
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if button != MouseButton::Left { return false; }
-        match state {
-            ElementState::Pressed => {
-                if self.hit_test(px, py, ctx) {
-                    self.pressed = true;
-                    return true;
-                }
-            }
-            ElementState::Released => {
-                if self.pressed && self.hit_test(px, py, ctx) {
-                    self.pressed = false;
-                    let mut child_guard = self.child.lock().unwrap();
-                    if child_guard.is_none() {
-                        let home = std::env::var("HOME").unwrap_or_default();
-                        let local_fonts = std::path::Path::new(&home).join(".local/bin/cce-fonts");
-                        let cmd_path = if local_fonts.exists() {
-                            local_fonts.to_string_lossy().into_owned()
-                        } else {
-                            "cce-fonts".to_string()
-                        };
-                        if let Ok(child) = std::process::Command::new(&cmd_path)
-                            .arg("--select")
-                            .arg(&self.font_family)
-                            .stdout(std::process::Stdio::piped())
-                            .spawn()
-                        {
-                            *child_guard = Some(child);
-                            ctx.register_tick_receiver(self.base.id());
-                        }
-                    }
-                    return true;
-                }
-                let was = self.pressed;
-                self.pressed = false;
-                return was;
-            }
-        }
-        false
-    }
-
-    fn tick(&mut self, _dt: f32, ctx: &mut UiContext) -> bool {
-        let mut child_guard = self.child.lock().unwrap();
-        if let Some(ref mut child) = *child_guard {
-            match child.try_wait() {
-                Ok(Some(status)) => {
-                    let child_val = child_guard.take().unwrap();
-                    ctx.unregister_tick_receiver(self.base.id());
-                    if status.success() {
-                        if let Ok(output) = child_val.wait_with_output() {
-                            let stdout = String::from_utf8_lossy(&output.stdout);
-                            let trimmed = stdout.trim().to_string();
-                            if !trimmed.is_empty() && trimmed != self.font_family {
-                                self.font_family = trimmed;
-                                self.just_changed = true;
-                                return true;
-                            }
-                        }
-                    }
-                }
-                Ok(None) => {}
-                Err(_) => {
-                    *child_guard = None;
-                    ctx.unregister_tick_receiver(self.base.id());
-                }
-            }
-        }
-        false
-    }
-
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        let top = self.base.label_offset();
-        let visual_h = self.base.h - top;
-        let bg_color = [0.08, 0.08, 0.12, 1.0];
-        let border_color = if self.pressed {
-            [0.30, 0.50, 0.32, 1.0]
-        } else if self.base.hovered {
-            [0.25, 0.25, 0.35, 1.0]
-        } else {
-            [0.18, 0.18, 0.24, 1.0]
-        };
-
-        quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, border_color));
-        quads.push((self.base.x + 1.0, self.base.y + top + 1.0, self.base.w - 2.0, visual_h - 2.0, bg_color));
-        quads
-    }
-
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        let r = crate::layout::font_selector_corner_radius();
-        if r > 0.0 {
-            (true, true, true, true)
-        } else {
-            (false, false, false, false)
-        }
-    }
-
-    fn corner_radius(&self) -> f32 {
-        crate::layout::font_selector_corner_radius()
-    }
-}
-
-impl Drop for FontSelector {
-    fn drop(&mut self) {
-        clear_widget_references(self);
-    }
-}
 
-unsafe impl Send for FontSelector {}
-unsafe impl Sync for FontSelector {}
-
-impl Control for FontSelector {}
-
-impl FontSelector {
-    pub(crate) fn own_labels(&self) -> Vec<TextLabel> {
+    /// The field's own labels at the laid-out rect: the family name (fade-truncated to
+    /// fit before the picker glyph when too wide) and the glyph itself.
+    fn field_labels(&self, rect: Rect) -> Vec<TextLabel> {
         let mut labels = Vec::new();
-        let top = self.base.label_offset();
-        let _visual_h = self.base.h - top;
-        if let Some(lbl) = self.control_label() {
-            labels.push(lbl);
-        }
-
-        let max_w = self.base.w - 33.0;
+        let max_w = rect.width - 33.0;
         let full_w = TextLabel::estimate_width(&self.font_family, 12.0);
-        let text_y = crate::layout::align_text_y(self.base.y, self.base.h, 12.0, top);
+        let text_y = crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0);
 
         if full_w <= max_w {
             labels.push(TextLabel {
                 text: self.font_family.clone(),
-                x: self.base.x + 8.0,
+                x: rect.x + 8.0,
                 y: text_y,
                 font_size: 12.0,
                 color: [0xdd, 0xdd, 0xe2],
             });
         } else {
-            // Find prefix that fits in max_w - 30.0
+            // Find the prefix that fits in max_w - 30.0
             let target_prefix_w = max_w - 30.0;
             let mut prefix = String::new();
             for c in self.font_family.chars() {
@@ -210,18 +62,17 @@ impl FontSelector {
                 }
                 prefix.push(c);
             }
-            
-            // Draw prefix
+
             let prefix_w = TextLabel::estimate_width(&prefix, 12.0);
             labels.push(TextLabel {
                 text: prefix.clone(),
-                x: self.base.x + 8.0,
+                x: rect.x + 8.0,
                 y: text_y,
                 font_size: 12.0,
                 color: [0xdd, 0xdd, 0xe2],
             });
 
-            // Gather the next 5 fading characters
+            // The next 5 characters fade out
             let remaining: Vec<char> = self.font_family.chars().skip(prefix.chars().count()).collect();
             let fade_colors = [
                 [187, 187, 193],
@@ -230,11 +81,10 @@ impl FontSelector {
                 [87, 87, 95],
                 [53, 53, 62],
             ];
-            let mut cur_x = self.base.x + 8.0 + prefix_w;
+            let mut cur_x = rect.x + 8.0 + prefix_w;
             for i in 0..5 {
                 if i < remaining.len() {
-                    let c = remaining[i];
-                    let c_str = c.to_string();
+                    let c_str = remaining[i].to_string();
                     let c_w = TextLabel::estimate_width(&c_str, 12.0);
                     labels.push(TextLabel {
                         text: c_str,
@@ -250,35 +100,159 @@ impl FontSelector {
 
         labels.push(TextLabel {
             text: "🔤".to_string(),
-            x: self.base.x + self.base.w - 20.0,
-            y: crate::layout::align_text_y(self.base.y, self.base.h, 11.0, top),
+            x: rect.x + rect.width - 20.0,
+            y: crate::layout::align_text_y(rect.y, rect.height, 11.0, 0.0),
             font_size: 11.0,
             color: [0x83, 0x83, 0x8a],
         });
 
         labels
     }
+}
+
+impl Layout for FontSelector {
+    fn intrinsic_size(&self) -> Option<Size> {
+        Some(Size::new(0.0, crate::layout::font_selector_height()))
+    }
+}
+
+impl Paint for FontSelector {
+    fn color(&self) -> [f32; 4] {
+        [0.08, 0.08, 0.12, 1.0]
+    }
+
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::font_selector_font())
+    }
 
-    pub(crate) fn own_fonted_labels(&self) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+    fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+        let r = crate::layout::font_selector_corner_radius();
+        if r > 0.0 {
+            Some((r, (true, true, true, true)))
+        } else {
+            None
+        }
+    }
+
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        // Rounded base plate (the legacy leaf default from color + corner style)
+        let r = crate::layout::font_selector_corner_radius();
+        if r > 0.0 {
+            ctx.rounded_rect(rect, r, (true, true, true, true), self.color());
+        }
+
+        // Border + inner background (the legacy extra_quads pair)
+        let bg_color = [0.08, 0.08, 0.12, 1.0];
+        let border_color = if self.pressed {
+            [0.30, 0.50, 0.32, 1.0]
+        } else if self.hovered {
+            [0.25, 0.25, 0.35, 1.0]
+        } else {
+            [0.18, 0.18, 0.24, 1.0]
+        };
+        ctx.quad(rect, border_color);
+        ctx.quad(
+            Rect { x: rect.x + 1.0, y: rect.y + 1.0, width: rect.width - 2.0, height: rect.height - 2.0 },
+            bg_color,
+        );
+
+        // Labels: family text clipped short of the picker glyph (the legacy per-label
+        // bounds), glyph unclipped.
         let font = self.widget_font();
-        let top = self.base.label_offset();
-        let clip_right = self.base.x + self.base.w - 24.0;
-        let bounds = Some([self.base.x, self.base.y + top, clip_right, self.base.y + self.base.h]);
-        
-        let labels = self.own_labels();
+        let clip_right = rect.x + rect.width - 24.0;
+        let bounds = Some([rect.x, rect.y, clip_right, rect.y + rect.height]);
+        let labels = self.field_labels(rect);
         let count = labels.len();
-        labels.into_iter().enumerate().map(|(idx, l)| {
-            let has_control = self.control_label().is_some();
-            let is_font_label = if has_control {
-                idx > 0 && idx < count - 1
-            } else {
-                idx < count - 1
-            };
-            if is_font_label {
-                (l, font.clone(), bounds)
-            } else {
-                (l, font.clone(), None)
+        for (idx, l) in labels.into_iter().enumerate() {
+            let b = if idx < count - 1 { bounds } else { None };
+            ctx.text_with(l.text, l.x, l.y, l.font_size, l.color, font.clone(), b);
+        }
+    }
+}
+
+impl Input for FontSelector {
+    fn wants_tick(&self) -> bool {
+        true
+    }
+
+    fn tick(&mut self, _dt: f32, _rect: Rect) -> bool {
+        let mut child_guard = self.child.lock().unwrap();
+        if let Some(ref mut child) = *child_guard {
+            match child.try_wait() {
+                Ok(Some(status)) => {
+                    let child_val = child_guard.take().unwrap();
+                    if status.success() {
+                        if let Ok(output) = child_val.wait_with_output() {
+                            let stdout = String::from_utf8_lossy(&output.stdout);
+                            let trimmed = stdout.trim().to_string();
+                            if !trimmed.is_empty() && trimmed != self.font_family {
+                                self.font_family = trimmed;
+                                self.just_changed = true;
+                                return true;
+                            }
+                        }
+                    }
+                }
+                Ok(None) => {}
+                Err(_) => {
+                    *child_guard = None;
+                }
             }
-        }).collect()
+        }
+        false
+    }
+
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        match event {
+            Event::MouseButton { button, state, x, y, .. } => {
+                if *button != MouseButton::Left {
+                    return false;
+                }
+                match state {
+                    ElementState::Pressed => {
+                        self.pressed = true;
+                        true
+                    }
+                    ElementState::Released => {
+                        let r = ectx.rect;
+                        let inside = *x >= r.x && *x <= r.x + r.width && *y >= r.y && *y <= r.y + r.height;
+                        if self.pressed && inside {
+                            self.pressed = false;
+                            let mut child_guard = self.child.lock().unwrap();
+                            if child_guard.is_none() {
+                                let home = std::env::var("HOME").unwrap_or_default();
+                                let local_fonts = std::path::Path::new(&home).join(".local/bin/cce-fonts");
+                                let cmd_path = if local_fonts.exists() {
+                                    local_fonts.to_string_lossy().into_owned()
+                                } else {
+                                    "cce-fonts".to_string()
+                                };
+                                if let Ok(child) = std::process::Command::new(&cmd_path)
+                                    .arg("--select")
+                                    .arg(&self.font_family)
+                                    .stdout(std::process::Stdio::piped())
+                                    .spawn()
+                                {
+                                    *child_guard = Some(child);
+                                }
+                            }
+                            return true;
+                        }
+                        let was = self.pressed;
+                        self.pressed = false;
+                        was
+                    }
+                }
+            }
+            Event::MouseEnter => {
+                self.hovered = true;
+                false
+            }
+            Event::MouseLeave => {
+                self.hovered = false;
+                false
+            }
+            _ => false,
+        }
     }
 }