GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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(®ion));
+ 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 {