GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Implement standalone widget styling support for MultiControl and add Dropdown corner radius config and spinbox control
src/backend/window_runner.rs | 29 ++++++++++
src/layout.rs | 117 ++++++++++++++++++++++++++++++++++++++
src/main.rs | 76 +++++++++++++++++++++++--
src/widget/input/checkbox.rs | 4 ++
src/widget/input/dropdown.rs | 13 +++++
src/widget/input/multi_control.rs | 47 ++++++++++-----
6 files changed, 269 insertions(+), 17 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b8b0472..c185a09 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -592,6 +592,35 @@ fn get_child_widget_for_quad<'a>(
}
}
}
+ if let Some(mc) = w.as_any().downcast_ref::<crate::widget::input::MultiControl>() {
+ let (bx, by, bw, bh) = mc.add_button.rect();
+ if qx >= bx - 0.1 && qx + qw <= bx + bw + 0.1 && qy >= by - 0.1 && qy + qh <= by + bh + 0.1 {
+ return &mc.add_button;
+ }
+ for row in &mc.rows {
+ let (kx, ky, kw, kh) = row.key_input.rect();
+ if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+ return &row.key_input;
+ }
+ let (tx, ty, tw, th) = row.type_dropdown.rect();
+ if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
+ return &row.type_dropdown;
+ }
+ let (rx, ry, rw, rh) = row.remove_button.rect();
+ if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+ return &row.remove_button;
+ }
+ let (vx, vy, vw, vh) = row.value_widget.rect();
+ if qx >= vx - 0.1 && qx + qw <= vx + vw + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + vh + 0.1 {
+ return match &row.value_widget {
+ crate::widget::input::InstancedWidget::TextBox(w) => w,
+ crate::widget::input::InstancedWidget::Spinbox(w) => w,
+ crate::widget::input::InstancedWidget::Toggle(w) => w,
+ crate::widget::input::InstancedWidget::Slider(w) => w,
+ };
+ }
+ }
+ }
w
}
diff --git a/src/layout.rs b/src/layout.rs
index d03e17b..a8eb1f1 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -57,6 +57,7 @@ static BUTTON_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
static SPINBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
static TEXTBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
static FONT_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
+static DROPDOWN_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
/// Standard line height multiplier for text layout in cce-ui.
pub const TEXT_LINE_HEIGHT_MULTIPLIER: f32 = 1.4;
@@ -181,6 +182,15 @@ pub fn reload_config() {
}
}
}
+ if let Some(rest) = trimmed.strip_prefix("dropdown_corner_radius") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
+ *lock = val;
+ }
+ }
+ }
if let Some(rest) = trimmed.strip_prefix("toggle_height") {
let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
let val_str = rest.trim_end_matches('"').trim();
@@ -1048,6 +1058,34 @@ pub fn set_font_selector_corner_radius(radius: f32) {
}
}
+pub fn dropdown_corner_radius() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("dropdown_corner_radius") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *DROPDOWN_CORNER_RADIUS.read().unwrap()
+}
+
+pub fn set_dropdown_corner_radius(radius: f32) {
+ if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
+ *lock = radius;
+ }
+}
+
pub fn color_selector_preview_margin() -> f32 {
use std::sync::Once;
static INIT: Once = Once::new();
@@ -1370,6 +1408,67 @@ impl RenderTarget for PopoverCollector {
}
}
+fn get_multicontrol_sub_widget_info(
+ mc: &crate::widget::input::MultiControl,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+) -> Option<((bool, bool, bool, bool), f32, (f32, f32, f32, f32), f32)> {
+ // Check add_button
+ let (bx, by, bw, bh) = mc.add_button.rect();
+ if qx >= bx - 0.1 && qx + qw <= bx + bw + 0.1 && qy >= by - 0.1 && qy + qh <= by + bh + 0.1 {
+ return Some((
+ mc.add_button.rounded_corners(),
+ mc.add_button.corner_radius(),
+ (bx, by, bw, bh),
+ crate::widget::label_offset(&mc.add_button),
+ ));
+ }
+ // Check rows
+ for row in &mc.rows {
+ // key_input
+ let (kx, ky, kw, kh) = row.key_input.rect();
+ if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+ return Some((
+ row.key_input.rounded_corners(),
+ row.key_input.corner_radius(),
+ (kx, ky, kw, kh),
+ crate::widget::label_offset(&row.key_input),
+ ));
+ }
+ // type_dropdown
+ let (tx, ty, tw, th) = row.type_dropdown.rect();
+ if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
+ return Some((
+ row.type_dropdown.rounded_corners(),
+ row.type_dropdown.corner_radius(),
+ (tx, ty, tw, th),
+ crate::widget::label_offset(&row.type_dropdown),
+ ));
+ }
+ // remove_button
+ let (rx, ry, rw, rh) = row.remove_button.rect();
+ if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+ return Some((
+ row.remove_button.rounded_corners(),
+ row.remove_button.corner_radius(),
+ (rx, ry, rw, rh),
+ crate::widget::label_offset(&row.remove_button),
+ ));
+ }
+ // value_widget
+ let (vx, vy, vw, vh) = row.value_widget.rect();
+ if qx >= vx - 0.1 && qx + qw <= vx + vw + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + vh + 0.1 {
+ let (corners, radius, label_offset) = match &row.value_widget {
+ crate::widget::input::InstancedWidget::TextBox(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+ crate::widget::input::InstancedWidget::Spinbox(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+ crate::widget::input::InstancedWidget::Toggle(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+ crate::widget::input::InstancedWidget::Slider(w) => (w.rounded_corners(), w.corner_radius(), crate::widget::label_offset(w)),
+ };
+ return Some((corners, radius, (vx, vy, vw, vh), label_offset));
+ }
+ }
+ None
+}
+
pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
let corners = w.rounded_corners();
@@ -1384,6 +1483,24 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
whh -= top_room;
for (qx, qy, qw, qh, qc) in w.all_quads(ctx) {
+ let mut corners = corners;
+ let mut r = r;
+ let mut wx = wx;
+ let mut wy = wy;
+ let mut www = www;
+ let mut whh = whh;
+
+ if let Some(mc) = w.as_any().downcast_ref::<crate::widget::input::MultiControl>() {
+ if let Some((sub_corners, sub_radius, (sub_x, sub_y, sub_w, sub_h), sub_label_offset)) = get_multicontrol_sub_widget_info(mc, qx, qy, qw, qh) {
+ corners = sub_corners;
+ r = sub_radius;
+ wx = sub_x;
+ wy = sub_y + sub_label_offset;
+ www = sub_w;
+ whh = sub_h - sub_label_offset;
+ }
+ }
+
if r <= 0.1 || corners == (false, false, false, false) {
pc.rect_with_radius_corners(qc, qx, qy, qw, qh, 0.0, (false, false, false, false));
continue;
diff --git a/src/main.rs b/src/main.rs
index 9fa802a..c592949 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -186,13 +186,81 @@ fn extra_quad_vertices(
sw: f32, sh: f32,
qc: [f32; 4],
) -> Vec<Vertex> {
- let corners = w.rounded_corners();
+ let mut corners = w.rounded_corners();
+ let mut r = w.corner_radius();
+ let (mut wx, mut wy, mut ww, mut wh) = w.rect();
+ let mut top_room = cce_ui::widget::label_offset(w);
+
+ if let Some(mc) = w.as_any().downcast_ref::<cce_ui::widget::input::MultiControl>() {
+ // check add_button
+ let (bx, by, b_w, b_h) = mc.add_button.rect();
+ if qx >= bx - 0.1 && qx + qw <= bx + b_w + 0.1 && qy >= by - 0.1 && qy + qh <= by + b_h + 0.1 {
+ corners = mc.add_button.rounded_corners();
+ r = mc.add_button.corner_radius();
+ wx = bx;
+ wy = by;
+ ww = b_w;
+ wh = b_h;
+ top_room = cce_ui::widget::label_offset(&mc.add_button);
+ }
+ for row in &mc.rows {
+ // key_input
+ let (kx, ky, kw, kh) = row.key_input.rect();
+ if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
+ corners = row.key_input.rounded_corners();
+ r = row.key_input.corner_radius();
+ wx = kx;
+ wy = ky;
+ ww = kw;
+ wh = kh;
+ top_room = cce_ui::widget::label_offset(&row.key_input);
+ }
+ // type_dropdown
+ let (tx, ty, tw, th) = row.type_dropdown.rect();
+ if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
+ corners = row.type_dropdown.rounded_corners();
+ r = row.type_dropdown.corner_radius();
+ wx = tx;
+ wy = ty;
+ ww = tw;
+ wh = th;
+ top_room = cce_ui::widget::label_offset(&row.type_dropdown);
+ }
+ // remove_button
+ let (rx, ry, rw, rh) = row.remove_button.rect();
+ if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
+ corners = row.remove_button.rounded_corners();
+ r = row.remove_button.corner_radius();
+ wx = rx;
+ wy = ry;
+ ww = rw;
+ wh = rh;
+ top_room = cce_ui::widget::label_offset(&row.remove_button);
+ }
+ // value_widget
+ let (vx, vy, v_w, v_h) = row.value_widget.rect();
+ if qx >= vx - 0.1 && qx + qw <= vx + v_w + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + v_h + 0.1 {
+ let (sub_corners, sub_radius, sub_top_room) = match &row.value_widget {
+ cce_ui::widget::input::InstancedWidget::TextBox(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+ cce_ui::widget::input::InstancedWidget::Spinbox(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+ cce_ui::widget::input::InstancedWidget::Toggle(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+ cce_ui::widget::input::InstancedWidget::Slider(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
+ };
+ corners = sub_corners;
+ r = sub_radius;
+ wx = vx;
+ wy = vy;
+ ww = v_w;
+ wh = v_h;
+ top_room = sub_top_room;
+ }
+ }
+ }
+
if corners == (false, false, false, false) {
return quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
}
- let (wx, mut wy, ww, mut wh) = w.rect();
- let top_room = cce_ui::widget::label_offset(w);
wy += top_room;
wh -= top_room;
let extra_corners = (
@@ -202,7 +270,7 @@ fn extra_quad_vertices(
corners.3 && qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5,
);
- rounded_rect_vertices_corners(qx, qy, qw, qh, w.corner_radius(), sw, sh, qc, extra_corners)
+ rounded_rect_vertices_corners(qx, qy, qw, qh, r, sw, sh, qc, extra_corners)
}
fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 179738c..a0aba44 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -206,6 +206,10 @@ impl Toggle {
impl Element for Toggle {
crate::impl_widget_base!(Toggle);
+ fn preferred_height(&self) -> Option<f32> {
+ Some(crate::layout::toggle_height())
+ }
+
fn get_value_string(&self) -> Option<String> {
Some(self.toggled.to_string())
}
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 153b4cb..789aa3c 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -166,6 +166,19 @@ impl Element for Dropdown {
Some(crate::layout::dropdown_height())
}
+ fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+ let r = crate::layout::dropdown_corner_radius();
+ if r > 0.0 {
+ (true, true, true, true)
+ } else {
+ (false, false, false, false)
+ }
+ }
+
+ fn corner_radius(&self) -> f32 {
+ crate::layout::dropdown_corner_radius()
+ }
+
fn widget_font(&self) -> Option<String> {
Some(self.font_family.clone())
}
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index f47c600..bb2978a 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -47,6 +47,15 @@ impl InstancedWidget {
}
}
+ pub fn preferred_height(&self) -> Option<f32> {
+ match self {
+ InstancedWidget::TextBox(w) => w.preferred_height(),
+ InstancedWidget::Spinbox(w) => w.preferred_height(),
+ InstancedWidget::Toggle(w) => w.preferred_height(),
+ InstancedWidget::Slider(w) => w.preferred_height(),
+ }
+ }
+
pub fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) {
match self {
InstancedWidget::TextBox(w) => w.layout(origin, constraints, ctx),
@@ -262,16 +271,24 @@ impl Element for MultiControl {
crate::impl_widget_base!(MultiControl);
fn preferred_height(&self) -> Option<f32> {
- let line_h = 44.0;
let gap_between_lines = 4.0;
let gap_between_rows = 12.0;
- let row_h = 2.0 * line_h + gap_between_lines;
let add_btn_h = 36.0;
- let total_h = if self.rows.is_empty() {
- add_btn_h + 16.0
+
+ let key_h = crate::layout::textbox_height();
+ let type_h = crate::layout::dropdown_height();
+ let line1_h = key_h.max(type_h);
+
+ let mut total_h = 0.0;
+ if self.rows.is_empty() {
+ total_h = add_btn_h + 16.0;
} else {
- self.rows.len() as f32 * row_h + (self.rows.len() - 1) as f32 * gap_between_rows + add_btn_h + 24.0
- };
+ for row in &self.rows {
+ let line2_h = row.value_widget.preferred_height().unwrap_or(44.0);
+ total_h += line1_h + gap_between_lines + line2_h;
+ }
+ total_h += (self.rows.len() - 1) as f32 * gap_between_rows + add_btn_h + 28.0;
+ }
Some(total_h)
}
@@ -285,10 +302,13 @@ impl Element for MultiControl {
let pad_x = 8.0;
let pad_y = 8.0;
- let line_h = 44.0;
let gap_between_lines = 4.0;
let gap_between_rows = 12.0;
+ let key_h = crate::layout::textbox_height();
+ let type_h = crate::layout::dropdown_height();
+ let line1_h = key_h.max(type_h);
+
let usable_w = (size.width - 2.0 * pad_x).max(1.0);
let gap_x = 6.0;
let top_usable_w = usable_w - 2.0 * gap_x;
@@ -304,18 +324,19 @@ impl Element for MultiControl {
for row in &mut self.rows {
// Line 1: Label, Type, Remove
let key_x = origin.x + pad_x;
- row.key_input.layout(Point { x: key_x, y: curr_y }, LayoutConstraints::new(key_w, key_w, line_h, line_h), ctx);
+ row.key_input.layout(Point { x: key_x, y: curr_y }, LayoutConstraints::new(key_w, key_w, line1_h, line1_h), ctx);
let type_x = key_x + key_w + gap_x;
- row.type_dropdown.layout(Point { x: type_x, y: curr_y }, LayoutConstraints::new(type_w, type_w, line_h, line_h), ctx);
+ row.type_dropdown.layout(Point { x: type_x, y: curr_y }, LayoutConstraints::new(type_w, type_w, line1_h, line1_h), ctx);
let remove_x = type_x + type_w + gap_x;
- row.remove_button.layout(Point { x: remove_x, y: curr_y }, LayoutConstraints::new(remove_w, remove_w, line_h, line_h), ctx);
+ row.remove_button.layout(Point { x: remove_x, y: curr_y }, LayoutConstraints::new(remove_w, remove_w, line1_h, line1_h), ctx);
// Line 2: Value Control Widget
- let value_y = curr_y + line_h + gap_between_lines;
+ let line2_h = row.value_widget.preferred_height().unwrap_or(44.0);
+ let value_y = curr_y + line1_h + gap_between_lines;
let value_x = origin.x + pad_x;
- row.value_widget.layout(Point { x: value_x, y: value_y }, LayoutConstraints::new(usable_w, usable_w, line_h, line_h), ctx);
+ row.value_widget.layout(Point { x: value_x, y: value_y }, LayoutConstraints::new(usable_w, usable_w, line2_h, line2_h), ctx);
link_child(self_ptr, self_id, &mut row.key_input, ctx);
link_child(self_ptr, self_id, &mut row.type_dropdown, ctx);
@@ -327,7 +348,7 @@ impl Element for MultiControl {
}
link_child(self_ptr, self_id, &mut row.remove_button, ctx);
- curr_y += 2.0 * line_h + gap_between_lines + gap_between_rows;
+ curr_y += line1_h + gap_between_lines + line2_h + gap_between_rows;
}
// Lay out add button