GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Add gap width spinbox, layout improvements, and switcher/page/layer container widgets
src/backend/window_runner.rs | 51 ++-
src/context.rs | 38 +-
src/widget/container/breadcrumb.rs | 25 +-
src/widget/container/content_bg.rs | 23 +-
src/widget/container/layer.rs | 170 ++++++++
src/widget/container/menu.rs | 685 +++++++++++++++++----------------
src/widget/container/mod.rs | 6 +
src/widget/container/page.rs | 153 ++++++++
src/widget/container/paginator.rs | 187 ++++++---
src/widget/container/parameters_bg.rs | 238 ++++++++----
src/widget/container/plate.rs | 226 +++++++----
src/widget/container/scrolling_list.rs | 5 +
src/widget/container/spreadsheet.rs | 25 +-
src/widget/container/switcher.rs | 513 ++++++++++++++++++++++++
src/widget/core.rs | 72 ++--
src/widget/display/float3.rs | 7 +
src/widget/display/graph.rs | 172 +++++----
src/widget/display/node.rs | 30 +-
src/widget/display/serialize.rs | 19 +-
src/widget/input/button.rs | 28 +-
src/widget/input/button_strip.rs | 198 +++++++++-
src/widget/input/checkbox.rs | 35 ++
src/widget/input/color_selector.rs | 34 ++
src/widget/input/dropdown.rs | 46 +++
src/widget/input/slider.rs | 47 +++
src/widget/input/spinbox.rs | 48 +++
src/widget/input/text_box.rs | 51 +++
src/widget/mod.rs | 189 +++++----
28 files changed, 2529 insertions(+), 792 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 2f9d4d5..e4ac1e0 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -440,6 +440,7 @@ pub fn push_plate_bevel_vertices(
clip_circle: [f32; 3],
out: &mut Vec<Vertex>,
) {
+ let r = r.min(ww * 0.5).min(h * 0.5);
let highlight_color = [1.0, 1.0, 1.0, 0.15];
let shadow_color = [0.0, 0.0, 0.0, 0.25];
@@ -491,6 +492,52 @@ pub fn push_plate_bevel_vertices(
);
}
+pub fn push_plate_solid_border_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ t: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ let r = r.min(ww * 0.5).min(h * 0.5);
+ out.extend_from_slice(&quad_vertices_with_clip(x + r, y, ww - 2.0 * r, t, sw, sh, color, clip_circle));
+ out.extend_from_slice(&quad_vertices_with_clip(x, y + r, t, h - 2.0 * r, sw, sh, color, clip_circle));
+ out.extend_from_slice(&quad_vertices_with_clip(x + r, y + h - t, ww - 2.0 * r, t, sw, sh, color, clip_circle));
+ out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r, t, h - 2.0 * r, sw, sh, color, clip_circle));
+
+ let segments = 16;
+
+ push_arc_background_vertices(
+ x + r, y + r, r, t,
+ std::f32::consts::PI, 1.5 * std::f32::consts::PI,
+ sw, sh, color, segments, clip_circle,
+ out,
+ );
+
+ push_arc_background_vertices(
+ x + ww - r, y + r, r, t,
+ 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
+ sw, sh, color, segments, clip_circle,
+ out,
+ );
+
+ push_arc_background_vertices(
+ x + ww - r, y + h - r, r, t,
+ 0.0, 0.5 * std::f32::consts::PI,
+ sw, sh, color, segments, clip_circle,
+ out,
+ );
+
+ push_arc_background_vertices(
+ x + r, y + h - r, r, t,
+ 0.5 * std::f32::consts::PI, std::f32::consts::PI,
+ sw, sh, color, segments, clip_circle,
+ out,
+ );
+}
+
pub fn widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
let mut verts = Vec::new();
push_widget_vertices(w, sw, sh, clip_circle, &mut verts);
@@ -506,8 +553,8 @@ pub fn push_widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, cl
out.extend_from_slice(&quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle));
}
- if w.is_plate() {
- push_plate_bevel_vertices(x, y, ww, h, 12.0, 1.5, sw, sh, clip_circle, out);
+ 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);
}
}
diff --git a/src/context.rs b/src/context.rs
index 681b90c..476ca22 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -339,34 +339,54 @@ impl UiContext {
// --- Context Menu ---
pub fn is_context_menu_visible(&self) -> bool {
- self.context_menu.visible
+ crate::widget::context_menu::is_visible()
}
- pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
- self.context_menu.show(x, y, options, target);
+ pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
+ crate::widget::context_menu::show(x, y, options, target);
+ }
+
+ pub fn handle_right_click(&mut self, target: *mut (dyn Element + 'static), px: f32, py: f32) {
+ let name = unsafe { (*target).type_name() };
+ let label = unsafe { (*target).label() };
+ let header = if let Some(lbl) = label {
+ format!("[{}]: {}", name, lbl)
+ } else {
+ format!("[{}]", name)
+ };
+
+ let options = if name == "TextBox" {
+ vec![header, "Cut".to_string(), "Copy".to_string(), "Paste".to_string(), "Select All".to_string()]
+ } else {
+ vec![header, "Copy".to_string(), "Paste".to_string()]
+ };
+
+ let scroll_y = crate::widget::hover_animation::get_scroll_offset();
+ let adjusted_py = py - scroll_y;
+ crate::widget::context_menu::show(px, adjusted_py, options, target);
}
pub fn hide_context_menu(&mut self) {
- self.context_menu.hide();
+ crate::widget::context_menu::hide();
}
pub fn hit_test_context_menu(&self, px: f32, py: f32) -> bool {
- self.context_menu.hit_test(px, py)
+ crate::widget::context_menu::hit_test(px, py)
}
pub fn cursor_moved_context_menu(&mut self, px: f32, py: f32) -> bool {
- self.context_menu.cursor_moved(px, py)
+ crate::widget::context_menu::cursor_moved(px, py)
}
pub fn mouse_input_context_menu(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- self.context_menu.mouse_input(button, state, px, py)
+ crate::widget::context_menu::mouse_input(button, state, px, py)
}
pub fn context_menu_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- self.context_menu.extra_quads()
+ crate::widget::context_menu::extra_quads()
}
pub fn context_menu_labels(&self) -> Vec<crate::widget::display::TextLabel> {
- self.context_menu.text_labels()
+ crate::widget::context_menu::text_labels()
}
}
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index a2f381d..604961e 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -68,20 +68,8 @@ impl Element for Breadcrumb {
false
}
- fn set_path(&mut self, segments: &[String]) {
- let mut s = Vec::with_capacity(segments.len().max(1));
- if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
- s.push("/".to_string());
- } else {
- s.push("/".to_string());
- for name in segments {
- s.push(format!(" \u{203A} {}", name));
- }
- }
- self.path = s;
- }
-
- fn path_click(&mut self) -> Option<usize> { self.clicked_seg.take() }
+ fn as_path_controller(&self) -> Option<&dyn PathController> { Some(self) }
+ fn as_path_controller_mut(&mut self) -> Option<&mut dyn PathController> { Some(self) }
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = Vec::new();
@@ -113,3 +101,12 @@ impl Element for Breadcrumb {
labels
}
}
+
+impl PathController for Breadcrumb {
+ fn set_path(&mut self, segments: &[String]) {
+ self.path = segments.to_vec();
+ }
+ fn path_click(&mut self) -> Option<usize> {
+ self.clicked_seg.take()
+ }
+}
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
index 5e53ad8..1d11bc2 100644
--- a/src/widget/container/content_bg.rs
+++ b/src/widget/container/content_bg.rs
@@ -33,10 +33,8 @@ impl Element for ContentBg {
fn hovered(&self) -> bool { self.hovered }
fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
- fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
- fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
- fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
- fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+ fn as_graph_controller(&self) -> Option<&dyn GraphController> { Some(self) }
+ fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { Some(self) }
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
@@ -234,3 +232,20 @@ impl Element for ContentBg {
quads
}
}
+
+impl GraphController for ContentBg {
+ fn set_nodes(&mut self, _nodes: &[GraphNode]) {}
+ fn get_nodes(&self) -> Vec<GraphNode> { Vec::new() }
+ fn selected_node(&self) -> Option<usize> { None }
+ fn set_selected_node(&mut self, _idx: Option<usize>) {}
+ fn double_clicked_node(&self) -> Option<usize> { None }
+ fn clear_double_clicked_node(&mut self) {}
+ fn set_grid_snap_enabled(&mut self, _enabled: bool) {}
+ fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { None }
+ fn set_grid_snap(&mut self, _gx: f32, _gy: f32) {}
+ fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+ fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+ 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; }
+}
diff --git a/src/widget/container/layer.rs b/src/widget/container/layer.rs
new file mode 100644
index 0000000..b49e760
--- /dev/null
+++ b/src/widget/container/layer.rs
@@ -0,0 +1,170 @@
+use crate::widget::*;
+use crate::context::UiContext;
+use crate::widget::display::TextLabel;
+
+#[derive(Debug, Clone)]
+pub struct Layer {
+ pub base: Widget,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub visible: bool,
+ pub padding: Option<f32>,
+}
+
+impl Layer {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self {
+ base: Widget::new_rect(x, y, w, h),
+ children: Vec::new(),
+ parent: None,
+ visible: true,
+ padding: None,
+ }
+ }
+
+ pub fn with_padding(mut self, padding: f32) -> Self {
+ self.padding = Some(padding);
+ self
+ }
+}
+
+impl Element for Layer {
+ fn base(&self) -> Option<&Widget> { Some(&self.base) }
+ fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base) }
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+ fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ }
+
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ (self.base.x, self.base.y, self.base.w, self.base.h)
+ }
+
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.base.x = x;
+ self.base.y = y;
+ self.base.w = w;
+ self.base.h = h;
+ }
+
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn visible(&self) -> bool {
+ self.visible
+ }
+
+ fn set_visible(&mut self, visible: bool) {
+ self.visible = visible;
+ }
+
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+ let id = self.base.id();
+ let self_ptr = self as *mut Layer as *mut (dyn Element + 'static);
+ self.children.push(child);
+ unsafe {
+ let c_id = (*child).base().map(|b| b.id()).unwrap();
+ ctx.register_widget(c_id, child);
+ ctx.link_ids(id, c_id);
+ (*child).set_parent(Some(self_ptr), ctx);
+ }
+ }
+
+ fn clear_children(&mut self, ctx: &mut UiContext) {
+ self.children.clear();
+ let id = self.base.id();
+ ctx.clear_children_ids(id);
+ }
+
+ fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ self.children.clone()
+ }
+
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+ self.parent = parent;
+ }
+
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ self.parent
+ }
+
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut quads = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ let c = widget.color();
+ if c[3] > 0.0 {
+ let (wx, wy, ww, wh) = widget.rect();
+ quads.push((wx, wy, ww, wh, c));
+ }
+ quads.extend(widget.all_quads(ctx));
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut labels = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ labels.extend(widget.text_labels());
+ }
+ labels
+ }
+
+ fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut result = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ result.extend(widget.text_labels_with_bounds(ctx));
+ }
+ result
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut result = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ result.extend(widget.text_labels_with_font_and_bounds(ctx));
+ }
+ result
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut result = Vec::new();
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ result.extend(widget.get_text_items());
+ }
+ result
+ }
+
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+ if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ for &child_ptr in &self.children {
+ let widget = unsafe { &*child_ptr };
+ if widget.hit_test(px, py, ctx) {
+ return true;
+ }
+ }
+ false
+ }
+}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 4e52ed3..f4270b4 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -3,7 +3,12 @@ use crate::widget::*;
use crate::widget::display::{make_widget_text_buffer, TextLabel};
pub struct MenuBar {
- pub base: Plate,
+ pub base: Widget,
+ pub visible: bool,
+ pub network_opacity: f32,
+ pub curved_circle: Option<(f32, f32, f32)>,
+ pub blur: bool,
+ pub color: Option<[f32; 4]>,
pub title: String,
pub menus: ButtonStrip,
pub menu_items: Vec<String>,
@@ -26,25 +31,40 @@ pub struct MenuBar {
pub context_hovered_item: Option<usize>,
pub context_title_hovered: bool,
pub context_item_bufs: Vec<glyphon::Buffer>,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub page_hidden: bool,
+ pub layout_dirty: bool,
+ pub on_context_change_cb: Option<Box<dyn Fn(usize) + Send + Sync>>,
+ pub on_menu_click_cb: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
}
impl MenuBar {
pub fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
- self.base.curved_circle = circle;
+ self.curved_circle = circle;
}
pub fn set_network_opacity(&mut self, opacity: f32) {
- self.base.set_network_opacity(opacity);
+ self.network_opacity = opacity;
}
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- let mut base_plate = Plate::new(x, y, w, h)
- .with_color(colors::PANEL_MENU_BG)
- .with_draggable(false);
- base_plate.blur = true;
+ pub fn with_color(mut self, color: [f32; 4]) -> Self {
+ self.color = Some(color);
+ self
+ }
+ pub fn with_blur(mut self, blur: bool) -> Self {
+ self.blur = blur;
+ self
+ }
+
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: base_plate,
+ base: Widget::new_rect(x, y, w, h),
+ visible: true,
+ network_opacity: 1.0,
+ curved_circle: None,
+ blur: false,
+ color: None,
title: String::new(),
menus: ButtonStrip::new(x, y, w, h),
menu_items: Vec::new(),
@@ -67,18 +87,34 @@ impl MenuBar {
context_hovered_item: None,
context_title_hovered: false,
context_item_bufs: Vec::new(),
+ parent: None,
+ page_hidden: false,
+ layout_dirty: true,
+ on_context_change_cb: None,
+ on_menu_click_cb: None,
}
}
+ pub fn on_context_change<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+ self.on_context_change_cb = Some(Box::new(cb));
+ self
+ }
+
+ pub fn on_menu_click<F: Fn(usize, usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+ self.on_menu_click_cb = Some(Box::new(cb));
+ self
+ }
+
pub fn with_label(mut self, label: &str) -> Self {
self.label = Some(label.to_string());
- self.base.base.label = Some(label.to_string());
+ self.base.label = Some(label.to_string());
self
}
pub fn set_label(&mut self, label: &str) {
self.label = Some(label.to_string());
- self.base.base.label = Some(label.to_string());
+ self.base.label = Some(label.to_string());
+ self.layout_dirty = true;
}
pub fn with_context_options(mut self, options: Vec<String>, selected: usize) -> Self {
@@ -118,19 +154,19 @@ impl MenuBar {
display_title.push_str(" ▼");
}
- if let Some((_ccx, _ccy, _ccr)) = self.base.curved_circle {
+ if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
if let Some((tx, ty)) = self.title_pos {
let title_w = display_title.len() as f32 * char_w + 24.0;
- (tx, ty, title_w, self.base.base.h)
+ (tx, ty, title_w, self.base.h)
} else {
- (self.base.base.x, self.base.base.y, display_title.len() as f32 * char_w + 24.0, self.base.base.h)
+ (self.base.x, self.base.y, display_title.len() as f32 * char_w + 24.0, self.base.h)
}
} else if self.vertical {
- let mut cy = 8.0;
+ let mut cy = 16.0;
if let Some(ref label) = self.label {
let line_height = font_size * 1.2;
let label_h = label.chars().count() as f32 * line_height;
- cy += label_h + 8.0;
+ cy += label_h + 20.0;
}
let line_height = font_size * 1.2;
let mut display_title_vertical = self.title.clone();
@@ -138,7 +174,7 @@ impl MenuBar {
display_title_vertical.push_str("▼");
}
let title_h = display_title_vertical.chars().count() as f32 * line_height;
- (self.base.base.x, self.base.base.y + cy, self.base.base.w, title_h)
+ (self.base.x, self.base.y + cy, self.base.w, title_h)
} else {
let mut start_x = 8.0;
if self.center_items {
@@ -147,12 +183,12 @@ impl MenuBar {
for btn_label in &self.menus.buttons {
total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
}
- if self.base.base.w > total_width {
- start_x = (self.base.base.w - total_width) / 2.0;
+ if self.base.w > total_width {
+ start_x = (self.base.w - total_width) / 2.0;
}
}
let title_w = display_title.len() as f32 * char_w + 24.0;
- (self.base.base.x + start_x, self.base.base.y, title_w, self.base.base.h)
+ (self.base.x + start_x, self.base.y, title_w, self.base.h)
}
}
@@ -219,6 +255,7 @@ impl MenuBar {
&self.menu_items
};
self.menus.buttons = src.clone();
+ self.menus.generate_rotated_labels();
}
pub fn with_vertical(mut self, vertical: bool) -> Self {
@@ -235,14 +272,23 @@ impl MenuBar {
}
impl Element for MenuBar {
- fn as_any(&self) -> &dyn std::any::Any { self }
- fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
- fn as_ptr(&self) -> *mut (dyn Element + 'static) {
- self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ crate::impl_widget_base!(MenuBar);
+
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ self.menus.set_modifiers(ctrl, shift, alt);
+ }
+
+ fn layout_ignore(&self) -> bool {
+ true
+ }
+
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ self.parent
}
- fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
- fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+ self.parent = parent;
+ }
fn label(&self) -> Option<String> {
self.label.clone()
@@ -250,11 +296,12 @@ impl Element for MenuBar {
fn set_text(&mut self, text: &str) {
self.label = Some(text.to_string());
- self.base.base.label = Some(text.to_string());
+ self.base.label = Some(text.to_string());
+ self.layout_dirty = true;
}
fn rect(&self) -> (f32, f32, f32, f32) {
- if !self.base.visible {
+ if !self.visible {
return (0.0, 0.0, 0.0, 0.0);
}
if self.vertical {
@@ -263,7 +310,7 @@ impl Element for MenuBar {
let font_size = 12.0;
let line_height = font_size * 1.2;
let label_h = label.chars().count() as f32 * line_height;
- h += label_h + 8.0;
+ h += label_h + 20.0;
}
if !self.title.is_empty() {
let font_size = 12.0;
@@ -273,17 +320,35 @@ impl Element for MenuBar {
display_title.push_str("▼");
}
let title_h = display_title.chars().count() as f32 * line_height;
- h += title_h + 8.0;
+ h += title_h + 36.0;
}
let (_, _, _, menus_h) = self.menus.rect();
- (self.base.base.x, self.base.base.y, self.base.base.w, h + menus_h)
+ (self.base.x, self.base.y, self.base.w, h + menus_h)
} else {
- (self.base.base.x, self.base.base.y, self.base.base.w, self.base.base.h)
+ (self.base.x, self.base.y, self.base.w, self.base.h)
}
}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.base.set_rect(x, y, w, h);
+ if self.base.x == x && self.base.y == y && self.base.w == w && self.base.h == h && !self.layout_dirty {
+ return;
+ }
+ self.layout_dirty = false;
+ let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
+ let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+ let cx = x.clamp(px, px + pw);
+ let cy = y.clamp(py, py + ph);
+ let cw = w.min(px + pw - cx);
+ let ch = h.min(py + ph - cy);
+ (cx, cy, cw, ch)
+ } else {
+ (x, y, w, h)
+ };
+
+ self.base.x = clamped_x;
+ self.base.y = clamped_y;
+ self.base.w = clamped_w;
+ self.base.h = clamped_h;
let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
let font_setting = crate::layout::menubar_font();
@@ -292,12 +357,12 @@ impl Element for MenuBar {
let char_w = 7.5 * (font_size / 12.0);
if self.vertical {
- let mut cy = 8.0;
+ let mut cy = 16.0;
if let Some(ref label) = self.label {
let font_size = 12.0;
let line_height = font_size * 1.2;
let label_h = label.chars().count() as f32 * line_height;
- cy += label_h + 8.0;
+ cy += label_h + 20.0;
}
if !self.title.is_empty() {
let font_size = 12.0;
@@ -307,10 +372,12 @@ impl Element for MenuBar {
display_title.push_str("▼");
}
let title_h = display_title.chars().count() as f32 * line_height;
- cy += title_h + 8.0;
+ cy += title_h + 36.0;
}
+ let menus_y = (clamped_y + cy).clamp(clamped_y, clamped_y + clamped_h);
+ let menus_h = (clamped_h - cy).min(clamped_y + clamped_h - menus_y).max(0.0);
self.menus.vertical = true;
- self.menus.set_rect(x, y + cy, w, (h - cy).max(0.0));
+ self.menus.set_rect(clamped_x, menus_y, clamped_w, menus_h);
let mut dummy = crate::context::UiContext::new();
self.menus.set_parent(Some(parent_ptr), &mut dummy);
} else {
@@ -330,8 +397,8 @@ impl Element for MenuBar {
btn_strip_w += btn_label.len() as f32 * char_w + 2.0 * padding_x;
}
total_width += btn_strip_w;
- if self.base.base.w > total_width {
- cx = (self.base.base.w - total_width) / 2.0;
+ if self.base.w > total_width {
+ cx = (self.base.w - total_width) / 2.0;
}
}
if !self.title.is_empty() {
@@ -345,38 +412,29 @@ impl Element for MenuBar {
for btn_label in &self.menus.buttons {
btn_strip_w += btn_label.len() as f32 * char_w + 2.0 * padding_x;
}
+ let menus_x = (clamped_x + cx).clamp(clamped_x, clamped_x + clamped_w);
+ let menus_w = btn_strip_w.min(clamped_x + clamped_w - menus_x);
self.menus.vertical = false;
- self.menus.set_rect(x + cx, y, btn_strip_w, h);
+ self.menus.set_rect(menus_x, clamped_y, menus_w, clamped_h);
let mut dummy = crate::context::UiContext::new();
self.menus.set_parent(Some(parent_ptr), &mut dummy);
}
}
fn color(&self) -> [f32; 4] {
- if !self.base.visible {
- [0.0, 0.0, 0.0, 0.0]
- } else if self.focused {
- let mut c = colors::PANEL_MENU_FOCUSED;
- c[3] *= self.base.network_opacity;
- if self.base.blur {
- c[3] = -c[3].abs();
- }
- c
- } else {
- self.base.color()
- }
+ [0.0, 0.0, 0.0, 0.0]
}
fn set_hovered(&mut self, v: bool) {
- self.base.base.hovered = v;
+ self.base.hovered = v;
}
fn hovered(&self) -> bool {
- self.base.base.hovered
+ self.base.hovered
}
fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
- if !self.base.visible {
+ if !self.visible {
return false;
}
if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
@@ -398,7 +456,7 @@ impl Element for MenuBar {
}
fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.base.visible {
+ if !self.visible {
return false;
}
let (rx, ry, rw, rh) = self.rect();
@@ -439,7 +497,7 @@ impl Element for MenuBar {
}
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.base.visible {
+ if !self.visible {
return false;
}
if button != MouseButton::Left {
@@ -460,6 +518,9 @@ impl Element for MenuBar {
self.context_just_changed = true;
self.context_dropdown_open = false;
self.unfocus();
+ if let Some(ref cb) = self.on_context_change_cb {
+ cb(di);
+ }
return true;
}
}
@@ -570,69 +631,33 @@ impl Element for MenuBar {
}
}
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- self.menus.set_modifiers(ctrl, shift, alt);
- }
-
- fn menu_names(&self) -> Vec<String> {
- self.menus.buttons.clone()
- }
-
- fn menu_click(&mut self) -> Option<(usize, usize)> {
- if let Some(idx) = self.menus.take_click() {
- return Some((idx, 0));
- }
- None
- }
-
- fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
- if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
- if item_idx < menu.len() {
- menu[item_idx] = Some(checked);
- }
- }
- }
-
- fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
- if menu_idx < self.menu_dropdowns.len() {
- self.menu_dropdowns[menu_idx] = items.to_vec();
- self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
- }
- }
-
- fn is_menu_bar(&self) -> bool {
- self.base.visible
- }
-
- fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
- None
- }
-
- fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
-
- fn is_menu_open(&self) -> bool {
- self.context_dropdown_open
- }
+ fn as_page_selector(&self) -> Option<&dyn PageSelector> { Some(self) }
+ fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { Some(self) }
+ fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+ fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.base.visible {
+ if !self.visible {
return Vec::new();
}
- let mut quads = Vec::new();
- quads.extend(self.base.all_quads(ctx));
- quads.extend(self.menus.all_quads(ctx));
+ let mut quads = self.extra_quads();
+ if let Some(hq) = self.menus.highlight_quad(ctx) {
+ if hq.4 != colors::HIGHLIGHT_SECONDARY {
+ quads.push(hq);
+ }
+ }
quads
}
fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- if !self.base.visible {
+ if !self.visible {
return Vec::new();
}
self.menus.extra_arcs()
}
fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- if !self.base.visible {
+ if !self.visible {
return;
}
let current_font = crate::layout::menubar_font();
@@ -649,7 +674,7 @@ impl Element for MenuBar {
if !self.context_options.is_empty() {
display_title.push_str(" ▼");
}
- if let Some((_ccx, _ccy, _ccr)) = self.base.curved_circle {
+ if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
if self.curved_title_char_bufs.len() != display_title.chars().count() {
let font_fam_clone = font_fam.clone();
self.curved_title_char_bufs = display_title.chars()
@@ -689,111 +714,11 @@ impl Element for MenuBar {
}
fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- if !self.base.visible {
- return Vec::new();
- }
- let mut items = Vec::new();
- let label_color = crate::colors::paginator_tab_label_color();
- let srgb = crate::colors::to_srgb(label_color);
- let color = glyphon::Color::rgb(
- (srgb[0] * 255.0) as u8,
- (srgb[1] * 255.0) as u8,
- (srgb[2] * 255.0) as u8,
- );
-
- let font_setting = crate::layout::menubar_font();
- let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
- let font_size = font_size_opt.unwrap_or(12.0);
- let char_w = 7.5 * (font_size / 12.0);
-
- let padding_x = crate::layout::paginator_tab_padding_x();
- if let Some((ccx, ccy, ccr)) = self.base.curved_circle {
- let r_mid = ccr - self.base.base.h / 2.0;
- let mut total_width = 8.0;
- let mut display_title = self.title.clone();
- if !self.context_options.is_empty() {
- display_title.push_str(" ▼");
- }
- if !self.title.is_empty() {
- total_width += display_title.len() as f32 * char_w + 24.0;
- }
- for btn_label in &self.menus.buttons {
- total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = display_title.len() as f32 * char_w + 24.0;
- let dtheta_title = title_w / r_mid;
-
- let char_widths: Vec<f32> = display_title.chars().map(|c| {
- TextLabel::estimate_width(&c.to_string(), font_size)
- }).collect();
- let total_chars_width: f32 = char_widths.iter().sum();
- let mid_angle = (current_angle + current_angle + dtheta_title) / 2.0;
- let angular_width = total_chars_width / r_mid;
- let text_start_angle = mid_angle - angular_width / 2.0;
- let mut cur_char_angle = text_start_angle;
-
- for (char_idx, c_buf) in self.curved_title_char_bufs.iter().enumerate() {
- if char_idx < char_widths.len() {
- let cw = char_widths[char_idx];
- let dtheta = cw / r_mid;
- let char_center_angle = cur_char_angle + dtheta / 2.0;
-
- let tx = ccx + r_mid * char_center_angle.cos() - cw / 2.0;
- let ty = ccy + r_mid * char_center_angle.sin() - font_size / 2.0;
-
- items.push((c_buf, tx, ty, color));
- cur_char_angle += dtheta;
- }
- }
- }
- } else if !self.vertical {
- let mut start_x = 8.0;
- let mut display_title = self.title.clone();
- if !self.context_options.is_empty() {
- display_title.push_str(" ▼");
- }
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += display_title.len() as f32 * char_w + 24.0;
- }
- for btn_label in &self.menus.buttons {
- total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
- }
- if self.base.base.w > total_width {
- start_x = (self.base.base.w - total_width) / 2.0;
- }
- }
- if let Some(ref title_buf) = self.title_buf {
- let text_y = self.base.base.y + (self.base.base.h - font_size) / 2.0;
- items.push((title_buf, self.base.base.x + start_x, text_y, color));
- }
- }
-
- if self.context_dropdown_open {
- if let Some((dx, dy, _, _)) = self.context_popover_rect() {
- for (i, item_buf) in self.context_item_bufs.iter().enumerate() {
- items.push((
- item_buf,
- dx + 8.0,
- dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
- color,
- ));
- }
- }
- }
-
- items.extend(self.menus.get_text_items());
- items
+ Vec::new()
}
fn text_labels(&self) -> Vec<TextLabel> {
- if !self.base.visible {
+ if !self.visible {
return Vec::new();
}
let label_color = crate::colors::paginator_tab_label_color();
@@ -810,11 +735,11 @@ impl Element for MenuBar {
if self.vertical {
let font_size = 12.0;
let line_height = font_size * 1.2;
- let start_y = self.base.base.y + 8.0;
+ let start_y = self.base.y + 16.0;
for (i, c) in label.chars().enumerate() {
let char_str = c.to_string();
let char_w = TextLabel::estimate_width(&char_str, font_size);
- let x_pos = self.base.base.x + (self.base.base.w - char_w) / 2.0;
+ let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
let y_pos = start_y + i as f32 * line_height;
labels.push(TextLabel {
text: char_str,
@@ -837,8 +762,8 @@ impl Element for MenuBar {
display_title.push_str(" ▼");
}
- if let Some((ccx, ccy, ccr)) = self.base.curved_circle {
- let r_mid = ccr - self.base.base.h / 2.0;
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - self.base.h / 2.0;
let mut total_width = 8.0;
if !self.title.is_empty() {
total_width += display_title.len() as f32 * char_w + 24.0;
@@ -863,16 +788,16 @@ impl Element for MenuBar {
}
} else if self.vertical {
if !self.title.is_empty() {
- let mut start_y = self.base.base.y + 8.0;
+ let mut start_y = self.base.y + 16.0;
if let Some(ref label) = self.label {
let font_size = 12.0;
let line_height = font_size * 1.2;
let label_h = label.chars().count() as f32 * line_height;
- start_y += label_h + 8.0;
+ start_y += label_h + 20.0;
}
let line_height = font_size * 1.2;
let char_w = TextLabel::estimate_width("o", font_size);
- let x_pos = self.base.base.x + (self.base.base.w - char_w) / 2.0;
+ let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
let mut display_title_vertical = self.title.clone();
if !self.context_options.is_empty() {
display_title_vertical.push_str("▼");
@@ -899,15 +824,15 @@ impl Element for MenuBar {
for btn_label in &self.menus.buttons {
total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
}
- if self.base.base.w > total_width {
- start_x = (self.base.base.w - total_width) / 2.0;
+ if self.base.w > total_width {
+ start_x = (self.base.w - total_width) / 2.0;
}
}
if !self.title.is_empty() {
- let text_y = self.base.base.y + (self.base.base.h - font_size) / 2.0;
+ let text_y = self.base.y + (self.base.h - font_size) / 2.0;
labels.push(TextLabel {
text: display_title,
- x: self.base.base.x + start_x,
+ x: self.base.x + start_x,
y: text_y,
font_size,
color: text_color,
@@ -936,12 +861,15 @@ impl Element for MenuBar {
}
fn set_visible(&mut self, visible: bool) {
- self.base.set_visible(visible);
- self.menus.set_visible(visible);
+ if self.visible != visible {
+ self.visible = visible;
+ self.menus.set_visible(visible);
+ self.layout_dirty = true;
+ }
}
fn visible(&self) -> bool {
- self.base.visible()
+ self.visible
}
fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
@@ -953,24 +881,17 @@ impl Element for MenuBar {
self.z_level
}
- fn set_center_items(&mut self, center: bool) {
- self.center_items = center;
- }
- fn menu_items_list(&self) -> Vec<Vec<String>> {
- self.menus.buttons.iter().map(|_| Vec::new()).collect()
- }
-
- fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
- self.menus.buttons.iter().map(|_| Vec::new()).collect()
- }
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.base.visible {
+ if !self.visible {
return Vec::new();
}
let mut quads = Vec::new();
+ // Draw main background
+ quads.push((self.base.x, self.base.y, self.base.w, self.base.h, colors::sidebar_bg_color()));
+
// 1. Highlight the title on hover or open
if !self.context_options.is_empty() {
let tr = self.title_rect();
@@ -999,14 +920,8 @@ impl Element for MenuBar {
Some(crate::layout::menubar_font())
}
- fn take_context_change(&mut self) -> Option<usize> {
- self.take_context_change()
- }
-
- fn set_context_selected(&mut self, selected: usize) {
- self.set_context_selected(selected);
- }
-}impl Drop for MenuBar {
+}
+impl Drop for MenuBar {
fn drop(&mut self) {
focus::clear_if_matches(self);
}
@@ -1224,20 +1139,8 @@ impl Element for Menu {
}
}
- fn menu_click(&mut self) -> Option<(usize, usize)> {
- self.clicked_item.take().map(|i| (0, i))
- }
-
- fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
- if item_idx < self.item_checked.len() {
- self.item_checked[item_idx] = Some(checked);
- self.item_bufs.clear();
- }
- }
-
- fn is_menu_open(&self) -> bool {
- self.open
- }
+ fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+ fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{
None
@@ -1364,18 +1267,6 @@ impl Element for Menu {
100
}
- fn menu_items(&self) -> Vec<String> {
- self.items.clone()
- }
-
- fn menu_item_checked(&self) -> Vec<Option<bool>> {
- self.item_checked.clone()
- }
-
- fn is_vertical(&self) -> bool {
- self.vertical
- }
-
fn focused(&self, ctx: &UiContext) -> bool {
self.base.focused
}
@@ -1430,77 +1321,219 @@ impl Element for Menu {
}
fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- let mut items = Vec::new();
- let label_color = crate::colors::paginator_tab_label_color();
- let srgb = crate::colors::to_srgb(label_color);
- let color = glyphon::Color::rgb(
- (srgb[0] * 255.0) as u8,
- (srgb[1] * 255.0) as u8,
- (srgb[2] * 255.0) as u8,
- );
+ Vec::new()
+ }
- let font_setting = crate::layout::menubar_font();
- let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
- let font_size = font_size_opt.unwrap_or(12.0);
+ fn widget_font(&self) -> Option<String> {
+ Some(crate::layout::menubar_font())
+ }
+}
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let r_mid = r - thickness / 2.0;
- let active_title = self.active_title();
-
- let char_widths: Vec<f32> = active_title.chars().map(|c| {
- TextLabel::estimate_width(&c.to_string(), font_size)
- }).collect();
- let total_chars_width: f32 = char_widths.iter().sum();
-
- let angular_width = total_chars_width / r_mid;
- let text_start_angle = (start_angle + end_angle) / 2.0 - angular_width / 2.0;
- let mut cur_char_angle = text_start_angle;
-
- for (char_idx, c_buf) in self.curved_char_bufs.iter().enumerate() {
- if char_idx < char_widths.len() {
- let cw = char_widths[char_idx];
- let dtheta = cw / r_mid;
- let char_center_angle = cur_char_angle + dtheta / 2.0;
-
- let tx = cx + r_mid * char_center_angle.cos() - cw / 2.0;
- let ty = cy + r_mid * char_center_angle.sin() - font_size / 2.0;
-
- items.push((c_buf, tx, ty, color));
- cur_char_angle += dtheta;
- }
- }
- } else if !self.vertical {
- if let Some(ref title_buf) = self.title_buf {
- let text_y = self.base.y + (self.base.h - font_size) / 2.0;
- items.push((title_buf, self.base.x + crate::layout::paginator_tab_padding_x(), text_y, color));
+unsafe impl Send for Menu {}
+unsafe impl Sync for Menu {}
+
+impl Drop for Menu {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
+impl MenuController for Menu {
+ fn menu_click(&mut self) -> Option<(usize, usize)> {
+ self.clicked_item.take().map(|i| (0, i))
+ }
+ fn trigger_menu_click(&mut self, _menu_idx: usize, item_idx: usize) {
+ if item_idx < self.items.len() {
+ self.clicked_item = Some(item_idx);
+ }
+ }
+ fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
+ if item_idx < self.item_checked.len() {
+ self.item_checked[item_idx] = Some(checked);
+ self.item_bufs.clear();
+ }
+ }
+ fn set_menu_items(&mut self, _menu_idx: usize, items: &[String]) {
+ self.items = items.to_vec();
+ self.item_checked = vec![None; items.len()];
+ self.item_bufs.clear();
+ }
+ fn is_menu_bar(&self) -> bool { false }
+ fn is_menu_open(&self) -> bool { self.open }
+ fn menu_items(&self) -> Vec<String> { self.items.clone() }
+ fn menu_item_checked(&self) -> Vec<Option<bool>> { self.item_checked.clone() }
+ fn is_vertical(&self) -> bool { self.vertical }
+ fn menu_names(&self) -> Vec<String> { vec![self.active_title().to_string()] }
+ fn menu_items_list(&self) -> Vec<Vec<String>> { vec![self.items.clone()] }
+ fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> { vec![self.item_checked.clone()] }
+ fn take_context_change(&mut self) -> Option<usize> { None }
+ fn set_context_selected(&mut self, _selected: usize) {}
+ fn set_center_items(&mut self, _center: bool) {}
+ fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> { None }
+}
+
+
+impl MenuController for MenuBar {
+ fn menu_click(&mut self) -> Option<(usize, usize)> {
+ if let Some(idx) = self.menus.take_click() {
+ if let Some(ref cb) = self.on_menu_click_cb {
+ cb(idx, 0);
}
+ return Some((idx, 0));
}
+ None
+ }
- if self.open {
- let (dx, dy, _, _) = self.dropdown_rect();
- for (i, item_buf) in self.item_bufs.iter().enumerate() {
- items.push((
- item_buf,
- dx + 8.0,
- dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
- color,
- ));
+ fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
+
+ fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+ if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
+ if item_idx < menu.len() {
+ menu[item_idx] = Some(checked);
}
}
+ }
- items
+ fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+ if menu_idx < self.menu_dropdowns.len() {
+ self.menu_dropdowns[menu_idx] = items.to_vec();
+ self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
+ self.layout_dirty = true;
+ }
}
- fn widget_font(&self) -> Option<String> {
- Some(crate::layout::menubar_font())
+ fn is_menu_bar(&self) -> bool {
+ self.visible
+ }
+
+ fn is_menu_open(&self) -> bool {
+ self.context_dropdown_open
+ }
+
+ fn menu_items(&self) -> Vec<String> {
+ self.menu_items.clone()
+ }
+
+ fn menu_item_checked(&self) -> Vec<Option<bool>> {
+ self.menu_dropdown_checked.iter().flatten().copied().collect()
+ }
+
+ fn is_vertical(&self) -> bool {
+ self.vertical
+ }
+
+ fn menu_names(&self) -> Vec<String> {
+ self.menus.buttons.clone()
+ }
+
+ fn menu_items_list(&self) -> Vec<Vec<String>> {
+ self.menu_dropdowns.clone()
+ }
+
+ fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+ self.menu_dropdown_checked.clone()
+ }
+
+ fn take_context_change(&mut self) -> Option<usize> {
+ self.take_context_change()
+ }
+
+ fn set_context_selected(&mut self, selected: usize) {
+ self.set_context_selected(selected);
+ }
+
+ fn set_center_items(&mut self, center: bool) {
+ self.center_items = center;
+ }
+
+ fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+ if !self.visible {
+ return None;
+ }
+ for i in 0..self.menus.buttons.len() {
+ let r = self.menus.item_rect(i);
+ if px >= r.0 && px < r.0 + r.2 && py >= r.1 && py < r.1 + r.3 {
+ let title = self.menus.buttons[i].clone();
+ let mut formatted_items = Vec::new();
+ if let Some(items) = self.menu_dropdowns.get(i) {
+ for (item_idx, item) in items.iter().enumerate() {
+ let checked = self.menu_dropdown_checked.get(i)
+ .and_then(|menu| menu.get(item_idx))
+ .and_then(|&v| v);
+ let prefix = match checked {
+ Some(true) => "✓ ",
+ Some(false) => " ",
+ None => "",
+ };
+ formatted_items.push(format!("{}{}", prefix, item));
+ }
+ }
+ return Some((i, title, formatted_items, r.0, r.1, r.2, r.3));
+ }
+ }
+ None
}
}
-unsafe impl Send for Menu {}
-unsafe impl Sync for Menu {}
+impl PageSelector for MenuBar {
+ fn selected_page(&self) -> usize {
+ self.menus.selected.unwrap_or(0)
+ }
-impl Drop for Menu {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
+ fn set_selected_page(&mut self, page: usize) {
+ self.menus.set_selected(Some(page));
}
+
+ fn is_page_hidden(&self) -> bool {
+ self.page_hidden
+ }
+
+ fn set_page_hidden(&mut self, hidden: bool) {
+ self.page_hidden = hidden;
+ }
+
+ fn set_pages(&mut self, pages: Vec<String>) {
+ self.menu_items = pages.clone();
+ self.vertical_items = pages.clone();
+ self.menus.buttons = pages;
+ self.menus.generate_rotated_labels();
+ }
+
+ fn set_pages_with_items(&mut self, pages: Vec<String>, items: Vec<Vec<String>>) {
+ self.menu_items = pages.clone();
+ self.vertical_items = pages.clone();
+ self.menu_dropdowns = items;
+ self.menu_dropdown_checked = vec![vec![None; 0]; self.menu_dropdowns.len()];
+ self.menus.buttons = pages;
+ self.menus.generate_rotated_labels();
+ }
+
+ fn sidebar_w(&self) -> f32 {
+ let padding_x = crate::layout::paginator_tab_padding_x();
+ let margin_x = crate::layout::paginator_tab_margin_x();
+ if self.vertical {
+ (12.0 + 2.0 * padding_x).max(24.0) + 2.0 * margin_x
+ } else {
+ let items = &self.menu_items;
+ let max_req_w = items.iter()
+ .map(|p| p.len() as f32 * 7.5 + 2.0 * padding_x)
+ .max_by(|a, b| a.partial_cmp(b).unwrap())
+ .unwrap_or(0.0);
+ max_req_w.max(24.0) + 2.0 * margin_x
+ }
+ }
+
+ fn set_sidebar_mode(&mut self, _enabled: bool) {}
+
+ fn set_sidebar_label(&mut self, label: Option<String>) {
+ self.label = label.clone();
+ self.base.label = label.clone();
+ if self.vertical {
+ self.title = label.unwrap_or_default();
+ self.label = None;
+ }
+ }
+
+ fn add_widget_to_page(&mut self, _page_idx: usize, _widget: *mut (dyn Element + 'static), _ctx: &mut UiContext) {}
+ fn clear_page_widgets(&mut self, _page_idx: usize, _ctx: &mut UiContext) {}
}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index 9bbff51..fc48ecd 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -10,6 +10,9 @@ pub mod scroll_box;
pub mod scrolling_list;
pub mod plate;
pub mod paginator;
+pub mod switcher;
+pub mod layer;
+pub mod page;
pub use container::Container;
pub use header::Header;
@@ -23,3 +26,6 @@ pub use scroll_box::ScrollBox;
pub use scrolling_list::ScrollingList;
pub use plate::Plate;
pub use paginator::Paginator;
+pub use switcher::Switcher;
+pub use layer::Layer;
+pub use page::Page;
diff --git a/src/widget/container/page.rs b/src/widget/container/page.rs
new file mode 100644
index 0000000..2b70760
--- /dev/null
+++ b/src/widget/container/page.rs
@@ -0,0 +1,153 @@
+use crate::widget::*;
+use crate::context::UiContext;
+use crate::widget::display::TextLabel;
+use super::layer::Layer;
+
+pub struct Page {
+ pub base: Layer,
+ pub visible: bool,
+ pub owned_children: Vec<Box<dyn Element>>,
+}
+
+impl std::fmt::Debug for Page {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Page")
+ .field("base", &self.base)
+ .field("visible", &self.visible)
+ .finish()
+ }
+}
+
+impl Page {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self {
+ base: Layer::new(x, y, w, h),
+ visible: true,
+ owned_children: Vec::new(),
+ }
+ }
+
+ pub fn add_child_owned(&mut self, child: Box<dyn Element>, ctx: &mut UiContext) {
+ let ptr = &*child as *const (dyn Element + 'static) as *mut (dyn Element + 'static);
+ self.owned_children.push(child);
+ self.add_child(ptr, ctx);
+ }
+}
+
+impl Element for Page {
+ fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
+ fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+ fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ }
+
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ self.base.rect()
+ }
+
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.base.set_rect(x, y, w, h);
+ if !self.visible {
+ return;
+ }
+ let padding_x = 8.0;
+ let padding_y = 10.0;
+ let left_x = x + padding_x;
+ let available_w = (w - 2.0 * padding_x).max(1.0);
+ let mut current_y = y + padding_y;
+ let spacing = 8.0;
+
+ for &child_ptr in &self.base.children {
+ let child = unsafe { &mut *child_ptr };
+ let (_, _, _, ch) = child.rect();
+ let use_h = if ch > 0.0 { ch } else { 42.0 };
+ child.set_rect(left_x, current_y, available_w, use_h);
+ current_y += use_h + spacing;
+ }
+ }
+
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn visible(&self) -> bool {
+ self.visible
+ }
+
+ fn set_visible(&mut self, visible: bool) {
+ self.visible = visible;
+ }
+
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+ let id = self.base.base.id();
+ let self_ptr = self as *mut Page as *mut (dyn Element + 'static);
+ self.base.children.push(child);
+ unsafe {
+ let c_id = (*child).base().map(|b| b.id()).unwrap();
+ ctx.register_widget(c_id, child);
+ ctx.link_ids(id, c_id);
+ (*child).set_parent(Some(self_ptr), ctx);
+ }
+ }
+
+ fn clear_children(&mut self, ctx: &mut UiContext) {
+ self.base.clear_children(ctx);
+ self.owned_children.clear();
+ }
+
+ fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ self.base.children(ctx)
+ }
+
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
+ self.base.set_parent(parent, ctx);
+ }
+
+ fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ self.base.parent(ctx)
+ }
+
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ self.base.all_quads(ctx)
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible {
+ return Vec::new();
+ }
+ self.base.text_labels()
+ }
+
+ fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ self.base.text_labels_with_bounds(ctx)
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ self.base.text_labels_with_font_and_bounds(ctx)
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ self.base.get_text_items()
+ }
+
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ self.base.hit_test(px, py, ctx)
+ }
+}
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index f3e1d1a..280ca93 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -36,6 +36,8 @@ pub struct Paginator {
pub context_options: Vec<String>,
pub context_selected: usize,
pub context_just_changed: bool,
+ pub tab_quads_cache: std::collections::HashMap<String, Vec<(f32, f32, f32, f32, [f32; 4])>>,
+ pub on_page_changed_cb: Option<Box<dyn Fn(usize) + Send + Sync>>,
}
impl Paginator {
@@ -91,6 +93,8 @@ impl Paginator {
context_options: Vec::new(),
context_selected: 0,
context_just_changed: false,
+ tab_quads_cache: std::collections::HashMap::new(),
+ on_page_changed_cb: None,
};
for page in &pages {
@@ -241,6 +245,11 @@ impl Paginator {
self
}
+ pub fn on_page_changed<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+ self.on_page_changed_cb = Some(Box::new(cb));
+ self
+ }
+
pub fn selected_page(&self) -> usize {
self.selected_page
}
@@ -255,6 +264,10 @@ impl Paginator {
// Update MenuBar focus/selection
self.sidebar_menu.menus.set_selected(Some(page));
+
+ if let Some(ref cb) = self.on_page_changed_cb {
+ cb(page);
+ }
}
}
}
@@ -398,6 +411,12 @@ impl Paginator {
continue;
}
+ let cache_key = format!("{}:{}:{}:{:?}:{}:{}", trimmed, w_px, h_px, color, font_fam, scale);
+ if let Some(cached_quads) = self.tab_quads_cache.get(&cache_key) {
+ self.tab_text_quads.push(cached_quads.clone());
+ continue;
+ }
+
let svg_data = format!(
r##"<svg width="{}" height="{}" viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">
<text x="{}" y="{}" font-family="{}" font-size="{}" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
@@ -446,6 +465,7 @@ impl Paginator {
if !page_quads.is_empty() {
eprintln!("DEBUG_PAGINATOR_EXAMPLES: {:?}", &page_quads[..5.min(page_quads.len())]);
}
+ self.tab_quads_cache.insert(cache_key, page_quads.clone());
self.tab_text_quads.push(page_quads);
}
}
@@ -490,6 +510,13 @@ impl Element for Paginator {
}
}
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ self.sidebar_menu.set_modifiers(ctrl, shift, alt);
+ if self.selected_page < self.plates.len() {
+ self.plates[self.selected_page].set_modifiers(ctrl, shift, alt);
+ }
+ }
+
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
@@ -518,10 +545,6 @@ impl Element for Paginator {
px >= x && px <= x + w && py >= y && py <= y + h
}
- fn is_menu_open(&self) -> bool {
- self.sidebar_menu.is_menu_open()
- }
-
fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{
if let Some(i) = self.hovered_tab {
if self.tabs_at_top {
@@ -961,15 +984,56 @@ impl Element for Paginator {
fn add_child(&mut self, _child: *mut (dyn Element + 'static), ctx: &mut UiContext) {}
fn clear_children(&mut self, ctx: &mut UiContext) {}
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- self.sidebar_menu.set_modifiers(ctrl, shift, alt);
+ fn as_page_selector(&self) -> Option<&dyn PageSelector> { Some(self) }
+ fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { Some(self) }
+ fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
+ fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+ self.sidebar_menu.prepare_text(fs);
+ for plate in &mut self.plates {
+ plate.prepare_text(fs);
+ }
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+ let mut result = Vec::new();
+ if self.tabs_rotated {
+ let font = self.widget_font();
+ for l in self.text_labels() {
+ result.push((l, font.clone(), None));
+ }
+ } else {
+ result.extend(self.sidebar_menu.text_labels_with_font_and_bounds(ctx));
+ }
+
if self.selected_page < self.plates.len() {
- self.plates[self.selected_page].set_modifiers(ctrl, shift, alt);
+ result.extend(self.plates[self.selected_page].text_labels_with_font_and_bounds(ctx));
}
+ result
}
- fn menu_names(&self) -> Vec<String> {
- self.pages.clone()
+
+ fn widget_font(&self) -> Option<String> {
+ Some(crate::layout::menubar_font())
+ }
+
+ fn focus(&mut self) {
+ self.sidebar_menu.focus();
+ }
+
+ fn unfocus(&mut self) {
+ self.sidebar_menu.unfocus();
}
+
+ fn focused(&self, ctx: &UiContext) -> bool {
+ self.sidebar_menu.focused(ctx)
+ }
+}
+
+unsafe impl Send for Paginator {}
+unsafe impl Sync for Paginator {}
+
+impl PageSelector for Paginator {
fn selected_page(&self) -> usize {
self.selected_page()
}
@@ -1011,21 +1075,9 @@ impl Element for Paginator {
fn clear_page_widgets(&mut self, page_idx: usize, ctx: &mut UiContext) {
self.clear_page_widgets(page_idx, ctx);
}
- fn menu_items_list(&self) -> Vec<Vec<String>> {
- self.sidebar_menu.menu_items_list()
- }
- fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
- self.sidebar_menu.menu_checked_list()
- }
-
- fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
- if self.sidebar_mode {
- self.sidebar_menu.get_menu_items_at(px, py)
- } else {
- None
- }
- }
+}
+impl MenuController for Paginator {
fn menu_click(&mut self) -> Option<(usize, usize)> {
if self.sidebar_mode {
self.sidebar_menu.menu_click()
@@ -1034,32 +1086,56 @@ impl Element for Paginator {
}
}
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- self.sidebar_menu.prepare_text(fs);
- for plate in &mut self.plates {
- plate.prepare_text(fs);
- }
+ fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+ self.sidebar_menu.trigger_menu_click(menu_idx, item_idx);
}
- fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- let mut result = Vec::new();
- if self.tabs_rotated {
- let font = self.widget_font();
- for l in self.text_labels() {
- result.push((l, font.clone(), None));
- }
- } else {
- result.extend(self.sidebar_menu.text_labels_with_font_and_bounds(ctx));
- }
+ fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+ self.sidebar_menu.set_item_checked(menu_idx, item_idx, checked);
+ }
- if self.selected_page < self.plates.len() {
- result.extend(self.plates[self.selected_page].text_labels_with_font_and_bounds(ctx));
+ fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+ self.sidebar_menu.set_menu_items(menu_idx, items);
+ }
+
+ fn is_menu_bar(&self) -> bool {
+ true
+ }
+
+ fn is_menu_open(&self) -> bool {
+ self.sidebar_menu.is_menu_open()
+ }
+
+ fn menu_items(&self) -> Vec<String> {
+ self.sidebar_menu.menu_items()
+ }
+
+ fn menu_item_checked(&self) -> Vec<Option<bool>> {
+ self.sidebar_menu.menu_item_checked()
+ }
+
+ fn is_vertical(&self) -> bool {
+ self.sidebar_menu.is_vertical()
+ }
+
+ fn menu_names(&self) -> Vec<String> {
+ self.pages.clone()
+ }
+
+ fn menu_items_list(&self) -> Vec<Vec<String>> {
+ if let Some(c) = self.sidebar_menu.as_menu_controller() {
+ c.menu_items_list()
+ } else {
+ vec![]
}
- result
}
- fn widget_font(&self) -> Option<String> {
- Some(crate::layout::menubar_font())
+ fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+ if let Some(c) = self.sidebar_menu.as_menu_controller() {
+ c.menu_checked_list()
+ } else {
+ vec![]
+ }
}
fn take_context_change(&mut self) -> Option<usize> {
@@ -1075,26 +1151,21 @@ impl Element for Paginator {
self.set_context_selected(selected);
}
- fn focus(&mut self) {
- self.sidebar_menu.focus();
- }
-
- fn unfocus(&mut self) {
- self.sidebar_menu.unfocus();
- }
-
- fn focused(&self, ctx: &UiContext) -> bool {
- self.sidebar_menu.focused(ctx)
+ fn set_center_items(&mut self, center: bool) {
+ if let Some(c) = self.sidebar_menu.as_menu_controller_mut() {
+ c.set_center_items(center);
+ }
}
- fn is_menu_bar(&self) -> bool {
- true
+ fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+ if let Some(c) = self.sidebar_menu.as_menu_controller() {
+ c.get_menu_items_at(px, py)
+ } else {
+ None
+ }
}
}
-unsafe impl Send for Paginator {}
-unsafe impl Sync for Paginator {}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index d493c2c..064a625 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -150,6 +150,36 @@ impl ParametersBg {
font_size: 12.0,
color: [0xee, 0xee, 0xf0],
});
+ } else if ptype.starts_with("choice") {
+ labels.push(TextLabel {
+ text: name.clone(),
+ x: self.base.x + 8.0,
+ y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+ 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],
+ });
+ } else if ptype == "button" {
+ labels.push(TextLabel {
+ text: name.clone(),
+ x: self.base.x + 8.0,
+ y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+ 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],
+ });
} else {
labels.push(TextLabel {
text: format!("{}: {}", name, value),
@@ -270,81 +300,8 @@ impl Element for ParametersBg {
px >= self.base.x && px <= self.base.x + self.base.w && py >= self.base.y && py <= self.base.y + self.base.h
}
- fn set_display_params(&mut self, params: &[(String, String, String)]) {
- let mut layout_changed = self.display_params.len() != params.len();
- if !layout_changed {
- for (p_old, p_new) in self.display_params.iter().zip(params.iter()) {
- if p_old.0 != p_new.0 || p_old.2 != p_new.2 {
- layout_changed = true;
- break;
- }
- }
- }
-
- if layout_changed {
- self.display_params = params.to_vec();
- self.focused_param = None;
- self.sliders = self.display_params.iter().map(|p| {
- if p.2.starts_with("slider") {
- let val = p.1.parse::<f32>().unwrap_or(0.0);
- let (min, max) = parse_slider_range(&p.2);
- let t = if max - min != 0.0 {
- ((val - min) / (max - min)).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- Some(Slider::new().with_value(t).with_range(min, max).with_readout(true))
- } else {
- None
- }
- }).collect();
- self.float3s = self.display_params.iter().map(|p| {
- if p.2.starts_with("float3") {
- let (min, max) = parse_slider_range(&p.2);
- let vals = parse_float3_value(&p.1, min, max);
- Some(Float3::new().with_values(vals).with_range(min, max).with_label(&p.0))
- } else {
- None
- }
- }).collect();
- self.spinboxes = self.display_params.iter().map(|p| {
- if p.2.starts_with("spinbox") {
- let (min, max, step) = parse_spinbox_range(&p.2);
- let val = p.1.parse::<i32>().unwrap_or(min);
- Some(Spinbox::new(val, min, max, step))
- } else {
- None
- }
- }).collect();
- } else {
- for (i, p_new) in params.iter().enumerate() {
- if Some(i) != self.focused_param && Some(i) != self.dragging_param {
- self.display_params[i].1 = p_new.1.clone();
- if let Some(ref mut s) = self.sliders[i] {
- let val = p_new.1.parse::<f32>().unwrap_or(0.0);
- let (min, max) = parse_slider_range(&p_new.2);
- let t = if max - min != 0.0 {
- ((val - min) / (max - min)).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- s.set_value(t);
- } else if let Some(ref mut f) = self.float3s[i] {
- let (min, max) = parse_slider_range(&p_new.2);
- let vals = parse_float3_value(&p_new.1, min, max);
- f.set_values(vals);
- } else if let Some(ref mut sb) = self.spinboxes[i] {
- if !sb.editing {
- let (min, _max, _step) = parse_spinbox_range(&p_new.2);
- let val = p_new.1.parse::<i32>().unwrap_or(min);
- sb.value = val;
- }
- }
- }
- }
- }
- self.update_slider_rects();
- }
+ fn as_param_controller(&self) -> Option<&dyn ParamController> { Some(self) }
+ fn as_param_controller_mut(&mut self) -> Option<&mut dyn ParamController> { Some(self) }
fn unfocus(&mut self) {
if let Some(idx) = self.focused_param {
@@ -382,10 +339,6 @@ impl Element for ParametersBg {
}
}
- fn node_params(&self) -> Vec<(String, String, String)> {
- self.display_params.clone()
- }
-
fn draggable(&self) -> bool {
self.dragging_param.is_some()
|| self.display_params.iter().any(|p| p.2.starts_with("slider") || p.2.starts_with("float3"))
@@ -569,6 +522,29 @@ impl Element for ParametersBg {
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) {
@@ -906,6 +882,28 @@ impl Element for ParametersBg {
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));
+ } 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));
+ } 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));
} 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()));
@@ -1026,3 +1024,85 @@ fn collect_child_quads(widget: &dyn Element) -> Vec<(f32, f32, f32, f32, [f32; 4
quads
}
+impl ParamController for ParametersBg {
+ fn node_params(&self) -> Vec<(String, String, String)> {
+ self.display_params.clone()
+ }
+
+ fn set_display_params(&mut self, params: &[(String, String, String)]) {
+ let mut layout_changed = self.display_params.len() != params.len();
+ if !layout_changed {
+ for (p_old, p_new) in self.display_params.iter().zip(params.iter()) {
+ if p_old.0 != p_new.0 || p_old.2 != p_new.2 {
+ layout_changed = true;
+ break;
+ }
+ }
+ }
+
+ if layout_changed {
+ self.display_params = params.to_vec();
+ self.focused_param = None;
+ self.sliders = self.display_params.iter().map(|p| {
+ if p.2.starts_with("slider") {
+ let val = p.1.parse::<f32>().unwrap_or(0.0);
+ let (min, max) = parse_slider_range(&p.2);
+ let t = if max - min != 0.0 {
+ ((val - min) / (max - min)).clamp(0.0, 1.0)
+ } else {
+ 0.0
+ };
+ Some(Slider::new().with_value(t).with_range(min, max).with_readout(true))
+ } else {
+ None
+ }
+ }).collect();
+ self.float3s = self.display_params.iter().map(|p| {
+ if p.2.starts_with("float3") {
+ let (min, max) = parse_slider_range(&p.2);
+ let vals = parse_float3_value(&p.1, min, max);
+ Some(Float3::new().with_values(vals).with_range(min, max).with_label(&p.0))
+ } else {
+ None
+ }
+ }).collect();
+ self.spinboxes = self.display_params.iter().map(|p| {
+ if p.2.starts_with("spinbox") {
+ let (min, max, step) = parse_spinbox_range(&p.2);
+ let val = p.1.parse::<i32>().unwrap_or(min);
+ Some(Spinbox::new(val, min, max, step))
+ } else {
+ None
+ }
+ }).collect();
+ } else {
+ for (i, p_new) in params.iter().enumerate() {
+ if Some(i) != self.focused_param && Some(i) != self.dragging_param {
+ self.display_params[i].1 = p_new.1.clone();
+ if let Some(ref mut s) = self.sliders[i] {
+ let val = p_new.1.parse::<f32>().unwrap_or(0.0);
+ let (min, max) = parse_slider_range(&p_new.2);
+ let t = if max - min != 0.0 {
+ ((val - min) / (max - min)).clamp(0.0, 1.0)
+ } else {
+ 0.0
+ };
+ s.set_value(t);
+ } else if let Some(ref mut f) = self.float3s[i] {
+ let (min, max) = parse_slider_range(&p_new.2);
+ let vals = parse_float3_value(&p_new.1, min, max);
+ f.set_values(vals);
+ } else if let Some(ref mut sb) = self.spinboxes[i] {
+ if !sb.editing {
+ let (min, _max, _step) = parse_spinbox_range(&p_new.2);
+ let val = p_new.1.parse::<i32>().unwrap_or(min);
+ sb.value = val;
+ }
+ }
+ }
+ }
+ }
+ self.update_slider_rects();
+ }
+}
+
diff --git a/src/widget/container/plate.rs b/src/widget/container/plate.rs
index cfab8ef..d883eaa 100644
--- a/src/widget/container/plate.rs
+++ b/src/widget/container/plate.rs
@@ -4,7 +4,7 @@ use crate::widget::display::TextLabel;
#[derive(Debug, Clone)]
pub struct Plate {
- pub base: Widget,
+ pub base: Layer,
pub dragging: bool,
pub drag_ox: f32,
pub drag_oy: f32,
@@ -15,11 +15,12 @@ pub struct Plate {
pub curved_circle: Option<(f32, f32, f32)>,
pub network_opacity: f32,
pub blur: bool,
- pub children: Vec<*mut (dyn Element + 'static)>,
- pub parent: Option<*mut (dyn Element + 'static)>,
pub visible: bool,
pub column_layout: bool,
pub draggable: bool,
+ pub solid_border: Option<([f32; 4], f32)>,
+ pub selected: bool,
+ pub padding: Option<f32>,
}
impl Plate {
@@ -33,7 +34,7 @@ impl Plate {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: Widget::new_rect(x, y, w, h),
+ base: Layer::new(x, y, w, h),
dragging: false,
drag_ox: 0.0,
drag_oy: 0.0,
@@ -44,11 +45,12 @@ impl Plate {
curved_circle: None,
network_opacity: 1.0,
blur: true,
- children: Vec::new(),
- parent: None,
visible: true,
column_layout: false,
draggable: true,
+ solid_border: None,
+ selected: false,
+ padding: None,
}
}
@@ -58,7 +60,7 @@ impl Plate {
}
pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
+ self.base.base.label = Some(label.to_string());
self
}
@@ -72,19 +74,48 @@ impl Plate {
self
}
+ pub fn with_solid_border(mut self, color: [f32; 4], thickness: f32) -> Self {
+ self.solid_border = Some((color, thickness));
+ self
+ }
+
+ pub fn with_padding(mut self, padding: f32) -> Self {
+ self.padding = Some(padding);
+ self
+ }
+
pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
self.bounds = Some((bx, by, bw, bh));
}
}
impl Element for Plate {
- crate::impl_widget_base!(Plate);
+ fn base(&self) -> Option<&Widget> { Some(&self.base.base) }
+ fn base_mut(&mut self) -> Option<&mut Widget> { Some(&mut self.base.base) }
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+ fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ }
+
fn is_plate(&self) -> bool { true }
fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
- fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
+ fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{ None }
+
+ fn solid_border(&self) -> Option<([f32; 4], f32)> {
+ if self.selected {
+ Some((colors::active_theme().primary_accent, 1.5))
+ } else {
+ self.solid_border
+ }
+ }
+
+ fn set_selected(&mut self, selected: bool) {
+ self.selected = selected;
+ }
fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
unsafe {
(*child_ptr).set_modifiers(ctrl, shift, alt);
}
@@ -97,11 +128,7 @@ impl Element for Plate {
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
- for &child_ptr in &self.children {
- unsafe {
- (*child_ptr).set_visible(visible);
- }
- }
+ self.base.visible = visible;
}
fn color(&self) -> [f32; 4] {
@@ -175,30 +202,40 @@ impl Element for Plate {
}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- if let Some(b) = self.base_mut() {
- b.x = x;
- b.y = y;
- b.w = w;
- b.h = h;
- }
+ let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.base.parent {
+ let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+ let cx = x.clamp(px, px + pw.max(0.0));
+ let cy = y.clamp(py, py + ph.max(0.0));
+ let cw = w.min((px + pw.max(0.0) - cx).max(0.0));
+ let ch = h.min((py + ph.max(0.0) - cy).max(0.0));
+ (cx, cy, cw, ch)
+ } else {
+ (x, y, w, h)
+ };
- if !self.visible {
+ self.base.base.x = clamped_x;
+ self.base.base.y = clamped_y;
+ self.base.base.w = clamped_w;
+ self.base.base.h = clamped_h;
+
+ if !self.base.visible {
return;
}
- let padding_x = crate::layout::plate_padding();
- let padding_y = crate::layout::plate_padding();
- let left_x = x + padding_x;
- let available_w = (w - 2.0 * padding_x).max(1.0);
- let start_y = y + padding_y;
- let available_h = (h - 2.0 * padding_y).max(1.0);
+ let pad = self.padding.unwrap_or_else(|| crate::layout::plate_padding());
+ let padding_x = pad;
+ let padding_y = pad;
+ let left_x = clamped_x + padding_x;
+ let available_w = (clamped_w - 2.0 * padding_x).max(1.0);
+ let start_y = clamped_y + padding_y;
+ let available_h = (clamped_h - 2.0 * padding_y).max(1.0);
let center_x = left_x + available_w / 2.0;
let center_y = start_y + available_h / 2.0;
let aspect_ratio = available_w / available_h;
let mut active_widgets = Vec::new();
- for &w_ptr in &self.children {
+ for &w_ptr in &self.base.children {
let w = unsafe { &*w_ptr };
if !w.layout_ignore() {
active_widgets.push(w_ptr);
@@ -215,8 +252,12 @@ impl Element for Plate {
let top = crate::widget::label_offset(w);
let use_h = if wh > 0.0 { wh } else { 24.0 + top };
- w.set_rect(left_x, current_y, use_w, use_h);
- current_y += use_h + spacing;
+ let max_y = clamped_y + clamped_h - padding_y;
+ let active_y = current_y.min(max_y);
+ let active_h = use_h.min(max_y - active_y);
+
+ w.set_rect(left_x, active_y, use_w, active_h);
+ current_y += active_h + spacing;
}
} else {
let mut total_diagonal = 0.0;
@@ -239,7 +280,11 @@ impl Element for Plate {
let use_h = if wh > 0.0 { wh } else { 50.0 };
if i == 0 {
- w.set_rect(center_x - use_w / 2.0, center_y - use_h / 2.0, use_w, use_h);
+ let cx = (center_x - use_w / 2.0).clamp(left_x, (left_x + available_w - use_w).max(left_x));
+ let cy = (center_y - use_h / 2.0).clamp(start_y, (start_y + available_h - use_h).max(start_y));
+ let cw = use_w.min(clamped_x + clamped_w - padding_x - cx);
+ let ch = use_h.min(clamped_y + clamped_h - padding_y - cy);
+ w.set_rect(cx, cy, cw, ch);
} else {
let mut ring = 1;
let mut ring_start = 1;
@@ -254,12 +299,15 @@ impl Element for Plate {
let x_offset = radius * angle.cos() * aspect_ratio;
let y_offset = radius * angle.sin();
- w.set_rect(
- center_x + x_offset - use_w / 2.0,
- center_y + y_offset - use_h / 2.0,
- use_w,
- use_h,
- );
+ let raw_x = center_x + x_offset - use_w / 2.0;
+ let raw_y = center_y + y_offset - use_h / 2.0;
+
+ let cx = raw_x.clamp(left_x, (left_x + available_w - use_w).max(left_x));
+ let cy = raw_y.clamp(start_y, (start_y + available_h - use_h).max(start_y));
+ let cw = use_w.min(clamped_x + clamped_w - padding_x - cx);
+ let ch = use_h.min(clamped_y + clamped_h - padding_y - cy);
+
+ w.set_rect(cx, cy, cw, ch);
placed = true;
} else {
ring_start += ring_capacity;
@@ -271,13 +319,13 @@ impl Element for Plate {
}
}
- fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- self.parent
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ self.base.parent
}
fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
- self.parent = parent;
- let id = self.base.id();
+ self.base.parent = parent;
+ let id = self.base.base.id();
if let Some(p_ptr) = parent {
if let Some(p_base) = unsafe { (*p_ptr).base() } {
let p_id = p_base.id();
@@ -291,24 +339,27 @@ impl Element for Plate {
}
fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
- self.children.clone()
+ self.base.children.clone()
}
fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
- self.children.push(child);
- let id = self.base.id();
+ self.base.children.push(child);
+ let id = self.base.base.id();
+ let self_ptr = self.as_ptr();
if let Some(c_base) = unsafe { (*child).base() } {
let c_id = c_base.id();
- let self_ptr = self.as_ptr();
ctx.register_widget(id, self_ptr);
ctx.register_widget(c_id, child);
ctx.link_ids(id, c_id);
}
+ unsafe {
+ (*child).set_parent(Some(self_ptr), ctx);
+ }
}
fn clear_children(&mut self, ctx: &mut UiContext) {
- self.children.clear();
- let id = self.base.id();
+ self.base.children.clear();
+ let id = self.base.base.id();
ctx.clear_children_ids(id);
}
@@ -320,7 +371,7 @@ impl Element for Plate {
let (px, py, pw, ph) = self.rect();
quads.push((px, py, pw, ph, self.color()));
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
let widget = unsafe { &*child_ptr };
let c = widget.color();
if c[3] > 0.0 {
@@ -337,16 +388,16 @@ impl Element for Plate {
return Vec::new();
}
let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
+ if let Some(ref label) = self.base.base.label {
labels.push(TextLabel {
text: label.clone(),
- x: self.base.x,
- y: self.base.y - (12.0 + crate::layout::label_margin()),
+ x: self.base.base.x,
+ y: self.base.base.y - (12.0 + crate::layout::label_margin()),
font_size: 12.0,
color: [0x83, 0x83, 0x8a],
});
}
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
let widget = unsafe { &*child_ptr };
labels.extend(widget.text_labels());
}
@@ -358,19 +409,19 @@ impl Element for Plate {
return Vec::new();
}
let mut result = Vec::new();
- if let Some(ref label) = self.base.label {
+ if let Some(ref label) = self.base.base.label {
result.push((
TextLabel {
text: label.clone(),
- x: self.base.x,
- y: self.base.y - (12.0 + crate::layout::label_margin()),
+ x: self.base.base.x,
+ y: self.base.base.y - (12.0 + crate::layout::label_margin()),
font_size: 12.0,
color: [0x83, 0x83, 0x8a],
},
None,
));
}
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
let widget = unsafe { &*child_ptr };
result.extend(widget.text_labels_with_bounds(ctx));
}
@@ -382,12 +433,12 @@ impl Element for Plate {
return Vec::new();
}
let mut result = Vec::new();
- if let Some(ref label) = self.base.label {
+ if let Some(ref label) = self.base.base.label {
result.push((
TextLabel {
text: label.clone(),
- x: self.base.x,
- y: self.base.y - (12.0 + crate::layout::label_margin()),
+ x: self.base.base.x,
+ y: self.base.base.y - (12.0 + crate::layout::label_margin()),
font_size: 12.0,
color: [0x83, 0x83, 0x8a],
},
@@ -395,31 +446,42 @@ impl Element for Plate {
None,
));
}
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
let widget = unsafe { &*child_ptr };
result.extend(widget.text_labels_with_font_and_bounds(ctx));
}
result
}
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut items = Vec::new();
+ for &child_ptr in &self.base.children {
+ let widget = unsafe { &*child_ptr };
+ items.extend(widget.get_text_items());
+ }
+ items
+ }
+
fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
if !self.visible {
return;
}
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
unsafe {
(*child_ptr).prepare_text(fs);
}
}
}
-
fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if !self.visible {
return false;
}
let mut changed = false;
- for &widget_ptr in &self.children {
+ for &widget_ptr in &self.base.children {
let widget = unsafe { &mut *widget_ptr };
if widget.is_dragging() {
if widget.drag_update(px, py) {
@@ -436,7 +498,7 @@ impl Element for Plate {
if !self.visible {
return false;
}
- for &widget_ptr in self.children.iter().rev() {
+ for &widget_ptr in self.base.children.iter().rev() {
let widget = unsafe { &mut *widget_ptr };
if widget.popover_rect().is_some() {
if widget.mouse_input(button, state, px, py, ctx) {
@@ -444,7 +506,7 @@ impl Element for Plate {
}
}
}
- for &widget_ptr in self.children.iter().rev() {
+ for &widget_ptr in self.base.children.iter().rev() {
let widget = unsafe { &mut *widget_ptr };
if widget.mouse_input(button, state, px, py, ctx) {
return true;
@@ -474,7 +536,7 @@ impl Element for Plate {
if !self.visible {
return false;
}
- for &widget_ptr in &self.children {
+ for &widget_ptr in &self.base.children {
let widget = unsafe { &mut *widget_ptr };
if widget.keyboard_input(event, ctx) {
return true;
@@ -487,7 +549,7 @@ impl Element for Plate {
if !self.visible {
return false;
}
- for &widget_ptr in self.children.iter().rev() {
+ for &widget_ptr in self.base.children.iter().rev() {
let widget = unsafe { &mut *widget_ptr };
if widget.mouse_wheel(delta, px, py, ctx) {
return true;
@@ -500,7 +562,7 @@ impl Element for Plate {
if !self.visible {
return None;
}
- for &widget_ptr in self.children.iter().rev() {
+ for &widget_ptr in self.base.children.iter().rev() {
let widget = unsafe { &*widget_ptr };
if let Some(r) = widget.popover_rect() {
return Some(r);
@@ -513,7 +575,7 @@ impl Element for Plate {
if !self.visible {
return;
}
- for &widget_ptr in self.children.iter().rev() {
+ for &widget_ptr in self.base.children.iter().rev() {
let widget = unsafe { &*widget_ptr };
widget.render_popover(pc);
}
@@ -524,7 +586,7 @@ impl Element for Plate {
return false;
}
let mut changed = false;
- for &widget_ptr in &self.children {
+ for &widget_ptr in &self.base.children {
let widget = unsafe { &mut *widget_ptr };
if widget.tick(dt, ctx) {
changed = true;
@@ -537,17 +599,17 @@ impl Element for Plate {
let nx = px - self.drag_ox;
let ny = py - self.drag_oy;
let (nx, ny) = if let Some((bx, by, bw, bh)) = self.bounds {
- (nx.clamp(bx, bx + bw - self.base.w), ny.clamp(by, by + bh - self.base.h))
+ (nx.clamp(bx, bx + bw - self.base.base.w), ny.clamp(by, by + bh - self.base.base.h))
} else {
(nx, ny)
};
- if (nx - self.base.x).abs() > 0.01 || (ny - self.base.y).abs() > 0.01 {
- let dx = nx - self.base.x;
- let dy = ny - self.base.y;
- self.base.x = nx;
- self.base.y = ny;
+ if (nx - self.base.base.x).abs() > 0.01 || (ny - self.base.base.y).abs() > 0.01 {
+ let dx = nx - self.base.base.x;
+ let dy = ny - self.base.base.y;
+ self.base.base.x = nx;
+ self.base.base.y = ny;
- for &child_ptr in &self.children {
+ for &child_ptr in &self.base.children {
unsafe {
let (cx, cy, cw, ch) = (*child_ptr).rect();
(*child_ptr).set_rect(cx + dx, cy + dy, cw, ch);
@@ -560,10 +622,10 @@ impl Element for Plate {
fn drag_begin(&mut self, px: f32, py: f32) {
self.dragging = true;
- self.drag_ox = px - self.base.x;
- self.drag_oy = py - self.base.y;
- self.drag_start_x = self.base.x;
- self.drag_start_y = self.base.y;
+ self.drag_ox = px - self.base.base.x;
+ self.drag_oy = py - self.base.base.y;
+ self.drag_start_x = self.base.base.x;
+ self.drag_start_y = self.base.base.y;
}
fn drag_end(&mut self) { self.dragging = false; }
diff --git a/src/widget/container/scrolling_list.rs b/src/widget/container/scrolling_list.rs
index 50e5552..2475122 100644
--- a/src/widget/container/scrolling_list.rs
+++ b/src/widget/container/scrolling_list.rs
@@ -104,6 +104,11 @@ impl Element for ScrollingList {
fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) { self.scroll_box.add_child(child, ctx); }
fn clear_children(&mut self, ctx: &mut UiContext) { self.scroll_box.clear_children(ctx); }
+ fn as_scroll_controller(&self) -> Option<&dyn ScrollController> { Some(self) }
+ fn as_scroll_controller_mut(&mut self) -> Option<&mut dyn ScrollController> { Some(self) }
+}
+
+impl ScrollController for ScrollingList {
fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
self.update_bounds(count, viewport_y, viewport_h);
}
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 22e7957..c0f1d42 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -91,16 +91,8 @@ impl Element for Spreadsheet {
self.visible
}
- fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
- self.headers = headers;
- self.rows = rows;
-
- // Clamp scroll_y to new bounds
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- let max_scroll_y = (content_h - visible_h).max(0.0);
- self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
- }
+ fn as_spreadsheet_controller(&self) -> Option<&dyn SpreadsheetController> { Some(self) }
+ fn as_spreadsheet_controller_mut(&mut self) -> Option<&mut dyn SpreadsheetController> { Some(self) }
fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
let was_hovered = self.hovered;
@@ -395,3 +387,16 @@ impl Element for Spreadsheet {
labels
}
}
+
+impl SpreadsheetController for Spreadsheet {
+ fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
+ self.headers = headers;
+ self.rows = rows;
+
+ // Clamp scroll_y to new bounds
+ let content_h = self.rows.len() as f32 * 24.0;
+ let visible_h = (self.h - 24.0).max(0.0);
+ let max_scroll_y = (content_h - visible_h).max(0.0);
+ self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
+ }
+}
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
new file mode 100644
index 0000000..9de8822
--- /dev/null
+++ b/src/widget/container/switcher.rs
@@ -0,0 +1,513 @@
+use crate::widget::*;
+use crate::widget::display::TextLabel;
+
+#[derive(Debug, Clone)]
+pub struct Switcher {
+ pub base: Widget,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+ pub active_index: Option<usize>,
+ pub visible: bool,
+}
+
+impl Switcher {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self {
+ base: Widget::new_rect(x, y, w, h),
+ parent: None,
+ children: Vec::new(),
+ active_index: None,
+ visible: true,
+ }
+ }
+
+ pub fn set_active_index(&mut self, index: Option<usize>) {
+ self.active_index = index;
+ for (i, &child_ptr) in self.children.iter().enumerate() {
+ unsafe {
+ (*child_ptr).set_visible(self.active_index == Some(i));
+ }
+ }
+ }
+
+ pub fn active_index(&self) -> Option<usize> {
+ self.active_index
+ }
+}
+
+impl Element for Switcher {
+ crate::impl_widget_base!(Switcher);
+
+ fn visible(&self) -> bool {
+ self.visible
+ }
+
+ fn set_visible(&mut self, visible: bool) {
+ self.visible = visible;
+ }
+
+ fn color(&self) -> [f32; 4] {
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).color() };
+ }
+ }
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ self.parent
+ }
+
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
+ self.parent = parent;
+ }
+
+ fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
+ self.children.clone()
+ }
+
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
+ self.children.push(child);
+ let id = self.base.id();
+ let self_ptr = self.as_ptr();
+ if let Some(c_base) = unsafe { (*child).base() } {
+ let c_id = c_base.id();
+ ctx.register_widget(id, self_ptr);
+ ctx.register_widget(c_id, child);
+ ctx.link_ids(id, c_id);
+ }
+ unsafe {
+ (*child).set_parent(Some(self_ptr), ctx);
+ }
+ // Sync visibility of newly added child
+ let idx = self.children.len() - 1;
+ unsafe {
+ (*child).set_visible(self.active_index == Some(idx));
+ }
+ }
+
+ fn clear_children(&mut self, ctx: &mut UiContext) {
+ self.children.clear();
+ let id = self.base.id();
+ ctx.clear_children_ids(id);
+ }
+
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ (self.base.x, self.base.y, self.base.w, self.base.h)
+ }
+
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
+ let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+ let cx = x.clamp(px, px + pw.max(0.0));
+ let cy = y.clamp(py, py + ph.max(0.0));
+ let cw = w.min((px + pw.max(0.0) - cx).max(0.0));
+ let ch = h.min((py + ph.max(0.0) - cy).max(0.0));
+ (cx, cy, cw, ch)
+ } else {
+ (x, y, w, h)
+ };
+
+ self.base.x = clamped_x;
+ self.base.y = clamped_y;
+ self.base.w = clamped_w;
+ self.base.h = clamped_h;
+
+ if !self.visible {
+ return;
+ }
+
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ unsafe {
+ (*self.children[idx]).set_rect(clamped_x, clamped_y, clamped_w, clamped_h);
+ }
+ }
+ }
+ }
+
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).hit_test(px, py, ctx) };
+ }
+ }
+ false
+ }
+
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut quads = Vec::new();
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ let widget = unsafe { &*self.children[idx] };
+ let c = widget.color();
+ if c[3] > 0.0 {
+ let (wx, wy, ww, wh) = widget.rect();
+ quads.push((wx, wy, ww, wh, c));
+ }
+ quads.extend(widget.all_quads(ctx));
+ }
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible {
+ return Vec::new();
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).text_labels() };
+ }
+ }
+ Vec::new()
+ }
+
+ fn text_labels_with_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<[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]).text_labels_with_bounds(ctx) };
+ }
+ }
+ Vec::new()
+ }
+
+ fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[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]).text_labels_with_font_and_bounds(ctx) };
+ }
+ }
+ Vec::new()
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).get_text_items() };
+ }
+ }
+ Vec::new()
+ }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+ if !self.visible {
+ return;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ unsafe {
+ (*self.children[idx]).prepare_text(fs);
+ }
+ }
+ }
+ }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ let widget = unsafe { &mut *self.children[idx] };
+ if widget.is_dragging() {
+ return widget.drag_update(px, py);
+ } else {
+ return widget.cursor_moved(px, py, ctx);
+ }
+ }
+ }
+ false
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ let widget = unsafe { &mut *self.children[idx] };
+ if widget.popover_rect().is_some() {
+ if widget.mouse_input(button, state, px, py, ctx) {
+ return true;
+ }
+ }
+ if widget.mouse_input(button, state, px, py, ctx) {
+ return true;
+ }
+ if state == ElementState::Pressed && !widget.hit_test(px, py, ctx) {
+ widget.unfocus();
+ }
+ }
+ }
+ false
+ }
+
+ fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).keyboard_input(event, ctx) };
+ }
+ }
+ false
+ }
+
+ fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).mouse_wheel(delta, px, py, ctx) };
+ }
+ }
+ false
+ }
+
+ fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+ if !self.visible {
+ return None;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).popover_rect() };
+ }
+ }
+ None
+ }
+
+ fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
+ if !self.visible {
+ return;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ unsafe {
+ (*self.children[idx]).render_popover(pc);
+ }
+ }
+ }
+ }
+
+ fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).tick(dt, ctx) };
+ }
+ }
+ false
+ }
+
+ fn rounded_corners(&self) -> (bool, bool, bool, bool) {
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).rounded_corners() };
+ }
+ }
+ (false, false, false, false)
+ }
+
+ fn solid_border(&self) -> Option<([f32; 4], f32)> {
+ if let Some(idx) = self.active_index {
+ if idx < self.children.len() {
+ return unsafe { (*self.children[idx]).solid_border() };
+ }
+ }
+ None
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, 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_quads() };
+ }
+ }
+ Vec::new()
+ }
+
+ fn extra_arcs(&self) -> Vec<(f32, f32, f32, 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_arcs() };
+ }
+ }
+ 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) }
+}
+
+impl MenuController for Switcher {
+ fn menu_click(&mut self) -> Option<(usize, usize)> {
+ let idx = self.active_index?;
+ let child_ptr = *self.children.get(idx)?;
+ unsafe { &mut *child_ptr }.as_menu_controller_mut()?.menu_click()
+ }
+ fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+ mc.trigger_menu_click(menu_idx, item_idx);
+ }
+ }
+ }
+ }
+ fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+ mc.set_item_checked(menu_idx, item_idx, checked);
+ }
+ }
+ }
+ }
+ fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+ mc.set_menu_items(menu_idx, items);
+ }
+ }
+ }
+ }
+ fn is_menu_bar(&self) -> bool {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.is_menu_bar();
+ }
+ }
+ }
+ false
+ }
+ fn is_menu_open(&self) -> bool {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.is_menu_open();
+ }
+ }
+ }
+ false
+ }
+ fn menu_items(&self) -> Vec<String> {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.menu_items();
+ }
+ }
+ }
+ Vec::new()
+ }
+ fn menu_item_checked(&self) -> Vec<Option<bool>> {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.menu_item_checked();
+ }
+ }
+ }
+ Vec::new()
+ }
+ fn is_vertical(&self) -> bool {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.is_vertical();
+ }
+ }
+ }
+ false
+ }
+ fn menu_names(&self) -> Vec<String> {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.menu_names();
+ }
+ }
+ }
+ Vec::new()
+ }
+ fn menu_items_list(&self) -> Vec<Vec<String>> {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.menu_items_list();
+ }
+ }
+ }
+ Vec::new()
+ }
+ fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &**child }.as_menu_controller() {
+ return mc.menu_checked_list();
+ }
+ }
+ }
+ Vec::new()
+ }
+ fn take_context_change(&mut self) -> Option<usize> {
+ let idx = self.active_index?;
+ let child_ptr = *self.children.get(idx)?;
+ unsafe { &mut *child_ptr }.as_menu_controller_mut()?.take_context_change()
+ }
+ fn set_context_selected(&mut self, selected: usize) {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+ mc.set_context_selected(selected);
+ }
+ }
+ }
+ }
+ fn set_center_items(&mut self, center: bool) {
+ if let Some(idx) = self.active_index {
+ if let Some(child) = self.children.get(idx) {
+ if let Some(mc) = unsafe { &mut **child }.as_menu_controller_mut() {
+ mc.set_center_items(center);
+ }
+ }
+ }
+ }
+ fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+ let idx = self.active_index?;
+ let child_ptr = *self.children.get(idx)?;
+ unsafe { &*child_ptr }.as_menu_controller()?.get_menu_items_at(px, py)
+ }
+}
+
+unsafe impl Send for Switcher {}
+unsafe impl Sync for Switcher {}
diff --git a/src/widget/core.rs b/src/widget/core.rs
index a88d662..aade366 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -411,27 +411,33 @@ pub mod clipboard {
}
pub fn read_from_clipboard() -> Option<String> {
- if let Ok(output) = std::process::Command::new("wl-paste")
+ match std::process::Command::new("wl-paste")
.arg("-n")
.output()
{
- if output.status.success() {
- if let Ok(text) = String::from_utf8(output.stdout) {
- return Some(text);
+ Ok(output) => {
+ if output.status.success() {
+ if let Ok(text) = String::from_utf8(output.stdout) {
+ return Some(text);
+ }
}
}
+ Err(_) => {}
}
- if let Ok(output) = std::process::Command::new("xclip")
+ match std::process::Command::new("xclip")
.arg("-selection")
.arg("clipboard")
.arg("-o")
.output()
{
- if output.status.success() {
- if let Ok(text) = String::from_utf8(output.stdout) {
- return Some(text);
+ Ok(output) => {
+ if output.status.success() {
+ if let Ok(text) = String::from_utf8(output.stdout) {
+ return Some(text);
+ }
}
}
+ Err(_) => {}
}
None
}
@@ -450,7 +456,7 @@ pub mod context_menu {
pub visible: bool,
pub options: Vec<String>,
pub hovered_item: Option<usize>,
- pub target: Option<*mut TextBox>,
+ pub target: Option<*mut (dyn Element + 'static)>,
}
impl ContextMenuState {
@@ -467,11 +473,13 @@ pub mod context_menu {
}
}
- pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+ pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
self.x = x;
self.y = y;
self.options = options;
self.h = self.options.len() as f32 * 24.0;
+ let max_len = self.options.iter().map(|s| s.len()).max().unwrap_or(0);
+ self.w = ((max_len as f32 * 7.5) + 24.0).max(120.0);
self.visible = true;
self.hovered_item = None;
self.target = Some(target);
@@ -493,7 +501,7 @@ pub mod context_menu {
self.hovered_item = None;
if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
let idx = ((py - self.y) / 24.0) as usize;
- if idx < self.options.len() {
+ if idx < self.options.len() && idx > 0 {
self.hovered_item = Some(idx);
}
}
@@ -513,28 +521,26 @@ pub mod context_menu {
if px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h {
let idx = ((py - self.y) / 24.0) as usize;
if idx < self.options.len() {
- let opt = self.options[idx].clone();
- if let Some(target_ptr) = self.target {
- unsafe {
- let target = &mut *target_ptr;
- match opt.as_str() {
- "Cut" => {
- if target.cut_selection() {
- target.just_changed = true;
+ if idx > 0 {
+ let opt = self.options[idx].clone();
+ if let Some(target_ptr) = self.target {
+ unsafe {
+ let target = &mut *target_ptr;
+ match opt.as_str() {
+ "Cut" => {
+ let _ = target.cut_selection();
}
- }
- "Copy" => {
- target.copy_selection();
- }
- "Paste" => {
- if target.paste_from_clipboard() {
- target.just_changed = true;
+ "Copy" => {
+ target.copy_selection();
}
+ "Paste" => {
+ let _ = target.paste_from_clipboard();
+ }
+ "Select All" => {
+ target.select_all();
+ }
+ _ => {}
}
- "Select All" => {
- target.select_all();
- }
- _ => {}
}
}
}
@@ -570,7 +576,9 @@ pub mod context_menu {
for (idx, opt) in self.options.iter().enumerate() {
let iy = self.y + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
- let text_color = if self.hovered_item == Some(idx) {
+ let text_color = if idx == 0 {
+ [0x70, 0x70, 0x78]
+ } else if self.hovered_item == Some(idx) {
[0xff, 0xff, 0xff]
} else {
[0xcc, 0xcc, 0xd4]
@@ -596,7 +604,7 @@ pub mod context_menu {
CONTEXT_MENU.with(|m| m.borrow().visible)
}
- pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+ pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut (dyn Element + 'static)) {
CONTEXT_MENU.with(|m| m.borrow_mut().show(x, y, options, target));
}
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index f0be9f6..6c2a3a0 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -307,3 +307,10 @@ impl Element for Float3 {
labels
}
}
+
+impl Drop for Float3 {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 7a394fd..b363541 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -4,6 +4,8 @@ use crate::widget::display::TextLabel;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct GraphNode {
+ #[serde(default)]
+ pub id: String,
pub name: String,
pub position: (f32, f32), // (column, row)
pub parameters: Vec<(String, String, String)>, // (name, value, type)
@@ -22,6 +24,7 @@ pub struct Graph {
skipped_col_w: f32,
nodes: Vec<GraphNode>,
selected_idx: Option<usize>,
+ selected_id: Option<String>,
double_clicked_idx: Option<usize>,
double_click_timer: Option<(std::time::Instant, usize)>,
grid_snap_enabled: bool,
@@ -29,6 +32,7 @@ pub struct Graph {
// For dragging a node
dragging_idx: Option<usize>,
+ dragging_id: Option<String>,
drag_ox: f32,
drag_oy: f32,
pub(crate) drag_node_pos: Option<(f32, f32)>,
@@ -69,11 +73,13 @@ impl Graph {
skipped_col_w: 37.5,
nodes: Vec::new(),
selected_idx: None,
+ selected_id: None,
double_clicked_idx: None,
double_click_timer: None,
grid_snap_enabled: false,
node_geom_toggled: None,
dragging_idx: None,
+ dragging_id: None,
drag_ox: 0.0,
drag_oy: 0.0,
drag_node_pos: None,
@@ -103,7 +109,9 @@ impl Graph {
pub fn toggle_rect(&self, idx: usize) -> Option<(f32, f32, f32, f32)> {
let (nx, ny, nw, nh) = self.node_rect(idx)?;
- Some((nx + nw - 30.0, ny + (nh - 18.0) / 2.0, 18.0, 18.0))
+ let scale_f = nw / 80.0;
+ let size = (18.0 * scale_f).clamp(6.0, 50.0);
+ Some((nx + nw - size - 6.0 * scale_f, ny + (nh - size) / 2.0, size, size))
}
fn find_empty_cell(&self, start_x: f32, start_y: f32, skip_idx: Option<usize>) -> (f32, f32) {
@@ -153,39 +161,23 @@ impl Element for Graph {
px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h
}
- fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
- fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
- fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
- fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
-
- fn set_nodes(&mut self, nodes: &[GraphNode]) {
- self.nodes = nodes.to_vec();
- if let Some(sel) = self.selected_idx {
- if sel >= self.nodes.len() {
- self.selected_idx = None;
- }
- }
- }
- fn get_nodes(&self) -> Vec<GraphNode> { self.nodes.clone() }
- fn selected_node(&self) -> Option<usize> { self.selected_idx }
- fn set_selected_node(&mut self, idx: Option<usize>) { self.selected_idx = idx; }
- fn double_clicked_node(&self) -> Option<usize> { self.double_clicked_idx }
- fn clear_double_clicked_node(&mut self) { self.double_clicked_idx = None; }
- fn set_grid_snap_enabled(&mut self, enabled: bool) { self.grid_snap_enabled = enabled; }
- fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { self.node_geom_toggled.take() }
+ fn as_graph_controller(&self) -> Option<&dyn GraphController> { Some(self) }
+ fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { Some(self) }
fn text_labels(&self) -> Vec<TextLabel> {
let mut labels = Vec::new();
for (i, node) in self.nodes.iter().enumerate() {
- if let Some((nx, ny, _nw, nh)) = self.node_rect(i) {
- let lx = nx + 8.0;
- let ly = ny + (nh - 12.0) / 2.0;
+ if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+ let scale_f = nw / 80.0;
+ let font_size = (14.0 * scale_f).clamp(6.0, 48.0);
+ let lx = nx + nw + 8.0 * scale_f;
+ let ly = ny + (nh - font_size) / 2.0;
if lx >= self.x && lx < self.x + self.w && ly >= self.y && ly < self.y + self.h {
labels.push(TextLabel {
text: node.name.clone(),
x: lx,
y: ly,
- font_size: 14.0,
+ font_size,
color: [0xcc, 0xcc, 0xd4],
});
}
@@ -248,6 +240,7 @@ impl Element for Graph {
} else {
self.dragging_idx = None;
}
+ self.dragging_id = None;
}
fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
@@ -286,7 +279,9 @@ impl Element for Graph {
}
self.double_click_timer = Some((now, i));
self.selected_idx = Some(i);
+ self.selected_id = Some(self.nodes[i].id.clone());
self.dragging_idx = Some(i);
+ self.dragging_id = Some(self.nodes[i].id.clone());
self.drag_ox = px - nx;
self.drag_oy = py - ny;
self.drag_node_pos = Some((nx, ny));
@@ -296,6 +291,7 @@ impl Element for Graph {
}
}
self.selected_idx = None;
+ self.selected_id = None;
false
}
ElementState::Released => {
@@ -327,8 +323,9 @@ impl Element for Graph {
};
// Draw connection wires
+ let scale_f = self.grid_size_x / 80.0;
let wire_color = [0.0, 0.75, 1.0, 0.7]; // Vibrant cyan glow
- let wire_thickness = 3.0;
+ let wire_thickness = (3.0 * scale_f).clamp(1.0, 15.0);
for i in 0..self.nodes.len() {
let node = &self.nodes[i];
if let Some((_, input_name, _)) = node.parameters.iter().find(|(name, _, _)| name.eq_ignore_ascii_case("input")) {
@@ -386,59 +383,28 @@ impl Element for Graph {
let step_x = self.grid_size_x + self.skipped_col_w;
if step_y >= 4.0 && step_x >= 4.0 {
- let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let ry_start = ry_start.max(-100_000);
- let ry_end = ry_end.min(100_000);
+ let ry_start = (((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1).max(-100_000);
+ let ry_end = (((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1).min(100_000);
- let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let cx_start = cx_start.max(-100_000);
- let cx_end = cx_end.min(100_000);
+ 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
- quads.push((self.x, self.y, self.w, self.h, [self.gap_color[0], self.gap_color[1], self.gap_color[2], self.network_opacity]));
+ 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
for r in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (r as f32) * step_y;
+ let y1 = (self.grid_origin_y + (r as f32) * step_y).round();
for c in cx_start..=cx_end {
- let x1 = self.grid_origin_x + (c as f32) * step_x;
- let cell_x = x1.max(self.x);
- let cell_y = y1.max(self.y);
- let cell_w = (x1 + self.grid_size_x).min(self.x + self.w) - cell_x;
- let cell_h = (y1 + self.grid_size_y).min(self.y + self.h) - cell_y;
- if cell_w > 0.0 && cell_h > 0.0 {
- quads.push((cell_x, cell_y, cell_w, cell_h, [self.cell_color[0], self.cell_color[1], self.cell_color[2], self.network_opacity]));
- }
- }
- }
-
- let grid_line_color = [0.18, 0.18, 0.22, 0.40];
-
- for k in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (k as f32) * step_y;
- let y2 = y1 + self.grid_size_y;
- if y1 < self.y + self.h {
- if y1 >= self.y {
- quads.push((self.x, y1, self.w, 1.0, grid_line_color));
- }
- if y2 >= self.y && y2 < self.y + self.h {
- quads.push((self.x, y2, self.w, 1.0, grid_line_color));
- }
- }
- }
-
- for k in cx_start..=cx_end {
- let x1 = self.grid_origin_x + (k as f32) * step_x;
- let x2 = x1 + self.grid_size_x;
- if x1 < self.x + self.w {
- if x1 >= self.x {
- quads.push((x1, self.y, 1.0, self.h, grid_line_color));
- }
- if x2 >= self.x && x2 < self.x + self.w {
- quads.push((x2, self.y, 1.0, self.h, grid_line_color));
- }
+ let x1 = (self.grid_origin_x + (c as f32) * step_x).round();
+ 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],
+ &mut quads,
+ );
}
}
}
@@ -446,6 +412,7 @@ impl Element for Graph {
for i in 0..self.nodes.len() {
if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
+ let scale_f = nw / 80.0;
let bg_color = if self.dragging_idx == Some(i) {
colors::node_drag_color()
} else if self.selected_idx == Some(i) {
@@ -464,7 +431,7 @@ impl Element for Graph {
push_clipped(tx, ty, tw, th, btn_color, &mut quads);
if self.nodes[i].geom_visible {
- let inset = 3.0;
+ let inset = 3.0 * scale_f;
push_clipped(tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, colors::TOGGLE_ON, &mut quads);
}
}
@@ -474,9 +441,7 @@ impl Element for Graph {
quads
}
- fn grid_origin(&self) -> (f32, f32) {
- (self.grid_origin_x, self.grid_origin_y)
- }
+
fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if self.hit_test(px, py, ctx) {
@@ -497,3 +462,58 @@ impl Element for Graph {
}
}
}
+
+impl Drop for Graph {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
+impl GraphController for Graph {
+ fn set_nodes(&mut self, nodes: &[GraphNode]) {
+ self.nodes = nodes.to_vec();
+
+ // Sync selected_idx from selected_id
+ if let Some(ref id) = self.selected_id {
+ self.selected_idx = self.nodes.iter().position(|n| n.id == *id);
+ if self.selected_idx.is_none() {
+ self.selected_id = None;
+ }
+ } else {
+ self.selected_idx = None;
+ }
+
+ // Sync dragging_idx from dragging_id
+ if let Some(ref id) = self.dragging_id {
+ self.dragging_idx = self.nodes.iter().position(|n| n.id == *id);
+ if self.dragging_idx.is_none() {
+ self.dragging_id = None;
+ self.drag_node_pos = None;
+ }
+ } else {
+ self.dragging_idx = None;
+ self.drag_node_pos = None;
+ }
+
+ self.double_clicked_idx = None;
+ self.double_click_timer = None;
+ self.toggle_hovered_idx = None;
+ }
+ fn get_nodes(&self) -> Vec<GraphNode> { self.nodes.clone() }
+ fn selected_node(&self) -> Option<usize> { self.selected_idx }
+ fn set_selected_node(&mut self, idx: Option<usize>) {
+ self.selected_idx = idx;
+ self.selected_id = idx.and_then(|i| self.nodes.get(i).map(|n| n.id.clone()));
+ }
+ fn double_clicked_node(&self) -> Option<usize> { self.double_clicked_idx }
+ fn clear_double_clicked_node(&mut self) { self.double_clicked_idx = None; }
+ fn set_grid_snap_enabled(&mut self, enabled: bool) { self.grid_snap_enabled = enabled; }
+ fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { self.node_geom_toggled.take() }
+ fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+ fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+ fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+ 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; }
+}
+
diff --git a/src/widget/display/node.rs b/src/widget/display/node.rs
index 57a545c..4d541be 100644
--- a/src/widget/display/node.rs
+++ b/src/widget/display/node.rs
@@ -45,6 +45,10 @@ impl Node {
pub(crate) fn toggle_rect(&self) -> (f32, f32, f32, f32) {
(self.x + self.w - 30.0, self.y + (self.h - 18.0) / 2.0, 18.0, 18.0)
}
+
+ pub fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_snap_x = gx; self.grid_snap_y = gy; }
+ pub fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+ pub fn set_node_name(&mut self, name: &str) { self.name = name.to_string(); }
}
impl Element for Node {
@@ -64,16 +68,16 @@ impl Element for Node {
}
fn unfocus(&mut self) { self.selected = false; }
- fn node_params(&self) -> Vec<(String, String, String)> { self.parameters.clone() }
- fn set_display_params(&mut self, params: &[(String, String, String)]) {
- self.parameters = params.to_vec();
- }
+ fn as_param_controller(&self) -> Option<&dyn ParamController> { Some(self) }
+ fn as_param_controller_mut(&mut self) -> Option<&mut dyn ParamController> { Some(self) }
+ fn as_geom_controller(&self) -> Option<&dyn GeomController> { Some(self) }
+ fn as_geom_controller_mut(&mut self) -> Option<&mut dyn GeomController> { Some(self) }
fn text_labels(&self) -> Vec<TextLabel> {
vec![TextLabel {
text: self.name.clone(),
- x: self.x + 8.0,
- y: self.y + (self.h - 12.0) / 2.0,
+ x: self.x + self.w + 8.0,
+ y: self.y + (self.h - 14.0) / 2.0,
font_size: 14.0,
color: [0xcc, 0xcc, 0xd4],
}]
@@ -119,10 +123,6 @@ impl Element for Node {
fn is_dragging(&self) -> bool { self.dragging }
fn draggable(&self) -> bool { !self.toggle_hovered }
- fn set_grid_snap(&mut self, gx: f32, gy: f32) { self.grid_snap_x = gx; self.grid_snap_y = gy; }
- fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
- fn set_node_name(&mut self, name: &str) { self.name = name.to_string(); }
-
fn drag_update(&mut self, px: f32, py: f32) -> bool {
let nx = px - self.drag_ox;
let ny = py - self.drag_oy;
@@ -177,6 +177,16 @@ impl Element for Node {
quads
}
+}
+
+impl ParamController for Node {
+ fn node_params(&self) -> Vec<(String, String, String)> { self.parameters.clone() }
+ fn set_display_params(&mut self, params: &[(String, String, String)]) {
+ self.parameters = params.to_vec();
+ }
+}
+
+impl GeomController for Node {
fn set_geom_visible(&mut self, visible: bool) { self.geom_visible = visible; }
fn geom_visible(&self) -> bool { self.geom_visible }
fn take_geom_toggle(&mut self) -> bool { std::mem::take(&mut self.geom_toggled) }
diff --git a/src/widget/display/serialize.rs b/src/widget/display/serialize.rs
index 131d5d4..b9cc436 100644
--- a/src/widget/display/serialize.rs
+++ b/src/widget/display/serialize.rs
@@ -19,20 +19,27 @@ fn serialize_single_widget(w: &dyn Element, json: &mut String) {
// Handle children
let dummy = crate::context::UiContext::new();
let children = w.children(&dummy);
- let menu_items = w.menu_items();
+ let mut menu_items = Vec::new();
+ let mut is_menu_open = false;
+ let mut is_vertical = false;
+ let mut checked_states = Vec::new();
+ if let Some(mc) = w.as_menu_controller() {
+ menu_items = mc.menu_items();
+ is_menu_open = mc.is_menu_open();
+ is_vertical = mc.is_vertical();
+ checked_states = mc.menu_item_checked();
+ }
- if type_name == "Menu" && w.is_menu_open() && !menu_items.is_empty() {
+ if type_name == "Menu" && is_menu_open && !menu_items.is_empty() {
json.push_str(",\"children\":[");
let mut max_len = 0;
for item in &menu_items {
max_len = max_len.max(item.len());
}
let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
- let vertical = w.is_vertical();
- let dx = if vertical { x + width } else { x };
- let dy = if vertical { y } else { y + height };
+ let dx = if is_vertical { x + width } else { x };
+ let dy = if is_vertical { y } else { y + height };
- let checked_states = w.menu_item_checked();
for (i, item) in menu_items.iter().enumerate() {
if i > 0 {
json.push(',');
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index b1acc6e..9132191 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -9,13 +9,27 @@ pub enum ButtonKind {
CopyIcon,
}
-#[derive(Debug, Clone)]
+#[derive(Clone)]
pub struct Button {
base: Widget,
pressed: bool,
just_clicked: bool,
kind: ButtonKind,
pub selected: bool,
+ pub on_click_cb: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
+}
+
+impl std::fmt::Debug for Button {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Button")
+ .field("base", &self.base)
+ .field("pressed", &self.pressed)
+ .field("just_clicked", &self.just_clicked)
+ .field("kind", &self.kind)
+ .field("selected", &self.selected)
+ .field("on_click_cb", &self.on_click_cb.as_ref().map(|_| "<callback>"))
+ .finish()
+ }
}
impl Button {
@@ -26,6 +40,7 @@ impl Button {
just_clicked: false,
kind: ButtonKind::Primary,
selected: false,
+ on_click_cb: None,
}
}
@@ -36,6 +51,7 @@ impl Button {
just_clicked: false,
kind: ButtonKind::Reset,
selected: false,
+ on_click_cb: None,
}
}
@@ -46,6 +62,7 @@ impl Button {
just_clicked: false,
kind: ButtonKind::ListRow,
selected: false,
+ on_click_cb: None,
}
}
@@ -56,6 +73,7 @@ impl Button {
just_clicked: false,
kind: ButtonKind::CopyIcon,
selected: false,
+ on_click_cb: None,
}
}
@@ -68,6 +86,11 @@ impl Button {
self.selected = selected;
self
}
+
+ pub fn on_click<F: Fn() + Send + Sync + 'static>(mut self, cb: F) -> Self {
+ self.on_click_cb = Some(std::sync::Arc::new(cb));
+ self
+ }
}
impl Element for Button {
@@ -123,6 +146,9 @@ impl Element for Button {
ElementState::Released => {
if self.pressed && self.hit_test(px, py, ctx) {
self.just_clicked = true;
+ if let Some(ref cb) = self.on_click_cb {
+ cb();
+ }
}
let was = self.pressed;
self.pressed = false;
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 6e9af66..8eccb87 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -1,5 +1,6 @@
use crate::colors;
use crate::widget::*;
+use crate::widget::input::get_font_db;
#[derive(Debug, Clone)]
pub struct ButtonStrip {
@@ -10,6 +11,8 @@ pub struct ButtonStrip {
pub just_clicked: Option<usize>,
pub hovered_idx: Option<usize>,
pub pressed_idx: Option<usize>,
+ pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
+ pub tab_quads_cache: std::collections::HashMap<String, Vec<(f32, f32, f32, f32, [f32; 4])>>,
}
impl ButtonStrip {
@@ -22,21 +25,26 @@ impl ButtonStrip {
just_clicked: None,
hovered_idx: None,
pressed_idx: None,
+ tab_text_quads: Vec::new(),
+ tab_quads_cache: std::collections::HashMap::new(),
}
}
pub fn with_buttons(mut self, buttons: Vec<String>) -> Self {
self.buttons = buttons;
+ self.generate_rotated_labels();
self
}
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
self.selected = selected;
+ self.generate_rotated_labels();
self
}
pub fn with_vertical(mut self, vertical: bool) -> Self {
self.vertical = vertical;
+ self.generate_rotated_labels();
self
}
@@ -45,7 +53,10 @@ impl ButtonStrip {
}
pub fn set_selected(&mut self, selected: Option<usize>) {
- self.selected = selected;
+ if self.selected != selected {
+ self.selected = selected;
+ self.generate_rotated_labels();
+ }
}
pub fn take_click(&mut self) -> Option<usize> {
@@ -54,6 +65,109 @@ impl ButtonStrip {
pub fn add_button(&mut self, label: &str) {
self.buttons.push(label.to_string());
+ self.generate_rotated_labels();
+ }
+
+ pub fn generate_rotated_labels(&mut self) {
+ self.tab_text_quads.clear();
+ if !self.vertical || self.buttons.is_empty() {
+ return;
+ }
+
+ let active_color = colors::paginator_tab_label_color();
+ let active_srgb = colors::to_srgb(active_color);
+ let active_r = (active_srgb[0] * 255.0) as u8;
+ let active_g = (active_srgb[1] * 255.0) as u8;
+ let active_b = (active_srgb[2] * 255.0) as u8;
+ let inactive_r = (active_r as f32 * 0.78) as u8;
+ let inactive_g = (active_g as f32 * 0.78) as u8;
+ let inactive_b = (active_b as f32 * 0.78) as u8;
+
+ let (font_fam, font_size) = crate::layout::menubar_font_parsed();
+ let scale = crate::scale::scale_factor().max(1.0);
+
+ for (i, page_name) in self.buttons.iter().enumerate() {
+ let color = if self.selected == Some(i) {
+ [active_r, active_g, active_b]
+ } else {
+ [inactive_r, inactive_g, inactive_b]
+ };
+ let hex_color = format!("#{:02X}{:02X}{:02X}", color[0], color[1], color[2]);
+
+ let trimmed = page_name.trim();
+ let has_icon = trimmed.find(' ').is_some();
+ let label_text = if let Some(space_idx) = trimmed.find(' ') {
+ trimmed.split_at(space_idx).1.trim()
+ } else {
+ trimmed
+ };
+
+ let r = self.item_rect(i);
+ let padding_y = crate::layout::paginator_tab_padding_y();
+ let y_offset = if has_icon { 2.0 * padding_y + 12.0 } else { 0.0 };
+ let usable_h = (r.3 - y_offset).max(1.0);
+
+ let w_px = (r.2 * scale) as u32;
+ let h_px = (usable_h * scale) as u32;
+
+ if w_px == 0 || h_px == 0 {
+ self.tab_text_quads.push(Vec::new());
+ continue;
+ }
+
+ let cache_key = format!("{}:{}:{}:{:?}:{}:{}", trimmed, w_px, h_px, color, font_fam, scale);
+ if let Some(cached_quads) = self.tab_quads_cache.get(&cache_key) {
+ self.tab_text_quads.push(cached_quads.clone());
+ continue;
+ }
+
+ let svg_data = format!(
+ r##"<svg width="{}" height="{}" viewBox="0 0 {} {}" xmlns="http://www.w3.org/2000/svg">
+ <text x="{}" y="{}" font-family="{}" font-size="{}" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
+</svg>"##,
+ w_px, h_px,
+ r.2, usable_h,
+ r.2 / 2.0, usable_h / 2.0,
+ font_fam,
+ font_size,
+ hex_color,
+ r.2 / 2.0, usable_h / 2.0,
+ label_text
+ );
+
+ let opt = resvg::usvg::Options::default();
+ let fontdb = get_font_db();
+
+ let mut page_quads = Vec::new();
+ if let Ok(tree) = resvg::usvg::Tree::from_data(svg_data.as_bytes(), &opt, fontdb) {
+ if let Some(mut pixmap) = resvg::tiny_skia::Pixmap::new(w_px, h_px) {
+ resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
+ let pixels = pixmap.data();
+ for row in 0..h_px {
+ for col in 0..w_px {
+ let idx = ((row * w_px + col) * 4) as usize;
+ if idx + 3 < pixels.len() {
+ let a = pixels[idx + 3] as f32 / 255.0;
+ if a > 0.0 {
+ let r = ((pixels[idx] as f32 / 255.0) / a).min(1.0);
+ let g = ((pixels[idx + 1] as f32 / 255.0) / a).min(1.0);
+ let b = ((pixels[idx + 2] as f32 / 255.0) / a).min(1.0);
+ page_quads.push((
+ col as f32 / scale,
+ row as f32 / scale,
+ 1.2 / scale,
+ 1.2 / scale,
+ [r, g, b, a],
+ ));
+ }
+ }
+ }
+ }
+ }
+ }
+ self.tab_quads_cache.insert(cache_key, page_quads.clone());
+ self.tab_text_quads.push(page_quads);
+ }
}
pub fn item_rect(&self, idx: usize) -> (f32, f32, f32, f32) {
@@ -63,8 +177,14 @@ impl ButtonStrip {
let n = self.buttons.len() as f32;
let (x, y, w, h) = self.rect();
if self.vertical {
- let btn_h = h / n;
- (x, y + idx as f32 * btn_h, w, btn_h)
+ let spacing = 8.0;
+ let max_btn_h = if n > 1.0 {
+ (h - spacing * (n - 1.0)) / n
+ } else {
+ h
+ };
+ let btn_h = max_btn_h.min(144.0).max(0.0);
+ (x, y + idx as f32 * (btn_h + spacing), w, btn_h)
} else {
let btn_w = w / n;
(x + idx as f32 * btn_w, y, btn_w, h)
@@ -75,6 +195,16 @@ impl ButtonStrip {
impl Element for ButtonStrip {
crate::impl_widget_base!(ButtonStrip);
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ if self.base.x != x || self.base.y != y || self.base.w != w || self.base.h != h {
+ self.base.x = x;
+ self.base.y = y;
+ self.base.w = w;
+ self.base.h = h;
+ self.generate_rotated_labels();
+ }
+ }
+
fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
None
}
@@ -106,6 +236,12 @@ impl Element for ButtonStrip {
if self.selected != Some(pressed) {
self.selected = Some(pressed);
self.just_clicked = Some(pressed);
+ self.generate_rotated_labels();
+ changed = true;
+ } else {
+ self.selected = None;
+ self.just_clicked = Some(pressed);
+ self.generate_rotated_labels();
changed = true;
}
}
@@ -144,6 +280,30 @@ impl Element for ButtonStrip {
if bg_color != [0.0, 0.0, 0.0, 0.0] {
quads.push((r.0, r.1, r.2, r.3, bg_color));
}
+
+ if self.vertical {
+ if i < self.tab_text_quads.len() {
+ let min_y = self.base.y;
+ let max_y = self.base.y + self.base.h;
+ let page_name = &self.buttons[i];
+ let trimmed = page_name.trim();
+ let has_icon = trimmed.find(' ').is_some();
+ let padding_y = crate::layout::paginator_tab_padding_y();
+ let y_offset = if has_icon { 2.0 * padding_y + 12.0 } else { 0.0 };
+
+ for &(qx, qy, qw, qh, qc) in &self.tab_text_quads[i] {
+ let absolute_x = r.0 + qx;
+ let absolute_y = r.1 + y_offset + qy;
+
+ let ry1 = absolute_y.max(min_y);
+ let ry2 = (absolute_y + qh).min(max_y);
+ let rh = ry2 - ry1;
+ if rh > 0.0 {
+ quads.push((absolute_x, ry1, qw, rh, qc));
+ }
+ }
+ }
+ }
}
quads
}
@@ -159,20 +319,23 @@ impl Element for ButtonStrip {
[0xa8, 0xa8, 0xb3]
};
if self.vertical {
- let line_height = font_size * 1.2;
- let label_len = btn_label.chars().count() as f32;
- let total_h = label_len * line_height;
- let start_y = r.1 + (r.3 - total_h) / 2.0;
- let char_w = TextLabel::estimate_width("o", font_size);
- let x_pos = r.0 + (r.2 - char_w) / 2.0;
- for (char_idx, c) in btn_label.chars().enumerate() {
- labels.push(TextLabel {
- text: c.to_string(),
- x: x_pos,
- y: start_y + char_idx as f32 * line_height,
- font_size,
- color,
- });
+ let trimmed = btn_label.trim();
+ if let Some(space_idx) = trimmed.find(' ') {
+ let (icon, _) = trimmed.split_at(space_idx);
+ let icon = icon.trim();
+ if !icon.is_empty() {
+ let icon_font_size = 14.0;
+ let est_icon_w = TextLabel::estimate_width(icon, icon_font_size);
+ let padding_y = crate::layout::paginator_tab_padding_y();
+ let icon_y = r.1 + (padding_y - 2.0).max(0.0);
+ labels.push(TextLabel {
+ text: icon.to_string(),
+ x: r.0 + (r.2 - est_icon_w) / 2.0,
+ y: icon_y,
+ font_size: icon_font_size,
+ color,
+ });
+ }
}
} else {
let est_w = TextLabel::estimate_width(btn_label, font_size);
@@ -216,6 +379,7 @@ impl Element for ButtonStrip {
if Some(next) != self.selected {
self.selected = Some(next);
self.just_clicked = Some(next);
+ self.generate_rotated_labels();
return true;
}
false
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index ac9dfa5..44f8e25 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -5,6 +5,7 @@ pub struct Checkbox {
base: Widget,
checked: bool,
just_clicked: bool,
+ pub just_changed: bool,
}
impl Checkbox {
@@ -13,6 +14,7 @@ impl Checkbox {
base: Widget::new(),
checked: false,
just_clicked: false,
+ just_changed: false,
}
}
@@ -33,6 +35,33 @@ impl Checkbox {
impl Element for Checkbox {
crate::impl_widget_base!(Checkbox);
+ fn get_value_string(&self) -> Option<String> {
+ Some(self.checked.to_string())
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ let val_trimmed = val.trim().to_lowercase();
+ let new_checked = if val_trimmed == "true" || val_trimmed == "1" || val_trimmed == "yes" || val_trimmed == "on" {
+ true
+ } else if val_trimmed == "false" || val_trimmed == "0" || val_trimmed == "no" || val_trimmed == "off" {
+ false
+ } else {
+ return false;
+ };
+ if self.checked != new_checked {
+ self.checked = new_checked;
+ self.just_changed = true;
+ return true;
+ }
+ false
+ }
+
+ fn take_change(&mut self) -> bool {
+ let ret = self.just_changed;
+ self.just_changed = false;
+ ret
+ }
+
fn color(&self) -> [f32; 4] {
let (_, _, w, _) = self.rect();
if w > 30.0 {
@@ -51,6 +80,12 @@ impl Element for Checkbox {
}
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.hit_test(px, py, ctx) {
+ ctx.handle_right_click(self.as_ptr(), px, py);
+ return true;
+ }
+ }
if button != MouseButton::Left { return false; }
match state {
ElementState::Released => {
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 7e68a84..5328a50 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -15,6 +15,7 @@ pub struct ColorSelector {
pub children: Vec<*mut (dyn Element + 'static)>,
child: std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>,
pub editor_state: TextEditorState,
+ pub just_changed: bool,
}
impl Clone for ColorSelector {
@@ -32,6 +33,7 @@ impl Clone for ColorSelector {
children: self.children.clone(),
child: std::sync::Arc::new(std::sync::Mutex::new(None)),
editor_state: self.editor_state.clone(),
+ just_changed: self.just_changed,
}
}
}
@@ -51,6 +53,7 @@ impl ColorSelector {
children: Vec::new(),
child: std::sync::Arc::new(std::sync::Mutex::new(None)),
editor_state: TextEditorState::new(String::new()),
+ just_changed: false,
}
}
@@ -73,6 +76,31 @@ impl ColorSelector {
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]))
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ if let Some(c) = parse_hex(val) {
+ if self.color != c {
+ self.color = c;
+ self.just_changed = true;
+ if self.editing {
+ self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+ self.cursor_idx = self.edit_buffer.chars().count();
+ }
+ return true;
+ }
+ }
+ false
+ }
+
+ fn take_change(&mut self) -> bool {
+ let ret = self.just_changed;
+ self.just_changed = false;
+ ret
+ }
+
fn preferred_height(&self) -> Option<f32> {
Some(crate::layout::color_selector_height())
}
@@ -95,6 +123,12 @@ impl Element for ColorSelector {
}
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.hit_test(px, py, ctx) {
+ ctx.handle_right_click(self.as_ptr(), px, py);
+ return true;
+ }
+ }
if button != MouseButton::Left { return false; }
if state != ElementState::Pressed { return false; }
if !self.hit_test(px, py, ctx) { return false; }
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 007bfc6..f3fa55d 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -122,6 +122,39 @@ impl Default for Dropdown {
impl Element for Dropdown {
crate::impl_widget_base!(Dropdown);
+ fn get_value_string(&self) -> Option<String> {
+ self.options.get(self.selected).cloned()
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ let val_trimmed = val.trim();
+ for (idx, opt) in self.options.iter().enumerate() {
+ if opt.eq_ignore_ascii_case(val_trimmed) {
+ if self.selected != idx {
+ self.selected = idx;
+ self.just_changed = true;
+ return true;
+ }
+ return false;
+ }
+ }
+ if let Ok(idx) = val_trimmed.parse::<usize>() {
+ if idx < self.options.len() {
+ if self.selected != idx {
+ self.selected = idx;
+ self.just_changed = true;
+ return true;
+ }
+ return false;
+ }
+ }
+ false
+ }
+
+ fn take_change(&mut self) -> bool {
+ self.take_change()
+ }
+
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
@@ -175,6 +208,12 @@ impl Element for Dropdown {
}
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.hit_test(px, py, ctx) {
+ ctx.handle_right_click(self.as_ptr(), px, py);
+ return true;
+ }
+ }
if button != MouseButton::Left || state != ElementState::Pressed { return false; }
let (x, y, w, h) = self.rect();
@@ -333,6 +372,13 @@ impl Element for Dropdown {
}
}
+impl Drop for Dropdown {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
+
unsafe impl Send for Dropdown {}
unsafe impl Sync for Dropdown {}
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 881f586..5b88ae6 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -14,6 +14,7 @@ pub struct Slider {
min: f32,
max: f32,
pub editor_state: TextEditorState,
+ pub just_changed: bool,
}
impl Slider {
@@ -30,6 +31,7 @@ impl Slider {
min: 0.0,
max: 1.0,
editor_state: TextEditorState::new(String::new()),
+ just_changed: false,
}
}
@@ -105,6 +107,38 @@ impl Slider {
impl Element for Slider {
crate::impl_widget_base!(Slider);
+ fn get_value_string(&self) -> Option<String> {
+ let scaled_val = self.min + self.value * (self.max - self.min);
+ Some(format!("{:.2}", scaled_val))
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ if let Ok(new_val) = val.trim().parse::<f32>() {
+ let old_val = self.value;
+ let range = self.max - self.min;
+ if range != 0.0 {
+ self.value = ((new_val - self.min) / range).clamp(0.0, 1.0);
+ } else {
+ self.value = 0.0;
+ }
+ if (self.value - old_val).abs() > 0.0001 {
+ self.just_changed = true;
+ if self.editing {
+ let scaled_val = self.min + self.value * (self.max - self.min);
+ self.edit_buffer = format!("{:.2}", scaled_val);
+ }
+ return true;
+ }
+ }
+ false
+ }
+
+ fn take_change(&mut self) -> bool {
+ let ret = self.just_changed;
+ self.just_changed = false;
+ ret
+ }
+
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
@@ -198,6 +232,12 @@ impl Element for Slider {
}
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.hit_test(px, py, ctx) {
+ ctx.handle_right_click(self.as_ptr(), px, py);
+ return true;
+ }
+ }
if button != MouseButton::Left { return false; }
let top = self.base.label_offset();
@@ -390,6 +430,13 @@ impl Element for Slider {
fn value(&self) -> i32 { (self.value * 100.0) as i32 }
}
+impl Drop for Slider {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActiveThumb {
Low,
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 375ccf1..46ac008 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -16,6 +16,7 @@ pub struct Spinbox {
pub parent: Option<*mut (dyn Element + 'static)>,
pub children: Vec<*mut (dyn Element + 'static)>,
pub editor_state: TextEditorState,
+ pub just_changed: bool,
}
impl Spinbox {
@@ -36,6 +37,7 @@ impl Spinbox {
parent: None,
children: Vec::new(),
editor_state: TextEditorState::new(String::new()),
+ just_changed: false,
}
}
@@ -66,6 +68,46 @@ impl Spinbox {
impl Element for Spinbox {
crate::impl_widget_base!(Spinbox);
+ fn get_value_string(&self) -> Option<String> {
+ if self.decimals > 0 {
+ let divisor = 10.0f32.powi(self.decimals as i32);
+ Some(format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize))
+ } else {
+ Some(self.value.to_string())
+ }
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ let val = val.trim();
+ let old_val = self.value;
+ if self.decimals > 0 {
+ if let Ok(val_f) = val.parse::<f32>() {
+ let divisor = 10.0f32.powi(self.decimals as i32);
+ self.value = (val_f * divisor).round() as i32;
+ self.value = self.value.clamp(self.min, self.max);
+ }
+ } else {
+ if let Ok(val_i) = val.parse::<i32>() {
+ self.value = val_i.clamp(self.min, self.max);
+ }
+ }
+ if self.value != old_val {
+ self.just_changed = true;
+ if self.editing {
+ self.edit_buffer = self.get_value_string().unwrap_or_default();
+ self.cursor_idx = self.edit_buffer.chars().count();
+ }
+ return true;
+ }
+ false
+ }
+
+ fn take_change(&mut self) -> bool {
+ let ret = self.just_changed;
+ self.just_changed = false;
+ ret
+ }
+
fn preferred_height(&self) -> Option<f32> {
Some(crate::layout::spinbox_height())
}
@@ -93,6 +135,12 @@ impl Element for Spinbox {
}
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.hit_test(px, py, ctx) {
+ ctx.handle_right_click(self.as_ptr(), px, py);
+ return true;
+ }
+ }
if button != MouseButton::Left { return false; }
if !self.hit_test(px, py, ctx) { return false; }
match state {
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 5bb2136..fdd58c3 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -323,6 +323,51 @@ impl Default for TextBox {
impl Element for TextBox {
crate::impl_widget_base!(TextBox);
+ fn get_value_string(&self) -> Option<String> {
+ Some(self.text.clone())
+ }
+
+ fn set_value_string(&mut self, val: &str) -> bool {
+ let val_str = val.to_string();
+ if self.text != val_str {
+ self.text = val_str.clone();
+ self.edit_buffer = val_str;
+ self.just_changed = true;
+ self.sync_editor_state();
+ true
+ } else {
+ false
+ }
+ }
+
+ fn take_change(&mut self) -> bool {
+ self.take_change()
+ }
+
+ fn cut_selection(&mut self) -> bool {
+ let res = self.cut_selection();
+ if res {
+ self.just_changed = true;
+ }
+ res
+ }
+
+ fn copy_selection(&self) {
+ self.copy_selection();
+ }
+
+ fn paste_from_clipboard(&mut self) -> bool {
+ let res = self.paste_from_clipboard();
+ if res {
+ self.just_changed = true;
+ }
+ res
+ }
+
+ fn select_all(&mut self) {
+ self.select_all();
+ }
+
fn preferred_height(&self) -> Option<f32> {
Some(crate::layout::textbox_height())
}
@@ -446,6 +491,12 @@ impl Element for TextBox {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if self.disabled { return false; }
+ if button == MouseButton::Right && state == ElementState::Pressed {
+ if self.hit_test(px, py, ctx) {
+ ctx.handle_right_click(self.as_ptr(), px, py);
+ return true;
+ }
+ }
if button != MouseButton::Left { return false; }
if !self.hit_test(px, py, ctx) { return false; }
match state {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index edaa801..5133165 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -179,7 +179,35 @@ pub trait Element {
}
}
- fn label(&self) -> Option<String> { None }
+ fn label(&self) -> Option<String> {
+ self.base().and_then(|b| b.label.clone())
+ }
+
+ fn get_value_string(&self) -> Option<String> { None }
+ fn set_value_string(&mut self, _val: &str) -> bool { false }
+ fn take_change(&mut self) -> bool { false }
+
+ fn cut_selection(&mut self) -> bool {
+ if let Some(val) = self.get_value_string() {
+ clipboard::copy_to_clipboard(&val);
+ self.set_value_string("")
+ } else {
+ false
+ }
+ }
+ fn copy_selection(&self) {
+ if let Some(val) = self.get_value_string() {
+ clipboard::copy_to_clipboard(&val);
+ }
+ }
+ fn paste_from_clipboard(&mut self) -> bool {
+ if let Some(text) = clipboard::read_from_clipboard() {
+ self.set_value_string(&text)
+ } else {
+ false
+ }
+ }
+ fn select_all(&mut self) {}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
if let Some(b) = self.base_mut() {
@@ -278,6 +306,7 @@ pub trait Element {
}
fn color(&self) -> [f32; 4];
+ fn solid_border(&self) -> Option<([f32; 4], f32)> { None }
fn is_dragging(&self) -> bool { false }
fn drag_update(&mut self, _px: f32, _py: f32) -> bool { false }
@@ -365,48 +394,27 @@ pub trait Element {
fn set_selected(&mut self, _selected: bool) {}
fn keyboard_input(&mut self, _event: &KeyEvent, _ctx: &mut UiContext) -> bool { false }
- fn menu_click(&mut self) -> Option<(usize, usize)> { None }
- fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> { None }
- fn trigger_menu_click(&mut self, _menu_idx: usize, _item_idx: usize) {}
- fn set_item_checked(&mut self, _menu_idx: usize, _item_idx: usize, _checked: bool) {}
- fn set_menu_items(&mut self, _menu_idx: usize, _items: &[String]) {}
- fn is_menu_bar(&self) -> bool { false }
- fn is_menu_open(&self) -> bool { false }
- fn set_grid_snap(&mut self, _gx: f32, _gy: f32) {}
- fn set_node_name(&mut self, _name: &str) {}
fn set_visible(&mut self, _visible: bool) {}
fn visible(&self) -> bool { true }
- fn set_path(&mut self, _segments: &[String]) {}
- fn path_click(&mut self) -> Option<usize> { None }
-
- fn node_params(&self) -> Vec<(String, String, String)> { vec![] }
- fn set_display_params(&mut self, _params: &[(String, String, String)]) {}
-
- fn set_config_toggle(&mut self, _id: usize, _val: bool) {}
- fn take_config_toggle(&mut self) -> Option<(usize, bool)> { None }
- fn set_show_network_grid(&mut self, _show: bool) {}
- fn set_config_spin(&mut self, _id: usize, _val: f32) {}
- fn take_config_spin(&mut self) -> Option<(usize, f32)> { None }
- fn set_grid_sizes(&mut self, _gx: f32, _gy: f32) {}
- fn set_grid_origin(&mut self, _ox: f32, _oy: f32) {}
- fn grid_origin(&self) -> (f32, f32) { (0.0, 0.0) }
- fn set_skipped_sizes(&mut self, _row_h: f32, _col_w: f32) {}
- fn set_palette_state(&mut self, _visible: bool, _query: &str, _items: &[String], _selected: usize) {}
-
- fn set_geom_visible(&mut self, _visible: bool) {}
- fn geom_visible(&self) -> bool { true }
- fn take_geom_toggle(&mut self) -> bool { false }
- fn set_spreadsheet_data(&mut self, _headers: Vec<String>, _rows: Vec<Vec<String>>) {}
fn tick(&mut self, _dt: f32, _ctx: &mut UiContext) -> bool { false }
+ fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
- fn set_nodes(&mut self, _nodes: &[GraphNode]) {}
- fn get_nodes(&self) -> Vec<GraphNode> { vec![] }
- fn selected_node(&self) -> Option<usize> { None }
- fn set_selected_node(&mut self, _idx: Option<usize>) {}
- fn double_clicked_node(&self) -> Option<usize> { None }
- fn clear_double_clicked_node(&mut self) {}
- fn set_grid_snap_enabled(&mut self, _enabled: bool) {}
- fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)> { None }
+ fn as_page_selector(&self) -> Option<&dyn PageSelector> { None }
+ fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { None }
+ fn as_menu_controller(&self) -> Option<&dyn MenuController> { None }
+ fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { None }
+ fn as_graph_controller(&self) -> Option<&dyn GraphController> { None }
+ fn as_graph_controller_mut(&mut self) -> Option<&mut dyn GraphController> { None }
+ fn as_spreadsheet_controller(&self) -> Option<&dyn SpreadsheetController> { None }
+ fn as_spreadsheet_controller_mut(&mut self) -> Option<&mut dyn SpreadsheetController> { None }
+ fn as_path_controller(&self) -> Option<&dyn PathController> { None }
+ fn as_path_controller_mut(&mut self) -> Option<&mut dyn PathController> { None }
+ fn as_param_controller(&self) -> Option<&dyn ParamController> { None }
+ fn as_param_controller_mut(&mut self) -> Option<&mut dyn ParamController> { None }
+ fn as_geom_controller(&self) -> Option<&dyn GeomController> { None }
+ fn as_geom_controller_mut(&mut self) -> Option<&mut dyn GeomController> { None }
+ fn as_scroll_controller(&self) -> Option<&dyn ScrollController> { None }
+ fn as_scroll_controller_mut(&mut self) -> Option<&mut dyn ScrollController> { None }
fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
let base = self.base()?;
@@ -465,36 +473,10 @@ pub trait Element {
}
fn z_index(&self) -> i32 { 0 }
- fn set_center_items(&mut self, _center: bool) {}
- fn menu_items(&self) -> Vec<String> { vec![] }
- fn menu_item_checked(&self) -> Vec<Option<bool>> { vec![] }
- fn is_vertical(&self) -> bool { false }
-
fn is_plate(&self) -> bool { false }
fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, false, false) }
fn layout_ignore(&self) -> bool { false }
-
- fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
- fn menu_names(&self) -> Vec<String> { vec![] }
- fn selected_page(&self) -> usize { 0 }
- fn set_selected_page(&mut self, _page: usize) {}
- fn is_page_hidden(&self) -> bool { false }
- fn set_page_hidden(&mut self, _hidden: bool) {}
- fn set_pages(&mut self, _pages: Vec<String>) {}
- fn set_pages_with_items(&mut self, _pages: Vec<String>, _items: Vec<Vec<String>>) {}
- fn sidebar_w(&self) -> f32 { 0.0 }
- fn set_sidebar_mode(&mut self, _enabled: bool) {}
- fn set_sidebar_label(&mut self, _label: Option<String>) {}
- fn add_widget_to_page(&mut self, _page_idx: usize, _widget: *mut (dyn Element + 'static), _ctx: &mut UiContext) {}
- fn clear_page_widgets(&mut self, _page_idx: usize, _ctx: &mut UiContext) {}
- fn menu_items_list(&self) -> Vec<Vec<String>> { vec![] }
- fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> { vec![] }
fn color_u8(&self) -> Option<[u8; 4]> { None }
- fn update_bounds(&mut self, _count: usize, _viewport_y: f32, _viewport_h: f32) {}
- fn get_item_draw_y(&self, _idx: usize, _offset: f32) -> Option<f32> { None }
-
- fn take_context_change(&mut self) -> Option<usize> { None }
- fn set_context_selected(&mut self, _selected: usize) {}
}
pub trait Control: Element {
@@ -542,7 +524,7 @@ pub use self::input::{
pub use self::container::{
Container, Header, ContentBg, ViewportBg, ParametersBg, ScrollingList,
ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
- Paginator
+ Paginator, Switcher, Layer, Page
};
pub use self::display::{
TextLabel, Label, SectionHeader, StyledLabel, TextItem, Svg, UsageBar,
@@ -551,6 +533,81 @@ pub use self::display::{
DotStatus, PreviewLayoutMode, Sidebar, Panel, serialize_widgets
};
+pub trait PageSelector {
+ fn selected_page(&self) -> usize;
+ fn set_selected_page(&mut self, page: usize);
+ fn is_page_hidden(&self) -> bool;
+ fn set_page_hidden(&mut self, hidden: bool);
+ fn set_pages(&mut self, pages: Vec<String>);
+ fn set_pages_with_items(&mut self, pages: Vec<String>, items: Vec<Vec<String>>);
+ fn sidebar_w(&self) -> f32;
+ fn set_sidebar_mode(&mut self, enabled: bool);
+ fn set_sidebar_label(&mut self, label: Option<String>);
+ fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Element + 'static), ctx: &mut UiContext);
+ fn clear_page_widgets(&mut self, page_idx: usize, ctx: &mut UiContext);
+}
+
+pub trait MenuController {
+ fn menu_click(&mut self) -> Option<(usize, usize)>;
+ fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize);
+ fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool);
+ fn set_menu_items(&mut self, menu_idx: usize, items: &[String]);
+ fn is_menu_bar(&self) -> bool;
+ fn is_menu_open(&self) -> bool;
+ fn menu_items(&self) -> Vec<String>;
+ fn menu_item_checked(&self) -> Vec<Option<bool>>;
+ fn is_vertical(&self) -> bool;
+ fn menu_names(&self) -> Vec<String>;
+ fn menu_items_list(&self) -> Vec<Vec<String>>;
+ fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>>;
+ fn take_context_change(&mut self) -> Option<usize>;
+ fn set_context_selected(&mut self, selected: usize);
+ fn set_center_items(&mut self, center: bool);
+ fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)>;
+}
+
+pub trait GraphController {
+ fn set_nodes(&mut self, nodes: &[GraphNode]);
+ fn get_nodes(&self) -> Vec<GraphNode>;
+ fn selected_node(&self) -> Option<usize>;
+ fn set_selected_node(&mut self, idx: Option<usize>);
+ fn double_clicked_node(&self) -> Option<usize>;
+ fn clear_double_clicked_node(&mut self);
+ fn set_grid_snap_enabled(&mut self, enabled: bool);
+ fn take_node_geom_toggle(&mut self) -> Option<(usize, bool)>;
+ fn set_grid_snap(&mut self, gx: f32, gy: f32);
+ fn set_grid_sizes(&mut self, gx: f32, gy: f32);
+ fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32);
+ 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);
+}
+
+pub trait SpreadsheetController {
+ fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>);
+}
+
+pub trait PathController {
+ fn set_path(&mut self, segments: &[String]);
+ fn path_click(&mut self) -> Option<usize>;
+}
+
+pub trait ParamController {
+ fn node_params(&self) -> Vec<(String, String, String)>;
+ fn set_display_params(&mut self, params: &[(String, String, String)]);
+}
+
+pub trait GeomController {
+ fn set_geom_visible(&mut self, visible: bool);
+ fn geom_visible(&self) -> bool;
+ fn take_geom_toggle(&mut self) -> bool;
+}
+
+pub trait ScrollController {
+ fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32);
+ fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32>;
+}
+
pub fn label_offset(w: &dyn Element) -> f32 {
let name = w.type_name();
if name == "Label" || name == "Button" || name == "Checkbox" || name == "Toggle" {