GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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,
+ }
}
}