GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Update components and system settings
src/color.rs | 1 +
src/main.rs | 10 +--
src/widget.rs | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
3 files changed, 262 insertions(+), 26 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index 8fe32eb..11fcfd1 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -38,6 +38,7 @@ pub const PARAM_BG: [f32; 4] = [0.10, 0.10, 0.14, 0.25];
pub const PANEL_MENU_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
pub const PANEL_MENU_HOVER: [f32; 4] = [0.18, 0.18, 0.25, 1.0];
+pub const PANEL_MENU_FOCUSED: [f32; 4] = [0.08, 0.16, 0.28, 1.0];
pub const SPLITTER_IDLE: [f32; 4] = [0.20, 0.20, 0.27, 1.0];
pub const SPLITTER_HOVER: [f32; 4] = [0.40, 0.40, 0.50, 1.0];
diff --git a/src/main.rs b/src/main.rs
index b93c64e..ce48e1c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -217,12 +217,12 @@ impl State {
Box::new(Header::new()),
Box::new(Sidebar::new(60.0)),
Box::new(ContentBg::new()),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button A")),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button B")),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button C")),
Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
- Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Click Me")),
+ Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0).with_label("Reset")),
Box::new(Checkbox::new()),
Box::new(Toggle::new()),
Box::new(ProgressBar::new(0.65)),
diff --git a/src/widget.rs b/src/widget.rs
index 363bed6..beb3888 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -683,6 +683,7 @@ pub struct MenuBar {
was_open: Option<usize>,
pub vertical: bool,
pub visible: bool,
+ pub focused: bool,
}
impl MenuBar {
@@ -701,6 +702,7 @@ impl MenuBar {
was_open: None,
vertical: false,
visible: true,
+ focused: false,
}
}
@@ -807,6 +809,8 @@ impl Widget for MenuBar {
fn color(&self) -> [f32; 4] {
if !self.visible {
[0.0, 0.0, 0.0, 0.0]
+ } else if self.focused {
+ colors::PANEL_MENU_FOCUSED
} else {
colors::PANEL_MENU_BG
}
@@ -951,7 +955,12 @@ impl Widget for MenuBar {
false
}
+ fn focus(&mut self) {
+ self.focused = true;
+ }
+
fn unfocus(&mut self) {
+ self.focused = false;
self.was_open = self.open_menu.take();
}
@@ -1033,7 +1042,7 @@ impl Widget for MenuBar {
for (i, item) in self.menu_items.iter().enumerate() {
labels.push(TextLabel {
text: item.clone(),
- x: self.x + menu_item_x(&self.title, &self.menu_items, i),
+ x: self.x + menu_item_x(&self.title, &self.menu_items, i) + 8.0,
y: self.y + 7.0,
font_size: 12.0,
color: [0xcc, 0xcc, 0xd4],
@@ -1084,15 +1093,21 @@ pub struct Button {
pressed: bool,
just_clicked: bool,
kind: ButtonKind,
+ label: Option<String>,
}
impl Button {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Primary }
+ Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Primary, label: None }
}
pub fn new_reset(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Reset }
+ Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Reset, label: None }
+ }
+
+ pub fn with_label(mut self, label: &str) -> Self {
+ self.label = Some(label.to_string());
+ self
}
}
@@ -1146,6 +1161,22 @@ impl Widget for Button {
fn take_click(&mut self) -> bool {
if self.just_clicked { self.just_clicked = false; true } else { false }
}
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = Vec::new();
+ if let Some(ref label) = self.label {
+ let font_size = 12.0;
+ let est_w = label.len() as f32 * 6.5;
+ labels.push(TextLabel {
+ text: label.clone(),
+ x: self.x + (self.w - est_w) / 2.0,
+ y: self.y + (self.h - font_size) / 2.0 - 1.0,
+ font_size,
+ color: [0xcc, 0xcc, 0xd4],
+ });
+ }
+ labels
+ }
}
pub enum PageButton {
@@ -1513,6 +1544,10 @@ impl Toggle {
pub fn set_toggled(&mut self, v: bool) {
self.toggled = v;
}
+
+ pub fn toggled(&self) -> bool {
+ self.toggled
+ }
}
impl Widget for Toggle {
@@ -1527,10 +1562,15 @@ impl Widget for Toggle {
let (x, y, w, h) = self.rect();
let hx = if self.row_w > 0.0 { self.row_x } else { x };
let hw = if self.row_w > 0.0 { self.row_w } else { w };
- px >= hx && px <= hx + hw && py >= y && py <= y + h
+ let (hy, hh) = if self.label.is_some() {
+ (y - 18.0, h + 18.0)
+ } else {
+ (y, h)
+ };
+ px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
}
- fn top_room(&self) -> f32 { 0.0 }
+ fn top_room(&self) -> f32 { if self.label.is_some() { 18.0 } else { 0.0 } }
fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
let was = self.hovering;
@@ -1558,9 +1598,14 @@ impl Widget for Toggle {
let bg = if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF };
quads.push((self.x, self.y, self.w, self.h, bg));
if self.hovering {
- let hx = if self.row_w > 0.0 { self.row_x } else { self.x - 87.0 };
- let hw = if self.row_w > 0.0 { self.row_w } else { 87.0 + self.w };
- quads.push((hx, self.y, hw, self.h, [1.0, 1.0, 1.0, 0.06]));
+ let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
+ let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
+ let (hy, hh) = if self.label.is_some() {
+ (self.y - 18.0, self.h + 18.0)
+ } else {
+ (self.y, self.h)
+ };
+ quads.push((hx, hy, hw, hh, [1.0, 1.0, 1.0, 0.06]));
}
quads
}
@@ -1573,8 +1618,8 @@ impl Widget for Toggle {
if let Some(ref label) = self.label {
vec![TextLabel {
text: label.clone(),
- x: self.x - 85.0,
- y: self.y + (self.h - 12.0) / 2.0,
+ x: self.x,
+ y: self.y - 18.0,
font_size: 12.0,
color: [0x83, 0x83, 0x8a],
}]
@@ -1584,17 +1629,112 @@ impl Widget for Toggle {
}
}
+#[derive(Debug, Clone)]
+pub struct Label {
+ x: f32, y: f32, w: f32, h: f32,
+ text: String,
+ font_size: f32,
+ color: [u8; 3],
+ hovered: bool,
+ row_x: f32,
+ row_w: f32,
+}
+
+impl Label {
+ pub fn new(text: &str) -> Self {
+ Self {
+ x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+ text: text.to_string(),
+ font_size: 12.0,
+ color: [0x83, 0x83, 0x8a],
+ hovered: false,
+ row_x: 0.0,
+ row_w: 0.0,
+ }
+ }
+
+ pub fn with_font_size(mut self, size: f32) -> Self {
+ self.font_size = size;
+ self
+ }
+
+ pub fn with_color(mut self, color: [u8; 3]) -> Self {
+ self.color = color;
+ self
+ }
+
+ pub fn set_text(&mut self, text: &str) {
+ self.text = text.to_string();
+ }
+
+ pub fn set_color(&mut self, color: [u8; 3]) {
+ self.color = color;
+ }
+}
+
+impl Widget for Label {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn set_row_rect(&mut self, x: f32, w: f32) { self.row_x = x; self.row_w = w; }
+ fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+
+ fn hit_test(&self, px: f32, py: f32) -> bool {
+ let (x, y, w, h) = self.rect();
+ let hx = if self.row_w > 0.0 { self.row_x } else { x };
+ let hw = if self.row_w > 0.0 { self.row_w } else { w };
+ px >= hx && px <= hx + hw && py >= y && py <= y + h
+ }
+
+ fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ let was = self.hovered;
+ self.hovered = self.hit_test(px, py);
+ was != self.hovered
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ if self.hovered {
+ let hx = if self.row_w > 0.0 { self.row_x } else { self.x };
+ let hw = if self.row_w > 0.0 { self.row_w } else { self.w };
+ quads.push((hx, self.y, hw, self.h, [1.0, 1.0, 1.0, 0.06]));
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ vec![TextLabel {
+ text: self.text.clone(),
+ x: self.x,
+ y: self.y + (self.h - self.font_size) / 2.0,
+ font_size: self.font_size,
+ color: self.color,
+ }]
+ }
+}
+
pub struct Slider {
x: f32, y: f32, w: f32, h: f32,
hovered: bool,
dragging: bool,
value: f32,
drag_offset: f32,
+ scroll_enabled: bool,
}
impl Slider {
pub fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, dragging: false, value: 0.5, drag_offset: 0.0 }
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, dragging: false, value: 0.5, drag_offset: 0.0, scroll_enabled: false }
+ }
+
+ pub fn with_scroll(mut self, enabled: bool) -> Self {
+ self.scroll_enabled = enabled;
+ self
+ }
+
+ pub fn set_scroll(&mut self, enabled: bool) {
+ self.scroll_enabled = enabled;
}
}
@@ -1631,6 +1771,9 @@ impl Widget for Slider {
fn drag_end(&mut self) { self.dragging = false; }
fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
+ if !self.scroll_enabled {
+ return false;
+ }
let (sx, sy, sw, sh) = self.rect();
if px >= sx && px <= sx + sw && py >= sy && py <= sy + sh {
let scroll_amount = match delta {
@@ -1638,7 +1781,7 @@ impl Widget for Slider {
MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
};
let step = 0.02;
- let new_val = (self.value + scroll_amount * step).clamp(0.0, 1.0);
+ let new_val = (self.value - scroll_amount * step).clamp(0.0, 1.0);
if (new_val - self.value).abs() > 0.0001 {
self.value = new_val;
return true;
@@ -1777,11 +1920,12 @@ pub struct Spinbox {
unit: Option<String>,
row_x: f32,
row_w: f32,
+ pub decimals: u32,
}
impl Spinbox {
pub fn new(value: i32, min: i32, max: i32, step: i32) -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value, min, max, step, editing: false, edit_buffer: String::new(), hovered: false, hover_dec: false, hover_inc: false, label: None, unit: None, row_x: 0.0, row_w: 0.0 }
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value, min, max, step, editing: false, edit_buffer: String::new(), hovered: false, hover_dec: false, hover_inc: false, label: None, unit: None, row_x: 0.0, row_w: 0.0, decimals: 0 }
}
pub fn with_label(mut self, label: &str) -> Self {
@@ -1801,6 +1945,11 @@ impl Spinbox {
pub fn set_unit(&mut self, unit: &str) {
self.unit = Some(unit.to_string());
}
+
+ pub fn with_decimals(mut self, decimals: u32) -> Self {
+ self.decimals = decimals;
+ self
+ }
}
impl Widget for Spinbox {
@@ -1872,14 +2021,27 @@ impl Widget for Spinbox {
fn focus(&mut self) {
self.editing = true;
- self.edit_buffer = self.value.to_string();
+ if self.decimals > 0 {
+ let divisor = 10.0f32.powi(self.decimals as i32);
+ self.edit_buffer = format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize);
+ } else {
+ self.edit_buffer = self.value.to_string();
+ }
}
fn unfocus(&mut self) {
if self.editing {
self.editing = false;
- if let Ok(val) = self.edit_buffer.parse::<i32>() {
- self.value = val.clamp(self.min, self.max);
+ if self.decimals > 0 {
+ if let Ok(val_f) = self.edit_buffer.parse::<f32>() {
+ let divisor = 10.0f32.powi(self.decimals as i32);
+ self.value = (val_f * divisor).round() as i32;
+ self.value = self.value.clamp(self.min, self.max);
+ }
+ } else {
+ if let Ok(val) = self.edit_buffer.parse::<i32>() {
+ self.value = val.clamp(self.min, self.max);
+ }
}
}
}
@@ -1893,8 +2055,16 @@ impl Widget for Spinbox {
true
}
Key::Named(NamedKey::Enter) => {
- if let Ok(val) = self.edit_buffer.parse::<i32>() {
- self.value = val.clamp(self.min, self.max);
+ if self.decimals > 0 {
+ if let Ok(val_f) = self.edit_buffer.parse::<f32>() {
+ let divisor = 10.0f32.powi(self.decimals as i32);
+ self.value = (val_f * divisor).round() as i32;
+ self.value = self.value.clamp(self.min, self.max);
+ }
+ } else {
+ if let Ok(val) = self.edit_buffer.parse::<i32>() {
+ self.value = val.clamp(self.min, self.max);
+ }
}
self.editing = false;
true
@@ -1909,6 +2079,7 @@ impl Widget for Spinbox {
for ch in text.chars() {
match ch {
'-' if self.edit_buffer.is_empty() => self.edit_buffer.push('-'),
+ '.' if self.decimals > 0 && !self.edit_buffer.contains('.') => self.edit_buffer.push('.'),
'0'..='9' => self.edit_buffer.push(ch),
_ => {}
}
@@ -1940,9 +2111,37 @@ impl Widget for Spinbox {
let btn_w = self.w * 0.225;
let inc_col = if self.hover_inc { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
let dec_col = if self.hover_dec { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
- quads.push((self.x, self.y, self.w * 0.55, self.h, colors::SPINBOX_DISPLAY));
+
+ let display_bg = if self.editing {
+ [0.06, 0.10, 0.18, 1.0] // Focused dark-blue input field look
+ } else {
+ colors::SPINBOX_DISPLAY
+ };
+
+ quads.push((self.x, self.y, self.w * 0.55, self.h, display_bg));
quads.push((split, self.y, btn_w, self.h, dec_col));
quads.push((split + btn_w, self.y, btn_w, self.h, inc_col));
+
+ if self.editing {
+ let border_color = [0.20, 0.50, 0.85, 1.0]; // Bright focused blue border
+ // Top border
+ quads.push((self.x, self.y, self.w * 0.55, 1.0, border_color));
+ // Bottom border
+ quads.push((self.x, self.y + self.h - 1.0, self.w * 0.55, 1.0, border_color));
+ // Left border
+ quads.push((self.x, self.y, 1.0, self.h, border_color));
+ // Right border
+ quads.push((self.x + self.w * 0.55 - 1.0, self.y, 1.0, self.h, border_color));
+
+ // Caret cursor
+ let char_width = 8.0;
+ let cursor_x = self.x + 6.0 + (self.edit_buffer.len() as f32 * char_width);
+ let max_cursor_x = split - 4.0;
+ let final_cursor_x = cursor_x.min(max_cursor_x);
+ let cursor_y = self.y + (self.h - 14.0) / 2.0;
+ quads.push((final_cursor_x, cursor_y, 1.5, 14.0, [0.80, 0.80, 0.85, 1.0]));
+ }
+
quads
}
@@ -1957,7 +2156,14 @@ impl Widget for Spinbox {
color: [0x83, 0x83, 0x8a],
});
}
- let value_text = if self.editing { self.edit_buffer.clone() } else { self.value.to_string() };
+ let value_text = if self.editing {
+ self.edit_buffer.clone()
+ } else if self.decimals > 0 {
+ let divisor = 10.0f32.powi(self.decimals as i32);
+ format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize)
+ } else {
+ self.value.to_string()
+ };
labels.push(TextLabel {
text: value_text,
x: self.x + 4.0,
@@ -2003,17 +2209,34 @@ pub struct ColorSelector {
label: Option<String>,
row_x: f32,
row_w: f32,
+ pub command: String,
}
impl ColorSelector {
pub fn new(color: [u8; 3]) -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, color, just_clicked: false, hovered: false, editing: false, edit_buffer: String::new(), label: None, row_x: 0.0, row_w: 0.0 }
+ Self {
+ x: 0.0, y: 0.0, w: 0.0, h: 0.0,
+ color,
+ just_clicked: false,
+ hovered: false,
+ editing: false,
+ edit_buffer: String::new(),
+ label: None,
+ row_x: 0.0,
+ row_w: 0.0,
+ command: "clear-color-interface".to_string(),
+ }
}
pub fn with_label(mut self, label: &str) -> Self {
self.label = Some(label.to_string());
self
}
+
+ pub fn with_command(mut self, command: &str) -> Self {
+ self.command = command.to_string();
+ self
+ }
}
impl Widget for ColorSelector {
@@ -2053,6 +2276,18 @@ impl Widget for ColorSelector {
if !self.hit_test(px, py) { return false; }
if px >= self.x + self.w * 0.65 {
self.just_clicked = true;
+ let hex = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+ if let Ok(output) = std::process::Command::new(&self.command)
+ .arg(&hex)
+ .output()
+ {
+ if output.status.success() {
+ let stdout_str = String::from_utf8_lossy(&output.stdout);
+ if let Some(new_color) = parse_hex(stdout_str.trim()) {
+ self.color = new_color;
+ }
+ }
+ }
return true;
}
self.focus();