GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Implement new Houdini-style Ramp control element and styling keys
src/color.rs | 18 +++
src/layout.rs | 30 ++++
src/widget/input/mod.rs | 2 +
src/widget/input/ramp.rs | 371 +++++++++++++++++++++++++++++++++++++++++++++++
src/widget/mod.rs | 2 +-
5 files changed, 422 insertions(+), 1 deletion(-)
diff --git a/src/color.rs b/src/color.rs
index 8c257f7..62cff78 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -63,6 +63,8 @@ static BACKPLATE_STATUSBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.06, 0.
static BACKPLATE_STATUSBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.6666, 0.6666, 0.7333, 1.0]);
static BACKPLATE_STATUSBAR_BLUR: RwLock<bool> = RwLock::new(false);
static BUTTON_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new(BUTTON_IDLE);
+static RAMP_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
+static RAMP_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
static TREE_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
static TREE_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
@@ -317,6 +319,12 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/layout/toggle_bg_color") {
if let Ok(mut lock) = TOGGLE_BG_COLOR.write() { *lock = c; }
}
+ if let Some(c) = get_color("/style/control/ramp/background") {
+ if let Ok(mut lock) = RAMP_BACKGROUND_COLOR.write() { *lock = c; }
+ }
+ if let Some(c) = get_color("/style/control/ramp/border_color") {
+ if let Ok(mut lock) = RAMP_BORDER_COLOR.write() { *lock = c; }
+ }
let parsed_list_bg = get_color("/layout/list_bg_color");
let parsed_breadcrumb_bg = get_color("/layout/breadcrumb_bg_color");
let parsed_popover_bg = get_color("/layout/popover_bg_color");
@@ -965,6 +973,16 @@ pub fn backplate_corner_radius() -> f32 {
*BACKPLATE_CORNER_RADIUS.read().unwrap()
}
+pub fn ramp_background_color() -> [f32; 4] {
+ load_colors_once();
+ *RAMP_BACKGROUND_COLOR.read().unwrap()
+}
+
+pub fn ramp_border_color() -> [f32; 4] {
+ load_colors_once();
+ *RAMP_BORDER_COLOR.read().unwrap()
+}
+
pub fn set_backplate_corner_radius(radius: f32) {
if let Ok(mut lock) = BACKPLATE_CORNER_RADIUS.write() {
*lock = radius;
diff --git a/src/layout.rs b/src/layout.rs
index 7149f6c..f0f92b1 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -104,6 +104,7 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.control.toggle.border_width" => "toggle_border_width",
"style.control.toggle.disabled_color" => "toggle_disabled_color",
"style.control.toggle.corner_radius" => "toggle_corner_radius",
+ "style.control.ramp.height" => "ramp_height",
"style.status.normal_color" => "status_normal_color",
"style.status.background_color" => "status_background_color",
"style.status.background_blur" => "status_background_blur",
@@ -244,6 +245,7 @@ static BREADCRUMB_FONT: RwLock<String> = RwLock::new(String::new());
static PAGINATOR_TAB_PADDING_X: RwLock<f32> = RwLock::new(10.0);
static BUTTON_PADDING: RwLock<f32> = RwLock::new(14.0);
static BUTTON_HEIGHT: RwLock<f32> = RwLock::new(40.0);
+static RAMP_HEIGHT: RwLock<f32> = RwLock::new(32.0);
static BUTTON_STRIP_SPACING: RwLock<f32> = RwLock::new(8.0);
static SCROLLBAR_WIDTH: RwLock<f32> = RwLock::new(4.0);
static TREE_OPACITY: RwLock<f32> = RwLock::new(1.0);
@@ -3102,6 +3104,34 @@ pub fn button_height() -> f32 {
*BUTTON_HEIGHT.read().unwrap()
}
+pub fn ramp_height() -> 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("ramp_height") {
+ 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) = RAMP_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *RAMP_HEIGHT.read().unwrap()
+}
+
+pub fn set_ramp_height(height: f32) {
+ if let Ok(mut lock) = RAMP_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
pub fn set_button_height(height: f32) {
if let Ok(mut lock) = BUTTON_HEIGHT.write() {
*lock = height;
diff --git a/src/widget/input/mod.rs b/src/widget/input/mod.rs
index 1b33d63..1ad13fd 100644
--- a/src/widget/input/mod.rs
+++ b/src/widget/input/mod.rs
@@ -12,6 +12,7 @@ pub mod button_strip;
pub mod multi_control;
pub mod keybinds_control;
pub mod keybind_recorder;
+pub mod ramp;
pub use canvas::Canvas;
pub use button::{Button, ButtonKind, PageButton};
@@ -27,6 +28,7 @@ pub use button_strip::ButtonStrip;
pub use multi_control::{MultiControl, InstancedControl, InstancedWidget, MultiControlRow};
pub use keybinds_control::{KeybindsControl, KeybindRow};
pub use keybind_recorder::KeybindRecorder;
+pub use ramp::{Ramp, RampKey};
pub const BREADCRUMB_PADDING: f32 = 8.0;
pub const SEGMENT_GAP: f32 = 4.0;
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
new file mode 100644
index 0000000..19650c3
--- /dev/null
+++ b/src/widget/input/ramp.rs
@@ -0,0 +1,371 @@
+use crate::colors;
+use crate::widget::*;
+use crate::widget::input::{Slider, Button};
+
+#[derive(Debug, Clone)]
+pub struct RampKey {
+ pub pos: f32,
+ pub color: [f32; 3],
+}
+
+pub struct Ramp {
+ pub base: Widget,
+ pub keys: Vec<RampKey>,
+ pub selected_key_idx: Option<usize>,
+ pub is_dragging_key: bool,
+
+ // Child controls for color editing & deletion
+ pub r_slider: Slider,
+ pub g_slider: Slider,
+ pub b_slider: Slider,
+ pub del_button: Button,
+
+ pub parent: Option<*mut (dyn Element + 'static)>,
+}
+
+impl Ramp {
+ pub fn new() -> Self {
+ let keys = vec![
+ RampKey { pos: 0.0, color: [0.0, 0.0, 0.0] },
+ RampKey { pos: 1.0, color: [1.0, 1.0, 1.0] },
+ ];
+
+ let r_slider = Slider::new().with_label("Red");
+ let g_slider = Slider::new().with_label("Green");
+ let b_slider = Slider::new().with_label("Blue");
+ let del_button = Button::new(0.0, 0.0, 70.0, 28.0).with_label("Delete Key");
+
+ Self {
+ base: Widget::new(),
+ keys,
+ selected_key_idx: None,
+ is_dragging_key: false,
+ r_slider,
+ g_slider,
+ b_slider,
+ del_button,
+ parent: None,
+ }
+ }
+
+ // Linear color interpolation helper
+ pub fn get_interpolated_color(&self, t: f32) -> [f32; 3] {
+ if self.keys.is_empty() {
+ return [0.0, 0.0, 0.0];
+ }
+ if t <= self.keys[0].pos {
+ return self.keys[0].color;
+ }
+ if t >= self.keys[self.keys.len() - 1].pos {
+ return self.keys[self.keys.len() - 1].color;
+ }
+
+ for i in 0..self.keys.len() - 1 {
+ let k1 = &self.keys[i];
+ let k2 = &self.keys[i+1];
+ if t >= k1.pos && t <= k2.pos {
+ let range = k2.pos - k1.pos;
+ if range.abs() < 0.0001 {
+ return k1.color;
+ }
+ let w = (t - k1.pos) / range;
+ return [
+ k1.color[0] * (1.0 - w) + k2.color[0] * w,
+ k1.color[1] * (1.0 - w) + k2.color[1] * w,
+ k1.color[2] * (1.0 - w) + k2.color[2] * w,
+ ];
+ }
+ }
+ self.keys[0].color
+ }
+
+ fn sort_keys(&mut self) {
+ let prev_selected_id = self.selected_key_idx.map(|idx| self.keys[idx].pos);
+ self.keys.sort_by(|a, b| a.pos.partial_cmp(&b.pos).unwrap());
+ if let Some(pos) = prev_selected_id {
+ if let Some(new_idx) = self.keys.iter().position(|k| (k.pos - pos).abs() < 0.0001) {
+ self.selected_key_idx = Some(new_idx);
+ }
+ }
+ }
+}
+
+impl Element for Ramp {
+ crate::impl_widget_base!(Ramp);
+
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.parent }
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+ self.parent = parent;
+ }
+
+ fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ let self_ptr = self as *const Self as *mut Self;
+ unsafe {
+ vec![
+ &mut (*self_ptr).r_slider as *mut Slider as *mut (dyn Element + 'static),
+ &mut (*self_ptr).g_slider as *mut Slider as *mut (dyn Element + 'static),
+ &mut (*self_ptr).b_slider as *mut Slider as *mut (dyn Element + 'static),
+ &mut (*self_ptr).del_button as *mut Button as *mut (dyn Element + 'static),
+ ]
+ }
+ }
+
+ fn color(&self) -> [f32; 4] {
+ colors::ramp_background_color()
+ }
+
+ 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;
+
+ let self_ptr = self.as_ptr_mut();
+ let mut dummy = crate::context::UiContext::new();
+ self.r_slider.set_parent(Some(self_ptr), &mut dummy);
+ self.g_slider.set_parent(Some(self_ptr), &mut dummy);
+ self.b_slider.set_parent(Some(self_ptr), &mut dummy);
+ self.del_button.set_parent(Some(self_ptr), &mut dummy);
+
+ let th = crate::layout::ramp_height();
+ let sy = y + th + 45.0;
+ let slider_w = w - 100.0;
+
+ if self.selected_key_idx.is_some() {
+ self.r_slider.set_rect(x + 10.0, sy, slider_w, 20.0);
+ self.g_slider.set_rect(x + 10.0, sy + 25.0, slider_w, 20.0);
+ self.b_slider.set_rect(x + 10.0, sy + 50.0, slider_w, 20.0);
+ self.del_button.set_rect(x + w - 80.0, sy + 20.0, 70.0, 28.0);
+ } else {
+ self.r_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
+ self.g_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
+ self.b_slider.set_rect(-1000.0, -1000.0, 0.0, 0.0);
+ self.del_button.set_rect(-1000.0, -1000.0, 0.0, 0.0);
+ }
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ let th = crate::layout::ramp_height();
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+
+ // Draw track border
+ let border_color = colors::ramp_border_color();
+ quads.push((track_x - 1.0, self.base.y - 1.0, track_w + 2.0, th + 2.0, border_color));
+
+ // Draw interpolated track slices (e.g. 100 slices)
+ let slices = 100;
+ let slice_w = track_w / slices as f32;
+ for i in 0..slices {
+ let t1 = i as f32 / slices as f32;
+ let t2 = (i + 1) as f32 / slices as f32;
+ let center_t = (t1 + t2) / 2.0;
+ let col = self.get_interpolated_color(center_t);
+ let sx = track_x + t1 * track_w;
+ quads.push((sx, self.base.y, slice_w, th, [col[0], col[1], col[2], 1.0]));
+ }
+
+ // Return child quads if selected
+ if self.selected_key_idx.is_some() {
+ let ctx_dummy = crate::context::UiContext::new();
+ quads.extend(self.r_slider.all_quads(&ctx_dummy));
+ quads.extend(self.g_slider.all_quads(&ctx_dummy));
+ quads.extend(self.b_slider.all_quads(&ctx_dummy));
+ quads.extend(self.del_button.all_quads(&ctx_dummy));
+ }
+
+ quads
+ }
+
+ fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
+ let mut circles = Vec::new();
+ let th = crate::layout::ramp_height();
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+ let py = self.base.y + th + 15.0;
+
+ for (idx, key) in self.keys.iter().enumerate() {
+ let cx = track_x + key.pos * track_w;
+
+ // Draw peg outline / shadow
+ circles.push((cx, py, 7.0, [0.0, 0.0, 0.0, 0.8]));
+
+ // Draw peg color preview
+ circles.push((cx, py, 6.0, [key.color[0], key.color[1], key.color[2], 1.0]));
+
+ // Highlight selected peg
+ if Some(idx) == self.selected_key_idx {
+ circles.push((cx, py, 8.0, [0.49, 1.0, 1.0, 0.5]));
+ }
+ }
+
+ circles
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py_event: f32, ctx: &mut UiContext) -> bool {
+ if button != MouseButton::Left { return false; }
+
+ let th = crate::layout::ramp_height();
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+ let py_peg = self.base.y + th + 15.0;
+
+ if state == ElementState::Pressed {
+ // 1. Check peg click selection/dragging
+ for (idx, key) in self.keys.iter().enumerate() {
+ let cx = track_x + key.pos * track_w;
+ let dx = px - cx;
+ let dy = py_event - py_peg;
+ if (dx*dx + dy*dy) <= 64.0 { // hit circle radius 8.0
+ self.selected_key_idx = Some(idx);
+ self.is_dragging_key = true;
+
+ // Update slider values to match key color
+ self.r_slider.set_value(key.color[0]);
+ self.g_slider.set_value(key.color[1]);
+ self.b_slider.set_value(key.color[2]);
+
+ self.set_rect(self.base.x, self.base.y, self.base.w, self.base.h);
+ return true;
+ }
+ }
+
+ // 2. Check track click to add key
+ if px >= track_x && px <= track_x + track_w && py_event >= self.base.y && py_event <= self.base.y + th {
+ let t = (px - track_x) / track_w;
+ let col = self.get_interpolated_color(t);
+ let new_key = RampKey { pos: t, color: col };
+ self.keys.push(new_key);
+ self.sort_keys();
+
+ // Select newly added key
+ if let Some(new_idx) = self.keys.iter().position(|k| (k.pos - t).abs() < 0.0001) {
+ self.selected_key_idx = Some(new_idx);
+ self.r_slider.set_value(col[0]);
+ self.g_slider.set_value(col[1]);
+ self.b_slider.set_value(col[2]);
+ }
+
+ self.set_rect(self.base.x, self.base.y, self.base.w, self.base.h);
+ return true;
+ }
+
+ // 3. Delegate to slider / button click
+ if self.selected_key_idx.is_some() {
+ if self.r_slider.mouse_input(button, state, px, py_event, ctx) { return true; }
+ if self.g_slider.mouse_input(button, state, px, py_event, ctx) { return true; }
+ if self.b_slider.mouse_input(button, state, px, py_event, ctx) { return true; }
+ if self.del_button.mouse_input(button, state, px, py_event, ctx) {
+ // Check if clicked
+ if self.del_button.take_click() {
+ if let Some(idx) = self.selected_key_idx {
+ if self.keys.len() > 2 {
+ self.keys.remove(idx);
+ self.selected_key_idx = None;
+ self.set_rect(self.base.x, self.base.y, self.base.w, self.base.h);
+ }
+ }
+ }
+ return true;
+ }
+ }
+ } else {
+ // Mouse released
+ self.is_dragging_key = false;
+
+ if self.selected_key_idx.is_some() {
+ self.r_slider.mouse_input(button, state, px, py_event, ctx);
+ self.g_slider.mouse_input(button, state, px, py_event, ctx);
+ self.b_slider.mouse_input(button, state, px, py_event, ctx);
+ if self.del_button.mouse_input(button, state, px, py_event, ctx) {
+ if self.del_button.take_click() {
+ if let Some(idx) = self.selected_key_idx {
+ if self.keys.len() > 2 {
+ self.keys.remove(idx);
+ self.selected_key_idx = None;
+ self.set_rect(self.base.x, self.base.y, self.base.w, self.base.h);
+ }
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ false
+ }
+
+ fn cursor_moved(&mut self, px: f32, py_event: f32, ctx: &mut UiContext) -> bool {
+ let mut changed = false;
+
+ let _th = crate::layout::ramp_height();
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+
+ // 1. Update key dragging
+ if self.is_dragging_key {
+ if let Some(idx) = self.selected_key_idx {
+ let t = ((px - track_x) / track_w).clamp(0.0, 1.0);
+ self.keys[idx].pos = t;
+ self.sort_keys();
+ changed = true;
+ }
+ }
+
+ // 2. Delegate to sliders / buttons
+ if self.selected_key_idx.is_some() {
+ if self.r_slider.cursor_moved(px, py_event, ctx) {
+ if let Some(idx) = self.selected_key_idx {
+ self.keys[idx].color[0] = self.r_slider.value();
+ changed = true;
+ }
+ }
+ if self.g_slider.cursor_moved(px, py_event, ctx) {
+ if let Some(idx) = self.selected_key_idx {
+ self.keys[idx].color[1] = self.g_slider.value();
+ changed = true;
+ }
+ }
+ if self.b_slider.cursor_moved(px, py_event, ctx) {
+ if let Some(idx) = self.selected_key_idx {
+ self.keys[idx].color[2] = self.b_slider.value();
+ changed = true;
+ }
+ }
+ if self.del_button.cursor_moved(px, py_event, ctx) {
+ changed = true;
+ }
+ }
+
+ changed
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ let mut labels = Vec::new();
+ if self.selected_key_idx.is_some() {
+ labels.extend(self.r_slider.text_labels_with_font_and_bounds(ctx));
+ labels.extend(self.g_slider.text_labels_with_font_and_bounds(ctx));
+ labels.extend(self.b_slider.text_labels_with_font_and_bounds(ctx));
+ labels.extend(self.del_button.text_labels_with_font_and_bounds(ctx));
+ }
+ labels
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ let mut items = Vec::new();
+ if self.selected_key_idx.is_some() {
+ items.extend(self.r_slider.get_text_items());
+ items.extend(self.g_slider.get_text_items());
+ items.extend(self.b_slider.get_text_items());
+ items.extend(self.del_button.get_text_items());
+ }
+ items
+ }
+}
+
+impl Drop for Ramp {
+ fn drop(&mut self) {
+ clear_widget_references(self);
+ }
+}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index d055da2..afe0207 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -679,7 +679,7 @@ pub use self::input::{
Button, TextBox, Spinbox, Dropdown, Checkbox, Toggle, Slider, RangeSlider,
ColorSelector, Finger, Trackpad, Canvas, get_font_db, ActiveThumb, FontSelector,
ButtonStrip, MultiControl, InstancedControl, InstancedWidget, MultiControlRow,
- KeybindsControl, KeybindRow, KeybindRecorder
+ KeybindsControl, KeybindRow, KeybindRecorder, Ramp, RampKey
};
pub use self::container::{
Container, ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,