GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Fix color space handling, add Oklab conversions and fix notifier/designer color mismatches
src/backend/window_runner.rs | 47 +++++++
src/color.rs | 238 ++++++++++++++++++++++++++++++----
src/config.rs | 164 +++++++++++++++++++++--
src/context.rs | 8 ++
src/engine.rs | 2 +-
src/layout.rs | 145 +++++++++++++++++++++
src/widget/container/content_bg.rs | 1 +
src/widget/container/parameters_bg.rs | 2 +-
src/widget/container/treelist.rs | 19 +++
src/widget/display/graph.rs | 118 +++++++++++------
src/widget/input/dropdown.rs | 42 ++++--
src/widget/input/keybind_recorder.rs | 237 +++++++++++++++++++++++++++++++++
src/widget/input/mod.rs | 2 +
src/widget/mod.rs | 7 +-
14 files changed, 948 insertions(+), 84 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 8dc46c9..ccd7db2 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -932,6 +932,15 @@ pub fn push_extra_quad_vertices(
clip_circle: [f32; 3],
out: &mut Vec<Vertex>,
) {
+ if let Some(graph) = w.as_any().downcast_ref::<crate::widget::display::Graph>() {
+ if graph.is_node_rect(qx, qy, qw, qh) {
+ let r = crate::layout::graph_node_corner_radius();
+ let extra_radii = crate::widget::CornerRadii::new(r, r, r, r);
+ push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, None, out);
+ return;
+ }
+ }
+
let target_w = get_child_widget_for_quad(w, qx, qy, qw, qh);
let radii = target_w.corner_radii();
if radii.top_left <= 0.1 && radii.top_right <= 0.1 && radii.bottom_right <= 0.1 && radii.bottom_left <= 0.1 {
@@ -991,6 +1000,15 @@ pub fn push_extra_quad_vertices_clipped(
clip_circle: [f32; 3],
out: &mut Vec<Vertex>,
) {
+ if let Some(graph) = w.as_any().downcast_ref::<crate::widget::display::Graph>() {
+ if graph.is_node_rect(qx, qy, qw, qh) {
+ let r = crate::layout::graph_node_corner_radius();
+ let extra_radii = crate::widget::CornerRadii::new(r, r, r, r);
+ push_rounded_rect_vertices_corners(qx, qy, qw, qh, extra_radii, sw, sh, qc, clip_circle, Some(clip), out);
+ return;
+ }
+ }
+
let target_w = get_child_widget_for_quad(w, qx, qy, qw, qh);
let radii = target_w.corner_radii();
if radii.top_left <= 0.1 && radii.top_right <= 0.1 && radii.bottom_right <= 0.1 && radii.bottom_left <= 0.1 {
@@ -1223,6 +1241,10 @@ pub trait Application: Sized + 'static {
None
}
+ fn ui_context_mut(&mut self) -> Option<&mut crate::context::UiContext> {
+ None
+ }
+
fn is_movable_backplate_at(&self, px: f32, py: f32) -> bool {
if let Some(ctx) = self.ui_context() {
ctx.is_movable_backplate_at(px, py)
@@ -1369,6 +1391,8 @@ pub struct EngineState<A: Application> {
pub first_configure_received: bool,
pub ctrl_pressed: bool,
pub shift_pressed: bool,
+ pub alt_pressed: bool,
+ pub logo_pressed: bool,
pub pressed_key: Option<PressedKey>,
pub sender: calloop::channel::Sender<A::Message>,
pub active_popup: Option<ActivePopup>,
@@ -2274,6 +2298,8 @@ impl<A: Application> KeyboardHandler for EngineState<A> {
) {
self.ctrl_pressed = modifiers.ctrl;
self.shift_pressed = modifiers.shift;
+ self.alt_pressed = modifiers.alt;
+ self.logo_pressed = modifiers.logo;
}
fn update_repeat_info(
@@ -2310,6 +2336,10 @@ impl<A: Application> EngineState<A> {
xkeysym::Keysym::Page_Down => Key::Named(NamedKey::PageDown),
xkeysym::Keysym::Home => Key::Named(NamedKey::Home),
xkeysym::Keysym::End => Key::Named(NamedKey::End),
+ xkeysym::Keysym::Super_L | xkeysym::Keysym::Super_R => Key::Named(NamedKey::Super),
+ 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),
_ => {
if let Some(ref text) = event.utf8 {
Key::Character(text.clone())
@@ -2349,6 +2379,13 @@ impl<A: Application> EngineState<A> {
}
}
+ if let Some(ctx) = self.inner.ui_context_mut() {
+ ctx.ctrl_pressed = self.ctrl_pressed;
+ ctx.shift_pressed = self.shift_pressed;
+ ctx.alt_pressed = self.alt_pressed;
+ ctx.logo_pressed = self.logo_pressed;
+ }
+
let mut rebuild = false;
if let Some(msg) = self.inner.handle_key_input(&custom_event, &mut rebuild) {
let mut update_rebuild = false;
@@ -2494,6 +2531,8 @@ pub fn run<A: Application>() {
first_configure_received: false,
ctrl_pressed: false,
shift_pressed: false,
+ alt_pressed: false,
+ logo_pressed: false,
pressed_key: None,
sender,
active_popup: None,
@@ -2594,6 +2633,14 @@ pub fn run<A: Application>() {
ctrl: engine_state.ctrl_pressed,
shift: engine_state.shift_pressed,
};
+
+ if let Some(ctx) = engine_state.inner.ui_context_mut() {
+ ctx.ctrl_pressed = engine_state.ctrl_pressed;
+ ctx.shift_pressed = engine_state.shift_pressed;
+ ctx.alt_pressed = engine_state.alt_pressed;
+ ctx.logo_pressed = engine_state.logo_pressed;
+ }
+
let mut key_rebuild = false;
if let Some(msg) = engine_state.inner.handle_key_input(&custom_event, &mut key_rebuild) {
let mut update_rebuild = false;
diff --git a/src/color.rs b/src/color.rs
index 3dd2b7f..8528b0a 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -29,6 +29,8 @@ static SLIDER_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_TRACK);
static PAGE_LOW_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0600316, 0.0600316, 0.080219, 1.0]);
static COLOR_BORDERS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.2039, 0.2039, 0.2530, 1.0]);
static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
+static NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
+static NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
static SIDEBAR_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(SIDEBAR_BG);
static HIGHLIGHT_PRIMARY_COLOR: RwLock<[f32; 4]> = RwLock::new(HIGHLIGHT_PRIMARY);
static MENUBAR_TAB_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]); // sRGB [230, 230, 242] linear
@@ -71,6 +73,17 @@ static TREE_SEPARATOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.19, 1
static SCROLLBAR_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.20, 0.3]);
static SCROLLBAR_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new([0.60, 0.60, 0.65, 0.4]);
+static GRAPH_CELL_COLOR: RwLock<[f32; 3]> = RwLock::new([0.13, 0.13, 0.16]);
+static GRAPH_GAP_COLOR: RwLock<[f32; 3]> = RwLock::new([0.07, 0.07, 0.09]);
+static GRAPH_OPACITY: RwLock<f32> = RwLock::new(0.95);
+
+static GRAPH_NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
+static GRAPH_NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
+static GRAPH_NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
+
+static GRAPH_WIRE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
+static GRAPH_WIRE_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
+
pub fn button_background_color() -> [f32; 4] {
*BUTTON_BACKGROUND_COLOR.read().unwrap()
}
@@ -83,25 +96,32 @@ pub fn set_button_background_color(color: [f32; 4]) {
pub fn button_hover_color() -> [f32; 4] {
let base = button_background_color();
+ let mut oklab = linear_srgb_to_oklab([base[0], base[1], base[2]]);
+ oklab[0] = (oklab[0] + 0.05).min(1.0); // increase lightness slightly
+ let rgb = oklab_to_linear_srgb(oklab);
[
- (base[0] + 0.10).min(1.0),
- (base[1] + 0.12).min(1.0),
- (base[2] + 0.13).min(1.0),
+ rgb[0].clamp(0.0, 1.0),
+ rgb[1].clamp(0.0, 1.0),
+ rgb[2].clamp(0.0, 1.0),
(base[3] + 0.20).min(1.0),
]
}
pub fn button_press_color() -> [f32; 4] {
let base = button_background_color();
+ let mut oklab = linear_srgb_to_oklab([base[0], base[1], base[2]]);
+ oklab[0] = (oklab[0] - 0.07).max(0.0); // decrease lightness
+ let rgb = oklab_to_linear_srgb(oklab);
[
- (base[0] - 0.08).max(0.0),
- (base[1] - 0.12).max(0.0),
- (base[2] - 0.15).max(0.0),
+ rgb[0].clamp(0.0, 1.0),
+ rgb[1].clamp(0.0, 1.0),
+ rgb[2].clamp(0.0, 1.0),
(base[3] + 0.40).min(1.0),
]
}
pub fn node_color() -> [f32; 4] {
+ load_colors_once();
*NODE_COLOR.read().unwrap()
}
@@ -112,23 +132,25 @@ pub fn set_node_color(color: [f32; 4]) {
}
pub fn node_selected_color() -> [f32; 4] {
- let base = node_color();
- [
- (base[0] + 0.10).min(1.0),
- (base[1] + 0.20).min(1.0),
- (base[2] + 0.20).min(1.0),
- base[3]
- ]
+ load_colors_once();
+ *NODE_SELECTED_COLOR.read().unwrap()
+}
+
+pub fn set_node_selected_color(color: [f32; 4]) {
+ if let Ok(mut lock) = NODE_SELECTED_COLOR.write() {
+ *lock = color;
+ }
}
pub fn node_drag_color() -> [f32; 4] {
- let base = node_color();
- [
- (base[0] + 0.20).min(1.0),
- (base[1] + 0.35).min(1.0),
- (base[2] + 0.30).min(1.0),
- base[3]
- ]
+ load_colors_once();
+ *NODE_DRAG_COLOR.read().unwrap()
+}
+
+pub fn set_node_drag_color(color: [f32; 4]) {
+ if let Ok(mut lock) = NODE_DRAG_COLOR.write() {
+ *lock = color;
+ }
}
fn read_config() -> Option<String> {
@@ -265,6 +287,34 @@ fn parse_and_set_colors(content: &str) {
}
}
+ if let Some(c) = get_color("/style/surface/graph/cell_color") {
+ if let Ok(mut lock) = GRAPH_CELL_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
+ }
+ if let Some(c) = get_color("/style/surface/graph/gap_color") {
+ if let Ok(mut lock) = GRAPH_GAP_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
+ }
+ if let Some(c) = get_color("/style/surface/graph/node/color") {
+ if let Ok(mut lock) = GRAPH_NODE_COLOR.write() { *lock = c; }
+ if let Ok(mut lock) = NODE_COLOR.write() { *lock = c; }
+ }
+ if let Some(c) = get_color("/style/surface/graph/node/selected_color") {
+ if let Ok(mut lock) = GRAPH_NODE_SELECTED_COLOR.write() { *lock = c; }
+ if let Ok(mut lock) = NODE_SELECTED_COLOR.write() { *lock = c; }
+ }
+ if let Some(c) = get_color("/style/surface/graph/node/drag_color") {
+ if let Ok(mut lock) = GRAPH_NODE_DRAG_COLOR.write() { *lock = c; }
+ if let Ok(mut lock) = NODE_DRAG_COLOR.write() { *lock = c; }
+ }
+ if let Some(c) = get_color("/style/surface/graph/node/wire_color") {
+ if let Ok(mut lock) = GRAPH_WIRE_COLOR.write() { *lock = c; }
+ }
+ if let Some(c) = get_color("/style/surface/graph/node/wire_highlight_color") {
+ if let Ok(mut lock) = GRAPH_WIRE_HIGHLIGHT_COLOR.write() { *lock = c; }
+ }
+ if let Some(opacity) = val.pointer("/style/surface/graph/opacity").and_then(|v| v.as_f64()) {
+ if let Ok(mut lock) = GRAPH_OPACITY.write() { *lock = opacity as f32; }
+ }
+
if let Some(c) = get_color("/style/data/tree/background_color") {
if let Ok(mut lock) = TREE_BACKGROUND_COLOR.write() { *lock = c; }
}
@@ -335,6 +385,94 @@ pub fn reload_colors(content: &str) {
parse_and_set_colors(content);
}
+pub fn graph_wire_color() -> [f32; 4] {
+ load_colors_once();
+ *GRAPH_WIRE_COLOR.read().unwrap()
+}
+
+pub fn set_graph_wire_color(color: [f32; 4]) {
+ if let Ok(mut lock) = GRAPH_WIRE_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_wire_highlight_color() -> [f32; 4] {
+ load_colors_once();
+ *GRAPH_WIRE_HIGHLIGHT_COLOR.read().unwrap()
+}
+
+pub fn set_graph_wire_highlight_color(color: [f32; 4]) {
+ if let Ok(mut lock) = GRAPH_WIRE_HIGHLIGHT_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_node_color() -> [f32; 4] {
+ load_colors_once();
+ *GRAPH_NODE_COLOR.read().unwrap()
+}
+
+pub fn set_graph_node_color(color: [f32; 4]) {
+ if let Ok(mut lock) = GRAPH_NODE_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_node_selected_color() -> [f32; 4] {
+ load_colors_once();
+ *GRAPH_NODE_SELECTED_COLOR.read().unwrap()
+}
+
+pub fn set_graph_node_selected_color(color: [f32; 4]) {
+ if let Ok(mut lock) = GRAPH_NODE_SELECTED_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_node_drag_color() -> [f32; 4] {
+ load_colors_once();
+ *GRAPH_NODE_DRAG_COLOR.read().unwrap()
+}
+
+pub fn set_graph_node_drag_color(color: [f32; 4]) {
+ if let Ok(mut lock) = GRAPH_NODE_DRAG_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_cell_color() -> [f32; 3] {
+ load_colors_once();
+ *GRAPH_CELL_COLOR.read().unwrap()
+}
+
+pub fn set_graph_cell_color(color: [f32; 3]) {
+ if let Ok(mut lock) = GRAPH_CELL_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_gap_color() -> [f32; 3] {
+ load_colors_once();
+ *GRAPH_GAP_COLOR.read().unwrap()
+}
+
+pub fn set_graph_gap_color(color: [f32; 3]) {
+ if let Ok(mut lock) = GRAPH_GAP_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn graph_opacity() -> f32 {
+ load_colors_once();
+ *GRAPH_OPACITY.read().unwrap()
+}
+
+pub fn set_graph_opacity(opacity: f32) {
+ if let Ok(mut lock) = GRAPH_OPACITY.write() {
+ *lock = opacity;
+ }
+}
+
pub fn page_low_color() -> [f32; 4] {
load_colors_once();
let mut color = *PAGE_LOW_COLOR.read().unwrap();
@@ -426,11 +564,19 @@ pub const TEXT_HEADER: [f32; 4] = [0.90, 0.90, 0.95, 1.0];
pub const TEXT_ACCENT: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
pub fn srgb_to_linear(c: f32) -> f32 {
- c.powf(2.2)
+ if c <= 0.04045 {
+ c / 12.92
+ } else {
+ ((c + 0.055) / 1.055).powf(2.4)
+ }
}
pub fn linear_to_srgb(c: f32) -> f32 {
- c.powf(1.0 / 2.2)
+ if c <= 0.0031308 {
+ c * 12.92
+ } else {
+ 1.055 * c.powf(1.0 / 2.4) - 0.055
+ }
}
pub fn to_linear(color: [f32; 4]) -> [f32; 4] {
@@ -451,6 +597,54 @@ pub fn to_srgb(color: [f32; 4]) -> [f32; 4] {
]
}
+pub fn to_linear_rgb(color: [f32; 3]) -> [f32; 3] {
+ [
+ srgb_to_linear(color[0]),
+ srgb_to_linear(color[1]),
+ srgb_to_linear(color[2]),
+ ]
+}
+
+pub fn to_srgb_rgb(color: [f32; 3]) -> [f32; 3] {
+ [
+ linear_to_srgb(color[0]),
+ linear_to_srgb(color[1]),
+ linear_to_srgb(color[2]),
+ ]
+}
+
+pub fn linear_srgb_to_oklab(rgb: [f32; 3]) -> [f32; 3] {
+ let l = 0.4122214708 * rgb[0] + 0.5363325363 * rgb[1] + 0.0514459929 * rgb[2];
+ let m = 0.2119034982 * rgb[0] + 0.6806995451 * rgb[1] + 0.1073969566 * rgb[2];
+ let s = 0.0883024619 * rgb[0] + 0.2817188376 * rgb[1] + 0.6299787005 * rgb[2];
+
+ let l_ = l.max(0.0).powf(1.0 / 3.0);
+ let m_ = m.max(0.0).powf(1.0 / 3.0);
+ let s_ = s.max(0.0).powf(1.0 / 3.0);
+
+ [
+ 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
+ 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
+ ]
+}
+
+pub fn oklab_to_linear_srgb(lab: [f32; 3]) -> [f32; 3] {
+ let l_ = lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2];
+ let m_ = lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2];
+ let s_ = lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2];
+
+ let l = l_ * l_ * l_;
+ let m = m_ * m_ * m_;
+ let s = s_ * s_ * s_;
+
+ [
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
+ -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
+ ]
+}
+
pub fn sidebar_bg_color() -> [f32; 4] {
load_colors_once();
let mut color = *SIDEBAR_BG_COLOR.read().unwrap();
diff --git a/src/config.rs b/src/config.rs
index e5efd93..ca7c05f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -18,7 +18,21 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
kdl::KdlValue::Base10(i) |
kdl::KdlValue::Base16(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
kdl::KdlValue::Base10Float(f) => {
- if let Some(num) = serde_json::Number::from_f64(*f) {
+ let mut val_f = *f;
+ if let Some(ty) = entry.ty() {
+ let ty_str = ty.value();
+ if ty_str.starts_with("f64:") {
+ let range_str = ty_str.trim_start_matches("f64:");
+ if let Some(dash_idx) = range_str.find('-') {
+ let min_str = &range_str[..dash_idx].trim();
+ let max_str = &range_str[dash_idx + 1..].trim();
+ if let (Ok(min_f), Ok(max_f)) = (min_str.parse::<f64>(), max_str.parse::<f64>()) {
+ val_f = val_f.clamp(min_f, max_f);
+ }
+ }
+ }
+ }
+ if let Some(num) = serde_json::Number::from_f64(val_f) {
serde_json::Value::Number(num)
} else {
serde_json::Value::Null
@@ -46,7 +60,21 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
kdl::KdlValue::Base10(i) |
kdl::KdlValue::Base16(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
kdl::KdlValue::Base10Float(f) => {
- if let Some(num) = serde_json::Number::from_f64(*f) {
+ let mut val_f = *f;
+ if let Some(ty) = entry.ty() {
+ let ty_str = ty.value();
+ if ty_str.starts_with("f64:") {
+ let range_str = ty_str.trim_start_matches("f64:");
+ if let Some(dash_idx) = range_str.find('-') {
+ let min_str = &range_str[..dash_idx].trim();
+ let max_str = &range_str[dash_idx + 1..].trim();
+ if let (Ok(min_f), Ok(max_f)) = (min_str.parse::<f64>(), max_str.parse::<f64>()) {
+ val_f = val_f.clamp(min_f, max_f);
+ }
+ }
+ }
+ }
+ if let Some(num) = serde_json::Number::from_f64(val_f) {
serde_json::Value::Number(num)
} else {
serde_json::Value::Null
@@ -79,7 +107,21 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
kdl::KdlValue::Base10(i) |
kdl::KdlValue::Base16(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
kdl::KdlValue::Base10Float(f) => {
- if let Some(num) = serde_json::Number::from_f64(*f) {
+ let mut val_f = *f;
+ if let Some(ty) = entry.ty() {
+ let ty_str = ty.value();
+ if ty_str.starts_with("f64:") {
+ let range_str = ty_str.trim_start_matches("f64:");
+ if let Some(dash_idx) = range_str.find('-') {
+ let min_str = &range_str[..dash_idx].trim();
+ let max_str = &range_str[dash_idx + 1..].trim();
+ if let (Ok(min_f), Ok(max_f)) = (min_str.parse::<f64>(), max_str.parse::<f64>()) {
+ val_f = val_f.clamp(min_f, max_f);
+ }
+ }
+ }
+ }
+ if let Some(num) = serde_json::Number::from_f64(val_f) {
serde_json::Value::Number(num)
} else {
serde_json::Value::Null
@@ -403,6 +445,12 @@ pub fn write_keybindings_to_kdl(path: &str, keybinds: &[serde_json::Value]) -> b
let action = obj.get("action").and_then(|a| a.as_str()).unwrap_or("");
let command = obj.get("command").and_then(|c| c.as_str()).unwrap_or("");
+ let full_key = if mods.is_empty() {
+ key.to_string()
+ } else {
+ format!("{}+{}", mods, key)
+ };
+
block_str.push_str(" bind");
if !action.is_empty() {
block_str.push_str(&format!(" action={:?}", action));
@@ -410,11 +458,8 @@ pub fn write_keybindings_to_kdl(path: &str, keybinds: &[serde_json::Value]) -> b
if !command.is_empty() {
block_str.push_str(&format!(" command={:?}", command));
}
- if !key.is_empty() {
- block_str.push_str(&format!(" key={:?}", key));
- }
- if !mods.is_empty() {
- block_str.push_str(&format!(" mods={:?}", mods));
+ if !full_key.is_empty() {
+ block_str.push_str(&format!(" key=(keybind){:?}", full_key));
}
block_str.push('\n');
}
@@ -493,3 +538,106 @@ mod tests {
assert_eq!(ty2, Some("bool".to_string()));
}
}
+
+pub fn value_to_kdl(key: &str, val: &serde_json::Value, indent: usize) -> String {
+ let indent_str = " ".repeat(indent);
+ match val {
+ serde_json::Value::Object(map) => {
+ let has_objects = map.values().any(|v| v.is_object());
+ if has_objects {
+ let mut out = format!("{}{} {{\n", indent_str, key);
+ for (k, v) in map {
+ out.push_str(&value_to_kdl(k, v, indent + 1));
+ }
+ out.push_str(&format!("{}}}\n", indent_str));
+ out
+ } else {
+ let mut prop_parts = Vec::new();
+ for (prop_name, prop_val) in map {
+ let (val_str, val_ty) = match prop_val {
+ serde_json::Value::Bool(b) => (b.to_string(), Some("bool")),
+ serde_json::Value::Number(num) => {
+ if num.is_f64() {
+ (num.to_string(), Some("f64"))
+ } else {
+ (num.to_string(), Some("i64"))
+ }
+ }
+ serde_json::Value::String(s) => {
+ if s.starts_with('#') {
+ let s_clean = s.trim_start_matches('#');
+ let ty = if s_clean.len() == 8 { "rgba" } else { "rgb" };
+ (format!("\"{}\"", s), Some(ty))
+ } else if prop_name == "key" || prop_name == "keybind" || prop_name == "shortcut" || prop_name.ends_with("_key") || prop_name.ends_with(".key") || prop_name.ends_with(".keybind") {
+ (format!("\"{}\"", s), Some("keybind"))
+ } else {
+ (format!("\"{}\"", s), None)
+ }
+ }
+ _ => (prop_val.to_string(), None),
+ };
+ if let Some(ty) = val_ty {
+ prop_parts.push(format!("{}=({}){}", prop_name, ty, val_str));
+ } else {
+ prop_parts.push(format!("{}={}", prop_name, val_str));
+ }
+ }
+ format!("{}{} {}\n", indent_str, key, prop_parts.join(" "))
+ }
+ }
+ serde_json::Value::Array(arr) => {
+ let mut out = String::new();
+ for item in arr {
+ out.push_str(&value_to_kdl(key, item, indent));
+ }
+ out
+ }
+ _ => {
+ let (val_str, val_ty) = match val {
+ serde_json::Value::Bool(b) => (b.to_string(), Some("bool")),
+ serde_json::Value::Number(num) => {
+ if num.is_f64() {
+ (num.to_string(), Some("f64"))
+ } else {
+ (num.to_string(), Some("i64"))
+ }
+ }
+ serde_json::Value::String(s) => {
+ if s.starts_with('#') {
+ let s_clean = s.trim_start_matches('#');
+ let ty = if s_clean.len() == 8 { "rgba" } else { "rgb" };
+ (format!("\"{}\"", s), Some(ty))
+ } else if key == "key" || key == "keybind" || key == "shortcut" || key.ends_with("_key") || key.ends_with(".key") || key.ends_with(".keybind") {
+ (format!("\"{}\"", s), Some("keybind"))
+ } else {
+ (format!("\"{}\"", s), None)
+ }
+ }
+ _ => (val.to_string(), None),
+ };
+ if let Some(ty) = val_ty {
+ format!("{}{} ({}){}\n", indent_str, key, ty, val_str)
+ } else {
+ format!("{}{} {}\n", indent_str, key, val_str)
+ }
+ }
+ }
+}
+
+pub fn json_to_kdl_string(val: &serde_json::Value) -> String {
+ let mut out = String::new();
+ if let serde_json::Value::Object(map) = val {
+ for (sec_name, sec_val) in map {
+ if let serde_json::Value::Object(sec_map) = sec_val {
+ out.push_str(&format!("{} {{\n", sec_name));
+ for (k, v) in sec_map {
+ out.push_str(&value_to_kdl(k, v, 1));
+ }
+ out.push_str("}\n");
+ } else {
+ out.push_str(&value_to_kdl(sec_name, sec_val, 0));
+ }
+ }
+ }
+ out
+}
diff --git a/src/context.rs b/src/context.rs
index d8c1bd8..41f46cd 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -67,6 +67,10 @@ pub struct UiContext {
pub last_scroll_time: Option<std::time::Instant>,
pub scroll_initiate_widget_id: Option<WidgetId>,
pub scroll_gesture_new: bool,
+ pub ctrl_pressed: bool,
+ pub shift_pressed: bool,
+ pub alt_pressed: bool,
+ pub logo_pressed: bool,
}
impl UiContext {
@@ -92,6 +96,10 @@ impl UiContext {
last_scroll_time: None,
scroll_initiate_widget_id: None,
scroll_gesture_new: false,
+ ctrl_pressed: false,
+ shift_pressed: false,
+ alt_pressed: false,
+ logo_pressed: false,
}
}
diff --git a/src/engine.rs b/src/engine.rs
index c8bfaf2..6addf21 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -7,5 +7,5 @@ pub use crate::backend::window_runner::{
push_plate_bevel_vertices, widget_vertices, push_widget_vertices,
extra_quad_vertices, push_extra_quad_vertices, extra_quad_vertices_clipped,
push_extra_quad_vertices_clipped, circle_vertices, circle_border_vertices,
- arc_background_vertices, push_arc_background_vertices,
+ arc_background_vertices, push_arc_background_vertices, push_plate_solid_border_vertices,
};
diff --git a/src/layout.rs b/src/layout.rs
index ac2fc71..8259634 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -140,6 +140,21 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.surface.backplate.corner_radius" => "backplate_corner_radius",
"style.surface.page.opacity" => "page_opacity",
"style.surface.page.margin" => "page_margin",
+ "style.surface.graph.cell_color" => "graph_cell_color",
+ "style.surface.graph.gap_color" => "graph_gap_color",
+ "style.surface.graph.opacity" => "graph_opacity",
+ "style.surface.graph.spacing_x" => "graph_spacing_x",
+ "style.surface.graph.spacing_y" => "graph_spacing_y",
+ "style.surface.graph.grid_snap" => "graph_grid_snap",
+ "style.surface.graph.blur" => "graph_blur",
+ "style.surface.graph.node.color" => "graph_node_color",
+ "style.surface.graph.node.selected_color" => "graph_node_selected_color",
+ "style.surface.graph.node.drag_color" => "graph_node_drag_color",
+ "style.surface.graph.node.corner_radius" => "graph_node_corner_radius",
+ "style.surface.graph.node.wire_color" => "graph_wire_color",
+ "style.surface.graph.node.wire_highlight_color" => "graph_wire_highlight_color",
+ "style.surface.graph.node.wire_size" => "graph_wire_size",
+ "style.surface.graph.node.wire_activation_radius" => "graph_wire_activation_radius",
other => {
if let Some(rest) = other.strip_prefix("layout.") {
@@ -2437,6 +2452,90 @@ pub fn set_tree_corner_radius(radius: f32) {
}
}
+pub fn graph_spacing_x() -> f32 {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_spacing_x").unwrap_or(150.0)
+}
+
+pub fn set_graph_spacing_x(spacing: f32) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_spacing_x", spacing);
+ }
+}
+
+pub fn graph_spacing_y() -> f32 {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_spacing_y").unwrap_or(75.0)
+}
+
+pub fn set_graph_spacing_y(spacing: f32) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_spacing_y", spacing);
+ }
+}
+
+pub fn graph_grid_snap() -> bool {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_grid_snap").unwrap_or(0.0) != 0.0
+}
+
+pub fn set_graph_grid_snap(snap: bool) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_grid_snap", if snap { 1.0 } else { 0.0 });
+ }
+}
+
+pub fn graph_blur() -> f32 {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_blur").unwrap_or(0.0)
+}
+
+pub fn set_graph_blur(blur: f32) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_blur", blur);
+ }
+}
+
+pub fn graph_node_corner_radius() -> f32 {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_node_corner_radius").unwrap_or(4.0)
+}
+
+pub fn set_graph_node_corner_radius(radius: f32) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_node_corner_radius", radius);
+ }
+}
+
+pub fn graph_wire_size() -> f32 {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_wire_size").unwrap_or(6.0)
+}
+
+pub fn set_graph_wire_size(size: f32) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_wire_size", size);
+ }
+}
+
+pub fn graph_wire_activation_radius() -> f32 {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float("graph_wire_activation_radius").unwrap_or(9.0)
+}
+
+pub fn set_graph_wire_activation_radius(radius: f32) {
+ lazy_init_style_registry();
+ if let Ok(mut registry) = get_style_registry().write() {
+ registry.set_float("graph_wire_activation_radius", radius);
+ }
+}
+
pub fn font_selector_corner_radius() -> f32 {
lazy_init_style_registry();
get_style_registry().read().unwrap().get_float("font_selector_corner_radius").unwrap_or(4.0)
@@ -4996,5 +5095,51 @@ mod tests {
println!("Parsed spinbox button padding: {}", padding);
assert!(padding >= 0.0);
}
+
+ #[test]
+ fn test_graph_style_configuration() {
+ // Trigger load_colors_once first so it doesn't overwrite values later
+ let _ = crate::color::page_low_color();
+
+ set_graph_spacing_x(200.0);
+ set_graph_spacing_y(100.0);
+ set_graph_grid_snap(true);
+ set_graph_blur(0.8);
+ set_graph_node_corner_radius(8.0);
+ set_graph_wire_size(10.0);
+ set_graph_wire_activation_radius(15.0);
+
+ assert_eq!(graph_spacing_x(), 200.0);
+ assert_eq!(graph_spacing_y(), 100.0);
+ assert_eq!(graph_grid_snap(), true);
+ assert_eq!(graph_blur(), 0.8);
+ assert_eq!(graph_node_corner_radius(), 8.0);
+ assert_eq!(graph_wire_size(), 10.0);
+ assert_eq!(graph_wire_activation_radius(), 15.0);
+
+ crate::color::set_graph_cell_color([0.1, 0.2, 0.3]);
+ crate::color::set_graph_gap_color([0.4, 0.5, 0.6]);
+ crate::color::set_graph_node_color([0.7, 0.8, 0.9, 1.0]);
+ crate::color::set_graph_node_selected_color([0.9, 0.8, 0.7, 1.0]);
+ crate::color::set_graph_node_drag_color([0.5, 0.5, 0.5, 1.0]);
+ crate::color::set_node_color([0.7, 0.8, 0.9, 1.0]);
+ crate::color::set_node_selected_color([0.9, 0.8, 0.7, 1.0]);
+ crate::color::set_node_drag_color([0.5, 0.5, 0.5, 1.0]);
+ crate::color::set_graph_wire_color([0.1, 0.1, 0.1, 1.0]);
+ crate::color::set_graph_wire_highlight_color([0.2, 0.2, 0.2, 1.0]);
+
+ let cell_color = crate::color::graph_cell_color();
+ assert_eq!(cell_color, [0.1, 0.2, 0.3]);
+ let gap_color = crate::color::graph_gap_color();
+ assert_eq!(gap_color, [0.4, 0.5, 0.6]);
+ assert_eq!(crate::color::graph_node_color(), [0.7, 0.8, 0.9, 1.0]);
+ assert_eq!(crate::color::graph_node_selected_color(), [0.9, 0.8, 0.7, 1.0]);
+ assert_eq!(crate::color::graph_node_drag_color(), [0.5, 0.5, 0.5, 1.0]);
+ assert_eq!(crate::color::node_color(), [0.7, 0.8, 0.9, 1.0]);
+ assert_eq!(crate::color::node_selected_color(), [0.9, 0.8, 0.7, 1.0]);
+ assert_eq!(crate::color::node_drag_color(), [0.5, 0.5, 0.5, 1.0]);
+ assert_eq!(crate::color::graph_wire_color(), [0.1, 0.1, 0.1, 1.0]);
+ assert_eq!(crate::color::graph_wire_highlight_color(), [0.2, 0.2, 0.2, 1.0]);
+ }
}
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 0bc0f73..6e26627 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -256,4 +256,5 @@ impl GraphController for ContentBg {
fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
fn take_pending_connection(&mut self) -> Option<(String, String)> { None }
fn cancel_connecting(&mut self) {}
+ fn is_node_rect(&self, _qx: f32, _qy: f32, _qw: f32, _qh: f32) -> bool { false }
}
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 67b5019..f48fe4a 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1023,7 +1023,7 @@ impl Element for ParametersBg {
if p.2.starts_with("slider") {
let r = rects[i];
let row_y = r.1;
- if py >= row_y - 2.0 && py <= row_y + 18.0 && px >= self.base.x && px <= self.base.x + self.base.w {
+ if py >= row_y - 2.0 && py <= row_y + r.3 && px >= self.base.x && px <= self.base.x + self.base.w {
if let Some(s) = &mut self.sliders[i] {
let was_scroll = s.scroll_enabled;
s.set_scroll(true);
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index f90df4b..14660f8 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -544,6 +544,8 @@ impl Element for TreeList {
} else {
Some("rgb")
}
+ } else if name == "key" || name == "keybind" || name == "shortcut" || name.ends_with("_key") || name.ends_with(".key") || name.ends_with(".keybind") || name.ends_with(".shortcut") {
+ Some("keybind")
} else if name == "font" || name.ends_with("_font") || name.ends_with(".font") {
Some("font")
} else {
@@ -972,4 +974,21 @@ mod tests {
}
assert!(quads.len() > 1, "Should have more than 1 quad!");
}
+
+ #[test]
+ fn test_keybind_label() {
+ let mut tree_list = TreeList::new();
+ tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
+ tree_list.set_flat_keys(vec![
+ ("input.key_bindings[0].key".to_string(), serde_json::Value::String("super+q".to_string()))
+ ]);
+
+ let labels = tree_list.text_labels();
+ for label in &labels {
+ println!("TEST LABEL: {:?}", label);
+ }
+
+ let has_keybind_label = labels.iter().any(|l| l.text == "(keybind)");
+ assert!(has_keybind_label, "Should have (keybind) label!");
+ }
}
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 8fd492b..d1479b3 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -56,8 +56,6 @@ pub struct Graph {
uniform_background: bool,
network_opacity: f32,
- cell_opacity: f32,
- gap_opacity: f32,
cell_color: [f32; 3],
gap_color: [f32; 3],
@@ -74,14 +72,6 @@ impl Graph {
}
pub fn set_network_opacity(&mut self, opacity: f32) {
self.network_opacity = opacity;
- self.cell_opacity = opacity;
- self.gap_opacity = opacity;
- }
- pub fn set_cell_opacity(&mut self, opacity: f32) {
- self.cell_opacity = opacity;
- }
- pub fn set_gap_opacity(&mut self, opacity: f32) {
- self.gap_opacity = opacity;
}
pub fn set_cell_color(&mut self, color: [f32; 3]) {
self.cell_color = color;
@@ -91,22 +81,31 @@ impl Graph {
}
pub fn new() -> Self {
+ crate::layout::lazy_init_style_registry();
+
+ let grid_size_x = crate::layout::graph_spacing_x();
+ let grid_size_y = crate::layout::graph_spacing_y();
+ let grid_snap_enabled = crate::layout::graph_grid_snap();
+
+ let cell_col = crate::color::graph_cell_color();
+ let gap_col = crate::color::graph_gap_color();
+
Self {
x: 0.0, y: 0.0, w: 0.0, h: 0.0,
hovered: false,
show_network_grid: false,
- grid_size_x: 150.0,
- grid_size_y: 75.0,
+ grid_size_x,
+ grid_size_y,
grid_origin_x: 0.0,
grid_origin_y: 0.0,
- skipped_row_h: 37.5,
- skipped_col_w: 37.5,
+ skipped_row_h: grid_size_y / 2.0,
+ skipped_col_w: grid_size_x / 2.0,
nodes: Vec::new(),
selected_idx: None,
selected_id: None,
double_clicked_idx: None,
double_click_timer: None,
- grid_snap_enabled: false,
+ grid_snap_enabled,
node_geom_toggled: None,
dragging_idx: None,
dragging_id: None,
@@ -115,11 +114,9 @@ impl Graph {
drag_node_pos: None,
toggle_hovered_idx: None,
uniform_background: false,
- network_opacity: 0.95,
- cell_opacity: 0.95,
- gap_opacity: 0.95,
- cell_color: [0.13, 0.13, 0.16],
- gap_color: [0.07, 0.07, 0.09],
+ network_opacity: crate::color::graph_opacity(),
+ cell_color: cell_col,
+ gap_color: gap_col,
connecting_from: None,
current_mouse_pos: (0.0, 0.0),
pending_connection: None,
@@ -143,6 +140,17 @@ impl Graph {
Some((nx, ny, self.grid_size_x, self.grid_size_y))
}
+ pub fn is_node_rect(&self, qx: f32, qy: f32, qw: f32, qh: f32) -> bool {
+ for i in 0..self.nodes.len() {
+ if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+ if (qx - nx).abs() < 0.1 && (qy - ny).abs() < 0.1 && (qw - nw).abs() < 0.1 && (qh - nh).abs() < 0.1 {
+ return true;
+ }
+ }
+ }
+ false
+ }
+
pub fn toggle_rect(&self, idx: usize) -> Option<(f32, f32, f32, f32)> {
if let Some(node) = self.nodes.get(idx) {
if node.node_type == "utility" {
@@ -239,15 +247,30 @@ impl Element for Graph {
fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, true, true) }
+
+
+ fn paint(&mut self, ctx: &mut UiContext) {
+ if let Some(hq) = self.highlight_quad(ctx) {
+ if hq.4 == colors::HIGHLIGHT_SECONDARY {
+ ctx.register_hovered(hq.0, hq.1, hq.2, hq.3, hq.4);
+ }
+ }
+ }
+
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 color(&self) -> [f32; 4] {
- if self.uniform_background {
- [0.10, 0.10, 0.13, self.network_opacity]
+ let mut c = if self.uniform_background {
+ [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity]
} else {
- [0.0, 0.0, 0.0, 0.0]
+ [0.0, 0.0, 0.0, 0.01 * self.network_opacity]
+ };
+ let blur_val = crate::layout::graph_blur();
+ if blur_val > 0.0 {
+ c[3] = -blur_val.abs() * self.network_opacity;
}
+ c
}
fn set_hovered(&mut self, v: bool) { self.hovered = v; }
fn hovered(&self) -> bool { self.hovered }
@@ -376,10 +399,12 @@ impl Element for Graph {
let was_hovered_port = self.hovered_port;
self.hovered_port = None;
+ let wire_act_r = crate::layout::graph_wire_activation_radius();
+
for i in 0..self.nodes.len() {
if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
let scale_f = nw / 80.0;
- let hit_radius = (6.0 * scale_f).max(2.0) * 1.5;
+ let hit_radius = (wire_act_r * scale_f).max(2.0);
let node = &self.nodes[i];
for k in 0..node.inputs {
@@ -590,7 +615,7 @@ impl Element for Graph {
// Draw connection wires
let scale_f = self.grid_size_x / 80.0;
- let wire_color = [0.0, 0.75, 1.0, 0.7]; // Vibrant cyan glow
+ let wire_color = [0.0, 0.75, 1.0, 0.7 * self.network_opacity]; // Vibrant cyan glow
let wire_thickness = (3.0 * scale_f).clamp(1.0, 15.0);
for i in 0..self.nodes.len() {
let node = &self.nodes[i];
@@ -746,7 +771,7 @@ impl Element for Graph {
y_cell_start,
gap_w,
cell_h,
- [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.gap_opacity],
+ [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity],
&mut quads,
);
@@ -756,7 +781,7 @@ impl Element for Graph {
y_cell_end,
x2 - x_cell_start,
gap_h,
- [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.gap_opacity],
+ [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity],
&mut quads,
);
@@ -766,7 +791,7 @@ impl Element for Graph {
y_cell_start,
cell_w,
cell_h,
- [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.cell_opacity],
+ [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity],
&mut quads,
);
}
@@ -778,36 +803,42 @@ impl Element for Graph {
let thickness = 2.0;
// X axis (horizontal) in the gap below row 0
let y_center = self.grid_origin_y + self.grid_size_y + self.skipped_row_h / 2.0;
- push_clipped(self.x, y_center - thickness / 2.0, self.w, thickness, [0.0, 0.0, 0.0, 1.0], &mut quads);
+ push_clipped(self.x, y_center - thickness / 2.0, self.w, thickness, [0.0, 0.0, 0.0, 1.0 * self.network_opacity], &mut quads);
// Y axis (vertical) in the gap to the left of col 0
let x_center = self.grid_origin_x - self.skipped_col_w / 2.0;
- push_clipped(x_center - thickness / 2.0, self.y, thickness, self.h, [0.0, 0.0, 0.0, 1.0], &mut quads);
+ push_clipped(x_center - thickness / 2.0, self.y, thickness, self.h, [0.0, 0.0, 0.0, 1.0 * self.network_opacity], &mut quads);
}
for i in 0..self.nodes.len() {
if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
let scale_f = nw / 80.0;
- let bg_color = if self.dragging_idx == Some(i) {
+ let mut bg_color = if self.dragging_idx == Some(i) {
colors::node_drag_color()
} else if self.selected_idx == Some(i) {
colors::node_selected_color()
} else {
colors::node_color()
};
- push_clipped(nx, ny, nw, nh, bg_color, &mut quads);
+ bg_color[3] *= self.network_opacity;
+ if nx + nw > min_x && nx < max_x && ny + nh > min_y && ny < max_y {
+ quads.push((nx, ny, nw, nh, bg_color));
+ }
if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
- let btn_color = if self.toggle_hovered_idx == Some(i) {
+ let mut btn_color = if self.toggle_hovered_idx == Some(i) {
colors::TOGGLE_HOVER
} else {
colors::TOGGLE_OFF
};
+ btn_color[3] *= self.network_opacity;
push_clipped(tx, ty, tw, th, btn_color, &mut quads);
if self.nodes[i].geom_visible {
let inset = 3.0 * scale_f;
- push_clipped(tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, colors::TOGGLE_ON, &mut quads);
+ let mut toggle_on_color = colors::TOGGLE_ON;
+ toggle_on_color[3] *= self.network_opacity;
+ push_clipped(tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, toggle_on_color, &mut quads);
}
}
}
@@ -829,10 +860,16 @@ impl Element for Graph {
}
};
+ let w_size = crate::layout::graph_wire_size();
+ let mut w_color = colors::graph_wire_color();
+ let mut w_hl_color = colors::graph_wire_highlight_color();
+ w_color[3] *= self.network_opacity;
+ w_hl_color[3] *= self.network_opacity;
+
for i in 0..self.nodes.len() {
if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
let scale_f = nw / 80.0;
- let port_size = (6.0 * scale_f).max(2.0);
+ let port_size = (w_size * scale_f).max(2.0);
let base_r = port_size / 2.0;
let node = &self.nodes[i];
@@ -846,9 +883,9 @@ impl Element for Graph {
let is_connecting = self.connecting_from == Some((i, PortType::Input, k));
let (r, color) = if is_hovered || is_connecting {
- (base_r * 1.4, [0.0, 1.0, 0.9, 1.0]) // neon cyan glow
+ (base_r * 1.4, w_hl_color)
} else {
- (base_r, [0.1, 0.8, 0.4, 1.0])
+ (base_r, w_color)
};
push_circle_clipped(cx, cy, r, color);
}
@@ -862,9 +899,9 @@ impl Element for Graph {
let is_connecting = self.connecting_from == Some((i, PortType::Output, k));
let (r, color) = if is_hovered || is_connecting {
- (base_r * 1.4, [0.0, 1.0, 0.9, 1.0]) // neon cyan glow
+ (base_r * 1.4, w_hl_color)
} else {
- (base_r, [0.1, 0.8, 0.4, 1.0])
+ (base_r, w_color)
};
push_circle_clipped(cx, cy, r, color);
}
@@ -954,5 +991,8 @@ impl GraphController for Graph {
fn cancel_connecting(&mut self) {
self.connecting_from = None;
}
+ fn is_node_rect(&self, qx: f32, qy: f32, qw: f32, qh: f32) -> bool {
+ self.is_node_rect(qx, qy, qw, qh)
+ }
}
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index d1c38e8..c5db180 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -62,29 +62,44 @@ impl Dropdown {
changed
}
+ pub fn popover_width(&self) -> f32 {
+ let mut w = self.base.w;
+ let font_setting = crate::layout::dropdown_font();
+ let (font_family, font_size_opt) = crate::layout::parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(12.0);
+ for opt in &self.options {
+ let opt_w = crate::widget::display::measure_text_width(opt, &font_family, font_size) + 32.0;
+ if opt_w > w {
+ w = opt_w;
+ }
+ }
+ w
+ }
+
pub fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
if !self.open { return; }
let dy = self.base.y + self.base.h;
let dh = self.options.len() as f32 * 24.0;
+ let pw = self.popover_width();
// 1. Soft layered drop shadows
- pc.rect([0.02, 0.02, 0.05, 0.15], self.base.x + 1.0, dy + 1.0, self.base.w, dh);
- pc.rect([0.02, 0.02, 0.05, 0.08], self.base.x + 3.0, dy + 3.0, self.base.w, dh);
- pc.rect([0.02, 0.02, 0.05, 0.04], self.base.x + 5.0, dy + 5.0, self.base.w, dh);
+ pc.rect([0.02, 0.02, 0.05, 0.15], self.base.x + 1.0, dy + 1.0, pw, dh);
+ pc.rect([0.02, 0.02, 0.05, 0.08], self.base.x + 3.0, dy + 3.0, pw, dh);
+ pc.rect([0.02, 0.02, 0.05, 0.04], self.base.x + 5.0, dy + 5.0, pw, dh);
let theme = colors::active_theme();
// 2. High-contrast premium outer border
- pc.rect(theme.surface_border, self.base.x, dy, self.base.w, dh);
+ pc.rect(theme.surface_border, self.base.x, dy, pw, dh);
// 3. Frosted glass background
- pc.rect(theme.surface_bg, self.base.x + 1.0, dy + 1.0, self.base.w - 2.0, dh - 2.0); // bg
+ pc.rect(theme.surface_bg, self.base.x + 1.0, dy + 1.0, pw - 2.0, dh - 2.0); // bg
if let Some(h_idx) = self.hovered_item {
let iy = dy + h_idx as f32 * 24.0;
// 4. Vibrantly colored translucent selection highlight
- pc.rect(theme.primary_accent, self.base.x + 2.0, iy + 2.0, self.base.w - 4.0, 20.0);
+ pc.rect(theme.primary_accent, self.base.x + 2.0, iy + 2.0, pw - 4.0, 20.0);
}
for (idx, opt) in self.options.iter().enumerate() {
@@ -104,7 +119,7 @@ impl Dropdown {
1.0,
];
- let bounds = Some([self.base.x, dy, self.base.x + self.base.w, dy + dh]);
+ let bounds = Some([self.base.x, dy, self.base.x + pw, dy + dh]);
if let Some(ref font) = self.widget_font() {
pc.text_with_font_and_bounds(
opt,
@@ -209,8 +224,9 @@ impl Element for Dropdown {
if self.open {
let dy = y + h;
let dh = self.options.len() as f32 * 24.0;
+ let pw = self.popover_width();
let hit_trigger = px >= hx && px <= hx + hw && py >= y && py <= y + h;
- let hit_popover = px >= x && px <= x + w && py >= dy && py <= dy + dh;
+ let hit_popover = px >= x && px <= x + pw && py >= dy && py <= dy + dh;
hit_trigger || hit_popover
} else {
px >= hx && px <= hx + hw && py >= y && py <= y + h
@@ -225,10 +241,11 @@ impl Element for Dropdown {
self.hovered_item = None;
if self.open {
- let (x, y, w, h) = self.rect();
+ let (x, y, _, h) = self.rect();
let dy = y + h;
let dh = self.options.len() as f32 * 24.0;
- if px >= x && px <= x + w && py >= dy && py <= dy + dh {
+ let pw = self.popover_width();
+ if px >= x && px <= x + pw && py >= dy && py <= dy + dh {
let idx = ((py - dy) / 24.0) as usize;
if idx < self.options.len() {
self.hovered_item = Some(idx);
@@ -251,9 +268,10 @@ impl Element for Dropdown {
let (x, y, w, h) = self.rect();
let dy = y + h;
let dh = self.options.len() as f32 * 24.0;
+ let pw = self.popover_width();
let inside_trigger = px >= x && px <= x + w && py >= y && py <= y + h;
- let inside_popover = self.open && px >= x && px <= x + w && py >= dy && py <= dy + dh;
+ let inside_popover = self.open && px >= x && px <= x + pw && py >= dy && py <= dy + dh;
if inside_popover {
let idx = ((py - dy) / 24.0) as usize;
@@ -398,7 +416,7 @@ impl Element for Dropdown {
fn take_click(&mut self) -> bool { self.take_change() }
fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
if self.open {
- Some((self.base.x, self.base.y + self.base.h, self.base.w, self.options.len() as f32 * 24.0))
+ Some((self.base.x, self.base.y + self.base.h, self.popover_width(), self.options.len() as f32 * 24.0))
} else {
None
}
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
new file mode 100644
index 0000000..a5dc5a4
--- /dev/null
+++ b/src/widget/input/keybind_recorder.rs
@@ -0,0 +1,237 @@
+use crate::colors;
+use crate::widget::*;
+
+#[derive(Debug, Clone)]
+pub struct KeybindRecorder {
+ pub base: Widget,
+ pub value: String,
+ pub recording: bool,
+ pub just_changed: bool,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+ pub pressed: bool,
+}
+
+impl KeybindRecorder {
+ pub fn new(value: String) -> Self {
+ Self {
+ base: Widget::new(),
+ value,
+ recording: false,
+ just_changed: false,
+ parent: None,
+ children: Vec::new(),
+ pressed: false,
+ }
+ }
+
+ pub fn with_config(mut self, file: &str, key: &str) -> Self {
+ self.base.config_file = Some(file.to_string());
+ self.base.config_key = Some(key.to_string());
+ self
+ }
+
+ pub fn take_change(&mut self) -> bool {
+ let changed = self.just_changed;
+ self.just_changed = false;
+ changed
+ }
+}
+
+impl Element for KeybindRecorder {
+ crate::impl_widget_base!(KeybindRecorder);
+
+ fn preferred_height(&self) -> Option<f32> {
+ Some(crate::layout::textbox_height())
+ }
+
+ fn color(&self) -> [f32; 4] {
+ [0.08, 0.08, 0.12, 1.0]
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if button != MouseButton::Left { return false; }
+ match state {
+ ElementState::Pressed => {
+ if self.hit_test(px, py, ctx) {
+ self.pressed = true;
+ return true;
+ }
+ }
+ ElementState::Released => {
+ if self.pressed && self.hit_test(px, py, ctx) {
+ self.pressed = false;
+ self.recording = true;
+ ctx.set_focused(self);
+ self.mark_dirty(ctx);
+ return true;
+ }
+ let was = self.pressed;
+ self.pressed = false;
+ return was;
+ }
+ }
+ false
+ }
+
+ fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+ if !self.recording {
+ return false;
+ }
+
+ match event.state {
+ ElementState::Pressed => {
+ match &event.logical_key {
+ Key::Named(NamedKey::Escape) => {
+ // Cancel recording
+ self.recording = false;
+ self.mark_dirty(ctx);
+ return true;
+ }
+ Key::Named(NamedKey::Control) |
+ Key::Named(NamedKey::Shift) |
+ Key::Named(NamedKey::Alt) |
+ Key::Named(NamedKey::Super) => {
+ // Modifier key pressed. Build current active modifiers list.
+ let mut parts = Vec::new();
+ if ctx.logo_pressed { parts.push("super"); }
+ if ctx.ctrl_pressed { parts.push("ctrl"); }
+ if ctx.alt_pressed { parts.push("alt"); }
+ if ctx.shift_pressed { parts.push("shift"); }
+
+ if !parts.is_empty() {
+ self.value = parts.join("+");
+ }
+ self.mark_dirty(ctx);
+ return true;
+ }
+ Key::Named(key) => {
+ // Named key pressed (e.g. Enter, Space, Backspace, Arrow keys)
+ let mut parts = Vec::new();
+ if ctx.logo_pressed { parts.push("super"); }
+ if ctx.ctrl_pressed { parts.push("ctrl"); }
+ if ctx.alt_pressed { parts.push("alt"); }
+ if ctx.shift_pressed { parts.push("shift"); }
+
+ let key_str = match key {
+ NamedKey::Backspace => "backspace",
+ NamedKey::Tab => "tab",
+ NamedKey::Enter => "enter",
+ NamedKey::Space => "space",
+ NamedKey::ArrowDown => "down",
+ NamedKey::ArrowLeft => "left",
+ NamedKey::ArrowRight => "right",
+ NamedKey::ArrowUp => "up",
+ NamedKey::End => "end",
+ NamedKey::Home => "home",
+ NamedKey::PageDown => "pagedown",
+ NamedKey::PageUp => "pageup",
+ NamedKey::Delete => "delete",
+ _ => "",
+ };
+
+ if !key_str.is_empty() {
+ parts.push(key_str);
+ self.value = parts.join("+");
+ self.just_changed = true;
+ self.recording = false;
+ self.mark_dirty(ctx);
+ }
+ return true;
+ }
+ Key::Character(ch) => {
+ // Character key pressed (e.g. "a", "q", "1", etc.)
+ let mut parts = Vec::new();
+ if ctx.logo_pressed { parts.push("super"); }
+ if ctx.ctrl_pressed { parts.push("ctrl"); }
+ if ctx.alt_pressed { parts.push("alt"); }
+ if ctx.shift_pressed { parts.push("shift"); }
+
+ parts.push(ch.as_str());
+ self.value = parts.join("+");
+ self.just_changed = true;
+ self.recording = false;
+ self.mark_dirty(ctx);
+ return true;
+ }
+ }
+ }
+ ElementState::Released => {
+ match &event.logical_key {
+ Key::Named(NamedKey::Control) |
+ Key::Named(NamedKey::Shift) |
+ Key::Named(NamedKey::Alt) |
+ Key::Named(NamedKey::Super) => {
+ // When a modifier key is released, if we have a non-empty value and no keys are pressed anymore,
+ // we commit the recorded modifier keybinding!
+ if !self.value.is_empty() && !ctx.ctrl_pressed && !ctx.shift_pressed && !ctx.alt_pressed && !ctx.logo_pressed {
+ self.just_changed = true;
+ self.recording = false;
+ self.mark_dirty(ctx);
+ }
+ return true;
+ }
+ _ => {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ let top = self.base.label_offset();
+ let visual_h = self.base.h - top;
+ let bg_color = [0.08, 0.08, 0.12, 1.0];
+
+ let border_color = if self.recording {
+ colors::HIGHLIGHT_PRIMARY
+ } else if self.pressed {
+ [0.30, 0.50, 0.32, 1.0]
+ } else if self.base.hovered {
+ [0.25, 0.25, 0.35, 1.0]
+ } else {
+ [0.18, 0.18, 0.24, 1.0]
+ };
+
+ quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, border_color));
+ quads.push((self.base.x + 1.0, self.base.y + top + 1.0, self.base.w - 2.0, visual_h - 2.0, bg_color));
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = Vec::new();
+ let top = self.base.label_offset();
+
+ if let Some(lbl) = self.control_label() {
+ labels.push(lbl);
+ }
+
+ let text_y = crate::layout::align_text_y(self.base.y, self.base.h, 12.0, top);
+
+ let (display_text, color) = if self.recording {
+ ("[ Press Keys... ]".to_string(), [135, 135, 153])
+ } else if self.value.is_empty() {
+ ("None".to_string(), [127, 127, 127])
+ } else {
+ (self.value.clone(), [221, 221, 226])
+ };
+
+ labels.push(TextLabel {
+ text: display_text,
+ x: self.base.x + 8.0,
+ y: text_y,
+ font_size: 12.0,
+ color,
+ });
+
+ labels
+ }
+
+ fn unfocus(&mut self) {
+ self.recording = false;
+ }
+}
+
+impl Control for KeybindRecorder {}
diff --git a/src/widget/input/mod.rs b/src/widget/input/mod.rs
index 2b7d13c..1b33d63 100644
--- a/src/widget/input/mod.rs
+++ b/src/widget/input/mod.rs
@@ -11,6 +11,7 @@ pub mod font_selector;
pub mod button_strip;
pub mod multi_control;
pub mod keybinds_control;
+pub mod keybind_recorder;
pub use canvas::Canvas;
pub use button::{Button, ButtonKind, PageButton};
@@ -25,6 +26,7 @@ pub use font_selector::FontSelector;
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 const BREADCRUMB_PADDING: f32 = 8.0;
pub const SEGMENT_GAP: f32 = 4.0;
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index e2dc580..0d37273 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -48,6 +48,10 @@ pub enum NamedKey {
PageDown,
PageUp,
Delete,
+ Control,
+ Shift,
+ Alt,
+ Super,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -669,7 +673,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
+ KeybindsControl, KeybindRow, KeybindRecorder
};
pub use self::container::{
Container, ContainerLayout, OverlayLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
@@ -734,6 +738,7 @@ pub trait GraphController {
fn set_show_network_grid(&mut self, show: bool);
fn take_pending_connection(&mut self) -> Option<(String, String)>;
fn cancel_connecting(&mut self);
+ fn is_node_rect(&self, qx: f32, qy: f32, qw: f32, qh: f32) -> bool;
}
pub trait SpreadsheetController {