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