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

commitcf494502d6f84ac4f2d206aca9cc5965fe697779
parentf0fe0c3841
authorLucas Galante <[email protected]>
date2026-07-04 19:13
Implement dropdown upward opening, dynamic sizing, and section container fixes

 src/color.rs                              |  4 +-
 src/layout.rs                             | 91 ++++++++++++++++++++++++++++++-
 src/widget/container/section_container.rs | 20 +++++--
 src/widget/display/panel.rs               |  5 ++
 src/widget/input/button.rs                | 11 +++-
 src/widget/input/dropdown.rs              | 28 ++++++----
 src/widget/input/spinbox.rs               |  7 +++
 7 files changed, 146 insertions(+), 20 deletions(-)

diff --git a/src/color.rs b/src/color.rs
index a53375b..8c257f7 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -278,7 +278,7 @@ fn parse_and_set_colors(content: &str) {
     if let Some(c) = get_color("/layout/color_borders_color") {
         if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() { *lock = c; }
     }
-    if let Some(c) = get_color("/layout/slider_track_color") {
+    if let Some(c) = get_color("/style/control/slider/color").or_else(|| get_color("/layout/slider_track_color")) {
         if let Ok(mut lock) = SLIDER_TRACK_COLOR.write() { *lock = c; }
     }
     if let Some(c) = get_color("/layout/paginator_sidebar_color") {
@@ -308,7 +308,7 @@ fn parse_and_set_colors(content: &str) {
     if let Some(c) = get_color("/layout/menubar_tab_label_color").or_else(|| get_color("/layout/paginator_tab_label_color")) {
         if let Ok(mut lock) = MENUBAR_TAB_LABEL_COLOR.write() { *lock = c; }
     }
-    if let Some(c) = get_color("/layout/toggle_enabled_color") {
+    if let Some(c) = get_color("/style/control/toggle/enabled_color").or_else(|| get_color("/layout/toggle_enabled_color")) {
         if let Ok(mut lock) = TOGGLE_ON_COLOR.write() { *lock = c; }
     }
     if let Some(c) = get_color("/style/control/toggle/disabled_color") {
diff --git a/src/layout.rs b/src/layout.rs
index 04b7f21..530bf14 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -135,9 +135,11 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.surface.backplate.menubar.color" => "backplate_menubar_color",
                 "style.surface.backplate.menubar.text_color" => "backplate_menubar_text_color",
                 "style.surface.backplate.menubar.blur" => "backplate_menubar_blur",
+                "style.surface.backplate.menubar.font" => "menubar_font",
                 "style.surface.backplate.statusbar.color" => "backplate_statusbar_color",
                 "style.surface.backplate.statusbar.text_color" => "backplate_statusbar_text_color",
                 "style.surface.backplate.statusbar.blur" => "backplate_statusbar_blur",
+                "style.surface.backplate.statusbar.font" => "statusbar_font",
                 "style.surface.page.opacity" => "page_opacity",
                 "style.surface.page.margin" => "page_margin",
                 "style.surface.graph.cell_color" => "graph_cell_color",
@@ -232,6 +234,8 @@ static COLOR_SELECTOR_PREVIEW_MARGIN: RwLock<f32> = RwLock::new(0.0);
 static COLOR_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 static MENUBAR_FONT: RwLock<String> = RwLock::new(String::new());
 static MENUBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
+static STATUSBAR_FONT: RwLock<String> = RwLock::new(String::new());
+static STATUSBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 static SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
 static NESTED_SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
 static BREADCRUMB_FONT: RwLock<String> = RwLock::new(String::new());
@@ -318,6 +322,7 @@ pub fn align_text_y(y: f32, height: f32, font_size: f32, top_offset: f32) -> f32
 pub fn reload_config() {
     if let Some(content) = read_config() {
         let mut menubar_font_changed = false;
+        let mut statusbar_font_changed = false;
         let mut toggle_font_changed = false;
         let mut font_selector_font_changed = false;
         let mut button_strip_font_changed = false;
@@ -649,6 +654,26 @@ pub fn reload_config() {
                     menubar_font_changed = true;
                 }
             }
+            if let Some(rest) = trimmed.strip_prefix("statusbar_font") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+                let rest = rest.trim();
+                let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+                    &rest[1..rest.len() - 1]
+                } else {
+                    rest
+                };
+                let font = val_str.trim().to_string();
+                let mut changed = false;
+                if let Ok(mut lock) = STATUSBAR_FONT.write() {
+                    if *lock != font {
+                        *lock = font;
+                        changed = true;
+                    }
+                }
+                if changed {
+                    statusbar_font_changed = true;
+                }
+            }
             if let Some(rest) = trimmed.strip_prefix("section_label_font") {
                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
                 let rest = mod_rest(rest);
@@ -999,6 +1024,11 @@ pub fn reload_config() {
                 *lock = None;
             }
         }
+        if statusbar_font_changed {
+            if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
+                *lock = None;
+            }
+        }
         if toggle_font_changed {
             if let Ok(mut lock) = TOGGLE_FONT_CACHED.write() {
                 *lock = None;
@@ -1064,7 +1094,9 @@ pub fn reload_config() {
                 *lock = None;
             }
         }
-        crate::color::reload_colors(&content);
+        if let Ok(raw_kdl) = std::fs::read_to_string(crate::config::get_config_path()) {
+            crate::color::reload_colors(&raw_kdl);
+        }
     }
 }
 
@@ -1671,6 +1703,63 @@ pub fn set_menubar_font(font: &str) {
     }
 }
 
+pub fn statusbar_font() -> String {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        let mut font = "Outfit".to_string();
+        if let Some(content) = read_config() {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("statusbar_font") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+                    let rest = rest.trim();
+                    let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+                        &rest[1..rest.len() - 1]
+                    } else {
+                        rest
+                    };
+                    font = val_str.trim().to_string();
+                }
+            }
+        }
+        if let Ok(mut lock) = STATUSBAR_FONT.write() {
+            *lock = font;
+        }
+    });
+    let lock = STATUSBAR_FONT.read().unwrap();
+    if lock.is_empty() {
+        "Outfit".to_string()
+    } else {
+        lock.clone()
+    }
+}
+
+pub fn statusbar_font_parsed() -> (String, f32) {
+    if let Ok(lock) = STATUSBAR_FONT_CACHED.read() {
+        if let Some(ref val) = *lock {
+            return val.clone();
+        }
+    }
+    let font_str = statusbar_font();
+    let parsed = parse_font_string(&font_str);
+    let size = parsed.1.unwrap_or(12.0);
+    let val = (parsed.0, size);
+    if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
+        *lock = Some(val.clone());
+    }
+    val
+}
+
+pub fn set_statusbar_font(font: &str) {
+    if let Ok(mut lock) = STATUSBAR_FONT.write() {
+        *lock = font.to_string();
+    }
+    if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
+        *lock = None;
+    }
+}
+
 pub fn toggle_font() -> String {
     use std::sync::Once;
     static INIT: Once = Once::new();
diff --git a/src/widget/container/section_container.rs b/src/widget/container/section_container.rs
index 3b451de..f669b35 100644
--- a/src/widget/container/section_container.rs
+++ b/src/widget/container/section_container.rs
@@ -114,23 +114,33 @@ impl Element for SectionContainer {
     }
 
     fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        self.container.all_quads(ctx)
+        let mut quads = self.header.all_quads(ctx);
+        quads.extend(self.container.all_quads(ctx));
+        quads
     }
 
     fn text_labels(&self) -> Vec<TextLabel> {
-        self.container.text_labels()
+        let mut labels = self.header.text_labels();
+        labels.extend(self.container.text_labels());
+        labels
     }
 
     fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
-        self.container.text_labels_with_bounds(ctx)
+        let mut labels = self.header.text_labels_with_bounds(ctx);
+        labels.extend(self.container.text_labels_with_bounds(ctx));
+        labels
     }
 
     fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        self.container.text_labels_with_font_and_bounds(ctx)
+        let mut labels = self.header.text_labels_with_font_and_bounds(ctx);
+        labels.extend(self.container.text_labels_with_font_and_bounds(ctx));
+        labels
     }
 
     fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
-        self.container.get_text_items()
+        let mut items = self.header.get_text_items();
+        items.extend(self.container.get_text_items());
+        items
     }
 
     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
diff --git a/src/widget/display/panel.rs b/src/widget/display/panel.rs
index aa6734b..9395457 100644
--- a/src/widget/display/panel.rs
+++ b/src/widget/display/panel.rs
@@ -38,6 +38,11 @@ impl Element for Panel {
 
     fn color(&self) -> [f32; 4] { if self.dragging { colors::PANEL_DRAG } else { colors::PANEL_IDLE } }
 
+    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (x, y, w, h) = self.rect();
+        vec![(x, y, w, h, self.color())]
+    }
+
     fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
         self.bounds = Some((bx, by, bw, bh));
     }
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 1da0ab8..68cabc4 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -232,8 +232,15 @@ impl Element for Button {
         }
         let mut labels = Vec::new();
         if let Some(ref label) = self.base.label {
-            let font_size = 12.0;
-            let font_family = self.widget_font().unwrap_or_else(|| "sans-serif".to_string());
+            let mut font_size = 12.0;
+            let mut font_family = "sans-serif".to_string();
+            if let Some(font_str) = self.widget_font() {
+                let (parsed_fam, parsed_size) = crate::layout::parse_font_string(&font_str);
+                font_family = parsed_fam;
+                if let Some(ps) = parsed_size {
+                    font_size = ps;
+                }
+            }
             let est_w = if label == "📋" {
                 12.0
             } else {
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index ca561b5..b70ca7c 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -76,11 +76,21 @@ impl Dropdown {
         w
     }
 
+    pub fn get_dy_dh(&self) -> (f32, f32) {
+        let open_upward = self.base.y > 400.0;
+        let dh = self.options.len() as f32 * 24.0;
+        let dy = if open_upward {
+            self.base.y - dh
+        } else {
+            self.base.y + self.base.h
+        };
+        (dy, dh)
+    }
+
     pub fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
         if !self.open { return; }
         
-        let dy = self.base.y + self.base.h;
-        let dh = self.options.len() as f32 * 24.0;
+        let (dy, dh) = self.get_dy_dh();
         let pw = self.popover_width();
         
         // 1. Soft layered drop shadows
@@ -258,8 +268,7 @@ impl Element for Dropdown {
         let hx = if self.base.row_w > 0.0 { self.base.row_x } else { x };
         let hw = if self.base.row_w > 0.0 { self.base.row_w } else { w };
         if self.open {
-            let dy = y + h;
-            let dh = self.options.len() as f32 * 24.0;
+            let (dy, dh) = self.get_dy_dh();
             let pw = self.popover_width();
             let hit_trigger = px >= hx && px <= hx + hw && py >= y && py <= y + h;
             let hit_popover = px >= x && px <= x + pw && py >= dy && py <= dy + dh;
@@ -277,9 +286,8 @@ impl Element for Dropdown {
         self.hovered_item = None;
 
         if self.open {
-            let (x, y, _, h) = self.rect();
-            let dy = y + h;
-            let dh = self.options.len() as f32 * 24.0;
+            let (x, _, _, _) = self.rect();
+            let (dy, dh) = self.get_dy_dh();
             let pw = self.popover_width();
             if px >= x && px <= x + pw && py >= dy && py <= dy + dh {
                 let idx = ((py - dy) / 24.0) as usize;
@@ -304,8 +312,7 @@ impl Element for Dropdown {
         if button != MouseButton::Left || state != ElementState::Pressed { return false; }
 
         let (x, y, w, h) = self.rect();
-        let dy = y + h;
-        let dh = self.options.len() as f32 * 24.0;
+        let (dy, dh) = self.get_dy_dh();
         let pw = self.popover_width();
 
         let inside_trigger = px >= x && px <= x + w && py >= y && py <= y + h;
@@ -479,7 +486,8 @@ impl Element for Dropdown {
     fn take_click(&mut self) -> bool { self.take_change() }
     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
         if self.open {
-            Some((self.base.x, self.base.y + self.base.h, self.popover_width(), self.options.len() as f32 * 24.0))
+            let (dy, dh) = self.get_dy_dh();
+            Some((self.base.x, dy, self.popover_width(), dh))
         } else {
             None
         }
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 99c4958..52d7bdf 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -78,6 +78,13 @@ impl Spinbox {
 impl Element for Spinbox {
     crate::impl_widget_base!(Spinbox);
 
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+        self.base.x = x;
+        self.base.y = y;
+        self.base.w = w;
+        self.base.h = h + self.base.label_offset();
+    }
+
     fn get_value_string(&self) -> Option<String> {
         if self.decimals > 0 {
             let divisor = 10.0f32.powi(self.decimals as i32);