git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit18b392fca1c9530e3f922b0ad9427efbab855061
parentbb1e038b40
authorLucas Galante <[email protected]>
date2026-07-01 17:14
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 {