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

commit8679930a67a38988d6f2c690c55fefe06848685b
parent612843cad6
authorLucas Galante <[email protected]>
date2026-07-05 01:45
Support right-aligned titles in MenuBar and resolve rounded corners overlapping bugs

 src/backend/window_runner.rs |  13 +++++
 src/shader.wgsl              |  29 +++++++++++
 src/widget/container/menu.rs |  30 +++++++++---
 src/widget/container/mod.rs  |   4 ++
 src/widget/input/button.rs   |   5 +-
 src/widget/input/checkbox.rs |  81 ++++++++++++++++---------------
 src/widget/input/slider.rs   | 111 +++++++++++++++++++++++++++++++++++++++++++
 src/widget/mod.rs            |   1 +
 8 files changed, 228 insertions(+), 46 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 5717437..43fd992 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1364,11 +1364,19 @@ pub trait Application: Sized + 'static {
     fn clear_color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
+
+    fn register_sources(&mut self, _handle: &calloop::LoopHandle<'_, EngineState<Self>>) {}
+
+    fn adjust_size(&self, width: f32, height: f32) -> (f32, f32) {
+        (width, height)
+    }
     
     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool);
     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message>;
     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool);
     fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message>;
+
+    fn custom_vertices(&mut self, _verts: &mut Vec<Vertex>, _size: LogicalSize, _scale: f64) {}
 }
 
 pub struct PressedKey {
@@ -1521,6 +1529,7 @@ impl<A: Application> EngineState<A> {
     }
     
     pub fn resize(&mut self, w: f32, h: f32) {
+        let (w, h) = self.inner.adjust_size(w, h);
         if w > 0.0 && h > 0.0 {
             self.logical_width = w;
             self.logical_height = h;
@@ -1580,6 +1589,7 @@ impl<A: Application> EngineState<A> {
         for &(vx1, vy1, vx2, vy2, vthickness, vcolor, vcap) in &vectors {
             verts.extend(vector_vertices(vx1, vy1, vx2, vy2, vthickness, logical_w, logical_h, vcolor, vcap));
         }
+        self.inner.custom_vertices(&mut verts, LogicalSize::new(logical_w, logical_h), scale_factor);
         self.vertex_count = verts.len() as u32;
         if self.vertex_count > 0 {
             let data = bytemuck::cast_slice(&verts);
@@ -2401,6 +2411,7 @@ impl<A: Application> EngineState<A> {
             xkeysym::Keysym::Alt_L | xkeysym::Keysym::Alt_R => Key::Named(NamedKey::Alt),
             xkeysym::Keysym::Control_L | xkeysym::Keysym::Control_R => Key::Named(NamedKey::Control),
             xkeysym::Keysym::Shift_L | xkeysym::Keysym::Shift_R => Key::Named(NamedKey::Shift),
+            xkeysym::Keysym::F5 => Key::Named(NamedKey::F5),
             _ => {
                 if let Some(ref text) = event.utf8 {
                     Key::Character(text.clone())
@@ -2716,6 +2727,8 @@ pub fn run<A: Application>() {
         }
     }).unwrap();
 
+    engine_state.inner.register_sources(&loop_handle);
+
     const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
     const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
 
diff --git a/src/shader.wgsl b/src/shader.wgsl
index 2d36cda..1e12bbb 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -30,6 +30,35 @@ fn vs_main(
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+    if (in.clip_circle.x == -999.0) {
+        let y = length(vec2f(in.ndc_position.x, in.ndc_position.y));
+        let x = atan2(in.ndc_position.y, in.ndc_position.x);
+
+        // Wavy boundary radius with 7 lobes
+        let R_theta = 0.60 + 0.06 * sin(7.0 * x);
+
+        // Radial density: 1.0 at center, fading out to 0.0 at R_theta
+        let density = 1.0 - smoothstep(R_theta - 0.25, R_theta, y);
+
+        // Sine wave effect driven by the x value (distance around the circle)
+        let sin_effect = sin(7.0 * x);
+
+        // Normalized radius from 0.0 (center) to 1.0 (boundary)
+        let r_normalized = clamp(y / R_theta, 0.0, 1.0);
+
+        let gray = in.color.xyz;
+        
+        // Scale the ripple amplitude by the normalized radius to fade it out at the center
+        let alpha = clamp(density * (1.0 - r_normalized * 0.25 * (1.0 - sin_effect)), 0.0, 1.0);
+        
+        if (y > R_theta + 0.02) {
+            discard;
+        }
+        
+        let final_alpha = alpha * (1.0 - smoothstep(R_theta - 0.02, R_theta + 0.02, y)) * in.color.w;
+        return vec4f(gray, final_alpha);
+    }
+
     if (in.clip_circle.z > 0.0) {
         let dx = in.clip_position.x - in.clip_circle.x;
         let dy = in.clip_position.y - in.clip_circle.y;
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 7ebe50f..d6a60b9 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -31,6 +31,7 @@ pub struct MenuBar {
     pub context_hovered_item: Option<usize>,
     pub context_title_hovered: bool,
     pub context_item_bufs: Vec<glyphon::Buffer>,
+    pub right_align_title: bool,
     pub parent: Option<*mut (dyn Element + 'static)>,
     pub page_hidden: bool,
     pub layout_dirty: bool,
@@ -96,9 +97,15 @@ impl MenuBar {
             on_menu_click_cb: None,
             hovered_dropdown_item: None,
             clicked_dropdown_item: None,
+            right_align_title: false,
         }
     }
 
+    pub fn with_right_aligned_title(mut self, right: bool) -> Self {
+        self.right_align_title = right;
+        self
+    }
+
     pub fn on_context_change<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
         self.on_context_change_cb = Some(Box::new(cb));
         self
@@ -192,7 +199,12 @@ impl MenuBar {
                 }
             }
             let title_w = display_title.len() as f32 * char_w + 24.0;
-            (self.base.x + start_x, self.base.y, title_w, self.base.h)
+            let tx = if self.right_align_title {
+                self.base.x + self.base.w - title_w - 20.0
+            } else {
+                self.base.x + start_x
+            };
+            (tx, self.base.y, title_w, self.base.h)
         }
     }
 
@@ -440,7 +452,7 @@ impl Element for MenuBar {
             let mut cx = 8.0;
             if self.center_items {
                 let mut total_width = 8.0;
-                if !self.title.is_empty() {
+                if !self.title.is_empty() && !self.right_align_title {
                     let mut display_title = self.title.clone();
                     if !self.context_options.is_empty() {
                         display_title.push_str(" ▼");
@@ -460,7 +472,7 @@ impl Element for MenuBar {
                     cx = (self.base.w - total_width) / 2.0;
                 }
             }
-            if !self.title.is_empty() {
+            if !self.title.is_empty() && !self.right_align_title {
                 let mut display_title = self.title.clone();
                 if !self.context_options.is_empty() {
                     display_title.push_str(" ▼");
@@ -978,7 +990,7 @@ impl Element for MenuBar {
         }
 
         let font_setting = crate::layout::menubar_font();
-        let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
+        let (font_fam, font_size_opt) = crate::layout::parse_font_string(&font_setting);
         let font_size = font_size_opt.unwrap_or(12.0);
         let char_w = 7.5 * (font_size / 12.0);
 
@@ -1043,7 +1055,7 @@ impl Element for MenuBar {
             let mut start_x = 8.0;
             if self.center_items {
                 let mut total_width = 8.0;
-                if !self.title.is_empty() {
+                if !self.title.is_empty() && !self.right_align_title {
                     total_width += display_title.len() as f32 * char_w + 24.0;
                 }
                 for btn_label in &self.menus.buttons {
@@ -1055,9 +1067,15 @@ impl Element for MenuBar {
             }
             if !self.title.is_empty() {
                 let text_y = crate::layout::align_text_y(self.base.y, self.base.h, font_size, 0.0);
+                let x_pos = if self.right_align_title {
+                    let title_w = crate::widget::display::measure_text_width(&display_title, &font_fam, font_size) + 24.0;
+                    self.base.x + self.base.w - title_w - 20.0
+                } else {
+                    self.base.x + start_x
+                };
                 labels.push(TextLabel {
                     text: display_title,
-                    x: self.base.x + start_x,
+                    x: x_pos,
                     y: text_y,
                     font_size,
                     color: text_color,
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index a7cfedf..025a482 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -19,6 +19,8 @@ pub mod paginator;
 pub mod scroll_bar;
 pub mod treelist;
 pub mod control_panel;
+pub mod vbox;
+pub mod hbox;
 
 pub use container::Container;
 pub use control_panel::ControlPanel;
@@ -41,3 +43,5 @@ pub use backplate::Backplate;
 pub use paginator::Paginator;
 pub use scroll_bar::ScrollBar;
 pub use treelist::{TreeList, TreeElement};
+pub use vbox::VBox;
+pub use hbox::HBox;
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 68cabc4..ec36e6f 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -290,7 +290,10 @@ impl Element for Button {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = vec![(self.base.x, self.base.y, self.base.w, self.base.h, self.color())];
+        let mut quads = Vec::new();
+        if self.corner_radius() <= 0.0 {
+            quads.push((self.base.x, self.base.y, self.base.w, self.base.h, self.color()));
+        }
         if let Some(ref svg) = self.svg {
             let svg_x = self.base.x + (self.base.w - svg.w) / 2.0;
             let svg_y = self.base.y + (self.base.h - svg.h) / 2.0;
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index aaa68c9..6e70412 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -293,46 +293,49 @@ impl Element for Toggle {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = vec![(self.base.x, self.base.y, self.base.w, self.base.h, self.color())];
-        
-        let border_w = crate::layout::toggle_border_width();
-        if border_w > 0.0 {
-            let x = self.base.x;
-            let y = self.base.y;
-            let w = self.base.w;
-            let h = self.base.h;
-            let r = crate::layout::toggle_corner_radius();
-            let t = border_w;
-            
-            let border_color = if self.toggled {
-                colors::toggle_on_color()
-            } else {
-                colors::toggle_off_color()
-            };
-            
-            let edge_h = ((h / 2.0) - r).max(0.0);
+        let mut quads = Vec::new();
+        if self.corner_radius() <= 0.0 {
+            quads.push((self.base.x, self.base.y, self.base.w, self.base.h, self.color()));
             
-            if self.toggled {
-                // Top edge
-                quads.push((x + r, y, w - 2.0 * r, t, border_color));
-                // Top half of left edge
-                if edge_h > 0.0 {
-                    quads.push((x, y + r, t, edge_h, border_color));
-                }
-                // Top half of right edge
-                if edge_h > 0.0 {
-                    quads.push((x + w - t, y + r, t, edge_h, border_color));
-                }
-            } else {
-                // Bottom edge
-                quads.push((x + r, y + h - t, w - 2.0 * r, t, border_color));
-                // Bottom half of left edge
-                if edge_h > 0.0 {
-                    quads.push((x, y + h / 2.0, t, edge_h, border_color));
-                }
-                // Bottom half of right edge
-                if edge_h > 0.0 {
-                    quads.push((x + w - t, y + h / 2.0, t, edge_h, border_color));
+            let border_w = crate::layout::toggle_border_width();
+            if border_w > 0.0 {
+                let x = self.base.x;
+                let y = self.base.y;
+                let w = self.base.w;
+                let h = self.base.h;
+                let r = crate::layout::toggle_corner_radius();
+                let t = border_w;
+                
+                let border_color = if self.toggled {
+                    colors::toggle_on_color()
+                } else {
+                    colors::toggle_off_color()
+                };
+                
+                let edge_h = ((h / 2.0) - r).max(0.0);
+                
+                if self.toggled {
+                    // Top edge
+                    quads.push((x + r, y, w - 2.0 * r, t, border_color));
+                    // Top half of left edge
+                    if edge_h > 0.0 {
+                        quads.push((x, y + r, t, edge_h, border_color));
+                    }
+                    // Top half of right edge
+                    if edge_h > 0.0 {
+                        quads.push((x + w - t, y + r, t, edge_h, border_color));
+                    }
+                } else {
+                    // Bottom edge
+                    quads.push((x + r, y + h - t, w - 2.0 * r, t, border_color));
+                    // Bottom half of left edge
+                    if edge_h > 0.0 {
+                        quads.push((x, y + h / 2.0, t, edge_h, border_color));
+                    }
+                    // Bottom half of right edge
+                    if edge_h > 0.0 {
+                        quads.push((x + w - t, y + h / 2.0, t, edge_h, border_color));
+                    }
                 }
             }
         }
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 371b121..6337378 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -382,6 +382,11 @@ impl Element for Slider {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (r1, r2, r3, r4) = self.rounded_corners();
+        if r1 || r2 || r3 || r4 {
+            return Vec::new();
+        }
+
         let mut quads = Vec::new();
         let top = self.base.label_offset();
         let visual_h = self.base.h - top;
@@ -429,6 +434,59 @@ impl Element for Slider {
         quads
     }
 
+    fn all_rounded_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+        let mut quads = Vec::new();
+        let (r1, r2, r3, r4) = self.rounded_corners();
+        if !(r1 || r2 || r3 || r4) {
+            return quads;
+        }
+        
+        let top = self.base.label_offset();
+        let visual_h = self.base.h - top;
+        let radius = self.corner_radius();
+        
+        let (track_x, track_w) = if self.show_readout {
+            let readout_w = 60.0;
+            let gap = 8.0;
+            let tw = (self.base.w - readout_w - gap).max(10.0);
+            
+            quads.push((self.base.x, self.base.y + top, tw, visual_h, radius, colors::slider_track(), (r1, r2, r3, r4)));
+            
+            let rx = self.base.x + self.base.w - readout_w;
+            let bg_color = if self.editing {
+                [0.06, 0.10, 0.18, 1.0]
+            } else {
+                [0.10, 0.10, 0.13, 1.0]
+            };
+            
+            if self.base.focused || self.editing {
+                let border_color = [0.20, 0.50, 0.85, 1.0];
+                quads.push((rx, self.base.y + top, readout_w, visual_h, radius, border_color, (r1, r2, r3, r4)));
+                let inner_radius = (radius - 1.0).max(0.0);
+                quads.push((rx + 1.0, self.base.y + top + 1.0, readout_w - 2.0, visual_h - 2.0, inner_radius, bg_color, (r1, r2, r3, r4)));
+            } else {
+                quads.push((rx, self.base.y + top, readout_w, visual_h, radius, bg_color, (r1, r2, r3, r4)));
+            }
+
+            (self.base.x, tw)
+        } else {
+            quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, radius, colors::slider_track(), (r1, r2, r3, r4)));
+            (self.base.x, self.base.w)
+        };
+
+        let thumb_size = visual_h * 0.9;
+        let thumb_x = track_x + self.value * (track_w - thumb_size);
+        let thumb_y = self.base.y + top + (visual_h - thumb_size) / 2.0;
+        let thumb_color = if self.dragging {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+        quads.push((thumb_x, thumb_y, thumb_size, thumb_size, thumb_size / 2.0, thumb_color, (true, true, true, true)));
+        
+        quads
+    }
+
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
         let top = self.base.label_offset();
@@ -671,6 +729,11 @@ impl Element for RangeSlider {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (r1, r2, r3, r4) = self.rounded_corners();
+        if r1 || r2 || r3 || r4 {
+            return Vec::new();
+        }
+
         let top = self.base.label_offset();
         let visual_h = self.base.h - top;
         let thumb_size = visual_h * 0.9;
@@ -706,6 +769,54 @@ impl Element for RangeSlider {
         ]
     }
 
+    fn all_rounded_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+        let mut quads = Vec::new();
+        let (r1, r2, r3, r4) = self.rounded_corners();
+        if !(r1 || r2 || r3 || r4) {
+            return quads;
+        }
+        
+        let top = self.base.label_offset();
+        let visual_h = self.base.h - top;
+        let radius = self.corner_radius();
+        
+        let thumb_size = visual_h * 0.9;
+        let range = self.base.w - thumb_size;
+        let thumb_low_x = self.base.x + self.value_low * range;
+        let thumb_high_x = self.base.x + self.value_high * range;
+        
+        let thumb_y = self.base.y + top + (visual_h - thumb_size) / 2.0;
+        
+        // Highlighted track segment
+        let highlight_x = thumb_low_x + thumb_size / 2.0;
+        let highlight_w = thumb_high_x - thumb_low_x;
+        let highlight_y = self.base.y + top + visual_h * 0.35;
+        let highlight_h = visual_h * 0.3;
+        
+        let low_color = if self.active_thumb == Some(ActiveThumb::Low) {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+
+        let high_color = if self.active_thumb == Some(ActiveThumb::High) {
+            colors::SLIDER_THUMB_DRAG
+        } else {
+            colors::SLIDER_THUMB
+        };
+        
+        // Track background
+        quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, radius, colors::slider_track(), (r1, r2, r3, r4)));
+        // Progress fill (highlight track)
+        quads.push((highlight_x, highlight_y, highlight_w, highlight_h, radius.min(highlight_h / 2.0), colors::PROGRESS_FILL, (true, true, true, true)));
+        // Low thumb
+        quads.push((thumb_low_x, thumb_y, thumb_size, thumb_size, thumb_size / 2.0, low_color, (true, true, true, true)));
+        // High thumb
+        quads.push((thumb_high_x, thumb_y, thumb_size, thumb_size, thumb_size / 2.0, high_color, (true, true, true, true)));
+        
+        quads
+    }
+
     fn value(&self) -> i32 {
         ((self.value_low * 100.0) as i32) | (((self.value_high * 100.0) as i32) << 16)
     }
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 8ff3950..f871d40 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -52,6 +52,7 @@ pub enum NamedKey {
     Shift,
     Alt,
     Super,
+    F5,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]