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

commit0d459c50289065c1379fa4feced063a09895fd07
parent524e84c923
authorLucas Galante <[email protected]>
date2026-07-05 21:35
Implement Font config centralization and LRU cache in cce-ui

 src/backend/mod.rs                   |   2 +-
 src/backend/window_runner.rs         | 134 ++++++++++++++++++++++++++++++++---
 src/layout.rs                        | 100 ++++++++++++++++++++++++++
 src/widget/input/dropdown.rs         | 118 +++++++++++++++++++++++++++---
 src/widget/input/keybinds_control.rs |  10 +--
 5 files changed, 340 insertions(+), 24 deletions(-)

diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index d15d26d..4652a0c 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -4,5 +4,5 @@ pub mod window_runner;
 pub use wgpu_adapter::WgpuAdapter;
 pub use window_runner::{
     EngineState, WindowSettings, LogicalPosition, LogicalSize, Application, run,
-    Vertex, LineCap, ActivePopup, PressedKey,
+    Vertex, LineCap, ActivePopup, PressedKey, get_text_buffer,
 };
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 3f5a5fb..999912c 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -64,8 +64,26 @@ struct BufferCacheKey {
     is_vertical: bool,
 }
 
+#[derive(Clone)]
+struct CachedBuffer {
+    buffer: Buffer,
+    last_accessed: std::time::Instant,
+}
+
 std::thread_local! {
-    static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, Buffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
+    static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, CachedBuffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
+}
+
+fn find_cased_family(fs: &FontSystem, name: &str) -> Option<String> {
+    let lower_name = name.to_lowercase();
+    for face in fs.db().faces() {
+        for (family, _) in &face.families {
+            if family.to_lowercase() == lower_name {
+                return Some(family.clone());
+            }
+        }
+    }
+    None
 }
 
 pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
@@ -92,7 +110,13 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
     };
 
     let cached = BUFFER_CACHE.with(|cache| {
-        cache.borrow().get(&key).cloned()
+        let mut cache = cache.borrow_mut();
+        if let Some(cached_item) = cache.get_mut(&key) {
+            cached_item.last_accessed = std::time::Instant::now();
+            Some(cached_item.buffer.clone())
+        } else {
+            None
+        }
     });
 
     if let Some(buf) = cached {
@@ -107,15 +131,97 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
     let metrics = Metrics::new(physical_size, line_height);
     let mut buf = Buffer::new(fs, metrics);
     let mut attrs = Attrs::new();
+
+    let (sans_fallback, serif_fallback, mono_fallback, _, _, _, _) = crate::layout::read_preferred_fonts();
+
+    let resolved_storage = family_name.as_deref().and_then(|font_name| match font_name {
+        "monospace" if !mono_fallback.is_empty() => find_cased_family(fs, &mono_fallback),
+        "sans-serif" if !sans_fallback.is_empty() => find_cased_family(fs, &sans_fallback),
+        "serif" if !serif_fallback.is_empty() => find_cased_family(fs, &serif_fallback),
+        _ => None,
+    });
+
+    let resolved_mono = if !mono_fallback.is_empty() {
+        find_cased_family(fs, &mono_fallback)
+    } else {
+        None
+    };
+
+    let resolved_sans = if !sans_fallback.is_empty() {
+        find_cased_family(fs, &sans_fallback)
+    } else {
+        None
+    };
+
     let family = if let Some(ref font_family) = family_name {
         match font_family.as_str() {
-            "monospace" => glyphon::Family::Name(crate::layout::get_system_monospace_font()),
-            "sans-serif" => glyphon::Family::Name(crate::layout::get_system_monospace_font()),
-            "serif" => glyphon::Family::Serif,
+            "monospace" => {
+                if !mono_fallback.is_empty() {
+                    if let Some(ref cased) = resolved_storage {
+                        glyphon::Family::Name(cased)
+                    } else {
+                        glyphon::Family::Name(&mono_fallback)
+                    }
+                } else {
+                    glyphon::Family::Name(crate::layout::get_system_monospace_font())
+                }
+            }
+            "sans-serif" => {
+                if !sans_fallback.is_empty() {
+                    if let Some(ref cased) = resolved_storage {
+                        glyphon::Family::Name(cased)
+                    } else {
+                        if !mono_fallback.is_empty() {
+                            if let Some(ref cased_mono) = resolved_mono {
+                                glyphon::Family::Name(cased_mono)
+                            } else {
+                                glyphon::Family::Name(&mono_fallback)
+                            }
+                        } else {
+                            glyphon::Family::Name(crate::layout::get_system_monospace_font())
+                        }
+                    }
+                } else {
+                    glyphon::Family::SansSerif
+                }
+            }
+            "serif" => {
+                if !serif_fallback.is_empty() {
+                    if let Some(ref cased) = resolved_storage {
+                        glyphon::Family::Name(cased)
+                    } else {
+                        if !mono_fallback.is_empty() {
+                            if let Some(ref cased_mono) = resolved_mono {
+                                glyphon::Family::Name(cased_mono)
+                            } else {
+                                glyphon::Family::Name(&mono_fallback)
+                            }
+                        } else {
+                            glyphon::Family::Name(crate::layout::get_system_monospace_font())
+                        }
+                    }
+                } else {
+                    glyphon::Family::Serif
+                }
+            }
             name => glyphon::Family::Name(name),
         }
     } else {
-        glyphon::Family::Name(crate::layout::get_system_monospace_font())
+        if !sans_fallback.is_empty() {
+            if let Some(ref cased) = resolved_sans {
+                glyphon::Family::Name(cased)
+            } else if !mono_fallback.is_empty() {
+                if let Some(ref cased_mono) = resolved_mono {
+                    glyphon::Family::Name(cased_mono)
+                } else {
+                    glyphon::Family::Name(&mono_fallback)
+                }
+            } else {
+                glyphon::Family::Name(crate::layout::get_system_monospace_font())
+            }
+        } else {
+            glyphon::Family::Name(crate::layout::get_system_monospace_font())
+        }
     };
     attrs = attrs.family(family);
     buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
@@ -123,10 +229,20 @@ pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<
 
     BUFFER_CACHE.with(|cache| {
         let mut cache = cache.borrow_mut();
-        if cache.len() > 2000 {
-            cache.clear();
+        if cache.len() >= 2000 {
+            let mut items: Vec<(BufferCacheKey, std::time::Instant)> = cache
+                .iter()
+                .map(|(k, v)| (k.clone(), v.last_accessed))
+                .collect();
+            items.sort_by_key(|&(_, time)| time);
+            for (k, _) in items.iter().take(100) {
+                cache.remove(k);
+            }
         }
-        cache.insert(key, buf.clone());
+        cache.insert(key, CachedBuffer {
+            buffer: buf.clone(),
+            last_accessed: std::time::Instant::now(),
+        });
     });
 
     buf
diff --git a/src/layout.rs b/src/layout.rs
index b5b7e8e..0b6b7d8 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -3398,6 +3398,64 @@ pub fn render_popovers(pc: &mut dyn RenderTarget, ctx: &UiContext) {
     }
 }
 
+pub fn partition_concentric_corners(
+    x: f32, y: f32, w: f32, h: f32,
+    _r_std: f32,
+    r_adjust: [f32; 4],
+    color: [f32; 4],
+) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+    let mut quads = Vec::new();
+
+    let r0 = r_adjust[0];
+    let r1 = r_adjust[1];
+    let r2 = r_adjust[2];
+    let r3 = r_adjust[3];
+
+    let max_left = r0.max(r3);
+    let max_right = r1.max(r2);
+
+    let mut push_valid_quad = |qx: f32, qy: f32, qw: f32, qh: f32, qr: f32, qcorners: (bool, bool, bool, bool)| {
+        if qw > 0.001 && qh > 0.001 {
+            quads.push((qx, qy, qw, qh, qr, color, qcorners));
+        }
+    };
+
+    // 1. Center vertical block
+    push_valid_quad(x + max_left, y, w - max_left - max_right, h, 0.0, (false, false, false, false));
+
+    // 2. Left block
+    push_valid_quad(x, y + r0, max_left, h - r0 - r3, 0.0, (false, false, false, false));
+
+    // 3. Top-left transition
+    push_valid_quad(x + r0, y, max_left - r0, r0, 0.0, (false, false, false, false));
+
+    // 4. Bottom-left transition
+    push_valid_quad(x + r3, y + h - r3, max_left - r3, r3, 0.0, (false, false, false, false));
+
+    // 5. Right block
+    push_valid_quad(x + w - max_right, y + r1, max_right, h - r1 - r2, 0.0, (false, false, false, false));
+
+    // 6. Top-right transition
+    push_valid_quad(x + w - max_right, y, max_right - r1, r1, 0.0, (false, false, false, false));
+
+    // 7. Bottom-right transition
+    push_valid_quad(x + w - max_right, y + h - r2, max_right - r2, r2, 0.0, (false, false, false, false));
+
+    // 8. Corner 0 (top-left)
+    push_valid_quad(x, y, r0, r0, r0, (true, false, false, false));
+
+    // 9. Corner 1 (top-right)
+    push_valid_quad(x + w - r1, y, r1, r1, r1, (false, true, false, false));
+
+    // 10. Corner 2 (bottom-right)
+    push_valid_quad(x + w - r2, y + h - r2, r2, r2, r2, (false, false, true, false));
+
+    // 11. Corner 3 (bottom-left)
+    push_valid_quad(x, y + h - r3, r3, r3, r3, (false, false, false, true));
+
+    quads
+}
+
 pub struct UiFrame;
 
 impl UiFrame {
@@ -5106,6 +5164,48 @@ pub fn get_system_sans_serif_font() -> &'static str {
     })
 }
 
+pub fn parse_font_for_alias(content: &str, alias: &str) -> Option<String> {
+    let lines: Vec<&str> = content.lines().collect();
+    for i in 0..lines.len() {
+        let line = lines[i].trim();
+        if line.contains("<test") && line.contains("name=\"family\"") && line.contains(&format!("<string>{}</string>", alias)) {
+            for j in (i + 1)..(i + 6).min(lines.len()) {
+                let next_line = lines[j].trim();
+                if next_line.contains("<edit") {
+                    for k in (j + 1)..(j + 6).min(lines.len()) {
+                        let str_line = lines[k].trim();
+                        if str_line.contains("<string>") && str_line.contains("</string>") {
+                            if let Some(start) = str_line.find("<string>") {
+                                if let Some(end) = str_line.find("</string>") {
+                                    let font = &str_line[start + 8..end];
+                                    return Some(font.to_string());
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    None
+}
+
+pub fn read_preferred_fonts() -> (String, String, String, String, String, String, String) {
+    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/lsgalante".to_string());
+    let path = std::path::Path::new(&home).join(".config/fontconfig/fonts.conf");
+    let content = std::fs::read_to_string(&path).unwrap_or_default();
+    
+    let sans = parse_font_for_alias(&content, "sans-serif").unwrap_or_else(|| "Noto Sans".to_string());
+    let serif = parse_font_for_alias(&content, "serif").unwrap_or_else(|| "Noto Serif".to_string());
+    let mono = parse_font_for_alias(&content, "monospace").unwrap_or_else(|| "Noto Sans Mono".to_string());
+    let borders = parse_font_for_alias(&content, "window-borders").unwrap_or_else(|| "Noto Sans".to_string());
+    let status = parse_font_for_alias(&content, "status-interface").unwrap_or_else(|| "Noto Sans".to_string());
+    let fuzzel_font = parse_font_for_alias(&content, "fuzzel").unwrap_or_else(|| "Noto Sans".to_string());
+    let term = parse_font_for_alias(&content, "terminal").unwrap_or_else(|| "Noto Sans Mono".to_string());
+    
+    (sans, serif, mono, borders, status, fuzzel_font, term)
+}
+
 impl crate::widget::ContainerLayout for FlexLayout {
     fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
         Box::new(self.clone())
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 5593eb7..ef520fb 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -14,6 +14,7 @@ pub struct Dropdown {
     pub font_family: String,
     pub custom_display_text: Option<String>,
     pub open_upward: Option<bool>,
+    pub auto_width: bool,
 }
 
 impl Dropdown {
@@ -30,6 +31,7 @@ impl Dropdown {
             font_family: "sans-serif".to_string(),
             custom_display_text: None,
             open_upward: None,
+            auto_width: false,
         }
     }
 
@@ -69,18 +71,27 @@ impl Dropdown {
         changed
     }
 
-    pub fn popover_width(&self) -> f32 {
-        let mut w = self.base.w;
+    pub fn with_auto_width(mut self, auto_width: bool) -> Self {
+        self.auto_width = auto_width;
+        self
+    }
+
+    pub fn content_width(&self) -> f32 {
         let font_setting = crate::layout::control_label_font_detached();
         let (font_family, font_size_opt) = crate::layout::parse_font_string(&font_setting);
         let font_size = font_size_opt.unwrap_or(12.0);
+        let mut max_w = 0.0f32;
         for opt in &self.options {
             let opt_w = crate::widget::display::measure_text_width(opt, &font_family, font_size) + 32.0;
-            if opt_w > w {
-                w = opt_w;
+            if opt_w > max_w {
+                max_w = opt_w;
             }
         }
-        w
+        max_w
+    }
+
+    pub fn popover_width(&self) -> f32 {
+        self.base.w.max(self.content_width())
     }
 
     pub fn get_popover_geom(&self) -> (f32, f32, f32, f32) {
@@ -268,6 +279,21 @@ impl Element for Dropdown {
         [0.0, 0.0, 0.0, 0.0]
     }
 
+    fn measure(&self, constraints: LayoutConstraints, _ctx: &UiContext) -> Size {
+        let (_, _, w, h) = self.rect();
+        let pref_w = if self.auto_width {
+            self.content_width()
+        } else {
+            w
+        };
+        let pref_h = self.preferred_height().unwrap_or(h);
+        
+        let width = pref_w.clamp(constraints.min_width, constraints.max_width);
+        let height = pref_h.clamp(constraints.min_height, constraints.max_height);
+        
+        Size { width, height }
+    }
+
     fn preferred_height(&self) -> Option<f32> {
         Some(crate::layout::dropdown_height())
     }
@@ -308,11 +334,70 @@ impl Element for Dropdown {
             let x = self.base.x + label_x;
             let w = self.base.w - label_x;
 
-            // Draw border
-            quads.push((x, self.base.y + top, w, visual_h, radius, border_color, (r1, r2, r3, r4)));
-            // Draw background (slightly inset to show border)
+            // Draw border and background (with potential parent-concentric corner adjustment)
             let inner_radius = (radius - 1.0).max(0.0);
-            quads.push((x + 1.0, self.base.y + top + 1.0, w - 2.0, visual_h - 2.0, inner_radius, bg_color, (r1, r2, r3, r4)));
+            let mut adjusted = false;
+            let mut outer_radii = [radius; 4];
+            let mut inner_radii = [inner_radius; 4];
+
+            let mut curr = self.parent(ctx);
+            let mut backplate_ptr = None;
+            while let Some(ptr) = curr {
+                if unsafe { (*ptr).is_backplate() } {
+                    backplate_ptr = Some(ptr);
+                    break;
+                }
+                curr = unsafe { (*ptr).parent(ctx) };
+            }
+
+            if let Some(bp) = backplate_ptr {
+                let (px, py, pw, ph) = unsafe { (*bp).rect() };
+                let pr = unsafe { (*bp).corner_radius() };
+                let (pr1, pr2, pr3, pr4) = unsafe { (*bp).rounded_corners() };
+
+                let g_left = x - px;
+                let g_top = (self.base.y + top) - py;
+                let g_right = (px + pw) - (x + w);
+                let g_bottom = (py + ph) - ((self.base.y + top) + visual_h);
+
+                if pr1 && (g_left - g_top).abs() < 1.0 && g_left >= 0.0 {
+                    outer_radii[0] = (pr - g_left).max(0.0);
+                    inner_radii[0] = (outer_radii[0] - 1.0).max(0.0);
+                    adjusted = true;
+                }
+                if pr2 && (g_right - g_top).abs() < 1.0 && g_right >= 0.0 {
+                    outer_radii[1] = (pr - g_right).max(0.0);
+                    inner_radii[1] = (outer_radii[1] - 1.0).max(0.0);
+                    adjusted = true;
+                }
+                if pr3 && (g_right - g_bottom).abs() < 1.0 && g_right >= 0.0 {
+                    outer_radii[2] = (pr - g_right).max(0.0);
+                    inner_radii[2] = (outer_radii[2] - 1.0).max(0.0);
+                    adjusted = true;
+                }
+                if pr4 && (g_left - g_bottom).abs() < 1.0 && g_left >= 0.0 {
+                    outer_radii[3] = (pr - g_left).max(0.0);
+                    inner_radii[3] = (outer_radii[3] - 1.0).max(0.0);
+                    adjusted = true;
+                }
+            }
+
+            if adjusted {
+                let border_quads = crate::layout::partition_concentric_corners(
+                    x, self.base.y + top, w, visual_h,
+                    radius, outer_radii, border_color
+                );
+                quads.extend(border_quads);
+
+                let bg_quads = crate::layout::partition_concentric_corners(
+                    x + 1.0, self.base.y + top + 1.0, w - 2.0, visual_h - 2.0,
+                    inner_radius, inner_radii, bg_color
+                );
+                quads.extend(bg_quads);
+            } else {
+                quads.push((x, self.base.y + top, w, visual_h, radius, border_color, (r1, r2, r3, r4)));
+                quads.push((x + 1.0, self.base.y + top + 1.0, w - 2.0, visual_h - 2.0, inner_radius, bg_color, (r1, r2, r3, r4)));
+            }
         }
         for &child_ptr in &self.children(ctx) {
             let widget = unsafe { &*child_ptr };
@@ -867,5 +952,20 @@ mod tests {
         assert_eq!(first_char.color, expected_color);
         assert_ne!(last_char.color, expected_color); // color has shifted towards background
     }
+
+    #[test]
+    fn test_dropdown_auto_width() {
+        let dummy = crate::context::UiContext::new();
+        let options = vec!["Short".to_string(), "A much longer option name".to_string()];
+        let mut dd = Dropdown::new(options, 0).with_auto_width(true);
+        dd.set_rect(10.0, 10.0, 50.0, 24.0);
+
+        let size = dd.measure(LayoutConstraints::new(0.0, 500.0, 24.0, 24.0), &dummy);
+        assert!(size.width > 50.0, "Measured auto-width {} should be greater than original width 50.0", size.width);
+        
+        let dd_no_auto = Dropdown::new(vec!["Short".to_string(), "A much longer option name".to_string()], 0);
+        let size_no_auto = dd_no_auto.measure(LayoutConstraints::new(0.0, 500.0, 24.0, 24.0), &dummy);
+        assert_eq!(size_no_auto.width, 0.0);
+    }
 }
 
diff --git a/src/widget/input/keybinds_control.rs b/src/widget/input/keybinds_control.rs
index 159ed09..067a8c0 100644
--- a/src/widget/input/keybinds_control.rs
+++ b/src/widget/input/keybinds_control.rs
@@ -82,8 +82,8 @@ impl KeybindsControl {
                     action.clone()
                 };
 
-                if cmd_val.starts_with("cce control ") {
-                    let sub = &cmd_val["cce control ".len()..];
+                if cmd_val.starts_with("ccectl ") {
+                    let sub = &cmd_val["ccectl ".len()..];
                     if sub.starts_with("view ") {
                         let num = &sub["view ".len()..];
                         cmd_val = format!("view-{}", num);
@@ -133,15 +133,15 @@ impl KeybindsControl {
             
             if built_in_actions.contains(&cmd_val.as_str()) {
                 obj.insert("action".to_string(), serde_json::Value::String("spawn".to_string()));
-                obj.insert("command".to_string(), serde_json::Value::String(format!("cce control {}", cmd_val)));
+                obj.insert("command".to_string(), serde_json::Value::String(format!("ccectl {}", cmd_val)));
             } else if cmd_val.starts_with("view-") {
                 let num = &cmd_val["view-".len()..];
                 obj.insert("action".to_string(), serde_json::Value::String("spawn".to_string()));
-                obj.insert("command".to_string(), serde_json::Value::String(format!("cce control view {}", num)));
+                obj.insert("command".to_string(), serde_json::Value::String(format!("ccectl view {}", num)));
             } else if cmd_val.starts_with("set-tag-") {
                 let num = &cmd_val["set-tag-".len()..];
                 obj.insert("action".to_string(), serde_json::Value::String("spawn".to_string()));
-                obj.insert("command".to_string(), serde_json::Value::String(format!("cce control set-tag {}", num)));
+                obj.insert("command".to_string(), serde_json::Value::String(format!("ccectl set-tag {}", num)));
             } else {
                 obj.insert("action".to_string(), serde_json::Value::String("spawn".to_string()));
                 obj.insert("command".to_string(), serde_json::Value::String(cmd_val));