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

commit4cd304f7eb5c5a4846de74f8125f9720b38cdb06
parentdf641ab5ff
authorLucas Galante <[email protected]>
date2026-06-13 21:30
Refactor UI layout, update widgets (button, checkbox, color_selector, dropdown, graph) and window runner

 src/backend/wgpu_adapter.rs           |   3 +-
 src/backend/window_runner.rs          |  43 ++-
 src/layout.rs                         | 124 ++++--
 src/main.rs                           |   6 +-
 src/widget/container/breadcrumb.rs    |  26 +-
 src/widget/container/content_bg.rs    |   2 +
 src/widget/container/menu.rs          |   2 +-
 src/widget/container/parameters_bg.rs | 691 +++++++++++++++++++++++++++-------
 src/widget/container/spreadsheet.rs   |   2 +
 src/widget/display/graph.rs           | 331 +++++++++++++++-
 src/widget/display/label.rs           |  11 +-
 src/widget/display/text_label.rs      |   2 +-
 src/widget/input/button.rs            |  87 ++++-
 src/widget/input/checkbox.rs          |   5 +-
 src/widget/input/color_selector.rs    | 192 ++++++++--
 src/widget/input/dropdown.rs          |   7 +-
 src/widget/mod.rs                     |   3 +
 17 files changed, 1305 insertions(+), 232 deletions(-)

diff --git a/src/backend/wgpu_adapter.rs b/src/backend/wgpu_adapter.rs
index fd16a54..1fda031 100644
--- a/src/backend/wgpu_adapter.rs
+++ b/src/backend/wgpu_adapter.rs
@@ -87,7 +87,8 @@ impl WgpuAdapter {
         surface.configure(&device, &config);
 
         // Initialize text rendering
-        let font_system = FontSystem::new();
+        let mut font_system = FontSystem::new();
+        font_system.db_mut().load_fonts_dir("/home/lsgalante/Dropbox/Fonts");
         let swash_cache = SwashCache::new();
         let cache = Cache::new(&device);
         let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index e4ac1e0..e11bd80 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -21,7 +21,7 @@ use smithay_client_toolkit::{
 };
 use wayland_client::{
     globals::{registry_queue_init, GlobalList},
-    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, wl_registry},
+    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, wl_registry, wl_region},
     Connection, QueueHandle, Proxy,
 };
 use smithay_client_toolkit::shell::xdg::popup::{Popup, PopupHandler, PopupConfigure};
@@ -70,7 +70,7 @@ fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font:
     let mut attrs = Attrs::new();
     if let Some(ref font_family) = family_name {
         let family = match font_family.as_str() {
-            "monospace" => glyphon::Family::Monospace,
+            "monospace" => glyphon::Family::Name(crate::layout::get_system_monospace_font()),
             "sans-serif" => glyphon::Family::SansSerif,
             "serif" => glyphon::Family::Serif,
             name => glyphon::Family::Name(name),
@@ -548,13 +548,13 @@ pub fn push_widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, cl
     let (x, y, ww, h) = w.rect();
     let corners = w.rounded_corners();
     if corners != (false, false, false, false) {
-        push_rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), clip_circle, corners, None, out);
+        push_rounded_rect_vertices_corners(x, y, ww, h, w.corner_radius(), sw, sh, w.color(), clip_circle, corners, None, out);
     } else {
         out.extend_from_slice(&quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle));
     }
 
     if let Some((color, thickness)) = w.solid_border() {
-        push_plate_solid_border_vertices(x, y, ww, h, 12.0, thickness, sw, sh, color, clip_circle, out);
+        push_plate_solid_border_vertices(x, y, ww, h, w.corner_radius(), thickness, sw, sh, color, clip_circle, out);
     }
 }
 
@@ -592,7 +592,7 @@ pub fn push_extra_quad_vertices(
         corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
     );
 
-    push_rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, None, out);
+    push_rounded_rect_vertices_corners(qx, qy, qw, qh, w.corner_radius(), sw, sh, qc, clip_circle, extra_corners, None, out);
 }
 
 pub fn extra_quad_vertices_clipped(
@@ -639,7 +639,7 @@ pub fn push_extra_quad_vertices_clipped(
         corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
     );
 
-    push_rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, Some(clip), out);
+    push_rounded_rect_vertices_corners(qx, qy, qw, qh, w.corner_radius(), sw, sh, qc, clip_circle, extra_corners, Some(clip), out);
 }
 
 pub fn circle_vertices(
@@ -812,6 +812,7 @@ pub trait Application: Sized + 'static {
     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
     fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64);
+    fn view_rounded_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
     fn view_vectors(&mut self, _vectors: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], LineCap)>, _size: LogicalSize, _scale: f64) {}
     fn overlay_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
     fn text_items(&self) -> &[TextItem];
@@ -1009,6 +1010,9 @@ impl<A: Application> EngineState<A> {
         let mut quads = Vec::new();
         self.inner.view(&mut quads, LogicalSize::new(logical_w, logical_h), scale_factor);
         
+        let mut rounded_quads = Vec::new();
+        self.inner.view_rounded_quads(&mut rounded_quads, LogicalSize::new(logical_w, logical_h), scale_factor);
+        
         let mut vectors = Vec::new();
         self.inner.view_vectors(&mut vectors, LogicalSize::new(logical_w, logical_h), scale_factor);
         
@@ -1020,6 +1024,13 @@ impl<A: Application> EngineState<A> {
         for &(qx, qy, qw, qh, qc) in &quads {
             verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
         }
+        for &(qx, qy, qw, qh, qr, qc) in &rounded_quads {
+            if qr > 0.1 {
+                push_rounded_rect_vertices_corners(qx, qy, qw, qh, qr, logical_w, logical_h, qc, [0.0, 0.0, 0.0], (true, true, true, true), None, &mut verts);
+            } else {
+                verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
+            }
+        }
         for &(vx1, vy1, vx2, vy2, vthickness, vcolor, vcap) in &vectors {
             verts.extend(vector_vertices(vx1, vy1, vx2, vy2, vthickness, logical_w, logical_h, vcolor, vcap));
         }
@@ -1721,6 +1732,17 @@ delegate_keyboard!(@<A: Application> EngineState<A>);
 delegate_registry!(@<A: Application> EngineState<A>);
 delegate_output!(@<A: Application> EngineState<A>);
 
+impl<A: Application> wayland_client::Dispatch<wl_region::WlRegion, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_region::WlRegion,
+        _event: wl_region::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
 pub fn run<A: Application>() {
     let conn = Connection::connect_to_env().unwrap();
     let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
@@ -1777,6 +1799,15 @@ pub fn run<A: Application>() {
 
     let surface = engine_state.compositor_state.create_surface(&qh);
     surface.set_buffer_scale(scale as i32);
+
+    if settings.app_id == "cce-status-interface" {
+        let compositor = engine_state.compositor_state.wl_compositor();
+        let region = compositor.create_region(&qh, ());
+        region.add(0, 0, settings.width as i32, settings.height as i32);
+        surface.set_input_region(Some(&region));
+        region.destroy();
+    }
+
     let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
     window.set_title(&settings.title);
     window.set_app_id(&settings.app_id);
diff --git a/src/layout.rs b/src/layout.rs
index 45a92b5..b8f62f0 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -52,6 +52,7 @@ static DROPDOWN_HEIGHT: RwLock<f32> = RwLock::new(44.0);
 static NESTED_SECTION_LABEL_ALIGNMENT: RwLock<u8> = RwLock::new(0);
 
 static LABEL_MARGIN: RwLock<f32> = RwLock::new(6.0);
+static BUTTON_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 
 pub fn reload_config() {
     if let Some(content) = read_config() {
@@ -298,6 +299,15 @@ pub fn reload_config() {
                     }
                 }
             }
+            if let Some(rest) = trimmed.strip_prefix("button_corner_radius") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                let val_str = rest.trim_end_matches('"').trim();
+                if let Ok(val) = val_str.parse::<f32>() {
+                    if let Ok(mut lock) = BUTTON_CORNER_RADIUS.write() {
+                        *lock = val;
+                    }
+                }
+            }
         }
         if menubar_font_changed {
             if let Ok(mut lock) = MENUBAR_FONT_CACHED.write() {
@@ -831,6 +841,34 @@ pub fn set_color_selector_preview_corner_radius(radius: f32) {
     }
 }
 
+pub fn button_corner_radius() -> f32 {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Some(content) = read_config() {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("button_corner_radius") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                    let val_str = rest.trim_end_matches('"').trim();
+                    if let Ok(val) = val_str.parse::<f32>() {
+                        if let Ok(mut lock) = BUTTON_CORNER_RADIUS.write() {
+                            *lock = val;
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *BUTTON_CORNER_RADIUS.read().unwrap()
+}
+
+pub fn set_button_corner_radius(radius: f32) {
+    if let Ok(mut lock) = BUTTON_CORNER_RADIUS.write() {
+        *lock = radius;
+    }
+}
+
 pub fn color_selector_preview_margin() -> f32 {
     use std::sync::Once;
     static INIT: Once = Once::new();
@@ -1078,6 +1116,9 @@ pub fn set_slider_height(height: f32) {
 
 pub trait RenderTarget {
     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
+    fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, _radius: f32) {
+        self.rect(color, x, y, w, h);
+    }
     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]);
     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _font: &str) {
         self.text(content, x, y, size, color);
@@ -1125,8 +1166,13 @@ impl RenderTarget for PopoverCollector {
 
 pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
     w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
+    let r = if w.rounded_corners() != (false, false, false, false) {
+        w.corner_radius()
+    } else {
+        0.0
+    };
     for (qx, qy, qw, qh, qc) in w.all_quads(ctx) {
-        pc.rect(qc, qx, qy, qw, qh);
+        pc.rect_with_radius(qc, qx, qy, qw, qh, r);
     }
     let font_opt = w.widget_font();
     for (label, bounds) in w.text_labels_with_bounds(ctx) {
@@ -1288,6 +1334,31 @@ impl<'a> Row<'a> {
     }
 }
 
+fn estimate_label_width_helper(label: &str, font_size: f32, font_fam: &str) -> f32 {
+    let fam_lower = font_fam.to_lowercase();
+    let is_mono = fam_lower.contains("mono") || fam_lower.contains("courier") || fam_lower == "monospace";
+    if is_mono {
+        label.chars().count() as f32 * font_size * 0.60
+    } else {
+        let mut width = 0.0;
+        for c in label.chars() {
+            let factor = match c {
+                'i' | 'l' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.30,
+                'f' | 'j' | 't' => 0.35,
+                'r' | 's' | 'c' | 'z' => 0.50,
+                'a' | 'b' | 'd' | 'e' | 'g' | 'h' | 'k' | 'n' | 'o' | 'p' | 'q' | 'u' | 'v' | 'x' | 'y' => 0.60,
+                'm' | 'w' | 'M' | 'W' | '&' | '@' | 'O' | 'Q' | 'G' => 0.85,
+                'A' | 'B' | 'C' | 'D' | 'H' | 'N' | 'U' | 'V' | 'X' | 'Y' => 0.75,
+                'E' | 'F' | 'K' | 'L' | 'P' | 'R' | 'S' | 'T' | 'Z' | 'J' => 0.68,
+                '0'..='9' => 0.60,
+                _ => 0.60,
+            };
+            width += factor * font_size;
+        }
+        width
+    }
+}
+
 pub struct Section {
     pub left: f32,
     pub top: f32,
@@ -1301,19 +1372,8 @@ impl Section {
     pub const DEFAULT_MARGIN_X: f32 = 12.0;
     pub const DEFAULT_ROW_GAP: f32 = 8.0;
 
-    fn estimate_label_width(label: &str, font_size: f32) -> f32 {
-        let mut width = 0.0;
-        for c in label.chars() {
-            let factor = match c {
-                'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
-                'r' | 's' | 'J' | 'c' | 'z' => 0.42,
-                'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
-                'A'..='Z' => 0.68,
-                _ => 0.55,
-            };
-            width += factor * font_size;
-        }
-        width
+    fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
+        estimate_label_width_helper(label, font_size, font_fam)
     }
 
     pub fn padding(&self) -> f32 {
@@ -1337,7 +1397,7 @@ impl Section {
         let (font_fam, font_size_opt) = parse_font_string(&font_setting);
         let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
         let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
-        let label_width = Self::estimate_label_width(label, font_size);
+        let label_width = Self::estimate_label_width(label, font_size, &font_fam);
         let label_x = if is_child {
             let base_x = match nested_section_label_alignment() {
                 0 => left + 12.0,
@@ -1968,19 +2028,8 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
     pub const DEFAULT_MARGIN_X: f32 = 12.0;
     pub const DEFAULT_ROW_GAP: f32 = 8.0;
 
-    fn estimate_label_width(label: &str, font_size: f32) -> f32 {
-        let mut width = 0.0;
-        for c in label.chars() {
-            let factor = match c {
-                'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
-                'r' | 's' | 'J' | 'c' | 'z' => 0.42,
-                'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
-                'A'..='Z' => 0.68,
-                _ => 0.55,
-            };
-            width += factor * font_size;
-        }
-        width
+    fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
+        estimate_label_width_helper(label, font_size, font_fam)
     }
 
     pub fn padding(&self) -> f32 {
@@ -2000,7 +2049,7 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
         let (font_fam, font_size_opt) = parse_font_string(&font_setting);
         let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
         let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
-        let label_width = Self::estimate_label_width(label, font_size);
+        let label_width = Self::estimate_label_width(label, font_size, &font_fam);
         let label_x = if is_child {
             let base_x = match nested_section_label_alignment() {
                 0 => left + 12.0,
@@ -2201,6 +2250,23 @@ impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
     }
 }
 
+pub fn get_system_monospace_font() -> &'static str {
+    static MONOSPACE_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
+    MONOSPACE_FONT.get_or_init(|| {
+        if let Ok(output) = std::process::Command::new("fc-match")
+            .args(["-f", "%{family}", "monospace"])
+            .output()
+        {
+            let name = String::from_utf8_lossy(&output.stdout);
+            let parsed = name.split(',').next().unwrap_or("monospace").trim();
+            if !parsed.is_empty() {
+                return parsed.to_string();
+            }
+        }
+        "monospace".to_string()
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/main.rs b/src/main.rs
index 6ab74c3..3c8f9ef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -174,7 +174,7 @@ fn widget_vertices(w: &dyn Element, sw: f32, sh: f32) -> Vec<Vertex> {
     let (x, y, ww, h) = w.rect();
     let corners = w.rounded_corners();
     if corners != (false, false, false, false) {
-        rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), corners)
+        rounded_rect_vertices_corners(x, y, ww, h, w.corner_radius(), sw, sh, w.color(), corners)
     } else {
         quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
     }
@@ -199,7 +199,7 @@ fn extra_quad_vertices(
         corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
     );
 
-    rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, extra_corners)
+    rounded_rect_vertices_corners(qx, qy, qw, qh, w.corner_radius(), sw, sh, qc, extra_corners)
 }
 
 fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
@@ -216,7 +216,7 @@ fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f3
     let mut attrs = Attrs::new();
     if let Some(font_name) = font {
         let family = match font_name {
-            "monospace" => glyphon::Family::Monospace,
+            "monospace" => glyphon::Family::Name(clear_ui::layout::get_system_monospace_font()),
             "sans-serif" => glyphon::Family::SansSerif,
             "serif" => glyphon::Family::Serif,
             _ => glyphon::Family::Name(font_name),
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index 604961e..bedf43b 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -22,9 +22,18 @@ impl Breadcrumb {
                path: Vec::new(), hovered_seg: None, clicked_seg: None, network_opacity: 1.0 }
     }
 
+    fn virtual_segs(&self) -> Vec<String> {
+        let mut segs = vec!["/".to_string()];
+        for s in &self.path {
+            segs.push(format!("{}/", s));
+        }
+        segs
+    }
+
     fn seg_at(&self, px: f32) -> Option<usize> {
         let mut cx = self.x + BREADCRUMB_PADDING;
-        for (i, seg) in self.path.iter().enumerate() {
+        let segs = self.virtual_segs();
+        for (i, seg) in segs.iter().enumerate() {
             let w = seg.len() as f32 * 7.5;
             if px >= cx && px < cx + w {
                 return Some(i);
@@ -42,6 +51,8 @@ impl Element for Breadcrumb {
         self as *const Self as *mut Self as *mut (dyn Element + 'static)
     }
 
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, false, false) }
+
     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; }
 
@@ -60,7 +71,7 @@ impl Element for Breadcrumb {
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, _py: f32, ctx: &mut UiContext) -> bool {
         if button != MouseButton::Left || state != ElementState::Pressed { return false; }
         if let Some(i) = self.seg_at(px) {
-            if i < self.path.len() - 1 {
+            if i < self.path.len() {
                 self.clicked_seg = Some(i);
                 return true;
             }
@@ -75,11 +86,12 @@ impl Element for Breadcrumb {
         let mut quads = Vec::new();
         if let Some(i) = self.hovered_seg {
             let mut cx = self.x + BREADCRUMB_PADDING;
+            let segs = self.virtual_segs();
             for j in 0..i {
-                let w = self.path[j].len() as f32 * 7.5;
+                let w = segs[j].len() as f32 * 7.5;
                 cx += w + SEGMENT_GAP;
             }
-            let w = self.path[i].len() as f32 * 7.5;
+            let w = segs[i].len() as f32 * 7.5;
             quads.push((cx, self.y, w, self.h, [1.0, 1.0, 1.0, 0.06]));
         }
         quads
@@ -88,13 +100,15 @@ impl Element for Breadcrumb {
     fn text_labels(&self) -> Vec<TextLabel> {
         let mut labels = Vec::new();
         let mut cx = self.x + BREADCRUMB_PADDING;
-        for (i, seg) in self.path.iter().enumerate() {
+        let segs = self.virtual_segs();
+        let len = segs.len();
+        for (i, seg) in segs.iter().enumerate() {
             labels.push(TextLabel {
                 text: seg.clone(),
                 x: cx,
                 y: self.y + 6.0,
                 font_size: 12.0,
-                color: if i == self.path.len() - 1 { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] },
+                color: if i == len - 1 { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] },
             });
             cx += seg.len() as f32 * 7.5 + SEGMENT_GAP;
         }
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 1d11bc2..2620413 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -248,4 +248,6 @@ impl GraphController for ContentBg {
     fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
     fn grid_origin(&self) -> (f32, f32) { (self.grid_origin_x, self.grid_origin_y) }
     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) {}
 }
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index f4270b4..e68d4ac 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -73,7 +73,7 @@ impl MenuBar {
             menu_dropdown_checked: Vec::new(),
             vertical: false,
             focused: false,
-            z_level: 100,
+            z_level: 0,
             center_items: false,
             title_pos: None,
             title_buf: None,
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 064a625..da05798 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1,6 +1,6 @@
 use crate::colors;
 use crate::widget::*;
-use crate::widget::input::{Slider, Spinbox};
+use crate::widget::input::{Slider, Spinbox, Button, Dropdown, TextBox, Checkbox};
 use crate::widget::display::{Float3, TextLabel};
 
 pub struct ParametersBg {
@@ -8,10 +8,15 @@ pub struct ParametersBg {
     display_params: Vec<(String, String, String)>,
     dragging_param: Option<usize>,
     pub focused_param: Option<usize>,
+    pub code_editor: Option<TextEditorState>,
     mouse_pos: Option<(f32, f32)>,
     sliders: Vec<Option<Slider>>,
     float3s: Vec<Option<Float3>>,
     spinboxes: Vec<Option<Spinbox>>,
+    pub buttons: Vec<Option<Button>>,
+    pub choices: Vec<Option<Dropdown>>,
+    pub texts: Vec<Option<TextBox>>,
+    pub checkboxes: Vec<Option<Checkbox>>,
     visible: bool,
     pub children: Vec<*mut (dyn Element + 'static)>,
     pub parent: Option<*mut (dyn Element + 'static)>,
@@ -24,10 +29,15 @@ impl ParametersBg {
             display_params: Vec::new(),
             dragging_param: None,
             focused_param: None,
+            code_editor: None,
             mouse_pos: None,
             sliders: Vec::new(),
             float3s: Vec::new(),
             spinboxes: Vec::new(),
+            buttons: Vec::new(),
+            choices: Vec::new(),
+            texts: Vec::new(),
+            checkboxes: Vec::new(),
             visible: true,
             children: Vec::new(),
             parent: None,
@@ -37,14 +47,25 @@ impl ParametersBg {
     pub fn get_param_rects(&self) -> Vec<(f32, f32, f32, f32)> {
         let mut rects = Vec::new();
         let mut cur_y = self.base.y + 30.0;
-        for p in &self.display_params {
+        for (i, p) in self.display_params.iter().enumerate() {
             let h = if p.2 == "code" {
-                200.0
+                let val_text = if self.focused_param == Some(i) {
+                    if let Some(ref editor) = self.code_editor {
+                        &editor.buffer
+                    } else {
+                        &p.1
+                    }
+                } else {
+                    &p.1
+                };
+                let line_count = val_text.split('\n').count();
+                let content_h = 22.0 + (line_count as f32 * 16.0) + 12.0;
+                content_h.max(200.0)
             } else if p.2 == "section" {
                 24.0
             } else if p.2.starts_with("float3") {
                 108.0
-            } else if p.2 == "text" {
+            } else if p.2 == "text" || p.2.starts_with("spinbox") || p.2.starts_with("choice") || p.2 == "button" || p.2 == "toggle" || p.2 == "checkbox" {
                 24.0
             } else {
                 20.0
@@ -81,6 +102,38 @@ impl ParametersBg {
                 sb.set_rect(box_x, r.1, box_w, r.3);
             }
         }
+        for (i, b_opt) in self.buttons.iter_mut().enumerate() {
+            if let Some(b) = b_opt {
+                let r = rects[i];
+                let box_x = self.base.x + 100.0;
+                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                b.set_rect(box_x, r.1, box_w, r.3);
+            }
+        }
+        for (i, d_opt) in self.choices.iter_mut().enumerate() {
+            if let Some(d) = d_opt {
+                let r = rects[i];
+                let box_x = self.base.x + 100.0;
+                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                d.set_rect(box_x, r.1, box_w, r.3);
+            }
+        }
+        for (i, tb_opt) in self.texts.iter_mut().enumerate() {
+            if let Some(tb) = tb_opt {
+                let r = rects[i];
+                let box_x = self.base.x + 100.0;
+                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                tb.set_rect(box_x, r.1, box_w, r.3);
+            }
+        }
+        for (i, cb_opt) in self.checkboxes.iter_mut().enumerate() {
+            if let Some(cb) = cb_opt {
+                let r = rects[i];
+                let box_x = self.base.x + 100.0;
+                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
+                cb.set_rect(box_x, r.1, box_w, r.3);
+            }
+        }
     }
 
     fn own_text_labels(&self) -> Vec<TextLabel> {
@@ -113,12 +166,28 @@ impl ParametersBg {
                 });
             } else if ptype == "code" {
                 labels.push(TextLabel {
-                    text: format!("{}:\n{}", name, value),
+                    text: format!("{}:", name),
                     x: self.base.x + 12.0,
                     y: r.1,
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
                 });
+                let val_text = if self.focused_param == Some(i) {
+                    if let Some(ref editor) = self.code_editor {
+                        editor.buffer.clone()
+                    } else {
+                        value.clone()
+                    }
+                } else {
+                    value.clone()
+                };
+                labels.push(TextLabel {
+                    text: val_text,
+                    x: r.0 + 12.0,
+                    y: r.1 + 22.0,
+                    font_size: 12.0,
+                    color: [0xee, 0xee, 0xf0],
+                });
             } else if ptype.starts_with("spinbox") {
                 labels.push(TextLabel {
                     text: name.clone(),
@@ -138,18 +207,9 @@ impl ParametersBg {
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
                 });
-                let val_text = if self.focused_param == Some(i) {
-                    format!("{}|", value)
-                } else {
-                    value.clone()
-                };
-                labels.push(TextLabel {
-                    text: val_text,
-                    x: self.base.x + 106.0,
-                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
-                    font_size: 12.0,
-                    color: [0xee, 0xee, 0xf0],
-                });
+                if let Some(tb) = &self.texts[i] {
+                    labels.extend(tb.text_labels());
+                }
             } else if ptype.starts_with("choice") {
                 labels.push(TextLabel {
                     text: name.clone(),
@@ -158,14 +218,14 @@ impl ParametersBg {
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
                 });
-                labels.push(TextLabel {
-                    text: value.clone(),
-                    x: self.base.x + 106.0,
-                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
-                    font_size: 12.0,
-                    color: [0xee, 0xee, 0xf0],
-                });
+                if let Some(d) = &self.choices[i] {
+                    labels.extend(d.text_labels());
+                }
             } else if ptype == "button" {
+                if let Some(b) = &self.buttons[i] {
+                    labels.extend(b.text_labels());
+                }
+            } else if ptype == "toggle" || ptype == "checkbox" {
                 labels.push(TextLabel {
                     text: name.clone(),
                     x: self.base.x + 8.0,
@@ -173,13 +233,9 @@ impl ParametersBg {
                     font_size: 12.0,
                     color: [0xaa, 0xaa, 0xbb],
                 });
-                labels.push(TextLabel {
-                    text: "Trigger".to_string(),
-                    x: self.base.x + 106.0,
-                    y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
-                    font_size: 12.0,
-                    color: [0xee, 0xee, 0xf0],
-                });
+                if let Some(cb) = &self.checkboxes[i] {
+                    labels.extend(cb.text_labels());
+                }
             } else {
                 labels.push(TextLabel {
                     text: format!("{}: {}", name, value),
@@ -246,6 +302,8 @@ fn parse_float3_value(val_str: &str, min: f32, max: f32) -> [f32; 3] {
 impl Element for ParametersBg {
     crate::impl_widget_base!(ParametersBg);
 
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
+
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         if let Some(b) = self.base_mut() {
             b.x = x;
@@ -297,7 +355,17 @@ impl Element for ParametersBg {
         if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
             return false;
         }
-        px >= self.base.x && px <= self.base.x + self.base.w && py >= self.base.y && py <= self.base.y + self.base.h
+        let (x, y, w, h) = self.rect();
+        let hit_base = px >= x && px <= x + w && py >= y && py <= y + h;
+        if hit_base {
+            return true;
+        }
+        if let Some((pop_x, pop_y, pop_w, pop_h)) = self.popover_rect() {
+            if px >= pop_x && px <= pop_x + pop_w && py >= pop_y && py <= pop_y + pop_h {
+                return true;
+            }
+        }
+        false
     }
 
     fn as_param_controller(&self) -> Option<&dyn ParamController> { Some(self) }
@@ -328,6 +396,23 @@ impl Element for ParametersBg {
                         let val2 = min + f.values[2] * (max - min);
                         p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
                     }
+                } else if p.2 == "text" {
+                    if let Some(tb) = &mut self.texts[idx] {
+                        tb.unfocus();
+                        p.1 = tb.text.clone();
+                    }
+                } else if p.2.starts_with("choice") {
+                    if let Some(d) = &mut self.choices[idx] {
+                        d.unfocus();
+                        if let Some(val) = d.get_value_string() {
+                            p.1 = val;
+                        }
+                    }
+                } else if p.2 == "code" {
+                    if let Some(ref editor) = self.code_editor {
+                        p.1 = editor.buffer.clone();
+                    }
+                    self.code_editor = None;
                 }
             }
         }
@@ -465,6 +550,49 @@ impl Element for ParametersBg {
         self.base.hovered = is_hit;
         let mut changed = was != is_hit;
 
+        for sb_opt in &mut self.spinboxes {
+            if let Some(sb) = sb_opt {
+                if sb.on_cursor_moved(px, py, ctx) {
+                    changed = true;
+                }
+            }
+        }
+        for f_opt in &mut self.float3s {
+            if let Some(f) = f_opt {
+                if f.on_cursor_moved(px, py, ctx) {
+                    changed = true;
+                }
+            }
+        }
+        for b_opt in &mut self.buttons {
+            if let Some(b) = b_opt {
+                if b.on_cursor_moved(px, py, ctx) {
+                    changed = true;
+                }
+            }
+        }
+        for d_opt in &mut self.choices {
+            if let Some(d) = d_opt {
+                if d.on_cursor_moved(px, py, ctx) {
+                    changed = true;
+                }
+            }
+        }
+        for tb_opt in &mut self.texts {
+            if let Some(tb) = tb_opt {
+                if tb.on_cursor_moved(px, py, ctx) {
+                    changed = true;
+                }
+            }
+        }
+        for cb_opt in &mut self.checkboxes {
+            if let Some(cb) = cb_opt {
+                if cb.on_cursor_moved(px, py, ctx) {
+                    changed = true;
+                }
+            }
+        }
+
         for &widget_ptr in &self.children {
             let widget = unsafe { &mut *widget_ptr };
             if widget.is_dragging() {
@@ -483,6 +611,22 @@ impl Element for ParametersBg {
             return false;
         }
 
+        // 1. Check open dropdown popovers first (since they are drawn on top)
+        for (i, d_opt) in self.choices.iter_mut().enumerate() {
+            if let Some(d) = d_opt {
+                if d.popover_rect().is_some() {
+                    if d.mouse_input(button, state, px, py, ctx) {
+                        if d.take_change() {
+                            if let Some(val) = d.get_value_string() {
+                                self.display_params[i].1 = val;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+
         for &widget_ptr in self.children.iter().rev() {
             let widget = unsafe { &mut *widget_ptr };
             if widget.popover_rect().is_some() {
@@ -492,6 +636,74 @@ impl Element for ParametersBg {
             }
         }
 
+        // 2. Propagate to our widgets
+        for (i, p) in self.display_params.iter_mut().enumerate() {
+            if p.2.starts_with("choice") {
+                if let Some(d) = &mut self.choices[i] {
+                    if d.mouse_input(button, state, px, py, ctx) {
+                        if d.take_change() {
+                            if let Some(val) = d.get_value_string() {
+                                p.1 = val;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            } else if p.2 == "button" {
+                if let Some(b) = &mut self.buttons[i] {
+                    if b.mouse_input(button, state, px, py, ctx) {
+                        if b.take_click() {
+                            p.1 = "clicked".to_string();
+                        }
+                        return true;
+                    }
+                }
+            } else if p.2 == "text" {
+                if let Some(tb) = &mut self.texts[i] {
+                    if tb.mouse_input(button, state, px, py, ctx) {
+                        if tb.editing {
+                            self.focused_param = Some(i);
+                        } else {
+                            if self.focused_param == Some(i) {
+                                self.focused_param = None;
+                            }
+                        }
+                        if tb.take_change() {
+                            if let Some(val) = tb.get_value_string() {
+                                p.1 = val;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            } else if p.2.starts_with("spinbox") {
+                if let Some(sb) = &mut self.spinboxes[i] {
+                    if sb.mouse_input(button, state, px, py, ctx) {
+                        p.1 = sb.value.to_string();
+                        if sb.editing {
+                            self.focused_param = Some(i);
+                        } else {
+                            if self.focused_param == Some(i) {
+                                self.focused_param = None;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            } else if p.2 == "toggle" || p.2 == "checkbox" {
+                if let Some(cb) = &mut self.checkboxes[i] {
+                    if cb.mouse_input(button, state, px, py, ctx) {
+                        if cb.take_change() {
+                            if let Some(val) = cb.get_value_string() {
+                                p.1 = val;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+
         for &widget_ptr in self.children.iter().rev() {
             let widget = unsafe { &mut *widget_ptr };
             if widget.mouse_input(button, state, px, py, ctx) {
@@ -510,53 +722,16 @@ impl Element for ParametersBg {
                     let r = rects[i];
                     if px >= r.0 && px <= r.0 + r.2 && py >= r.1 + 18.0 && py <= r.1 + r.3 {
                         self.focused_param = Some(i);
+                        let mut editor = TextEditorState::new(p.1.clone());
+                        let click_x = px - (r.0 + 12.0);
+                        let click_y = py - (r.1 + 22.0);
+                        let line = (click_y / 16.0).floor().max(0.0) as usize;
+                        let col = (click_x / 7.2 + 0.5).floor().max(0.0) as usize;
+                        editor.cursor_idx = map_2d_to_1d(&editor.buffer, line, col);
+                        self.code_editor = Some(editor);
                         clicked_any_focusable = true;
                         break;
                     }
-                } else if p.2 == "text" {
-                    let box_x = self.base.x + 100.0;
-                    let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
-                    let r = rects[i];
-                    if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
-                        self.focused_param = Some(i);
-                        clicked_any_focusable = true;
-                        break;
-                    }
-                } else if p.2.starts_with("choice") {
-                    let box_x = self.base.x + 100.0;
-                    let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
-                    let r = rects[i];
-                    if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
-                        if let Some(options_str) = p.2.strip_prefix("choice:") {
-                            let options: Vec<&str> = options_str.split(',').collect();
-                            if !options.is_empty() {
-                                let cur_idx = options.iter().position(|&o| o == p.1).unwrap_or(0);
-                                let next_idx = (cur_idx + 1) % options.len();
-                                p.1 = options[next_idx].to_string();
-                                return true;
-                            }
-                        }
-                    }
-                } else if p.2 == "button" {
-                    let box_x = self.base.x + 100.0;
-                    let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
-                    let r = rects[i];
-                    if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
-                        p.1 = "clicked".to_string();
-                        return true;
-                    }
-                } else if p.2.starts_with("spinbox") {
-                    if let Some(sb) = &mut self.spinboxes[i] {
-                        if sb.mouse_input(button, state, px, py, ctx) {
-                            p.1 = sb.value.to_string();
-                            if sb.editing {
-                                self.focused_param = Some(i);
-                            } else {
-                                self.unfocus();
-                            }
-                            return true;
-                        }
-                    }
                 } else if p.2.starts_with("slider") {
                     let r = rects[i];
                     if py >= r.1 && py <= r.1 + r.3 {
@@ -608,44 +783,156 @@ impl Element for ParametersBg {
             if event.state == ElementState::Pressed {
                 let p = &mut self.display_params[idx];
                 if p.2 == "code" {
-                    match &event.logical_key {
-                        Key::Named(NamedKey::Backspace) => {
-                            if !p.1.is_empty() {
-                                p.1.pop();
-                                return true;
+                    if let Some(mut editor) = self.code_editor.take() {
+                        let mut changed = false;
+                        let mut handled = true;
+                        let mut should_unfocus = false;
+                        match &event.logical_key {
+                            Key::Named(NamedKey::Backspace) => {
+                                changed = editor.delete_backwards();
+                            }
+                            Key::Named(NamedKey::Delete) => {
+                                changed = editor.delete_forwards();
+                            }
+                            Key::Named(NamedKey::Enter) => {
+                                editor.insert_text("\n");
+                                changed = true;
+                            }
+                            Key::Named(NamedKey::Escape) => {
+                                should_unfocus = true;
+                            }
+                            Key::Named(NamedKey::ArrowLeft) => {
+                                editor.move_cursor_left(false);
+                            }
+                            Key::Named(NamedKey::ArrowRight) => {
+                                editor.move_cursor_right(false);
+                            }
+                            Key::Named(NamedKey::ArrowUp) => {
+                                let (line, col) = get_cursor_line_col(&editor.buffer, editor.cursor_idx);
+                                if line > 0 {
+                                    editor.cursor_idx = map_2d_to_1d(&editor.buffer, line - 1, col);
+                                }
+                            }
+                            Key::Named(NamedKey::ArrowDown) => {
+                                let (line, col) = get_cursor_line_col(&editor.buffer, editor.cursor_idx);
+                                let total_lines = editor.buffer.split('\n').count();
+                                if line + 1 < total_lines {
+                                    editor.cursor_idx = map_2d_to_1d(&editor.buffer, line + 1, col);
+                                }
+                            }
+                            Key::Named(NamedKey::Home) => {
+                                editor.cursor_idx = get_line_start(&editor.buffer, editor.cursor_idx);
+                            }
+                            Key::Named(NamedKey::End) => {
+                                editor.cursor_idx = get_line_end(&editor.buffer, editor.cursor_idx);
+                            }
+                            Key::Character(s) => {
+                                if event.ctrl {
+                                    match s.to_lowercase().as_str() {
+                                        "f" => {
+                                            editor.move_cursor_right(false);
+                                        }
+                                        "b" => {
+                                            editor.move_cursor_left(false);
+                                        }
+                                        "p" => {
+                                            let (line, col) = get_cursor_line_col(&editor.buffer, editor.cursor_idx);
+                                            if line > 0 {
+                                                editor.cursor_idx = map_2d_to_1d(&editor.buffer, line - 1, col);
+                                            }
+                                        }
+                                        "n" => {
+                                            let (line, col) = get_cursor_line_col(&editor.buffer, editor.cursor_idx);
+                                            let total_lines = editor.buffer.split('\n').count();
+                                            if line + 1 < total_lines {
+                                                editor.cursor_idx = map_2d_to_1d(&editor.buffer, line + 1, col);
+                                            }
+                                        }
+                                        "a" => {
+                                            editor.cursor_idx = get_line_start(&editor.buffer, editor.cursor_idx);
+                                        }
+                                        "e" => {
+                                            editor.cursor_idx = get_line_end(&editor.buffer, editor.cursor_idx);
+                                        }
+                                        "d" => {
+                                            changed = editor.delete_forwards();
+                                        }
+                                        "h" => {
+                                            changed = editor.delete_backwards();
+                                        }
+                                        "k" => {
+                                            let current_idx = editor.cursor_idx;
+                                            let end_idx = get_line_end(&editor.buffer, current_idx);
+                                            let chars: Vec<char> = editor.buffer.chars().collect();
+                                            if chars.is_empty() {
+                                                // do nothing
+                                            } else if current_idx < chars.len() {
+                                                let delete_end = if chars[current_idx] == '\n' {
+                                                    current_idx + 1
+                                                } else {
+                                                    end_idx
+                                                };
+                                                let mut new_buf = String::new();
+                                                for i in 0..current_idx {
+                                                    new_buf.push(chars[i]);
+                                                }
+                                                for i in delete_end..chars.len() {
+                                                    new_buf.push(chars[i]);
+                                                }
+                                                editor.buffer = new_buf;
+                                                changed = true;
+                                            }
+                                        }
+                                        _ => {
+                                            handled = false;
+                                        }
+                                    }
+                                } else {
+                                    editor.insert_text(s);
+                                    changed = true;
+                                }
+                            }
+                            _ => {
+                                handled = false;
                             }
                         }
-                        Key::Named(NamedKey::Enter) => {
-                            p.1.push('\n');
-                            return true;
+                        if changed {
+                            p.1 = editor.buffer.clone();
                         }
-                        Key::Named(NamedKey::Escape) => {
+                        if should_unfocus {
+                            p.1 = editor.buffer;
                             self.focused_param = None;
-                            return true;
+                            self.code_editor = None;
+                        } else {
+                            self.code_editor = Some(editor);
                         }
-                        Key::Character(s) => {
-                            p.1.push_str(s);
+                        if handled {
                             return true;
                         }
-                        _ => {}
                     }
                 } else if p.2 == "text" {
-                    match &event.logical_key {
-                        Key::Named(NamedKey::Backspace) => {
-                            if !p.1.is_empty() {
-                                p.1.pop();
-                                return true;
+                    if let Some(tb) = &mut self.texts[idx] {
+                        if tb.keyboard_input(event, ctx) {
+                            if !tb.editing {
+                                p.1 = tb.text.clone();
+                                self.focused_param = None;
+                            } else {
+                                p.1 = tb.edit_buffer.clone();
                             }
-                        }
-                        Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Escape) => {
-                            self.focused_param = None;
                             return true;
                         }
-                        Key::Character(s) => {
-                            p.1.push_str(s);
+                    }
+                } else if p.2.starts_with("choice") {
+                    if let Some(d) = &mut self.choices[idx] {
+                        if d.keyboard_input(event, ctx) {
+                            if !d.open {
+                                if let Some(val) = d.get_value_string() {
+                                    p.1 = val;
+                                }
+                                self.focused_param = None;
+                            }
                             return true;
                         }
-                        _ => {}
                     }
                 } else if p.2.starts_with("spinbox") {
                     if let Some(sb) = &mut self.spinboxes[idx] {
@@ -867,48 +1154,37 @@ impl Element for ParametersBg {
                 quads.push((bx, by + bh - 1.0, bw, 1.0, border_color));
                 quads.push((bx, by, 1.0, bh, border_color));
                 quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
+                if self.focused_param == Some(i) {
+                    if let Some(ref editor) = self.code_editor {
+                        let (cursor_l, cursor_c) = get_cursor_line_col(&editor.buffer, editor.cursor_idx);
+                        let cursor_x = r.0 + 12.0 + (cursor_c as f32 * 7.2);
+                        let cursor_y = r.1 + 22.0 + (cursor_l as f32 * 16.0) + (16.0 - 13.0) / 2.0;
+                        if cursor_y >= r.1 + 18.0 && cursor_y + 13.0 <= r.1 + r.3 {
+                            quads.push((cursor_x, cursor_y, 1.5, 13.0, [0.80, 0.80, 0.85, 1.0]));
+                        }
+                    }
+                }
             } else if p.2 == "text" {
-                let box_x = self.base.x + 100.0;
-                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
-                quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
-                let border_color = if self.focused_param == Some(i) {
-                    [0.25, 0.45, 0.85, 1.0]
-                } else {
-                    [0.20, 0.20, 0.25, 1.0]
-                };
-                let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
-                let border_t = 1.0;
-                quads.push((bx, by, bw, border_t, border_color));
-                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
-                quads.push((bx, by, border_t, bh, border_color));
-                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+                if let Some(tb) = &self.texts[i] {
+                    quads.extend(tb.extra_quads());
+                }
             } else if p.2.starts_with("choice") {
-                let box_x = self.base.x + 100.0;
-                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
-                quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
-                let border_color = [0.20, 0.20, 0.25, 1.0];
-                let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
-                let border_t = 1.0;
-                quads.push((bx, by, bw, border_t, border_color));
-                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
-                quads.push((bx, by, border_t, bh, border_color));
-                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+                if let Some(d) = &self.choices[i] {
+                    quads.extend(d.extra_quads());
+                }
             } else if p.2 == "button" {
-                let box_x = self.base.x + 100.0;
-                let box_w = (self.base.w - 100.0 - 16.0).max(10.0);
-                quads.push((box_x, r.1, box_w, r.3, [0.15, 0.22, 0.38, 1.0]));
-                let border_color = [0.25, 0.35, 0.58, 1.0];
-                let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
-                let border_t = 1.0;
-                quads.push((bx, by, bw, border_t, border_color));
-                quads.push((bx, by + bh - border_t, bw, border_t, border_color));
-                quads.push((bx, by, border_t, bh, border_color));
-                quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+                if let Some(b) = &self.buttons[i] {
+                    quads.extend(b.extra_quads());
+                }
             } else if p.2.starts_with("spinbox") {
                 if let Some(sb) = &self.spinboxes[i] {
                     quads.push((sb.base.x, sb.base.y, sb.base.w, sb.base.h, sb.color()));
                     quads.extend(sb.extra_quads());
                 }
+            } else if p.2 == "toggle" || p.2 == "checkbox" {
+                if let Some(cb) = &self.checkboxes[i] {
+                    quads.extend(cb.extra_quads());
+                }
             }
         }
 
@@ -953,8 +1229,21 @@ impl Element for ParametersBg {
         }
         let mut result = Vec::new();
         let font = self.widget_font();
+        let rects = self.get_param_rects();
         for l in self.own_text_labels() {
-            result.push((l, font.clone(), None));
+            let mut bounds = None;
+            let mut label_font = font.clone();
+            for (i, p) in self.display_params.iter().enumerate() {
+                if p.2 == "code" {
+                    let r = rects[i];
+                    if l.y >= r.1 + 18.0 && l.y <= r.1 + r.3 {
+                        bounds = Some([r.0 + 1.0, r.1 + 19.0, r.0 + r.2 - 1.0, r.1 + r.3 - 1.0]);
+                        label_font = Some("monospace".to_string());
+                        break;
+                    }
+                }
+            }
+            result.push((l, label_font, bounds));
         }
         for &child_ptr in &self.children {
             let widget = unsafe { &*child_ptr };
@@ -967,6 +1256,13 @@ impl Element for ParametersBg {
         if !self.visible {
             return None;
         }
+        for d_opt in &self.choices {
+            if let Some(d) = d_opt {
+                if let Some(r) = d.popover_rect() {
+                    return Some(r);
+                }
+            }
+        }
         for &widget_ptr in self.children.iter().rev() {
             let widget = unsafe { &*widget_ptr };
             if let Some(r) = widget.popover_rect() {
@@ -980,6 +1276,11 @@ impl Element for ParametersBg {
         if !self.visible {
             return;
         }
+        for d_opt in &self.choices {
+            if let Some(d) = d_opt {
+                d.render_popover(pc);
+            }
+        }
         for &widget_ptr in self.children.iter().rev() {
             let widget = unsafe { &*widget_ptr };
             widget.render_popover(pc);
@@ -997,6 +1298,13 @@ impl Element for ParametersBg {
                 changed = true;
             }
         }
+        for cb_opt in &mut self.checkboxes {
+            if let Some(cb) = cb_opt {
+                if cb.tick(dt, ctx) {
+                    changed = true;
+                }
+            }
+        }
         changed
     }
 }
@@ -1075,6 +1383,40 @@ impl ParamController for ParametersBg {
                     None
                 }
             }).collect();
+            self.buttons = self.display_params.iter().map(|p| {
+                if p.2 == "button" {
+                    Some(Button::new(0.0, 0.0, 0.0, 0.0).with_label(&p.0))
+                } else {
+                    None
+                }
+            }).collect();
+            self.choices = self.display_params.iter().map(|p| {
+                if p.2.starts_with("choice:") {
+                    let options_str = p.2.strip_prefix("choice:").unwrap_or("");
+                    let options: Vec<String> = options_str.split(',').map(|s| s.to_string()).collect();
+                    let selected = options.iter().position(|o| o == &p.1).unwrap_or(0);
+                    Some(Dropdown::new(options, selected))
+                } else {
+                    None
+                }
+            }).collect();
+            self.texts = self.display_params.iter().map(|p| {
+                if p.2 == "text" {
+                    Some(TextBox::new(p.1.clone()))
+                } else {
+                    None
+                }
+            }).collect();
+            self.checkboxes = self.display_params.iter().map(|p| {
+                if p.2 == "toggle" || p.2 == "checkbox" {
+                    let checked = p.1.trim().to_lowercase() == "true";
+                    let mut cb = Checkbox::new();
+                    cb.set_checked(checked);
+                    Some(cb)
+                } else {
+                    None
+                }
+            }).collect();
         } else {
             for (i, p_new) in params.iter().enumerate() {
                 if Some(i) != self.focused_param && Some(i) != self.dragging_param {
@@ -1098,6 +1440,25 @@ impl ParamController for ParametersBg {
                             let val = p_new.1.parse::<i32>().unwrap_or(min);
                             sb.value = val;
                         }
+                    } else if let Some(ref mut d) = self.choices[i] {
+                        if !d.open {
+                            if let Some(options_str) = p_new.2.strip_prefix("choice:") {
+                                let options: Vec<String> = options_str.split(',').map(|s| s.to_string()).collect();
+                                if d.options != options {
+                                    d.options = options.clone();
+                                }
+                                if let Some(idx) = options.iter().position(|o| o == &p_new.1) {
+                                    d.selected = idx;
+                                }
+                            }
+                        }
+                    } else if let Some(ref mut tb) = self.texts[i] {
+                        if !tb.editing {
+                            tb.set_value_string(&p_new.1);
+                        }
+                    } else if let Some(ref mut cb) = self.checkboxes[i] {
+                        let checked = p_new.1.trim().to_lowercase() == "true";
+                        cb.set_checked(checked);
                     }
                 }
             }
@@ -1106,3 +1467,63 @@ impl ParamController for ParametersBg {
     }
 }
 
+fn get_cursor_line_col(buffer: &str, cursor_idx: usize) -> (usize, usize) {
+    let mut cur_line = 0;
+    let mut cur_col = 0;
+    let mut count = 0;
+    for c in buffer.chars() {
+        if count == cursor_idx {
+            return (cur_line, cur_col);
+        }
+        if c == '\n' {
+            cur_line += 1;
+            cur_col = 0;
+        } else {
+            cur_col += 1;
+        }
+        count += 1;
+    }
+    (cur_line, cur_col)
+}
+
+fn map_2d_to_1d(buffer: &str, line: usize, col: usize) -> usize {
+    let mut target_line = line;
+    let lines: Vec<Vec<char>> = buffer.split('\n').map(|l| l.chars().collect()).collect();
+    if lines.is_empty() {
+        return 0;
+    }
+    if target_line >= lines.len() {
+        target_line = lines.len() - 1;
+    }
+    let mut target_col = col;
+    if target_col > lines[target_line].len() {
+        target_col = lines[target_line].len();
+    }
+    let mut index = 0;
+    for i in 0..target_line {
+        index += lines[i].len() + 1; // +1 for the '\n'
+    }
+    index += target_col;
+    index
+}
+
+fn get_line_start(buffer: &str, cursor_idx: usize) -> usize {
+    let (line, _) = get_cursor_line_col(buffer, cursor_idx);
+    map_2d_to_1d(buffer, line, 0)
+}
+
+fn get_line_end(buffer: &str, cursor_idx: usize) -> usize {
+    let (line, _) = get_cursor_line_col(buffer, cursor_idx);
+    let lines: Vec<Vec<char>> = buffer.split('\n').map(|l| l.chars().collect()).collect();
+    if lines.is_empty() {
+        return 0;
+    }
+    let line_len = if line < lines.len() {
+        lines[line].len()
+    } else {
+        lines[lines.len() - 1].len()
+    };
+    map_2d_to_1d(buffer, line, line_len)
+}
+
+
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index c0f1d42..2440960 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -41,6 +41,8 @@ impl Spreadsheet {
 }
 
 impl Element for Spreadsheet {
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
+
     fn rect(&self) -> (f32, f32, f32, f32) {
         if !self.visible {
             (0.0, 0.0, 0.0, 0.0)
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index b363541..39130b0 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -2,6 +2,14 @@ use crate::colors;
 use crate::widget::*;
 use crate::widget::display::TextLabel;
 
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum PortType {
+    Input,
+    Output,
+}
+
+fn default_outputs() -> usize { 1 }
+
 #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
 pub struct GraphNode {
     #[serde(default)]
@@ -10,6 +18,12 @@ pub struct GraphNode {
     pub position: (f32, f32), // (column, row)
     pub parameters: Vec<(String, String, String)>, // (name, value, type)
     pub geom_visible: bool,
+    #[serde(default)]
+    pub node_type: String,
+    #[serde(default)]
+    pub inputs: usize,
+    #[serde(default = "default_outputs")]
+    pub outputs: usize,
 }
 
 pub struct Graph {
@@ -42,8 +56,15 @@ pub struct Graph {
 
     uniform_background: bool,
     network_opacity: f32,
+    cell_opacity: f32,
+    gap_opacity: f32,
     cell_color: [f32; 3],
     gap_color: [f32; 3],
+
+    // Connection state
+    connecting_from: Option<(usize, PortType, usize)>,
+    current_mouse_pos: (f32, f32),
+    pending_connection: Option<(String, String)>,
 }
 
 impl Graph {
@@ -52,6 +73,14 @@ 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;
@@ -86,8 +115,13 @@ impl Graph {
             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],
+            connecting_from: None,
+            current_mouse_pos: (0.0, 0.0),
+            pending_connection: None,
         }
     }
 
@@ -108,6 +142,11 @@ impl Graph {
     }
 
     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" {
+                return None;
+            }
+        }
         let (nx, ny, nw, nh) = self.node_rect(idx)?;
         let scale_f = nw / 80.0;
         let size = (18.0 * scale_f).clamp(6.0, 50.0);
@@ -142,6 +181,8 @@ impl Element for Graph {
         self as *const Self as *mut Self as *mut (dyn Element + 'static)
     }
 
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, true, true) }
+
     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; }
 
@@ -186,6 +227,17 @@ impl Element for Graph {
         labels
     }
 
+    fn text_labels_with_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+        let bounds = Some([self.x, self.y, self.x + self.w, self.y + self.h]);
+        self.text_labels().into_iter().map(|l| (l, bounds)).collect()
+    }
+
+    fn text_labels_with_font_and_bounds(&self, _ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let font = self.widget_font();
+        let bounds = Some([self.x, self.y, self.x + self.w, self.y + self.h]);
+        self.text_labels().into_iter().map(|l| (l, font.clone(), bounds)).collect()
+    }
+
     fn focus(&mut self) {
         focus::set_focused(self);
     }
@@ -244,6 +296,11 @@ impl Element for Graph {
     }
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        let mut changed = false;
+        if self.connecting_from.is_some() {
+            self.current_mouse_pos = (px, py);
+            changed = true;
+        }
         let was_toggle_hovered = self.toggle_hovered_idx;
         self.toggle_hovered_idx = None;
         for i in 0..self.nodes.len() {
@@ -254,13 +311,127 @@ impl Element for Graph {
                 }
             }
         }
-        was_toggle_hovered != self.toggle_hovered_idx
+        if was_toggle_hovered != self.toggle_hovered_idx {
+            changed = true;
+        }
+        changed
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if button == MouseButton::Right && state == ElementState::Pressed {
+            if self.connecting_from.is_some() {
+                self.connecting_from = None;
+                return true;
+            }
+        }
         if button != MouseButton::Left { return false; }
         match state {
             ElementState::Pressed => {
+                println!("DEBUG Graph::mouse_input: Pressed px={}, py={}, connecting_from={:?}", px, py, self.connecting_from);
+                // First, check direct port clicks
+                for i in (0..self.nodes.len()).rev() {
+                    if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+                        let scale_f = nw / 80.0;
+                        let port_click_radius = (20.0 * scale_f).max(12.0);
+                        let port_click_radius_sq = port_click_radius * port_click_radius;
+
+                        let node = &self.nodes[i];
+                        // Check inputs (top edge)
+                        for k in 0..node.inputs {
+                            let port_x = nx + nw * (k + 1) as f32 / (node.inputs + 1) as f32;
+                            let port_y = ny;
+                            let dx = px - port_x;
+                            let dy = py - port_y;
+                            println!("  Checking input node={} port={} port_x={} port_y={} dist_sq={}", node.name, k, port_x, port_y, dx*dx + dy*dy);
+                            if dx * dx + dy * dy <= port_click_radius_sq {
+                                if let Some((src_idx, src_port_type, _src_port_idx)) = self.connecting_from {
+                                    if src_idx != i && src_port_type == PortType::Output {
+                                        let output_node = &self.nodes[src_idx];
+                                        let input_node = &self.nodes[i];
+                                        self.pending_connection = Some((input_node.id.clone(), output_node.name.clone()));
+                                        self.connecting_from = None;
+                                        println!("  Port connection created: Input={} from Output={}", input_node.name, output_node.name);
+                                        return true;
+                                    } else {
+                                        self.connecting_from = None;
+                                        println!("  Port connection aborted (same node or incompatible ports)");
+                                        return true;
+                                    }
+                                } else {
+                                    self.connecting_from = Some((i, PortType::Input, k));
+                                    self.current_mouse_pos = (px, py);
+                                    println!("  Start connecting from Input port of node={}", node.name);
+                                    return true;
+                                }
+                            }
+                        }
+
+                        // Check outputs (bottom edge)
+                        for k in 0..node.outputs {
+                            let port_x = nx + nw * (k + 1) as f32 / (node.outputs + 1) as f32;
+                            let port_y = ny + nh;
+                            let dx = px - port_x;
+                            let dy = py - port_y;
+                            println!("  Checking output node={} port={} port_x={} port_y={} dist_sq={}", node.name, k, port_x, port_y, dx*dx + dy*dy);
+                            if dx * dx + dy * dy <= port_click_radius_sq {
+                                if let Some((src_idx, src_port_type, _src_port_idx)) = self.connecting_from {
+                                    if src_idx != i && src_port_type == PortType::Input {
+                                        let output_node = &self.nodes[i];
+                                        let input_node = &self.nodes[src_idx];
+                                        self.pending_connection = Some((input_node.id.clone(), output_node.name.clone()));
+                                        self.connecting_from = None;
+                                        println!("  Port connection created: Input={} from Output={}", input_node.name, output_node.name);
+                                        return true;
+                                    } else {
+                                        self.connecting_from = None;
+                                        println!("  Port connection aborted (same node or incompatible ports)");
+                                        return true;
+                                    }
+                                } else {
+                                    self.connecting_from = Some((i, PortType::Output, k));
+                                    self.current_mouse_pos = (px, py);
+                                    println!("  Start connecting from Output port of node={}", node.name);
+                                    return true;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // Fallback: If we are actively connecting and clicked on a target node body, connect to its closest compatible port
+                if let Some((src_idx, src_port_type, _src_port_idx)) = self.connecting_from {
+                    for i in (0..self.nodes.len()).rev() {
+                        if src_idx != i {
+                            if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+                                println!("  Checking fallback body node={} nx={} ny={} nw={} nh={}", self.nodes[i].name, nx, ny, nw, nh);
+                                if px >= nx && px < nx + nw && py >= ny && py < ny + nh {
+                                    let node = &self.nodes[i];
+                                    if src_port_type == PortType::Output && node.inputs > 0 {
+                                        let output_node = &self.nodes[src_idx];
+                                        let input_node = &self.nodes[i];
+                                        self.pending_connection = Some((input_node.id.clone(), output_node.name.clone()));
+                                        self.connecting_from = None;
+                                        println!("  Fallback connection created: Input={} from Output={}", input_node.name, output_node.name);
+                                        return true;
+                                    } else if src_port_type == PortType::Input && node.outputs > 0 {
+                                        let output_node = &self.nodes[i];
+                                        let input_node = &self.nodes[src_idx];
+                                        self.pending_connection = Some((input_node.id.clone(), output_node.name.clone()));
+                                        self.connecting_from = None;
+                                        println!("  Fallback connection created: Input={} from Output={}", input_node.name, output_node.name);
+                                        return true;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if self.connecting_from.is_some() {
+                    println!("  Clearing connecting_from because it didn't hit any ports or node bodies");
+                    self.connecting_from = None;
+                }
+
                 for i in (0..self.nodes.len()).rev() {
                     if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
                         if px >= tx && px < tx + tw && py >= ty && py < ty + th {
@@ -331,9 +502,21 @@ impl Element for Graph {
             if let Some((_, input_name, _)) = node.parameters.iter().find(|(name, _, _)| name.eq_ignore_ascii_case("input")) {
                 if let Some(src_idx) = self.nodes.iter().position(|n| n.name == *input_name) {
                     if let (Some((sx, sy, sw, sh)), Some((ex, ey, ew, _eh))) = (self.node_rect(src_idx), self.node_rect(i)) {
-                        let start_x = sx + sw / 2.0;
+                        let src_outputs = self.nodes[src_idx].outputs;
+                        let target_inputs = node.inputs;
+
+                        let start_x = if src_outputs > 0 {
+                            sx + sw * 1.0 / (src_outputs + 1) as f32
+                        } else {
+                            sx + sw / 2.0
+                        };
                         let start_y = sy + sh;
-                        let end_x = ex + ew / 2.0;
+
+                        let end_x = if target_inputs > 0 {
+                            ex + ew * 1.0 / (target_inputs + 1) as f32
+                        } else {
+                            ex + ew / 2.0
+                        };
                         let end_y = ey;
 
                         let mid_y = start_y + (end_y - start_y) / 2.0;
@@ -378,6 +561,62 @@ impl Element for Graph {
             }
         }
 
+        // Draw connection wire preview if in progress
+        if let Some((node_idx, port_type, port_idx)) = self.connecting_from {
+            if let Some((nx, ny, nw, nh)) = self.node_rect(node_idx) {
+                let start_x = match port_type {
+                    PortType::Input => nx + nw * (port_idx + 1) as f32 / (self.nodes[node_idx].inputs + 1) as f32,
+                    PortType::Output => nx + nw * (port_idx + 1) as f32 / (self.nodes[node_idx].outputs + 1) as f32,
+                };
+                let start_y = match port_type {
+                    PortType::Input => ny,
+                    PortType::Output => ny + nh,
+                };
+
+                let end_x = self.current_mouse_pos.0;
+                let end_y = self.current_mouse_pos.1;
+
+                let preview_color = [1.0, 0.6, 0.0, 0.8]; // Golden orange preview
+                let mid_y = start_y + (end_y - start_y) / 2.0;
+
+                // Vertical segment 1
+                let v1_min_y = start_y.min(mid_y);
+                let v1_max_y = start_y.max(mid_y);
+                push_clipped(
+                    start_x - wire_thickness / 2.0,
+                    v1_min_y,
+                    wire_thickness,
+                    v1_max_y - v1_min_y,
+                    preview_color,
+                    &mut quads,
+                );
+
+                // Horizontal segment
+                let h_min_x = start_x.min(end_x);
+                let h_max_x = start_x.max(end_x);
+                push_clipped(
+                    h_min_x,
+                    mid_y - wire_thickness / 2.0,
+                    h_max_x - h_min_x,
+                    wire_thickness,
+                    preview_color,
+                    &mut quads,
+                );
+
+                // Vertical segment 2
+                let v2_min_y = mid_y.min(end_y);
+                let v2_max_y = mid_y.max(end_y);
+                push_clipped(
+                    end_x - wire_thickness / 2.0,
+                    v2_min_y,
+                    wire_thickness,
+                    v2_max_y - v2_min_y,
+                    preview_color,
+                    &mut quads,
+                );
+            }
+        }
+
         if self.show_network_grid && self.grid_size_x > 0.0 && self.grid_size_y > 0.0 && !self.uniform_background {
             let step_y = self.grid_size_y + self.skipped_row_h;
             let step_x = self.grid_size_x + self.skipped_col_w;
@@ -389,20 +628,50 @@ impl Element for Graph {
                 let cx_start = (((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1).max(-100_000);
                 let cx_end = (((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1).min(100_000);
 
-                // Draw gap color as solid background color of grid
-                push_clipped(self.x, self.y, self.w, self.h, [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity], &mut quads);
-
-                // Draw filled cells with cell color
+                // Draw gap rectangles and cells in a single loop with matching rounded coordinate boundaries to prevent seams
                 for r in ry_start..=ry_end {
-                    let y1 = (self.grid_origin_y + (r as f32) * step_y).round();
+                    let y_cell_start = (self.grid_origin_y + (r as f32) * step_y).round();
+                    let y_cell_end = (self.grid_origin_y + (r as f32) * step_y + self.grid_size_y).round();
+                    let y2 = (self.grid_origin_y + ((r + 1) as f32) * step_y).round();
+
+                    let cell_h = y_cell_end - y_cell_start;
+                    let gap_h = y2 - y_cell_end;
+
                     for c in cx_start..=cx_end {
-                        let x1 = (self.grid_origin_x + (c as f32) * step_x).round();
+                        let x_cell_start = (self.grid_origin_x + (c as f32) * step_x).round();
+                        let x_cell_end = (self.grid_origin_x + (c as f32) * step_x + self.grid_size_x).round();
+                        let x2 = (self.grid_origin_x + ((c + 1) as f32) * step_x).round();
+
+                        let cell_w = x_cell_end - x_cell_start;
+                        let gap_w = x2 - x_cell_end;
+
+                        // Draw right gap rectangle (shares cell height, sits to the right of the cell)
                         push_clipped(
-                            x1,
-                            y1,
-                            self.grid_size_x.round(),
-                            self.grid_size_y.round(),
-                            [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity],
+                            x_cell_end,
+                            y_cell_start,
+                            gap_w,
+                            cell_h,
+                            [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.gap_opacity],
+                            &mut quads,
+                        );
+
+                        // Draw bottom gap rectangle (shares the full step width, sits below the cell/right gap)
+                        push_clipped(
+                            x_cell_start,
+                            y_cell_end,
+                            x2 - x_cell_start,
+                            gap_h,
+                            [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.gap_opacity],
+                            &mut quads,
+                        );
+
+                        // Draw cell rectangle with the exact same bounding boundaries
+                        push_clipped(
+                            x_cell_start,
+                            y_cell_start,
+                            cell_w,
+                            cell_h,
+                            [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.cell_opacity],
                             &mut quads,
                         );
                     }
@@ -410,6 +679,17 @@ impl Element for Graph {
             }
         }
 
+        if self.grid_size_x > 0.0 && self.grid_size_y > 0.0 {
+            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);
+
+            // 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);
+        }
+
         for i in 0..self.nodes.len() {
             if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
                 let scale_f = nw / 80.0;
@@ -422,6 +702,23 @@ impl Element for Graph {
                 };
                 push_clipped(nx, ny, nw, nh, bg_color, &mut quads);
 
+                // Draw input ports on top edge
+                let port_size = (6.0 * scale_f).max(2.0);
+                let port_color = [0.1, 0.8, 0.4, 1.0]; // Bright green/emerald
+                let node = &self.nodes[i];
+                for k in 0..node.inputs {
+                    let px = nx + nw * (k + 1) as f32 / (node.inputs + 1) as f32 - port_size / 2.0;
+                    let py = ny - port_size / 2.0;
+                    push_clipped(px, py, port_size, port_size, port_color, &mut quads);
+                }
+
+                // Draw output ports on bottom edge
+                for k in 0..node.outputs {
+                    let px = nx + nw * (k + 1) as f32 / (node.outputs + 1) as f32 - port_size / 2.0;
+                    let py = ny + nh - port_size / 2.0;
+                    push_clipped(px, py, port_size, port_size, port_color, &mut quads);
+                }
+
                 if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
                     let btn_color = if self.toggle_hovered_idx == Some(i) {
                         colors::TOGGLE_HOVER
@@ -515,5 +812,11 @@ impl GraphController for Graph {
     fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
     fn grid_origin(&self) -> (f32, f32) { (self.grid_origin_x, self.grid_origin_y) }
     fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
+    fn take_pending_connection(&mut self) -> Option<(String, String)> {
+        self.pending_connection.take()
+    }
+    fn cancel_connecting(&mut self) {
+        self.connecting_from = None;
+    }
 }
 
diff --git a/src/widget/display/label.rs b/src/widget/display/label.rs
index d85c19b..a83fe17 100644
--- a/src/widget/display/label.rs
+++ b/src/widget/display/label.rs
@@ -106,7 +106,7 @@ impl StyledLabel {
         let metrics = glyphon::Metrics::new(physical_size, physical_size * 1.4);
         let mut buffer = glyphon::Buffer::new(fs, metrics);
         let family_enum = match family {
-            "monospace" => glyphon::Family::Monospace,
+            "monospace" => glyphon::Family::Name(crate::layout::get_system_monospace_font()),
             "sans-serif" => glyphon::Family::SansSerif,
             "serif" => glyphon::Family::Serif,
             name => glyphon::Family::Name(name),
@@ -158,11 +158,12 @@ impl StyledLabel {
             let font_size = self.buffer.metrics().font_size / scale;
             let line_y = self.buffer.layout_runs().next().map(|r| r.line_y).unwrap_or(font_size * scale * 1.05) / scale;
             let offset_y = line_y - 0.28 * font_size;
+            let padding = 4.0;
             Some((
-                x,
-                y + offset_y * scale,
-                self.w,
-                1.0 * scale,
+                x - padding,
+                y + offset_y,
+                self.w + 2.0 * padding,
+                1.0,
                 col,
             ))
         } else {
diff --git a/src/widget/display/text_label.rs b/src/widget/display/text_label.rs
index ef18314..6df7db1 100644
--- a/src/widget/display/text_label.rs
+++ b/src/widget/display/text_label.rs
@@ -80,7 +80,7 @@ pub(crate) fn make_widget_text_buffer(fs: &mut glyphon::FontSystem, text: &str,
     let mut buf = glyphon::Buffer::new(fs, metrics);
     let (family_name, _) = crate::layout::parse_font_string(font_family);
     let family = match family_name.as_str() {
-        "monospace" => glyphon::Family::Monospace,
+        "monospace" => glyphon::Family::Name(crate::layout::get_system_monospace_font()),
         "sans-serif" => glyphon::Family::SansSerif,
         "serif" => glyphon::Family::Serif,
         _ => glyphon::Family::Name(&family_name),
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 9132191..468ecc8 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -17,6 +17,10 @@ pub struct Button {
     kind: ButtonKind,
     pub selected: bool,
     pub on_click_cb: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
+    pub bg: Option<[f32; 4]>,
+    pub hover_bg: Option<[f32; 4]>,
+    pub label_color: Option<[f32; 4]>,
+    pub left_align: bool,
 }
 
 impl std::fmt::Debug for Button {
@@ -41,6 +45,10 @@ impl Button {
             kind: ButtonKind::Primary,
             selected: false,
             on_click_cb: None,
+            bg: None,
+            hover_bg: None,
+            label_color: None,
+            left_align: false,
         }
     }
 
@@ -52,6 +60,10 @@ impl Button {
             kind: ButtonKind::Reset,
             selected: false,
             on_click_cb: None,
+            bg: None,
+            hover_bg: None,
+            label_color: None,
+            left_align: false,
         }
     }
 
@@ -63,6 +75,10 @@ impl Button {
             kind: ButtonKind::ListRow,
             selected: false,
             on_click_cb: None,
+            bg: None,
+            hover_bg: None,
+            label_color: None,
+            left_align: false,
         }
     }
 
@@ -74,6 +90,10 @@ impl Button {
             kind: ButtonKind::CopyIcon,
             selected: false,
             on_click_cb: None,
+            bg: None,
+            hover_bg: None,
+            label_color: None,
+            left_align: false,
         }
     }
 
@@ -91,6 +111,26 @@ impl Button {
         self.on_click_cb = Some(std::sync::Arc::new(cb));
         self
     }
+
+    pub fn with_bg(mut self, bg: [f32; 4]) -> Self {
+        self.bg = Some(bg);
+        self
+    }
+
+    pub fn with_hover_bg(mut self, hover_bg: [f32; 4]) -> Self {
+        self.hover_bg = Some(hover_bg);
+        self
+    }
+
+    pub fn with_label_color(mut self, label_color: [f32; 4]) -> Self {
+        self.label_color = Some(label_color);
+        self
+    }
+
+    pub fn with_left_align(mut self, left_align: bool) -> Self {
+        self.left_align = left_align;
+        self
+    }
 }
 
 impl Element for Button {
@@ -98,6 +138,15 @@ impl Element for Button {
     fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
 
     fn color(&self) -> [f32; 4] {
+        if self.pressed || self.base.hovered {
+            if let Some(hbg) = self.hover_bg {
+                return hbg;
+            }
+        } else {
+            if let Some(bg) = self.bg {
+                return bg;
+            }
+        }
         match self.kind {
             ButtonKind::Primary => {
                 if self.pressed { colors::BUTTON_PRESS }
@@ -171,16 +220,29 @@ impl Element for Button {
             } else {
                 TextLabel::estimate_width(label, font_size)
             };
-            let color = match self.kind {
-                ButtonKind::ListRow | ButtonKind::CopyIcon => {
-                    if self.selected { [230, 230, 242] }
-                    else { [178, 178, 191] }
+            let color = if let Some(lc) = self.label_color {
+                [
+                    (lc[0] * 255.0) as u8,
+                    (lc[1] * 255.0) as u8,
+                    (lc[2] * 255.0) as u8,
+                ]
+            } else {
+                match self.kind {
+                    ButtonKind::ListRow | ButtonKind::CopyIcon => {
+                        if self.selected { [230, 230, 242] }
+                        else { [178, 178, 191] }
+                    }
+                    _ => [0xcc, 0xcc, 0xd4]
                 }
-                _ => [0xcc, 0xcc, 0xd4]
+            };
+            let x = if self.left_align {
+                self.base.x + 8.0
+            } else {
+                self.base.x + (self.base.w - est_w) / 2.0
             };
             labels.push(TextLabel {
                 text: label.clone(),
-                x: self.base.x + (self.base.w - est_w) / 2.0,
+                x,
                 y: self.base.y + (self.base.h - font_size) / 2.0 - 1.0,
                 font_size,
                 color,
@@ -195,6 +257,19 @@ impl Element for Button {
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         vec![(self.base.x, self.base.y, self.base.w, self.base.h, self.color())]
     }
+
+    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+        let r = crate::layout::button_corner_radius();
+        if r > 0.0 {
+            (true, true, true, true)
+        } else {
+            (false, false, false, false)
+        }
+    }
+
+    fn corner_radius(&self) -> f32 {
+        crate::layout::button_corner_radius()
+    }
 }
 
 pub enum PageButton {
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 44f8e25..6746d7a 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -88,10 +88,11 @@ impl Element for Checkbox {
         }
         if button != MouseButton::Left { return false; }
         match state {
-            ElementState::Released => {
+            ElementState::Pressed => {
                 if self.hit_test(px, py, ctx) {
                     self.checked = !self.checked;
                     self.just_clicked = true;
+                    self.just_changed = true;
                     return true;
                 }
             }
@@ -218,7 +219,7 @@ impl Element for Toggle {
     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::Released => {
+            ElementState::Pressed => {
                 if self.hit_test(px, py, ctx) {
                     self.toggled = !self.toggled;
                     self.just_toggled = true;
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 5328a50..fd3bfc3 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -5,6 +5,7 @@ use crate::widget::*;
 pub struct ColorSelector {
     pub(crate) base: Widget,
     pub color: [u8; 3],
+    pub alpha: u8,
     just_clicked: bool,
     pub editing: bool,
     pub(crate) edit_buffer: String,
@@ -16,6 +17,7 @@ pub struct ColorSelector {
     child: std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>,
     pub editor_state: TextEditorState,
     pub just_changed: bool,
+    pub with_alpha: bool,
 }
 
 impl Clone for ColorSelector {
@@ -23,6 +25,7 @@ impl Clone for ColorSelector {
         Self {
             base: self.base.clone(),
             color: self.color,
+            alpha: self.alpha,
             just_clicked: self.just_clicked,
             editing: self.editing,
             edit_buffer: self.edit_buffer.clone(),
@@ -34,6 +37,7 @@ impl Clone for ColorSelector {
             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
             editor_state: self.editor_state.clone(),
             just_changed: self.just_changed,
+            with_alpha: self.with_alpha,
         }
     }
 }
@@ -43,20 +47,47 @@ impl ColorSelector {
         Self {
             base: Widget::new(),
             color,
+            alpha: 255,
             just_clicked: false,
             editing: false,
             edit_buffer: String::new(),
             cursor_idx: 0,
             font_family: crate::layout::color_selector_font(),
-            command: "clear-color-interface".to_string(),
+            command: "cce-color-interface".to_string(),
             parent: None,
             children: Vec::new(),
             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
             editor_state: TextEditorState::new(String::new()),
             just_changed: false,
+            with_alpha: false,
         }
     }
 
+    pub fn new_rgba(color: [u8; 4]) -> Self {
+        Self {
+            base: Widget::new(),
+            color: [color[0], color[1], color[2]],
+            alpha: color[3],
+            just_clicked: false,
+            editing: false,
+            edit_buffer: String::new(),
+            cursor_idx: 0,
+            font_family: crate::layout::color_selector_font(),
+            command: "cce-color-interface".to_string(),
+            parent: None,
+            children: Vec::new(),
+            child: std::sync::Arc::new(std::sync::Mutex::new(None)),
+            editor_state: TextEditorState::new(String::new()),
+            just_changed: false,
+            with_alpha: true,
+        }
+    }
+
+    pub fn with_alpha(mut self, with_alpha: bool) -> Self {
+        self.with_alpha = with_alpha;
+        self
+    }
+
     pub fn with_label(mut self, label: &str) -> Self {
         self.base.label = Some(label.to_string());
         self
@@ -77,16 +108,23 @@ impl Element for ColorSelector {
     crate::impl_widget_base!(ColorSelector);
 
     fn get_value_string(&self) -> Option<String> {
-        Some(format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]))
+        if self.with_alpha {
+            Some(format!("#{:02x}{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2], self.alpha))
+        } else {
+            Some(format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]))
+        }
     }
 
     fn set_value_string(&mut self, val: &str) -> bool {
         if let Some(c) = parse_hex(val) {
-            if self.color != c {
-                self.color = c;
+            let target_color = [c[0], c[1], c[2]];
+            let target_alpha = if self.with_alpha { c[3] } else { 255 };
+            if self.color != target_color || (self.with_alpha && self.alpha != target_alpha) {
+                self.color = target_color;
+                self.alpha = target_alpha;
                 self.just_changed = true;
                 if self.editing {
-                    self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+                    self.edit_buffer = self.get_value_string().unwrap();
                     self.cursor_idx = self.edit_buffer.chars().count();
                 }
                 return true;
@@ -110,7 +148,7 @@ impl Element for ColorSelector {
     }
 
     fn color_u8(&self) -> Option<[u8; 4]> {
-        Some([self.color[0], self.color[1], self.color[2], 255])
+        Some([self.color[0], self.color[1], self.color[2], self.alpha])
     }
 
     fn color(&self) -> [f32; 4] {
@@ -118,7 +156,7 @@ impl Element for ColorSelector {
             self.color[0] as f32 / 255.0,
             self.color[1] as f32 / 255.0,
             self.color[2] as f32 / 255.0,
-            1.0,
+            self.alpha as f32 / 255.0,
         ])
     }
 
@@ -133,7 +171,7 @@ impl Element for ColorSelector {
         if state != ElementState::Pressed { return false; }
         if !self.hit_test(px, py, ctx) { return false; }
         if px >= self.base.x + self.base.w * 0.65 {
-            let hex = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+            let hex = self.get_value_string().unwrap();
             let mut child_guard = self.child.lock().unwrap();
             if let Some(mut old_child) = child_guard.take() {
                 let _ = old_child.kill();
@@ -165,7 +203,8 @@ impl Element for ColorSelector {
                         let stdout_str = String::from_utf8_lossy(&output.stdout);
                         for line in stdout_str.lines().rev() {
                             if let Some(c) = parse_hex(line.trim()) {
-                                self.color = c;
+                                self.color = [c[0], c[1], c[2]];
+                                self.alpha = if self.with_alpha { c[3] } else { 255 };
                                 self.just_clicked = true;
                                 return true;
                             }
@@ -184,7 +223,7 @@ impl Element for ColorSelector {
 
     fn focus(&mut self) {
         self.editing = true;
-        self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+        self.edit_buffer = self.get_value_string().unwrap();
         self.cursor_idx = self.edit_buffer.chars().count();
         focus::set_focused(self);
     }
@@ -193,7 +232,8 @@ impl Element for ColorSelector {
         if self.editing {
             self.editing = false;
             if let Some(c) = parse_hex(&self.edit_buffer) {
-                self.color = c;
+                self.color = [c[0], c[1], c[2]];
+                self.alpha = if self.with_alpha { c[3] } else { 255 };
             }
         }
     }
@@ -229,7 +269,8 @@ impl Element for ColorSelector {
             }
             Key::Named(NamedKey::Enter) => {
                 if let Some(c) = parse_hex(&state.buffer) {
-                    self.color = c;
+                    self.color = [c[0], c[1], c[2]];
+                    self.alpha = if self.with_alpha { c[3] } else { 255 };
                 }
                 self.editing = false;
                 handled = true;
@@ -253,7 +294,11 @@ impl Element for ColorSelector {
                             }
                             '0'..='9' | 'a'..='f' | 'A'..='F' => {
                                 let count = state.buffer.chars().count();
-                                let max_len = if state.buffer.starts_with('#') { 7 } else { 6 };
+                                let max_len = if state.buffer.starts_with('#') {
+                                    if self.with_alpha { 9 } else { 7 }
+                                } else {
+                                    if self.with_alpha { 8 } else { 6 }
+                                };
                                 if count < max_len {
                                     state.insert_text(&ch.to_ascii_lowercase().to_string());
                                     handled = true;
@@ -310,7 +355,7 @@ impl Element for ColorSelector {
             self.color[0] as f32 / 255.0,
             self.color[1] as f32 / 255.0,
             self.color[2] as f32 / 255.0,
-            1.0,
+            self.alpha as f32 / 255.0,
         ]);
         let border_w = 1.0;
         let border_c = colors::color_borders_color();
@@ -373,6 +418,41 @@ impl Element for ColorSelector {
         }
 
         add_rounded_rect(&mut quads, border_c, px, py, pw, ph, preview_radius);
+
+        if self.with_alpha {
+            add_rounded_rect(
+                &mut quads,
+                [0.8, 0.8, 0.8, 1.0],
+                px + border_w,
+                py + border_w,
+                (pw - 2.0 * border_w).max(0.0),
+                (ph - 2.0 * border_w).max(0.0),
+                (preview_radius - border_w).max(0.0),
+            );
+            
+            let grid_size = 6.0;
+            let start_x = px + border_w;
+            let start_y = py + border_w;
+            let inner_w = (pw - 2.0 * border_w).max(0.0);
+            let inner_h = (ph - 2.0 * border_w).max(0.0);
+            
+            let cols = (inner_w / grid_size).ceil() as i32;
+            let rows = (inner_h / grid_size).ceil() as i32;
+            for r in 0..rows {
+                for c in 0..cols {
+                    if (r + c) % 2 == 1 {
+                        let qx = start_x + c as f32 * grid_size;
+                        let qy = start_y + r as f32 * grid_size;
+                        let qw = grid_size.min(start_x + inner_w - qx);
+                        let qh = grid_size.min(start_y + inner_h - qy);
+                        if qw > 0.0 && qh > 0.0 {
+                            quads.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
+                        }
+                    }
+                }
+            }
+        }
+
         add_rounded_rect(
             &mut quads,
             linear_c,
@@ -390,7 +470,7 @@ impl Element for ColorSelector {
         if let Some(lbl) = self.control_label() {
             labels.push(lbl);
         }
-        let hex = if self.editing { self.edit_buffer.clone() } else { format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]) };
+        let hex = if self.editing { self.edit_buffer.clone() } else { self.get_value_string().unwrap() };
         let top = self.base.label_offset();
         let visual_h = self.base.h - top;
         labels.push(TextLabel {
@@ -410,13 +490,22 @@ impl Drop for ColorSelector {
     }
 }
 
-fn parse_hex(s: &str) -> Option<[u8; 3]> {
+fn parse_hex(s: &str) -> Option<[u8; 4]> {
     let s = s.trim_start_matches('#');
-    if s.len() != 6 { return None; }
-    let r = u8::from_str_radix(&s[0..2], 16).ok()?;
-    let g = u8::from_str_radix(&s[2..4], 16).ok()?;
-    let b = u8::from_str_radix(&s[4..6], 16).ok()?;
-    Some([r, g, b])
+    if s.len() == 6 {
+        let r = u8::from_str_radix(&s[0..2], 16).ok()?;
+        let g = u8::from_str_radix(&s[2..4], 16).ok()?;
+        let b = u8::from_str_radix(&s[4..6], 16).ok()?;
+        Some([r, g, b, 255])
+    } else if s.len() == 8 {
+        let r = u8::from_str_radix(&s[0..2], 16).ok()?;
+        let g = u8::from_str_radix(&s[2..4], 16).ok()?;
+        let b = u8::from_str_radix(&s[4..6], 16).ok()?;
+        let a = u8::from_str_radix(&s[6..8], 16).ok()?;
+        Some([r, g, b, a])
+    } else {
+        None
+    }
 }
 
 impl Control for ColorSelector {}
@@ -433,6 +522,7 @@ mod tests {
         let mut dummy = crate::context::UiContext::new();
         let mut cs = ColorSelector::new([255, 0, 0]);
         assert_eq!(cs.color, [255, 0, 0]);
+        assert_eq!(cs.alpha, 255);
         assert!(!cs.editing);
 
         // 1. Focus color selector
@@ -571,6 +661,66 @@ mod tests {
         assert!(handled);
         assert!(!cs.editing);
         assert_eq!(cs.color, [0xcf, 0xfb, 0x05]);
+        assert_eq!(cs.alpha, 255);
+    }
+
+    #[test]
+    fn test_colorselector_alpha_support() {
+        let mut dummy = crate::context::UiContext::new();
+        let mut cs = ColorSelector::new_rgba([255, 0, 0, 128]);
+        assert_eq!(cs.color, [255, 0, 0]);
+        assert_eq!(cs.alpha, 128);
+        assert!(cs.with_alpha);
+
+        cs.focus();
+        assert!(cs.editing);
+        assert_eq!(cs.edit_buffer, "#ff000080");
+        assert_eq!(cs.cursor_idx, 9);
+
+        let type_a_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Character("a".to_string()),
+            text: Some("a".to_string()),
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        let backspace_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::Backspace),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        cs.keyboard_input(&backspace_ev, &mut dummy);
+        cs.keyboard_input(&backspace_ev, &mut dummy);
+        assert_eq!(cs.edit_buffer, "#ff0000");
+
+        let type_b_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Character("b".to_string()),
+            text: Some("b".to_string()),
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        cs.keyboard_input(&type_a_ev, &mut dummy);
+        cs.keyboard_input(&type_b_ev, &mut dummy);
+        assert_eq!(cs.edit_buffer, "#ff0000ab");
+
+        let enter_ev = KeyEvent {
+            state: ElementState::Pressed,
+            logical_key: Key::Named(NamedKey::Enter),
+            text: None,
+            repeat: false,
+            ctrl: false,
+            shift: false,
+        };
+        cs.keyboard_input(&enter_ev, &mut dummy);
+        assert!(!cs.editing);
+        assert_eq!(cs.color, [255, 0, 0]);
+        assert_eq!(cs.alpha, 171);
     }
 }
 
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index f3fa55d..b031710 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -91,22 +91,25 @@ impl Dropdown {
                 1.0,
             ];
             
+            let bounds = Some([self.base.x, dy, self.base.x + self.base.w, dy + dh]);
             if let Some(ref font) = self.widget_font() {
-                pc.text_with_font(
+                pc.text_with_font_and_bounds(
                     opt,
                     self.base.x + 8.0,
                     iy,
                     12.0,
                     color_f32,
                     font,
+                    bounds,
                 );
             } else {
-                pc.text(
+                pc.text_with_bounds(
                     opt,
                     self.base.x + 8.0,
                     iy,
                     12.0,
                     color_f32,
+                    bounds,
                 );
             }
         }
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 5133165..596a81c 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -475,6 +475,7 @@ pub trait Element {
     fn z_index(&self) -> i32 { 0 }
     fn is_plate(&self) -> bool { false }
     fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, false, false) }
+    fn corner_radius(&self) -> f32 { 12.0 }
     fn layout_ignore(&self) -> bool { false }
     fn color_u8(&self) -> Option<[u8; 4]> { None }
 }
@@ -581,6 +582,8 @@ pub trait GraphController {
     fn set_grid_origin(&mut self, ox: f32, oy: f32);
     fn grid_origin(&self) -> (f32, f32);
     fn set_show_network_grid(&mut self, show: bool);
+    fn take_pending_connection(&mut self) -> Option<(String, String)>;
+    fn cancel_connecting(&mut self);
 }
 
 pub trait SpreadsheetController {