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