GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: render node port connectors as circles with hover highlights
src/main.rs | 40 +++++++++++++
src/widget/container/menu.rs | 7 +++
src/widget/container/switcher.rs | 12 ++++
src/widget/display/graph.rs | 120 ++++++++++++++++++++++++++++++++-------
src/widget/mod.rs | 1 +
5 files changed, 161 insertions(+), 19 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 032a285..00011a4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -60,6 +60,40 @@ impl Vertex {
}
}
+fn circle_vertices(
+ cx: f32, cy: f32, r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+) -> Vec<Vertex> {
+ let mut verts = Vec::with_capacity(segments * 3);
+ for i in 0..segments {
+ let a1 = i as f32 * 2.0 * std::f32::consts::PI / segments as f32;
+ let a2 = (i + 1) as f32 * 2.0 * std::f32::consts::PI / segments as f32;
+ let x1 = cx + r * a1.cos();
+ let y1 = cy + r * a1.sin();
+ let x2 = cx + r * a2.cos();
+ let y2 = cy + r * a2.sin();
+
+ verts.push(Vertex {
+ position: [(cx / sw) * 2.0 - 1.0, 1.0 - (cy / sh) * 2.0],
+ color,
+ clip_circle: [0.0, 0.0, 0.0],
+ });
+ verts.push(Vertex {
+ position: [(x1 / sw) * 2.0 - 1.0, 1.0 - (y1 / sh) * 2.0],
+ color,
+ clip_circle: [0.0, 0.0, 0.0],
+ });
+ verts.push(Vertex {
+ position: [(x2 / sw) * 2.0 - 1.0, 1.0 - (y2 / sh) * 2.0],
+ color,
+ clip_circle: [0.0, 0.0, 0.0],
+ });
+ }
+ verts
+}
+
fn quad_vertices(
x: f32, y: f32, w: f32, h: f32,
surface_w: f32, surface_h: f32,
@@ -652,6 +686,9 @@ impl State {
for (qx, qy, qw, qh, qc) in jl.extra_quads() {
verts.extend(extra_quad_vertices(jl, qx, qy, qw, qh, sw, sh, qc));
}
+ for (cx, cy, r, qc) in jl.extra_circles() {
+ verts.extend(circle_vertices(cx, cy, r, sw, sh, qc, 16));
+ }
}
} else {
let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
@@ -662,6 +699,9 @@ impl State {
for (qx, qy, qw, qh, qc) in w.extra_quads() {
verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc));
}
+ for (cx, cy, r, qc) in w.extra_circles() {
+ verts.extend(circle_vertices(cx, cy, r, sw, sh, qc, 16));
+ }
}
// Draw popover quads on top
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 325cfd9..46a282b 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -665,6 +665,13 @@ impl Element for MenuBar {
self.menus.extra_arcs()
}
+ fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ self.menus.extra_circles()
+ }
+
fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
if !self.visible {
return;
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
index f541615..dc4645f 100644
--- a/src/widget/container/switcher.rs
+++ b/src/widget/container/switcher.rs
@@ -393,6 +393,18 @@ impl Element for Switcher {
Vec::new()
}
+ fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).extra_circles() };
+ }
+ }
+ Vec::new()
+ }
+
fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
}
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index e2b9b77..cd1b220 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -65,6 +65,7 @@ pub struct Graph {
connecting_from: Option<(usize, PortType, usize)>,
current_mouse_pos: (f32, f32),
pending_connection: Option<(String, String)>,
+ hovered_port: Option<(usize, PortType, usize)>,
}
impl Graph {
@@ -122,6 +123,7 @@ impl Graph {
connecting_from: None,
current_mouse_pos: (0.0, 0.0),
pending_connection: None,
+ hovered_port: None,
}
}
@@ -354,6 +356,19 @@ impl Element for Graph {
self.dragging_id = None;
}
+ fn handle_event(&mut self, event: &crate::widget::Event, _ctx: &mut UiContext) -> bool {
+ match event {
+ crate::widget::Event::MouseLeave => {
+ let changed = self.hovered_port.is_some() || self.toggle_hovered_idx.is_some();
+ self.hovered_port = None;
+ self.toggle_hovered_idx = None;
+ return changed;
+ }
+ _ => {}
+ }
+ false
+ }
+
fn on_cursor_moved(&mut self, px: f32, py: f32, _ctx: &mut UiContext) -> bool {
let mut changed = false;
if self.connecting_from.is_some() {
@@ -362,15 +377,41 @@ impl Element for Graph {
}
let was_toggle_hovered = self.toggle_hovered_idx;
self.toggle_hovered_idx = None;
+
+ let was_hovered_port = self.hovered_port;
+ self.hovered_port = None;
+
for i in 0..self.nodes.len() {
+ if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+ let scale_f = nw / 80.0;
+ let hit_radius = (6.0 * scale_f).max(2.0) * 1.5;
+ let node = &self.nodes[i];
+
+ for k in 0..node.inputs {
+ let cx = nx + nw * (k + 1) as f32 / (node.inputs + 1) as f32;
+ let cy = ny;
+ if (px - cx).powi(2) + (py - cy).powi(2) <= hit_radius.powi(2) {
+ self.hovered_port = Some((i, PortType::Input, k));
+ }
+ }
+
+ for k in 0..node.outputs {
+ let cx = nx + nw * (k + 1) as f32 / (node.outputs + 1) as f32;
+ let cy = ny + nh;
+ if (px - cx).powi(2) + (py - cy).powi(2) <= hit_radius.powi(2) {
+ self.hovered_port = Some((i, PortType::Output, k));
+ }
+ }
+ }
+
if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
if px >= tx && px < tx + tw && py >= ty && py < ty + th {
self.toggle_hovered_idx = Some(i);
- break;
}
}
}
- if was_toggle_hovered != self.toggle_hovered_idx {
+
+ if was_toggle_hovered != self.toggle_hovered_idx || was_hovered_port != self.hovered_port {
changed = true;
}
changed
@@ -761,23 +802,6 @@ 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
@@ -797,6 +821,63 @@ impl Element for Graph {
quads
}
+ fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
+ let mut circles = Vec::new();
+ let min_x = self.x;
+ let min_y = self.y;
+ let max_x = self.x + self.w;
+ let max_y = self.y + self.h;
+
+ let mut push_circle_clipped = |cx: f32, cy: f32, r: f32, color: [f32; 4]| {
+ if cx >= min_x && cx <= max_x && cy >= min_y && cy <= max_y {
+ circles.push((cx, cy, r, color));
+ }
+ };
+
+ for i in 0..self.nodes.len() {
+ if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+ let scale_f = nw / 80.0;
+ let port_size = (6.0 * scale_f).max(2.0);
+ let base_r = port_size / 2.0;
+
+ let node = &self.nodes[i];
+
+ // Input ports (top edge)
+ for k in 0..node.inputs {
+ let cx = nx + nw * (k + 1) as f32 / (node.inputs + 1) as f32;
+ let cy = ny;
+
+ let is_hovered = self.hovered_port == Some((i, PortType::Input, k));
+ let is_connecting = self.connecting_from == Some((i, PortType::Input, k));
+
+ let (r, color) = if is_hovered || is_connecting {
+ (base_r * 1.4, [0.0, 1.0, 0.9, 1.0]) // neon cyan glow
+ } else {
+ (base_r, [0.1, 0.8, 0.4, 1.0])
+ };
+ push_circle_clipped(cx, cy, r, color);
+ }
+
+ // Output ports (bottom edge)
+ for k in 0..node.outputs {
+ let cx = nx + nw * (k + 1) as f32 / (node.outputs + 1) as f32;
+ let cy = ny + nh;
+
+ let is_hovered = self.hovered_port == Some((i, PortType::Output, k));
+ let is_connecting = self.connecting_from == Some((i, PortType::Output, k));
+
+ let (r, color) = if is_hovered || is_connecting {
+ (base_r * 1.4, [0.0, 1.0, 0.9, 1.0]) // neon cyan glow
+ } else {
+ (base_r, [0.1, 0.8, 0.4, 1.0])
+ };
+ push_circle_clipped(cx, cy, r, color);
+ }
+ }
+ }
+ circles
+ }
+
fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
@@ -828,6 +909,7 @@ impl Drop for Graph {
impl GraphController for Graph {
fn set_nodes(&mut self, nodes: &[GraphNode]) {
self.nodes = nodes.to_vec();
+ self.hovered_port = None;
// Sync selected_idx from selected_id
if let Some(ref id) = self.selected_id {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index d26bf73..68e55ae 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -417,6 +417,7 @@ pub trait Element {
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { Vec::new() }
+ fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> { Vec::new() }
fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = self.extra_quads();