GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Rename color ramp to ColorRamp and implement Houdini-style float Ramp
src/widget/input/mod.rs | 2 +-
src/widget/input/ramp.rs | 374 ++++++++++++++++++++++++++++++++++++++++++-----
src/widget/mod.rs | 2 +-
3 files changed, 341 insertions(+), 37 deletions(-)
diff --git a/src/widget/input/mod.rs b/src/widget/input/mod.rs
index 1ad13fd..804f23a 100644
--- a/src/widget/input/mod.rs
+++ b/src/widget/input/mod.rs
@@ -28,7 +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 use ramp::{Ramp, RampKey, ColorRamp, ColorRampKey};
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
index 19650c3..f1f3e47 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -2,15 +2,19 @@ use crate::colors;
use crate::widget::*;
use crate::widget::input::{Slider, Button};
+// ==========================================
+// 1. Color Ramp (renamed from Ramp)
+// ==========================================
+
#[derive(Debug, Clone)]
-pub struct RampKey {
+pub struct ColorRampKey {
pub pos: f32,
pub color: [f32; 3],
}
-pub struct Ramp {
+pub struct ColorRamp {
pub base: Widget,
- pub keys: Vec<RampKey>,
+ pub keys: Vec<ColorRampKey>,
pub selected_key_idx: Option<usize>,
pub is_dragging_key: bool,
@@ -23,11 +27,11 @@ pub struct Ramp {
pub parent: Option<*mut (dyn Element + 'static)>,
}
-impl Ramp {
+impl ColorRamp {
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] },
+ ColorRampKey { pos: 0.0, color: [0.0, 0.0, 0.0] },
+ ColorRampKey { pos: 1.0, color: [1.0, 1.0, 1.0] },
];
let r_slider = Slider::new().with_label("Red");
@@ -48,7 +52,6 @@ impl Ramp {
}
}
- // 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];
@@ -90,8 +93,8 @@ impl Ramp {
}
}
-impl Element for Ramp {
- crate::impl_widget_base!(Ramp);
+impl Element for ColorRamp {
+ crate::impl_widget_base!(ColorRamp);
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) {
@@ -166,7 +169,6 @@ impl Element for Ramp {
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));
@@ -187,14 +189,8 @@ impl Element for Ramp {
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]));
}
@@ -212,52 +208,43 @@ impl Element for Ramp {
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
+ if (dx*dx + dy*dy) <= 64.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 };
+ let new_key = ColorRampKey { 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 {
@@ -271,9 +258,7 @@ impl Element for Ramp {
}
}
} 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);
@@ -292,18 +277,14 @@ impl Element for Ramp {
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);
@@ -313,7 +294,6 @@ impl Element for Ramp {
}
}
- // 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 {
@@ -337,7 +317,6 @@ impl Element for Ramp {
changed = true;
}
}
-
changed
}
@@ -364,6 +343,331 @@ impl Element for Ramp {
}
}
+impl Drop for ColorRamp {
+ fn drop(&mut self) {
+ clear_widget_references(self);
+ }
+}
+
+
+// ==========================================
+// 2. Houdini-Style Float Ramp
+// ==========================================
+
+#[derive(Debug, Clone)]
+pub struct RampKey {
+ pub pos: f32,
+ pub value: f32,
+}
+
+pub struct Ramp {
+ pub base: Widget,
+ pub keys: Vec<RampKey>,
+ pub selected_key_idx: Option<usize>,
+ pub is_dragging_key: bool,
+
+ // Child controls for value editing & deletion
+ pub val_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, value: 0.0 },
+ RampKey { pos: 1.0, value: 1.0 },
+ ];
+
+ let val_slider = Slider::new().with_label("Value");
+ 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,
+ val_slider,
+ del_button,
+ parent: None,
+ }
+ }
+
+ pub fn get_interpolated_value(&self, t: f32) -> f32 {
+ if self.keys.is_empty() {
+ return 0.0;
+ }
+ if t <= self.keys[0].pos {
+ return self.keys[0].value;
+ }
+ if t >= self.keys[self.keys.len() - 1].pos {
+ return self.keys[self.keys.len() - 1].value;
+ }
+
+ 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.value;
+ }
+ let w = (t - k1.pos) / range;
+ return k1.value * (1.0 - w) + k2.value * w;
+ }
+ }
+ self.keys[0].value
+ }
+
+ 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).val_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.val_slider.set_parent(Some(self_ptr), &mut dummy);
+ self.del_button.set_parent(Some(self_ptr), &mut dummy);
+
+ let gh = 80.0;
+ let sy = y + gh + 15.0;
+ let slider_w = w - 100.0;
+
+ if self.selected_key_idx.is_some() {
+ self.val_slider.set_rect(x + 10.0, sy, slider_w, 20.0);
+ self.del_button.set_rect(x + w - 80.0, sy - 4.0, 70.0, 28.0);
+ } else {
+ self.val_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 gh = 80.0;
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+
+ // Draw graph background
+ quads.push((track_x, self.base.y, track_w, gh, [0.15, 0.15, 0.18, 1.0]));
+ // Border
+ quads.push((track_x - 1.0, self.base.y - 1.0, track_w + 2.0, gh + 2.0, colors::ramp_border_color()));
+
+ // Draw grid lines
+ for ratio in [0.25, 0.5, 0.75] {
+ let gy = self.base.y + gh * (1.0 - ratio);
+ quads.push((track_x, gy, track_w, 1.0, [0.25, 0.25, 0.28, 0.5]));
+ }
+ for ratio in [0.25, 0.5, 0.75] {
+ let gx = track_x + track_w * ratio;
+ quads.push((gx, self.base.y, 1.0, gh, [0.25, 0.25, 0.28, 0.5]));
+ }
+
+ // Curve area fill and outline
+ let slices = 80;
+ let slice_w = track_w / slices as f32;
+ for i in 0..slices {
+ let t1 = i as f32 / slices as f32;
+ let v1 = self.get_interpolated_value(t1);
+ let sx1 = track_x + t1 * track_w;
+
+ let slice_h = v1 * gh;
+ let sy = self.base.y + gh - slice_h;
+ quads.push((sx1, sy, slice_w, slice_h, [0.3, 0.45, 0.6, 0.25]));
+
+ let outline_h = 2.0;
+ let outline_y = self.base.y + gh - v1 * gh - 1.0;
+ quads.push((sx1, outline_y, slice_w, outline_h, [0.5, 0.75, 1.0, 1.0]));
+ }
+
+ if self.selected_key_idx.is_some() {
+ let ctx_dummy = crate::context::UiContext::new();
+ quads.extend(self.val_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 gh = 80.0;
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+
+ for (idx, key) in self.keys.iter().enumerate() {
+ let cx = track_x + key.pos * track_w;
+ let cy = self.base.y + gh - key.value * gh;
+
+ circles.push((cx, cy, 7.0, [0.0, 0.0, 0.0, 0.8]));
+ circles.push((cx, cy, 5.0, [0.5, 0.75, 1.0, 1.0]));
+ if Some(idx) == self.selected_key_idx {
+ circles.push((cx, cy, 9.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 gh = 80.0;
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+
+ if state == ElementState::Pressed {
+ for (idx, key) in self.keys.iter().enumerate() {
+ let cx = track_x + key.pos * track_w;
+ let cy = self.base.y + gh - key.value * gh;
+ let dx = px - cx;
+ let dy = py_event - cy;
+ if (dx*dx + dy*dy) <= 64.0 {
+ self.selected_key_idx = Some(idx);
+ self.is_dragging_key = true;
+ self.val_slider.set_value(key.value);
+ self.set_rect(self.base.x, self.base.y, self.base.w, self.base.h);
+ return true;
+ }
+ }
+
+ if px >= track_x && px <= track_x + track_w && py_event >= self.base.y && py_event <= self.base.y + gh {
+ let t = (px - track_x) / track_w;
+ let val = 1.0 - (py_event - self.base.y) / gh;
+ let new_key = RampKey { pos: t, value: val };
+ self.keys.push(new_key);
+ self.sort_keys();
+
+ if let Some(new_idx) = self.keys.iter().position(|k| (k.pos - t).abs() < 0.0001) {
+ self.selected_key_idx = Some(new_idx);
+ self.val_slider.set_value(val);
+ }
+ self.set_rect(self.base.x, self.base.y, self.base.w, self.base.h);
+ return true;
+ }
+
+ if self.selected_key_idx.is_some() {
+ if self.val_slider.mouse_input(button, state, px, py_event, ctx) { return true; }
+ 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;
+ }
+ }
+ } else {
+ self.is_dragging_key = false;
+ if self.selected_key_idx.is_some() {
+ self.val_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 gh = 80.0;
+ let track_x = self.base.x + 10.0;
+ let track_w = self.base.w - 20.0;
+
+ 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);
+ let val = (1.0 - (py_event - self.base.y) / gh).clamp(0.0, 1.0);
+ self.keys[idx].pos = t;
+ self.keys[idx].value = val;
+ self.val_slider.set_value(val);
+ self.sort_keys();
+ changed = true;
+ }
+ }
+
+ if self.selected_key_idx.is_some() {
+ if self.val_slider.cursor_moved(px, py_event, ctx) {
+ if let Some(idx) = self.selected_key_idx {
+ self.keys[idx].value = self.val_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.val_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.val_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 afe0207..85b779e 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, Ramp, RampKey
+ KeybindsControl, KeybindRow, KeybindRecorder, Ramp, RampKey, ColorRamp, ColorRampKey
};
pub use self::container::{
Container, ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,