GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Refactor widgets to Element trait, split widget.rs, and implement FontSelector (modified: src/color.rs, src/engine.rs, src/layout.rs and 8 others)
src/color.rs | 20 +
src/engine.rs | 8 +-
src/layout.rs | 427 +-
src/main.rs | 10 +-
src/widget.rs | 10734 --------------------------------------------
src/widget/container.rs | 5076 +++++++++++++++++++++
src/widget/core.rs | 685 +++
src/widget/display.rs | 2357 ++++++++++
src/widget/input.rs | 3076 +++++++++++++
src/widget/json_layout.rs | 53 +-
src/widget/mod.rs | 370 ++
11 files changed, 12020 insertions(+), 10796 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index ed61384..cb541cf 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -458,3 +458,23 @@ pub fn set_toggle_off_color(color: [f32; 4]) {
}
}
+#[derive(Debug, Clone, Copy)]
+pub struct Theme {
+ pub surface_bg: [f32; 4],
+ pub surface_border: [f32; 4],
+ pub primary_accent: [f32; 4],
+ pub press_overlay: [f32; 4],
+ pub hover_overlay: [f32; 4],
+}
+
+pub fn active_theme() -> Theme {
+ Theme {
+ surface_bg: [0.10, 0.10, 0.14, 0.95],
+ surface_border: [0.25, 0.25, 0.35, 0.8],
+ primary_accent: [0.20, 0.50, 0.75, 1.0],
+ press_overlay: [1.0, 1.0, 1.0, 0.15],
+ hover_overlay: [1.0, 1.0, 1.0, 0.08],
+ }
+}
+
+
diff --git a/src/engine.rs b/src/engine.rs
index f487f75..f51d8e0 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -452,7 +452,7 @@ pub fn plate_bevel_vertices(
verts
}
-pub fn widget_vertices(w: &dyn crate::widget::Widget, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
+pub fn widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
let (x, y, ww, h) = w.rect();
let corners = w.rounded_corners();
let mut verts = if corners != (false, false, false, false) {
@@ -469,7 +469,7 @@ pub fn widget_vertices(w: &dyn crate::widget::Widget, sw: f32, sh: f32, clip_cir
}
pub fn extra_quad_vertices(
- w: &dyn crate::widget::Widget,
+ w: &dyn crate::widget::Element,
qx: f32, qy: f32, qw: f32, qh: f32,
sw: f32, sh: f32,
qc: [f32; 4],
@@ -492,7 +492,7 @@ pub fn extra_quad_vertices(
}
pub fn extra_quad_vertices_clipped(
- w: &dyn crate::widget::Widget,
+ w: &dyn crate::widget::Element,
qx: f32, qy: f32, qw: f32, qh: f32,
sw: f32, sh: f32,
qc: [f32; 4],
@@ -1631,7 +1631,7 @@ impl<A: Application> PopupHandler for EngineState<A> {
fn done(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _popup: &Popup) {
for popover_ptr in crate::widget::popovers::get_active() {
unsafe {
- let popover = &mut *(popover_ptr as *mut dyn crate::widget::Widget);
+ let popover = &mut *(popover_ptr as *mut dyn crate::widget::Element);
popover.unfocus();
}
}
diff --git a/src/layout.rs b/src/layout.rs
index c513710..32f9612 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1,4 +1,4 @@
-use crate::widget::Widget;
+use crate::widget::Element;
pub trait RenderTarget {
fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
@@ -47,7 +47,7 @@ impl RenderTarget for PopoverCollector {
}
}
-pub fn render_widget<T: Widget + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
+pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
w.set_rect(x, y, ww, wh);
for (qx, qy, qw, qh, qc) in w.all_quads() {
pc.rect(qc, qx, qy, qw, qh);
@@ -152,8 +152,8 @@ impl Column {
pc.text(text, x, y, font_size, color);
}
- pub fn widget<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
- let top_room = w.top_room();
+ pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ let top_room = crate::widget::label_offset(w);
let total_h = wh + top_room;
let x = self.ax(x_off);
let y = self.ay();
@@ -200,7 +200,7 @@ impl<'a> Row<'a> {
self.cursor_x += width + self.spacing;
}
- pub fn widget<T: Widget + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
+ pub fn widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
render_widget(self.pc, w, self.base_x + self.cursor_x, self.y, ww, wh);
self.cursor_x += ww + self.spacing;
}
@@ -254,18 +254,18 @@ impl Section {
pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
}
- pub fn widget<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
let x = self.ax(x_off);
let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
let clamped_w = ww.min((right_edge - x).max(0.0));
- let top_room = w.top_room();
+ let top_room = crate::widget::label_offset(w);
let total_h = wh + top_room;
render_widget(pc, w, x, self.ay(), clamped_w, total_h);
self.content_y += total_h;
}
- pub fn widget_full<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
+ pub fn widget_full<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
let x_off = 12.0;
let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off); // cw - 40.0
self.widget(pc, w, x_off, ww, wh);
@@ -349,8 +349,8 @@ impl Section {
self.content_y + 20.0
}
- pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> VStack<'a> {
- VStack {
+ pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SectionVStack<'a> {
+ SectionVStack {
section: self,
pc,
spacing,
@@ -358,14 +358,14 @@ impl Section {
}
}
-pub struct VStack<'a> {
+pub struct SectionVStack<'a> {
section: &'a mut Section,
pc: &'a mut dyn RenderTarget,
spacing: f32,
}
-impl<'a> VStack<'a> {
- pub fn add_widget<T: Widget + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
+impl<'a> SectionVStack<'a> {
+ pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
self.section.widget(self.pc, w, Section::DEFAULT_MARGIN_X, ww, wh);
self.section.spacing(self.spacing);
}
@@ -427,18 +427,18 @@ impl Subsection {
pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
}
- pub fn widget<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
let x = self.ax(x_off);
let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
let clamped_w = ww.min((right_edge - x).max(0.0));
- let top_room = w.top_room();
+ let top_room = crate::widget::label_offset(w);
let total_h = wh + top_room;
render_widget(pc, w, x, self.ay(), clamped_w, total_h);
self.content_y += total_h;
}
- pub fn widget_full<T: Widget + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
+ pub fn widget_full<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
let x_off = 12.0;
let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off);
self.widget(pc, w, x_off, ww, wh);
@@ -522,8 +522,8 @@ impl Subsection {
self.content_y + 20.0
}
- pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SubVStack<'a> {
- SubVStack {
+ pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SubsectionVStack<'a> {
+ SubsectionVStack {
subsection: self,
pc,
spacing,
@@ -531,14 +531,14 @@ impl Subsection {
}
}
-pub struct SubVStack<'a> {
+pub struct SubsectionVStack<'a> {
subsection: &'a mut Subsection,
pc: &'a mut dyn RenderTarget,
spacing: f32,
}
-impl<'a> SubVStack<'a> {
- pub fn add_widget<T: Widget + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
+impl<'a> SubsectionVStack<'a> {
+ pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
self.subsection.widget(self.pc, w, Subsection::DEFAULT_MARGIN_X, ww, wh);
self.subsection.spacing(self.spacing);
}
@@ -750,7 +750,7 @@ impl Radial {
}
}
- pub fn layout_widgets<T: Widget + 'static>(&self, widgets: &mut [&mut T]) {
+ pub fn layout_widgets<T: Element + 'static>(&self, widgets: &mut [&mut T]) {
let mut active_idx = 0;
for w in widgets.iter_mut() {
if !w.layout_ignore() {
@@ -764,7 +764,7 @@ impl Radial {
}
}
- pub fn layout_widget_ptors(&self, widgets: &[*mut (dyn Widget + 'static)]) {
+ pub fn layout_widget_ptors(&self, widgets: &[*mut (dyn Element + 'static)]) {
let mut active_idx = 0;
for &w_ptr in widgets {
let w = unsafe { &mut *w_ptr };
@@ -950,16 +950,385 @@ impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
self
}
- pub fn add_section<F>(&mut self, final_pc: &mut P, mut render_fn: F)
+ pub fn add_section<F>(&mut self, final_pc: &mut P, label: &str, focused: bool, mut render_fn: F)
where
- F: FnMut(&mut P, f32, f32) -> f32,
+ F: FnMut(&mut SectionContext<'_, P>),
{
let mut dummy = P::default();
- let wh = render_fn(&mut dummy, 0.0, 0.0);
+ let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, self.section_width, label, focused);
+ render_fn(&mut dummy_ctx);
+ let wh = dummy_ctx.finish();
let (rx, ry, _, _) = self.strategy.allocate(self.section_width, wh);
- render_fn(final_pc, rx, ry);
+ let mut real_ctx = SectionContext::new(final_pc, rx, ry, self.section_width, label, focused);
+ render_fn(&mut real_ctx);
+ real_ctx.finish();
self.idx += 1;
}
+
+ pub fn add_section_with_width<F>(&mut self, final_pc: &mut P, width: f32, label: &str, focused: bool, mut render_fn: F)
+ where
+ F: FnMut(&mut SectionContext<'_, P>),
+ {
+ let mut dummy = P::default();
+ let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused);
+ render_fn(&mut dummy_ctx);
+ let wh = dummy_ctx.finish();
+ let (rx, ry, _, _) = self.strategy.allocate(width, wh);
+ let mut real_ctx = SectionContext::new(final_pc, rx, ry, width, label, focused);
+ render_fn(&mut real_ctx);
+ real_ctx.finish();
+ self.idx += 1;
+ }
+}
+
+pub struct SectionContext<'a, P> {
+ pub pc: &'a mut P,
+ pub left: f32,
+ pub top: f32,
+ pub content_y: f32,
+ pub cw: f32,
+ pub label_width: f32,
+ pub focused: bool,
+}
+
+impl<'a, P: RenderTarget> SectionContext<'a, P> {
+ pub const ROW_PADDING_X: f32 = 8.0;
+ pub const DEFAULT_MARGIN_X: f32 = 12.0;
+ pub const DEFAULT_ROW_GAP: f32 = 8.0;
+
+ fn estimate_label_width(label: &str) -> f32 {
+ let mut width = 0.0;
+ for c in label.chars() {
+ let factor = match c {
+ 'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
+ 'r' | 's' | 'J' | 'c' | 'z' => 0.42,
+ 'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
+ 'A'..='Z' => 0.68,
+ _ => 0.55,
+ };
+ width += factor * 14.0;
+ }
+ width
+ }
+
+ pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool) -> Self {
+ let label_width = Self::estimate_label_width(label);
+ let label_x = left + (cw - label_width) / 2.0;
+ pc.text(label, label_x, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
+ Self {
+ pc,
+ left,
+ top,
+ content_y: top + 19.0,
+ cw,
+ label_width,
+ focused,
+ }
+ }
+
+ pub fn ax(&self, x_off: f32) -> f32 {
+ let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
+ self.left + x_off + shift
+ }
+
+ pub fn ay(&self) -> f32 {
+ self.content_y
+ }
+
+ pub fn spacing(&mut self, dy: f32) {
+ self.content_y += dy;
+ }
+
+ pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+ self.pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
+ }
+
+ pub fn widget<T: Element + 'static>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+ let x = self.ax(x_off);
+ let y = self.ay();
+ let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
+ let clamped_w = ww.min((right_edge - x).max(0.0));
+ let top_room = crate::widget::label_offset(w);
+ let total_h = wh + top_room;
+ render_widget(self.pc, w, x, y, clamped_w, total_h);
+ self.content_y += total_h;
+ }
+
+ pub fn widget_full<T: Element + 'static>(&mut self, w: &mut T, wh: f32) {
+ let x_off = 12.0;
+ let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off); // cw - 40.0
+ self.widget(w, x_off, ww, wh);
+ }
+
+ pub fn separator(&mut self) {
+ let x = self.ax(Self::ROW_PADDING_X);
+ let y = self.ay();
+ self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
+ self.content_y += 8.0;
+ }
+
+ pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+ self.pc.rect(color, self.ax(x_off), self.ay(), w, h);
+ self.content_y += h;
+ }
+
+ pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
+ let margin_x = Self::ROW_PADDING_X + 12.0;
+ let usable_w = self.cw - 2.0 * margin_x;
+ if count == 0 {
+ return Vec::new();
+ }
+ let total_gap = gap * (count - 1) as f32;
+ let col_w = (usable_w - total_gap).max(0.0) / count as f32;
+
+ let mut cols = Vec::with_capacity(count);
+ for i in 0..count {
+ let x = self.left + margin_x + i as f32 * (col_w + gap);
+ cols.push((x, col_w));
+ }
+ cols
+ }
+
+ pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
+ where
+ F: FnMut(usize, f32, f32),
+ {
+ let cols = self.row_layout(count, gap);
+ for (i, &(x, w)) in cols.iter().enumerate() {
+ f(i, x, w);
+ }
+ self.content_y += h;
+ }
+
+ pub fn vstack(&mut self, spacing: f32) -> VStack<'_, 'a, P> {
+ VStack {
+ context: self,
+ spacing,
+ }
+ }
+
+ pub fn add_subsection<F>(&mut self, label: &str, focused: bool, mut render_fn: F)
+ where
+ F: FnMut(&mut SubsectionContext<'_, P>),
+ {
+ let left = self.ax(0.0) + Self::ROW_PADDING_X;
+ let top = self.content_y;
+ let cw = self.cw - 2.0 * Self::ROW_PADDING_X;
+
+ let mut sub_ctx = SubsectionContext::new(self.pc, left, top, cw, label, focused);
+ render_fn(&mut sub_ctx);
+ self.content_y = sub_ctx.finish();
+ }
+
+ pub fn finish(self) -> f32 {
+ let border: [f32; 4] = if self.focused {
+ [0.30, 0.50, 0.32, 1.0] // Focused green
+ } else {
+ [0.25, 0.25, 0.35, 1.0] // Default gray
+ };
+ let x = self.left + Self::ROW_PADDING_X;
+ let y = self.top + 7.0;
+ let w = self.cw - 2.0 * Self::ROW_PADDING_X;
+ let h = self.content_y - y;
+
+ let left_edge = x;
+ let right_edge = x + w;
+ if self.label_width > 0.0 {
+ let label_x = self.left + (self.cw - self.label_width) / 2.0;
+ let gap_margin = 6.0;
+ let gap_start = label_x - gap_margin;
+ let gap_end = label_x + self.label_width + gap_margin;
+ if gap_start > left_edge {
+ self.pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+ }
+ if right_edge > gap_end {
+ self.pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+ }
+ } else {
+ self.pc.rect(border, left_edge, y, w, 1.0);
+ }
+
+ self.pc.rect(border, x, y + h + 12.0, w, 1.0);
+ self.pc.rect(border, x, y, 1.0, h + 12.0);
+ self.pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
+ self.content_y + 20.0
+ }
+}
+
+pub struct SubsectionContext<'a, P> {
+ pub pc: &'a mut P,
+ pub left: f32,
+ pub top: f32,
+ pub content_y: f32,
+ pub cw: f32,
+ pub label_width: f32,
+ pub focused: bool,
+}
+
+impl<'a, P: RenderTarget> SubsectionContext<'a, P> {
+ pub const ROW_PADDING_X: f32 = 8.0;
+ pub const DEFAULT_MARGIN_X: f32 = 12.0;
+ pub const DEFAULT_ROW_GAP: f32 = 8.0;
+
+ fn estimate_label_width(label: &str) -> f32 {
+ let mut width = 0.0;
+ for c in label.chars() {
+ let factor = match c {
+ 'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
+ 'r' | 's' | 'J' | 'c' | 'z' => 0.42,
+ 'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
+ 'A'..='Z' => 0.68,
+ _ => 0.55,
+ };
+ width += factor * 12.0;
+ }
+ width
+ }
+
+ pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool) -> Self {
+ let label_width = Self::estimate_label_width(label);
+ let label_x = left + (cw - label_width) / 2.0;
+ pc.text(label, label_x, top, 12.0, [0.53, 0.53, 0.60, 1.0]);
+ Self {
+ pc,
+ left,
+ top,
+ content_y: top + 17.0,
+ cw,
+ label_width,
+ focused,
+ }
+ }
+
+ pub fn ax(&self, x_off: f32) -> f32 {
+ let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
+ self.left + x_off + shift
+ }
+
+ pub fn ay(&self) -> f32 {
+ self.content_y
+ }
+
+ pub fn spacing(&mut self, dy: f32) {
+ self.content_y += dy;
+ }
+
+ pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+ self.pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
+ }
+
+ pub fn widget<T: Element + 'static>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+ let x = self.ax(x_off);
+ let y = self.ay();
+ let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
+ let clamped_w = ww.min((right_edge - x).max(0.0));
+ let top_room = crate::widget::label_offset(w);
+ let total_h = wh + top_room;
+ render_widget(self.pc, w, x, y, clamped_w, total_h);
+ self.content_y += total_h;
+ }
+
+ pub fn widget_full<T: Element + 'static>(&mut self, w: &mut T, wh: f32) {
+ let x_off = 12.0;
+ let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off);
+ self.widget(w, x_off, ww, wh);
+ }
+
+ pub fn separator(&mut self) {
+ let x = self.ax(Self::ROW_PADDING_X);
+ let y = self.ay();
+ self.pc.rect([0.15, 0.15, 0.22, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
+ self.content_y += 8.0;
+ }
+
+ pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+ self.pc.rect(color, self.ax(x_off), self.ay(), w, h);
+ self.content_y += h;
+ }
+
+ pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
+ let margin_x = Self::ROW_PADDING_X + 12.0;
+ let usable_w = self.cw - 2.0 * margin_x;
+ if count == 0 {
+ return Vec::new();
+ }
+ let total_gap = gap * (count - 1) as f32;
+ let col_w = (usable_w - total_gap).max(0.0) / count as f32;
+
+ let mut cols = Vec::with_capacity(count);
+ for i in 0..count {
+ let x = self.left + margin_x + i as f32 * (col_w + gap);
+ cols.push((x, col_w));
+ }
+ cols
+ }
+
+ pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
+ where
+ F: FnMut(usize, f32, f32),
+ {
+ let cols = self.row_layout(count, gap);
+ for (i, &(x, w)) in cols.iter().enumerate() {
+ f(i, x, w);
+ }
+ self.content_y += h;
+ }
+
+ pub fn finish(self) -> f32 {
+ let border: [f32; 4] = if self.focused {
+ [0.22, 0.38, 0.24, 1.0]
+ } else {
+ [0.18, 0.18, 0.25, 1.0]
+ };
+ let x = self.left + Self::ROW_PADDING_X;
+ let y = self.top + 7.0;
+ let w = self.cw - 2.0 * Self::ROW_PADDING_X;
+ let h = self.content_y - y;
+
+ let left_edge = x;
+ let right_edge = x + w;
+ if self.label_width > 0.0 {
+ let label_x = self.left + (self.cw - self.label_width) / 2.0;
+ let gap_margin = 6.0;
+ let gap_start = label_x - gap_margin;
+ let gap_end = label_x + self.label_width + gap_margin;
+ if gap_start > left_edge {
+ self.pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+ }
+ if right_edge > gap_end {
+ self.pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+ }
+ } else {
+ self.pc.rect(border, left_edge, y, w, 1.0);
+ }
+
+ self.pc.rect(border, x, y + h + 12.0, w, 1.0);
+ self.pc.rect(border, x, y, 1.0, h + 12.0);
+ self.pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
+ self.content_y + 20.0
+ }
+}
+
+pub struct VStack<'b, 'a, P> {
+ context: &'b mut SectionContext<'a, P>,
+ spacing: f32,
+}
+
+impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
+ pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
+ self.context.widget(w, SectionContext::<P>::DEFAULT_MARGIN_X, ww, wh);
+ self.context.spacing(self.spacing);
+ }
+
+ pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, f: F)
+ where
+ F: FnMut(usize, f32, f32),
+ {
+ self.context.row(count, gap, h, f);
+ self.context.spacing(self.spacing);
+ }
}
#[cfg(test)]
@@ -984,7 +1353,7 @@ mod tests {
h: f32,
}
- impl Widget for MockWidget {
+ impl Element for MockWidget {
fn rect(&self) -> (f32, f32, f32, f32) {
(self.x, self.y, self.w, self.h)
}
@@ -1007,7 +1376,7 @@ mod tests {
top_room: f32,
}
- impl Widget for MockWidgetWithLabel {
+ impl Element for MockWidgetWithLabel {
fn rect(&self) -> (f32, f32, f32, f32) {
(self.x, self.y - self.top_room, self.w, self.h + self.top_room)
}
diff --git a/src/main.rs b/src/main.rs
index 303724a..f293378 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
use clear_ui::widget::{
Button, Checkbox, ContentBg, Header, Panel, ProgressBar, RangeSlider, Sidebar, Slider, Spinbox,
- StatusBar, TextLabel, Toggle, Widget, JsonLayoutWidget, JsonLayoutConfig,
+ StatusBar, TextLabel, Toggle, Element, JsonLayoutWidget, JsonLayoutConfig,
};
use glyphon::{
@@ -170,7 +170,7 @@ fn rounded_rect_vertices(
rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, (true, true, true, true))
}
-fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32) -> Vec<Vertex> {
+fn widget_vertices(w: &dyn Element, sw: f32, sh: f32) -> Vec<Vertex> {
let (x, y, ww, h) = w.rect();
let corners = w.rounded_corners();
if corners != (false, false, false, false) {
@@ -181,7 +181,7 @@ fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32) -> Vec<Vertex> {
}
fn extra_quad_vertices(
- w: &dyn Widget,
+ w: &dyn Element,
qx: f32, qy: f32, qw: f32, qh: f32,
sw: f32, sh: f32,
qc: [f32; 4],
@@ -237,7 +237,7 @@ struct State {
vertex_buffer: wgpu::Buffer,
vertex_count: u32,
- widgets: Vec<Box<dyn Widget>>,
+ widgets: Vec<Box<dyn Element>>,
positions: Vec<(f32, f32, f32, f32)>,
font_system: FontSystem,
@@ -380,7 +380,7 @@ impl State {
let status_buffer = make_text_buffer(&mut font_system, "Click a button to interact", 12.0);
let layout_mode = json_layout_config.is_some();
- let mut widgets: Vec<Box<dyn Widget>> = Vec::new();
+ let mut widgets: Vec<Box<dyn Element>> = Vec::new();
let mut positions = Vec::new();
let json_layout = if let Some(ref config) = json_layout_config {
let mut jl = JsonLayoutWidget::new(config);
diff --git a/src/widget.rs b/src/widget.rs
deleted file mode 100644
index a99d97f..0000000
--- a/src/widget.rs
+++ /dev/null
@@ -1,10734 +0,0 @@
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum ElementState {
- Pressed,
- Released,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum MouseButton {
- Left,
- Right,
- Middle,
- Back,
- Forward,
- Other(u16),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Position {
- pub x: f64,
- pub y: f64,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub enum MouseScrollDelta {
- LineDelta(f32, f32),
- PixelDelta(Position),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum Key {
- Named(NamedKey),
- Character(String),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum NamedKey {
- Backspace,
- Tab,
- Enter,
- Escape,
- Space,
- ArrowDown,
- ArrowLeft,
- ArrowRight,
- ArrowUp,
- End,
- Home,
- PageDown,
- PageUp,
- Delete,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct KeyEvent {
- pub state: ElementState,
- pub logical_key: Key,
- pub text: Option<String>,
- pub repeat: bool,
- pub ctrl: bool,
- pub shift: bool,
-}
-
-pub mod json_layout;
-pub use json_layout::{JsonLayoutWidget, JsonLayoutConfig, JsonWidgetConfig, JsonPageConfig, JsonWidget};
-
-use crate::colors;
-
-pub mod focus {
- use super::Widget;
- use std::cell::Cell;
-
- thread_local! {
- static FOCUSED_WIDGET: Cell<Option<*mut (dyn Widget + 'static)>> = Cell::new(None);
- }
-
- pub fn set_focused(w: &mut dyn Widget) {
- FOCUSED_WIDGET.with(|cell| {
- let new_ptr = unsafe {
- std::mem::transmute::<*mut dyn Widget, *mut (dyn Widget + 'static)>(w as *mut dyn Widget)
- };
- if let Some(old_ptr) = cell.get() {
- let old_data = old_ptr as *mut () as usize;
- let new_data = new_ptr as *mut () as usize;
- if old_data != new_data {
- unsafe {
- (*old_ptr).unfocus();
- }
- cell.set(Some(new_ptr));
- }
- } else {
- cell.set(Some(new_ptr));
- }
- });
- }
-
- pub fn is_focused(w: &dyn Widget) -> bool {
- FOCUSED_WIDGET.with(|cell| {
- if let Some(ptr) = cell.get() {
- let current_data = ptr as *const () as usize;
- let query_data = w as *const dyn Widget as *const () as usize;
- current_data == query_data
- } else {
- false
- }
- })
- }
-
- pub fn clear_focus() {
- FOCUSED_WIDGET.with(|cell| {
- if let Some(ptr) = cell.take() {
- unsafe {
- (*ptr).unfocus();
- }
- }
- });
- }
-
- pub fn clear_if_matches(w: &dyn Widget) {
- FOCUSED_WIDGET.with(|cell| {
- if let Some(ptr) = cell.get() {
- let current_data = ptr as *const () as usize;
- let query_data = w as *const dyn Widget as *const () as usize;
- if current_data == query_data {
- cell.set(None);
- }
- }
- });
- }
-
- pub fn has_focus() -> bool {
- FOCUSED_WIDGET.with(|cell| cell.get().is_some())
- }
-
- pub fn link_parent_child(parent: &mut dyn Widget, child: &mut dyn Widget) {
- let parent_ptr = unsafe {
- std::mem::transmute::<*mut dyn Widget, *mut (dyn Widget + 'static)>(parent as *mut dyn Widget)
- };
- let child_ptr = unsafe {
- std::mem::transmute::<*mut dyn Widget, *mut (dyn Widget + 'static)>(child as *mut dyn Widget)
- };
- parent.add_child(child_ptr);
- child.set_parent(Some(parent_ptr));
- }
-
- pub fn navigate_focus(key: &super::Key, ctrl: bool) -> bool {
- FOCUSED_WIDGET.with(|cell| {
- let ptr = match cell.get() {
- Some(p) => p,
- None => return false,
- };
-
- unsafe {
- match (key, ctrl) {
- (super::Key::Character(c), true) if c == "u" || c == "U" => {
- if let Some(parent_ptr) = (*ptr).parent() {
- let parent_ref = &mut *parent_ptr;
- set_focused(parent_ref);
- parent_ref.focus();
- return true;
- }
- }
- (super::Key::Character(c), true) if c == "i" || c == "I" => {
- let mut children = (*ptr).children();
- if !children.is_empty() {
- let child_ref = &mut *children[0];
- set_focused(child_ref);
- child_ref.focus();
- return true;
- }
- }
- (super::Key::Character(c), true) if c == "j" || c == "J" => {
- if let Some(parent_ptr) = (*ptr).parent() {
- let mut siblings = (*parent_ptr).children();
- let current_idx = siblings.iter().position(|&x| {
- let a = x as *mut () as usize;
- let b = ptr as *mut () as usize;
- a == b
- });
- if let Some(idx) = current_idx {
- let next_idx = (idx + 1) % siblings.len();
- let sibling_ref = &mut *siblings[next_idx];
- set_focused(sibling_ref);
- sibling_ref.focus();
- return true;
- }
- }
- }
- (super::Key::Character(c), true) if c == "k" || c == "K" => {
- if let Some(parent_ptr) = (*ptr).parent() {
- let mut siblings = (*parent_ptr).children();
- let current_idx = siblings.iter().position(|&x| {
- let a = x as *mut () as usize;
- let b = ptr as *mut () as usize;
- a == b
- });
- if let Some(idx) = current_idx {
- let prev_idx = if idx == 0 { siblings.len() - 1 } else { idx - 1 };
- let sibling_ref = &mut *siblings[prev_idx];
- set_focused(sibling_ref);
- sibling_ref.focus();
- return true;
- }
- }
- }
- _ => {}
- }
- }
- false
- })
- }
-}
-
-pub mod popovers {
- use super::Widget;
- use std::cell::RefCell;
-
- thread_local! {
- static ACTIVE_POPOVERS: RefCell<Vec<*const (dyn Widget + 'static)>> = RefCell::new(Vec::new());
- }
-
- pub fn clear() {
- ACTIVE_POPOVERS.with(|list| {
- list.borrow_mut().clear();
- });
- }
-
- pub fn register(w: &(dyn Widget + 'static)) {
- ACTIVE_POPOVERS.with(|list| {
- let ptr = w as *const (dyn Widget + 'static);
- let mut list = list.borrow_mut();
- if !list.contains(&ptr) {
- list.push(ptr);
- }
- });
- }
-
- pub fn get_active() -> Vec<*const (dyn Widget + 'static)> {
- ACTIVE_POPOVERS.with(|list| {
- list.borrow().clone()
- })
- }
-
- pub fn is_coordinate_covered(query_address: usize, px: f32, py: f32) -> bool {
- ACTIVE_POPOVERS.with(|list| {
- let list = list.borrow();
- for popover_ptr in list.iter() {
- let current_data = *popover_ptr as *const () as usize;
- if query_address == current_data {
- continue;
- }
- unsafe {
- if let Some(popover) = popover_ptr.as_ref() {
- if let Some((x, y, width, height)) = popover.popover_rect() {
- if px >= x && px <= x + width && py >= y && py <= y + height {
- return true;
- }
- }
- }
- }
- }
- false
- })
- }
-}
-
-pub mod hover_animation {
- use std::cell::RefCell;
- use crate::colors;
-
- #[derive(Debug, Clone)]
- pub struct HoverState {
- pub current_x: f32,
- pub current_y: f32,
- pub current_w: f32,
- pub current_h: f32,
- pub current_alpha: f32,
-
- pub target_x: Option<f32>,
- pub target_y: Option<f32>,
- pub target_w: Option<f32>,
- pub target_h: Option<f32>,
- pub target_alpha: f32,
-
- pub registered_this_frame: bool,
- pub scroll_offset: f32,
- }
-
- impl HoverState {
- fn new() -> Self {
- Self {
- current_x: 0.0,
- current_y: 0.0,
- current_w: 0.0,
- current_h: 0.0,
- current_alpha: 0.0,
-
- target_x: None,
- target_y: None,
- target_w: None,
- target_h: None,
- target_alpha: 0.0,
-
- registered_this_frame: false,
- scroll_offset: 0.0,
- }
- }
- }
-
- thread_local! {
- pub static HOVER_STATE: RefCell<HoverState> = RefCell::new(HoverState::new());
- pub static CURSOR_POS: RefCell<(f32, f32)> = RefCell::new((0.0, 0.0));
- }
-
- pub fn set_cursor_pos(x: f32, y: f32) {
- CURSOR_POS.with(|pos| {
- *pos.borrow_mut() = (x, y);
- });
- }
-
- pub fn reset_frame_registration() {
- HOVER_STATE.with(|state| {
- state.borrow_mut().registered_this_frame = false;
- });
- }
-
- pub fn set_scroll_offset(offset: f32) {
- HOVER_STATE.with(|state| {
- state.borrow_mut().scroll_offset = offset;
- });
- }
-
- pub fn get_scroll_offset() -> f32 {
- HOVER_STATE.with(|state| {
- state.borrow().scroll_offset
- })
- }
-
- pub fn register_hovered(x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) {
- HOVER_STATE.with(|state| {
- let mut s = state.borrow_mut();
- s.target_x = Some(x);
- s.target_y = Some(y);
- s.target_w = Some(w);
- s.target_h = Some(h);
- s.target_alpha = color[3];
- s.registered_this_frame = true;
- });
- }
-
- pub fn post_render_check() {
- HOVER_STATE.with(|state| {
- let mut s = state.borrow_mut();
- if !s.registered_this_frame {
- s.target_alpha = 0.0;
- let (cx, cy) = CURSOR_POS.with(|pos| *pos.borrow());
- s.target_x = Some(cx);
- s.target_y = Some(cy + s.scroll_offset);
- s.target_w = Some(0.0);
- s.target_h = Some(0.0);
- }
- });
- }
-
- pub fn tick(dt: f32) -> bool {
- HOVER_STATE.with(|state| {
- let mut s = state.borrow_mut();
- let decay = 15.0;
- let mut changed = false;
-
- if s.current_alpha <= 0.001 && s.target_alpha > 0.0 {
- if let (Some(tx), Some(ty), Some(tw), Some(th)) = (s.target_x, s.target_y, s.target_w, s.target_h) {
- s.current_x = tx;
- s.current_y = ty;
- s.current_w = tw;
- s.current_h = th;
- }
- }
-
- if (s.current_alpha - s.target_alpha).abs() > 0.001 {
- s.current_alpha += (s.target_alpha - s.current_alpha) * (1.0 - (-decay * dt).exp());
- changed = true;
- } else if s.current_alpha != s.target_alpha {
- s.current_alpha = s.target_alpha;
- changed = true;
- }
-
- if let (Some(tx), Some(ty), Some(tw), Some(th)) = (s.target_x, s.target_y, s.target_w, s.target_h) {
- if (s.current_x - tx).abs() > 0.1 {
- s.current_x += (tx - s.current_x) * (1.0 - (-decay * dt).exp());
- changed = true;
- } else if s.current_x != tx {
- s.current_x = tx;
- changed = true;
- }
-
- if (s.current_y - ty).abs() > 0.1 {
- s.current_y += (ty - s.current_y) * (1.0 - (-decay * dt).exp());
- changed = true;
- } else if s.current_y != ty {
- s.current_y = ty;
- changed = true;
- }
-
- if (s.current_w - tw).abs() > 0.1 {
- s.current_w += (tw - s.current_w) * (1.0 - (-decay * dt).exp());
- changed = true;
- } else if s.current_w != tw {
- s.current_w = tw;
- changed = true;
- }
-
- if (s.current_h - th).abs() > 0.1 {
- s.current_h += (th - s.current_h) * (1.0 - (-decay * dt).exp());
- changed = true;
- } else if s.current_h != th {
- s.current_h = th;
- changed = true;
- }
- }
-
- changed
- })
- }
-
- pub fn get_quad() -> Option<(f32, f32, f32, f32, [f32; 4])> {
- HOVER_STATE.with(|state| {
- let s = state.borrow();
- if s.current_alpha > 0.001 {
- Some((
- s.current_x,
- s.current_y,
- s.current_w,
- s.current_h,
- [1.0, 1.0, 1.0, s.current_alpha],
- ))
- } else {
- None
- }
- })
- }
-}
-
-pub mod clipboard {
- pub fn copy_to_clipboard(text: &str) {
- let text = text.to_string();
- std::thread::spawn(move || {
- if let Ok(mut child) = std::process::Command::new("wl-copy")
- .stdin(std::process::Stdio::piped())
- .spawn()
- {
- if let Some(mut stdin) = child.stdin.take() {
- use std::io::Write;
- let _ = stdin.write_all(text.as_bytes());
- }
- let _ = child.wait();
- } else if let Ok(mut child) = std::process::Command::new("xclip")
- .arg("-selection")
- .arg("clipboard")
- .stdin(std::process::Stdio::piped())
- .spawn()
- {
- if let Some(mut stdin) = child.stdin.take() {
- use std::io::Write;
- let _ = stdin.write_all(text.as_bytes());
- }
- let _ = child.wait();
- }
- });
- }
-
- pub fn read_from_clipboard() -> Option<String> {
- if let Ok(output) = 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);
- }
- }
- }
- if let Ok(output) = 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);
- }
- }
- }
- None
- }
-}
-
-pub mod context_menu {
- use super::{Widget, TextBox, MouseButton, ElementState, TextLabel};
- use std::cell::RefCell;
-
- #[derive(Debug, Clone)]
- pub struct ContextMenuState {
- pub x: f32,
- pub y: f32,
- pub w: f32,
- pub h: f32,
- pub visible: bool,
- pub options: Vec<String>,
- pub hovered_item: Option<usize>,
- pub target: Option<*mut TextBox>,
- }
-
- impl ContextMenuState {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 120.0,
- h: 0.0,
- visible: false,
- options: Vec::new(),
- hovered_item: None,
- target: None,
- }
- }
-
- pub fn show(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
- self.x = x;
- self.y = y;
- self.options = options;
- self.h = self.options.len() as f32 * 24.0;
- self.visible = true;
- self.hovered_item = None;
- self.target = Some(target);
- }
-
- pub fn hide(&mut self) {
- self.visible = false;
- self.target = None;
- }
-
- pub fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible { return false; }
- px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
- }
-
- pub fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
- if !self.visible { return false; }
- let was_hovered = self.hovered_item;
- 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() {
- self.hovered_item = Some(idx);
- }
- }
- self.hovered_item != was_hovered
- }
-
- pub fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if !self.visible { return false; }
- if button != MouseButton::Left || state != ElementState::Pressed {
- if state == ElementState::Pressed {
- self.hide();
- return true;
- }
- return false;
- }
-
- 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;
- }
- }
- "Copy" => {
- target.copy_selection();
- }
- "Paste" => {
- if target.paste_from_clipboard() {
- target.just_changed = true;
- }
- }
- "Select All" => {
- target.select_all();
- }
- _ => {}
- }
- }
- }
- }
- self.hide();
- return true;
- } else {
- self.hide();
- return true;
- }
- }
-
- pub fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if !self.visible { return quads; }
-
- // border
- quads.push((self.x, self.y, self.w, self.h, [0.22, 0.22, 0.28, 1.0]));
- // bg
- quads.push((self.x + 1.0, self.y + 1.0, self.w - 2.0, self.h - 2.0, [0.06, 0.06, 0.09, 1.0]));
-
- if let Some(h_idx) = self.hovered_item {
- let iy = self.y + h_idx as f32 * 24.0;
- quads.push((self.x + 2.0, iy + 2.0, self.w - 4.0, 20.0, [0.20, 0.40, 0.65, 0.6]));
- }
-
- quads
- }
-
- pub fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if !self.visible { return labels; }
-
- 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) {
- [0xff, 0xff, 0xff]
- } else {
- [0xcc, 0xcc, 0xd4]
- };
-
- labels.push(TextLabel {
- text: opt.clone(),
- x: self.x + 8.0,
- y: iy,
- font_size: 12.0,
- color: text_color,
- });
- }
- labels
- }
- }
-
- thread_local! {
- pub static CONTEXT_MENU: RefCell<ContextMenuState> = RefCell::new(ContextMenuState::new());
- }
-
- pub fn is_visible() -> bool {
- CONTEXT_MENU.with(|m| m.borrow().visible)
- }
-
- pub fn show(x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
- CONTEXT_MENU.with(|m| m.borrow_mut().show(x, y, options, target));
- }
-
- pub fn hide() {
- CONTEXT_MENU.with(|m| m.borrow_mut().hide());
- }
-
- pub fn x() -> f32 { CONTEXT_MENU.with(|m| m.borrow().x) }
- pub fn y() -> f32 { CONTEXT_MENU.with(|m| m.borrow().y) }
- pub fn w() -> f32 { CONTEXT_MENU.with(|m| m.borrow().w) }
- pub fn h() -> f32 { CONTEXT_MENU.with(|m| m.borrow().h) }
- pub fn hovered_item() -> Option<usize> { CONTEXT_MENU.with(|m| m.borrow().hovered_item) }
- pub fn options() -> Vec<String> { CONTEXT_MENU.with(|m| m.borrow().options.clone()) }
-
- pub fn hit_test(px: f32, py: f32) -> bool {
- CONTEXT_MENU.with(|m| m.borrow().hit_test(px, py))
- }
-
- pub fn cursor_moved(px: f32, py: f32) -> bool {
- CONTEXT_MENU.with(|m| m.borrow_mut().cursor_moved(px, py))
- }
-
- pub fn mouse_input(button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- CONTEXT_MENU.with(|m| m.borrow_mut().mouse_input(button, state, px, py))
- }
-
- pub fn extra_quads() -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- CONTEXT_MENU.with(|m| m.borrow().extra_quads())
- }
-
- pub fn text_labels() -> Vec<TextLabel> {
- CONTEXT_MENU.with(|m| m.borrow().text_labels())
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct TextLabel {
- pub text: String,
- pub x: f32,
- pub y: f32,
- pub font_size: f32,
- pub color: [u8; 3],
-}
-
-impl TextLabel {
- pub fn estimate_width(text: &str, font_size: f32) -> f32 {
- let mut weight_sum = 0.0;
- for c in text.chars() {
- weight_sum += match c {
- 'i' | 'l' | 't' | 'j' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '1' | '-' | '(' | ')' | '[' | ']' => 0.26,
- 'f' | 'r' | 's' | 'J' => 0.35,
- 'w' | 'm' | 'M' | 'W' => 0.72,
- 'A' | 'B' | 'C' | 'D' | 'E' | 'G' | 'H' | 'K' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'X' | 'Y' | 'Z' => 0.65,
- _ => 0.52,
- };
- }
- weight_sum * font_size
- }
-
- pub fn curved_layout(
- text: &str,
- cx: f32,
- cy: f32,
- r: f32,
- start_angle: f32,
- end_angle: f32,
- font_size: f32,
- color: [u8; 3],
- ) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- let char_widths: Vec<f32> = text.chars().map(|c| {
- Self::estimate_width(&c.to_string(), font_size)
- }).collect();
- let total_width: f32 = char_widths.iter().sum();
-
- let mid_angle = (start_angle + end_angle) / 2.0;
- let angular_width = total_width / r;
- let text_start_angle = mid_angle - angular_width / 2.0;
-
- let mut current_angle = text_start_angle;
- for (i, c) in text.chars().enumerate() {
- let cw = char_widths[i];
- let dtheta = cw / r;
- let char_center_angle = current_angle + dtheta / 2.0;
-
- let x = cx + r * char_center_angle.cos() - cw / 2.0;
- let y = cy + r * char_center_angle.sin() - font_size / 2.0;
-
- labels.push(TextLabel {
- text: c.to_string(),
- x,
- y,
- font_size,
- color,
- });
-
- current_angle += dtheta;
- }
- labels
- }
-
- pub fn is_covered_by(&self, px: f32, py: f32, pw: f32, ph: f32) -> bool {
- let text_w = Self::estimate_width(&self.text, self.font_size);
- let x_overlap = self.x <= px + pw && (self.x + text_w) >= px;
- let y_overlap = self.y <= py + ph && (self.y + self.font_size) >= py;
- x_overlap && y_overlap
- }
-}
-
-
-#[derive(Debug, Clone)]
-pub struct WidgetBase {
- pub x: f32,
- pub y: f32,
- pub w: f32,
- pub h: f32,
- pub label: Option<String>,
- pub hovered: bool,
- pub row_x: f32,
- pub row_w: f32,
- pub focused: bool,
-}
-
-impl WidgetBase {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- label: None,
- hovered: false,
- row_x: 0.0,
- row_w: 0.0,
- focused: false,
- }
- }
-
- pub fn new_rect(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- x,
- y,
- w,
- h,
- label: None,
- hovered: false,
- row_x: 0.0,
- row_w: 0.0,
- focused: false,
- }
- }
-}
-
-#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
-pub struct GraphNode {
- pub name: String,
- pub position: (f32, f32), // (column, row)
- pub parameters: Vec<(String, String, String)>, // (name, value, type)
- pub geom_visible: bool,
-}
-
-pub trait Widget {
- fn base(&self) -> Option<&WidgetBase> { None }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { None }
-
- fn rect(&self) -> (f32, f32, f32, f32) {
- if let Some(b) = self.base() {
- (b.x, b.y, b.w, b.h)
- } else {
- (0.0, 0.0, 0.0, 0.0)
- }
- }
-
- fn label(&self) -> Option<String> { None }
-
- fn set_curved_circle(&mut self, _circle: Option<(f32, f32, f32)>) {}
- fn set_uniform_background(&mut self, _uniform: bool) {}
- fn set_network_opacity(&mut self, _opacity: f32) {}
- fn set_cell_color(&mut self, _color: [f32; 3]) {}
- fn set_gap_color(&mut self, _color: [f32; 3]) {}
-
- 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;
- }
- }
-
- fn set_row_rect(&mut self, x: f32, w: f32) {
- if let Some(b) = self.base_mut() {
- b.row_x = x;
- b.row_w = w;
- }
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- let (x, y, w, h) = self.rect();
- let (hx, hw) = if let Some(b) = self.base() {
- if b.row_w > 0.0 {
- (b.row_x, b.row_w)
- } else {
- (x, w)
- }
- } else {
- (x, w)
- };
- let top = self.top_room();
- let hy = y - top;
- let hh = h + top;
- px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
- }
-
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
- hover_animation::set_cursor_pos(px, py);
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- let was = self.hovered();
- self.set_hovered(false);
- return was;
- }
- self.on_cursor_moved(px, py)
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- if self.base().is_some() {
- let was = self.hovered();
- let is_hit = self.hit_test(px, py);
- self.set_hovered(is_hit);
- was != is_hit
- } else {
- false
- }
- }
-
- fn mouse_input(&mut self, _button: MouseButton, _state: ElementState, _px: f32, _py: f32) -> bool { false }
- fn mouse_wheel(&mut self, _delta: &MouseScrollDelta, _px: f32, _py: f32) -> bool { false }
-
- fn set_hovered(&mut self, hovered: bool) {
- if let Some(b) = self.base_mut() {
- b.hovered = hovered;
- }
- }
-
- fn hovered(&self) -> bool {
- if let Some(b) = self.base() {
- b.hovered
- } else {
- false
- }
- }
-
- fn is_active(&self) -> bool { false }
-
- fn highlight_color(&self) -> Option<[f32; 4]> {
- let is_focused = self.base().map_or(false, |b| b.focused);
- if is_focused || self.is_active() {
- Some(colors::highlight_primary_color())
- } else if self.hovered() {
- Some(colors::HIGHLIGHT_SECONDARY)
- } else {
- None
- }
- }
-
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- let hc = self.highlight_color()?;
- if let Some(b) = self.base() {
- let hx = if b.row_w > 0.0 { b.row_x } else { b.x };
- let hw = if b.row_w > 0.0 { b.row_w } else { b.w };
- let top_offset = self.top_room();
- let hy = b.y - top_offset;
- let hh = b.h + top_offset;
- Some((hx, hy, hw, hh, hc))
- } else {
- let (x, y, w, h) = self.rect();
- let top_offset = self.top_room();
- Some((x, y - top_offset, w, h + top_offset, hc))
- }
- }
-
- fn color(&self) -> [f32; 4];
-
- fn is_dragging(&self) -> bool { false }
- fn drag_update(&mut self, _px: f32, _py: f32) -> bool { false }
- fn drag_begin(&mut self, _px: f32, _py: f32) {}
- fn drag_end(&mut self) {}
- fn take_click(&mut self) -> bool { false }
- fn draggable(&self) -> bool { false }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { Vec::new() }
- fn all_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = self.extra_quads();
- if let Some(hq) = self.highlight_quad() {
- if hq.4 == colors::HIGHLIGHT_SECONDARY {
- hover_animation::register_hovered(hq.0, hq.1, hq.2, hq.3, hq.4);
- } else {
- quads.push(hq);
- }
- }
- quads
- }
- fn text_labels(&self) -> Vec<TextLabel> {
- if let Some(b) = self.base() {
- if let Some(ref label) = b.label {
- return vec![TextLabel {
- text: label.clone(),
- x: b.x,
- y: b.y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- }];
- }
- }
- Vec::new()
- }
- fn widget_font(&self) -> Option<String> { None }
- fn value(&self) -> i32 { 0 }
- fn type_name(&self) -> &'static str {
- let full_name = std::any::type_name::<Self>();
- full_name.split("::").last().unwrap_or("Widget")
- }
- fn top_room(&self) -> f32 {
- if let Some(b) = self.base() {
- if b.label.is_some() {
- return 18.0;
- }
- }
- 0.0
- }
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { None }
- fn render_popover(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
- fn set_text(&mut self, text: &str) {
- if let Some(b) = self.base_mut() {
- b.label = Some(text.to_string());
- }
- }
-
- fn set_drag_bounds(&mut self, _bx: f32, _by: f32, _bw: f32, _bh: f32) {}
-
- fn focus(&mut self) {
- if let Some(b) = self.base_mut() {
- b.focused = true;
- }
- }
- fn unfocus(&mut self) {
- if let Some(b) = self.base_mut() {
- b.focused = false;
- }
- }
- fn focused(&self) -> bool {
- if let Some(b) = self.base() {
- b.focused
- } else {
- false
- }
- }
- fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem) {}
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> { Vec::new() }
- fn set_selected(&mut self, _selected: bool) {}
- fn keyboard_input(&mut self, _event: &KeyEvent) -> 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) -> bool { false }
-
- 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 parent(&self) -> Option<*mut (dyn Widget + 'static)> { None }
- fn set_parent(&mut self, _parent: Option<*mut (dyn Widget + 'static)>) {}
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { vec![] }
- fn add_child(&mut self, _child: *mut (dyn Widget + 'static)) {}
- fn clear_children(&mut self) {}
- 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 text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
- self.text_labels().into_iter().map(|l| (l, None)).collect()
- }
- fn text_labels_with_font_and_bounds(&self) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
- self.text_labels().into_iter().map(|l| (l, None, None)).collect()
- }
-
- 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 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 Widget + 'static)) {}
- fn clear_page_widgets(&mut self, _page_idx: usize) {}
- 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 }
-}
-
-#[derive(Clone)]
-pub struct Container {
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
-}
-
-impl Container {
- pub fn new() -> Self {
- Self { parent: None, children: Vec::new() }
- }
-}
-
-impl Widget for Container {
- fn rect(&self) -> (f32, f32, f32, f32) { (0.0, 0.0, 0.0, 0.0) }
- fn set_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-
- fn focus(&mut self) {
- focus::set_focused(self);
- }
- fn unfocus(&mut self) {}
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for Container {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-
-pub struct Header {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl Header {
- pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
-}
-
-impl Widget for Header {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::HEADER_BG }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-}
-
-pub struct ContentBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- show_network_grid: bool,
- grid_size_x: f32,
- grid_size_y: f32,
- grid_origin_x: f32,
- grid_origin_y: f32,
- skipped_row_h: f32,
- skipped_col_w: f32,
-}
-
-impl ContentBg {
- pub fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 }
- }
-}
-
-impl Widget for ContentBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] {
- if self.show_network_grid {
- [0.0, 0.0, 0.0, 0.0]
- } else {
- colors::CONTENT_BG
- }
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32) -> 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 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 {
- return vec![];
- }
- let mut quads = Vec::new();
- let grid_color = [0.0, 0.0, 0.0, 0.0];
- let max_alpha = colors::CONTENT_BG[3]; // Peak opacity in the middle of gradient cells matches non-gradient cells
- let steps = 20; // Silky-smooth gradient transition
-
- let step_y = self.grid_size_y + self.skipped_row_h;
- 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 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);
-
- // Draw individual cell backgrounds to avoid stacking with gradients
- for ry in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (ry as f32) * step_y;
- let draw_start_y = y1.max(self.y);
- let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
- if draw_start_y < draw_end_y {
- for cx in cx_start..=cx_end {
- let x1 = self.grid_origin_x + (cx as f32) * step_x;
- let draw_start_x = x1.max(self.x);
- let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
- if draw_start_x < draw_end_x {
- quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
- }
- }
- }
- }
- }
-
- // Draw interstitial row gradients (horizontal bands fading to 0 alpha at left and right sides)
- if self.skipped_row_h > 0.0 {
- let step_y = self.grid_size_y + self.skipped_row_h;
- let step_x = self.grid_size_x + self.skipped_col_w;
- if step_y >= 4.0 && step_x >= 4.0 {
- let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.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);
-
- for k in k_start..=k_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 {
- continue;
- }
- let draw_start_y = y2.max(self.y);
- let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
- if draw_start_y >= draw_end_y {
- continue;
- }
-
- for cx in cx_start..=cx_end {
- let x1 = self.grid_origin_x + (cx as f32) * step_x;
- let x_mid = x1 + self.grid_size_x / 2.0;
- let w_total = self.grid_size_x;
- let sub_w = w_total / steps as f32;
-
- for i in 0..steps {
- let sx_start = x1 + i as f32 * sub_w;
- let sx_end = sx_start + sub_w;
- let draw_start_x = sx_start.max(self.x);
- let draw_end_x = sx_end.min(self.x + self.w);
- if draw_start_x < draw_end_x {
- let sx_mid = (sx_start + sx_end) / 2.0;
- let dist = (sx_mid - x_mid).abs();
- let d = (dist / (w_total / 2.0)).min(1.0);
-
- // Fade the cell background color from max_alpha in the middle to transparent at the edges
- let alpha = max_alpha * (1.0 - d);
- if alpha > 0.001 {
- quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
- }
- }
- }
- }
- }
- }
- }
-
- // Draw interstitial column gradients (vertical bands fading to 0 alpha at top and bottom)
- if self.skipped_col_w > 0.0 {
- let step_y = self.grid_size_y + self.skipped_row_h;
- let step_x = self.grid_size_x + self.skipped_col_w;
- if step_y >= 4.0 && step_x >= 4.0 {
- let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
-
- 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);
-
- for k in k_start..=k_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 {
- continue;
- }
- let draw_start_x = x2.max(self.x);
- let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
- if draw_start_x >= draw_end_x {
- continue;
- }
-
- for ry in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (ry as f32) * step_y;
- let y_mid = y1 + self.grid_size_y / 2.0;
- let h_total = self.grid_size_y;
- let sub_h = h_total / steps as f32;
-
- for i in 0..steps {
- let sy_start = y1 + i as f32 * sub_h;
- let sy_end = sy_start + sub_h;
- let draw_start_y = sy_start.max(self.y);
- let draw_end_y = sy_end.min(self.y + self.h);
- if draw_start_y < draw_end_y {
- let sy_mid = (sy_start + sy_end) / 2.0;
- let dist = (sy_mid - y_mid).abs();
- let d = (dist / (h_total / 2.0)).min(1.0);
-
- // Fade the cell background color from max_alpha in the middle to transparent at the edges
- let alpha = max_alpha * (1.0 - d);
- if alpha > 0.001 {
- quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
- }
- }
- }
- }
- }
- }
- }
-
- // Draw the grid borders
- let step_y = self.grid_size_y + self.skipped_row_h;
- if step_y >= 4.0 {
- let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
- for k in k_start..=k_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 {
- continue;
- }
- if y1 >= self.y {
- quads.push((self.x, y1, self.w, 1.0, grid_color));
- }
- if y2 >= self.y && y2 < self.y + self.h {
- quads.push((self.x, y2, self.w, 1.0, grid_color));
- }
- }
- }
-
- let step_x = self.grid_size_x + self.skipped_col_w;
- if step_x >= 4.0 {
- let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
- for k in k_start..=k_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 {
- continue;
- }
- if x1 >= self.x {
- quads.push((x1, self.y, 1.0, self.h, grid_color));
- }
- if x2 >= self.x && x2 < self.x + self.w {
- quads.push((x2, self.y, 1.0, self.h, grid_color));
- }
- }
- }
- quads
- }
-}
-
-pub struct ViewportBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl ViewportBg {
- pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
-}
-
-impl Widget for ViewportBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::VIEWPORT_BG }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32) -> bool { false }
-}
-
-pub struct ParametersBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- display_params: Vec<(String, String, String)>,
- dragging_param: Option<usize>,
- drag_offset: f32,
- pub focused_param: Option<usize>,
- mouse_pos: Option<(f32, f32)>,
- sliders: Vec<Option<Slider>>,
- float3s: Vec<Option<Float3>>,
- spinboxes: Vec<Option<Spinbox>>,
- visible: bool,
-}
-
-impl ParametersBg {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
- display_params: Vec::new(),
- dragging_param: None,
- drag_offset: 0.0,
- focused_param: None,
- mouse_pos: None,
- sliders: Vec::new(),
- float3s: Vec::new(),
- spinboxes: Vec::new(),
- visible: true,
- }
- }
-
- pub fn get_param_rects(&self) -> Vec<(f32, f32, f32, f32)> {
- let mut rects = Vec::new();
- let mut cur_y = self.y + 30.0;
- for p in &self.display_params {
- let h = if p.2 == "code" {
- 200.0
- } else if p.2 == "section" {
- 24.0
- } else if p.2.starts_with("float3") {
- 108.0
- } else if p.2 == "text" {
- 24.0
- } else {
- 20.0
- };
- rects.push((self.x + 8.0, cur_y, self.w - 16.0, h));
- cur_y += h + 8.0;
- }
- rects
- }
-
- fn update_slider_rects(&mut self) {
- let rects = self.get_param_rects();
- for (i, s_opt) in self.sliders.iter_mut().enumerate() {
- if let Some(s) = s_opt {
- let r = rects[i];
- let track_x = self.x + 100.0;
- let track_w = (self.w - 100.0 - 20.0).max(10.0);
- let track_y = r.1 + 4.0;
- let track_h = 12.0;
- s.set_rect(track_x, track_y, track_w, track_h);
- }
- }
- for (i, f_opt) in self.float3s.iter_mut().enumerate() {
- if let Some(f) = f_opt {
- let r = rects[i];
- f.set_rect(r.0, r.1, r.2, r.3);
- }
- }
- for (i, sb_opt) in self.spinboxes.iter_mut().enumerate() {
- if let Some(sb) = sb_opt {
- let r = rects[i];
- let box_x = self.x + 100.0;
- let box_w = (self.w - 100.0 - 16.0).max(10.0);
- sb.set_rect(box_x, r.1, box_w, r.3);
- }
- }
- }
-}
-
-fn parse_slider_range(ptype: &str) -> (f32, f32) {
- if ptype.starts_with("slider:") || ptype.starts_with("float3:") {
- let parts: Vec<&str> = ptype.split(':').collect();
- if parts.len() >= 3 {
- if let (Ok(min), Ok(max)) = (parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
- return (min, max);
- }
- }
- }
- (0.0, 2.0)
-}
-
-fn parse_spinbox_range(ptype: &str) -> (i32, i32, i32) {
- if ptype.starts_with("spinbox:") {
- let parts: Vec<&str> = ptype.split(':').collect();
- if parts.len() >= 4 {
- if let (Ok(min), Ok(max), Ok(step)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>(), parts[3].parse::<i32>()) {
- return (min, max, step);
- }
- } else if parts.len() == 3 {
- if let (Ok(min), Ok(max)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>()) {
- return (min, max, 1);
- }
- }
- }
- (0, 10000, 1)
-}
-
-fn parse_float3_value(val_str: &str, min: f32, max: f32) -> [f32; 3] {
- let mut out = [0.5, 0.5, 0.5];
- let parts: Vec<&str> = val_str
- .split(|c| c == ':' || c == ',' || c == ' ')
- .filter(|s| !s.is_empty())
- .collect();
- for i in 0..3 {
- if i < parts.len() {
- if let Ok(v) = parts[i].parse::<f32>() {
- let range = max - min;
- if range != 0.0 {
- out[i] = ((v - min) / range).clamp(0.0, 1.0);
- } else {
- out[i] = 0.0;
- }
- }
- }
- }
- out
-}
-
-impl Widget for ParametersBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x; self.y = y; self.w = w; self.h = h;
- self.update_slider_rects();
- }
- fn color(&self) -> [f32; 4] {
- if !self.visible {
- return [0.0, 0.0, 0.0, 0.0];
- }
- colors::PARAM_BG
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- }
- fn visible(&self) -> bool {
- self.visible
- }
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.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 unfocus(&mut self) {
- if let Some(idx) = self.focused_param {
- if idx < self.display_params.len() {
- let p = &mut self.display_params[idx];
- if p.2.starts_with("spinbox") {
- if let Some(sb) = &mut self.spinboxes[idx] {
- sb.unfocus();
- p.1 = sb.value.to_string();
- }
- } else if p.2.starts_with("slider") {
- if let Some(s) = &mut self.sliders[idx] {
- s.unfocus();
- let (min, max) = parse_slider_range(&p.2);
- let new_val = min + s.value * (max - min);
- p.1 = format!("{:.2}", new_val);
- }
- } else if p.2.starts_with("float3") {
- if let Some(f) = &mut self.float3s[idx] {
- f.unfocus();
- let (min, max) = parse_slider_range(&p.2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- }
- }
- }
- }
- self.focused_param = None;
- }
-
- 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"))
- }
-
- fn is_dragging(&self) -> bool {
- self.dragging_param.is_some()
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- let rects = self.get_param_rects();
- for (i, p) in self.display_params.iter().enumerate() {
- if p.2.starts_with("slider") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y - 2.0 && py <= row_y + 18.0 {
- if let Some(s) = &mut self.sliders[i] {
- s.drag_begin(px, py);
- self.dragging_param = Some(i);
- break;
- }
- }
- } else if p.2.starts_with("float3") {
- let r = rects[i];
- if py >= r.1 && py <= r.1 + r.3 {
- if let Some(f) = &mut self.float3s[i] {
- if f.mouse_input(MouseButton::Left, ElementState::Pressed, px, py) {
- self.dragging_param = Some(i);
- break;
- }
- }
- }
- }
- }
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- if let Some(i) = self.dragging_param {
- if let Some(s) = &mut self.sliders[i] {
- if s.drag_update(px, py) {
- let (min, max) = parse_slider_range(&self.display_params[i].2);
- let new_val = min + s.value * (max - min);
- let old_val = &self.display_params[i].1;
- let new_val_str = format!("{:.2}", new_val);
- if *old_val != new_val_str {
- self.display_params[i].1 = new_val_str;
- return true;
- }
- }
- } else if let Some(f) = &mut self.float3s[i] {
- if f.drag_update(px, py) {
- let (min, max) = parse_slider_range(&self.display_params[i].2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- let old_val = &self.display_params[i].1;
- if *old_val != new_val_str {
- self.display_params[i].1 = new_val_str;
- return true;
- }
- }
- }
- }
- false
- }
-
- fn drag_end(&mut self) {
- if let Some(i) = self.dragging_param.take() {
- if let Some(s) = &mut self.sliders[i] {
- s.drag_end();
- } else if let Some(f) = &mut self.float3s[i] {
- f.drag_end();
- }
- }
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.mouse_pos = Some((px, py));
- true
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button == MouseButton::Left && state == ElementState::Pressed {
- let rects = self.get_param_rects();
- let mut clicked_any_focusable = false;
- for (i, p) in self.display_params.iter_mut().enumerate() {
- if p.2 == "code" {
- let r = rects[i];
- if px >= r.0 && px <= r.0 + r.2 && py >= r.1 + 18.0 && py <= r.1 + r.3 {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- break;
- }
- } else if p.2 == "text" {
- let box_x = self.x + 100.0;
- let box_w = (self.w - 100.0 - 16.0).max(10.0);
- let r = rects[i];
- if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- break;
- }
- } else if p.2.starts_with("spinbox") {
- if let Some(sb) = &mut self.spinboxes[i] {
- if sb.mouse_input(button, state, px, py) {
- p.1 = sb.value.to_string();
- if sb.editing {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- } else {
- self.unfocus();
- }
- return true;
- }
- }
- } else if p.2.starts_with("slider") {
- let r = rects[i];
- if py >= r.1 && py <= r.1 + r.3 {
- if let Some(s) = &mut self.sliders[i] {
- if s.mouse_input(button, state, px, py) {
- if s.editing {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- }
- break;
- }
- }
- }
- } else if p.2.starts_with("float3") {
- let r = rects[i];
- if py >= r.1 && py <= r.1 + r.3 {
- if let Some(f) = &mut self.float3s[i] {
- if f.mouse_input(button, state, px, py) {
- if f.editing_idx.is_some() {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- }
- break;
- }
- }
- }
- }
- }
- if !clicked_any_focusable {
- self.unfocus();
- }
- return true;
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if let Some(idx) = self.focused_param {
- if event.state == ElementState::Pressed {
- let p = &mut self.display_params[idx];
- if p.2 == "code" {
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if !p.1.is_empty() {
- p.1.pop();
- return true;
- }
- }
- Key::Named(NamedKey::Enter) => {
- p.1.push('\n');
- return true;
- }
- Key::Named(NamedKey::Escape) => {
- self.focused_param = None;
- return true;
- }
- Key::Character(s) => {
- p.1.push_str(s);
- return true;
- }
- _ => {}
- }
- } else if p.2 == "text" {
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if !p.1.is_empty() {
- p.1.pop();
- return true;
- }
- }
- Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Escape) => {
- self.focused_param = None;
- return true;
- }
- Key::Character(s) => {
- p.1.push_str(s);
- return true;
- }
- _ => {}
- }
- } else if p.2.starts_with("spinbox") {
- if let Some(sb) = &mut self.spinboxes[idx] {
- if sb.keyboard_input(event) {
- if !sb.editing {
- p.1 = sb.value.to_string();
- self.focused_param = None;
- } else {
- p.1 = sb.edit_buffer.clone();
- }
- return true;
- }
- }
- } else if p.2.starts_with("slider") {
- if let Some(s) = &mut self.sliders[idx] {
- if s.keyboard_input(event) {
- let (min, max) = parse_slider_range(&p.2);
- let new_val = min + s.value * (max - min);
- p.1 = format!("{:.2}", new_val);
- if !s.editing {
- self.focused_param = None;
- }
- return true;
- }
- }
- } else if p.2.starts_with("float3") {
- if let Some(f) = &mut self.float3s[idx] {
- if f.keyboard_input(event) {
- let (min, max) = parse_slider_range(&p.2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- if f.editing_idx.is_none() {
- self.focused_param = None;
- }
- return true;
- }
- }
- }
- }
- }
- false
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- let mut changed = false;
- let rects = self.get_param_rects();
- for (i, p) in self.display_params.iter_mut().enumerate() {
- if p.2.starts_with("slider") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y - 2.0 && py <= row_y + 18.0 && px >= self.x && px <= self.x + self.w {
- if let Some(s) = &mut self.sliders[i] {
- let was_scroll = s.scroll_enabled;
- s.set_scroll(true);
- if s.mouse_wheel(delta, px, py) {
- let (min, max) = parse_slider_range(&p.2);
- let new_val = min + s.value * (max - min);
- let old_val = &p.1;
- let new_val_str = format!("{:.2}", new_val);
- if *old_val != new_val_str {
- p.1 = new_val_str;
- changed = true;
- }
- }
- s.set_scroll(was_scroll);
- }
- }
- } else if p.2.starts_with("float3") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
- if let Some(f) = &mut self.float3s[i] {
- let rects_inner = f.get_row_rects();
- for j in 0..3 {
- let r_inner = rects_inner[j];
- if py >= r_inner.1 && py <= r_inner.1 + r_inner.3 {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y,
- MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
- };
- let step = 0.02;
- let new_val = (f.values[j] - scroll_amount * step).clamp(0.0, 1.0);
- if (new_val - f.values[j]).abs() > 0.0001 {
- f.values[j] = new_val;
- if f.editing_idx == Some(j) {
- let scaled_val = f.mins[j] + f.values[j] * (f.maxs[j] - f.mins[j]);
- f.edit_buffer = format!("{:.2}", scaled_val);
- }
- let (min, max) = parse_slider_range(&p.2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- if p.1 != new_val_str {
- p.1 = new_val_str;
- changed = true;
- }
- }
- }
- }
- }
- }
- } else if p.2.starts_with("spinbox") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
- if let Some(sb) = &mut self.spinboxes[i] {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y as i32,
- MouseScrollDelta::PixelDelta(pos) => {
- let dy = pos.y;
- if dy > 0.0 { 1 } else if dy < 0.0 { -1 } else { 0 }
- }
- };
- let new_val = (sb.value + scroll_amount * sb.step).clamp(sb.min, sb.max);
- if sb.value != new_val {
- sb.value = new_val;
- p.1 = new_val.to_string();
- changed = true;
- }
- }
- }
- }
- }
- changed
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
- let rects = self.get_param_rects();
-
- // Find sections and their ranges
- let mut sections = Vec::new();
- let mut current_section: Option<(usize, usize)> = None;
- let mut in_section = false;
- for (i, p) in self.display_params.iter().enumerate() {
- if p.2 == "section" {
- if let Some((start, end)) = current_section {
- sections.push((start, end));
- }
- current_section = None;
- in_section = true;
- } else {
- if in_section {
- if let Some((start, ref mut end)) = current_section {
- *end = i;
- } else {
- current_section = Some((i, i));
- }
- }
- }
- }
- if let Some((start, end)) = current_section {
- sections.push((start, end));
- }
-
- // Draw section border boxes
- for (start, end) in sections {
- if start <= end && start < rects.len() && end < rects.len() {
- let r_start = rects[start];
- let r_end = rects[end];
- let bx = self.x + 4.0;
- let bw = self.w - 8.0;
- let by = r_start.1 - 4.0;
- let bh = (r_end.1 + r_end.3 + 4.0) - by;
-
- let border_color = [0.18, 0.18, 0.27, 1.0];
- let border_t = 1.0;
-
- // Top border
- quads.push((bx, by, bw, border_t, border_color));
- // Bottom border
- quads.push((bx, by + bh - border_t, bw, border_t, border_color));
- // Left border
- quads.push((bx, by, border_t, bh, border_color));
- // Right border
- quads.push((bx + bw - border_t, by, border_t, bh, border_color));
- }
- }
-
- for (i, p) in self.display_params.iter().enumerate() {
- let r = rects[i];
- if p.2.starts_with("slider") {
- if let Some(s) = &self.sliders[i] {
- let (sx, sy, sw, sh) = s.rect();
- quads.push((sx, sy, sw, sh, s.color()));
- quads.extend(s.extra_quads());
- }
- } else if p.2 == "section" {
- // Section header line is handled by the border box top border now
- } else if p.2.starts_with("float3") {
- if let Some(f) = &self.float3s[i] {
- quads.extend(f.extra_quads());
- }
- } else if p.2 == "code" {
- quads.push((r.0, r.1 + 18.0, r.2, r.3 - 18.0, [0.08, 0.08, 0.10, 1.0]));
- let border_color = if self.focused_param == Some(i) {
- [0.25, 0.45, 0.85, 1.0]
- } else {
- [0.20, 0.20, 0.25, 1.0]
- };
- let (bx, by, bw, bh) = (r.0, r.1 + 18.0, r.2, r.3 - 18.0);
- quads.push((bx, by, bw, 1.0, border_color));
- quads.push((bx, by + bh - 1.0, bw, 1.0, border_color));
- quads.push((bx, by, 1.0, bh, border_color));
- quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
- } else if p.2 == "text" {
- let box_x = self.x + 100.0;
- let box_w = (self.w - 100.0 - 16.0).max(10.0);
- quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
- let border_color = if self.focused_param == Some(i) {
- [0.25, 0.45, 0.85, 1.0]
- } else {
- [0.20, 0.20, 0.25, 1.0]
- };
- let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
- let border_t = 1.0;
- quads.push((bx, by, bw, border_t, border_color));
- quads.push((bx, by + bh - border_t, bw, border_t, border_color));
- quads.push((bx, by, border_t, bh, border_color));
- quads.push((bx + bw - border_t, by, border_t, bh, border_color));
- } else if p.2.starts_with("spinbox") {
- if let Some(sb) = &self.spinboxes[i] {
- quads.push((sb.base.x, sb.base.y, sb.base.w, sb.base.h, sb.color()));
- quads.extend(sb.extra_quads());
- }
- }
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let rects = self.get_param_rects();
- let mut labels = Vec::new();
- for (i, (name, value, ptype)) in self.display_params.iter().enumerate() {
- let r = rects[i];
- if ptype.starts_with("slider") {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 8.0,
- y: r.1,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- if let Some(s) = &self.sliders[i] {
- labels.extend(s.text_labels());
- }
- } else if ptype.starts_with("float3") {
- if let Some(f) = &self.float3s[i] {
- labels.extend(f.text_labels());
- }
- } else if ptype == "section" {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 12.0,
- y: r.1 + 2.0,
- font_size: 13.0,
- color: [0xee, 0xee, 0xf0],
- });
- } else if ptype == "code" {
- labels.push(TextLabel {
- text: format!("{}:\n{}", name, value),
- x: self.x + 12.0,
- y: r.1,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- } else if ptype.starts_with("spinbox") {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 8.0,
- y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- if let Some(sb) = &self.spinboxes[i] {
- labels.extend(sb.text_labels());
- }
- } else if ptype == "text" {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 8.0,
- y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- let val_text = if self.focused_param == Some(i) {
- format!("{}|", value)
- } else {
- value.clone()
- };
- labels.push(TextLabel {
- text: val_text,
- x: self.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),
- x: self.x + 8.0,
- y: r.1,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- }
- }
- labels
- }
-}
-
-pub struct Canvas {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl Canvas {
- pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
-}
-
-impl Widget for Canvas {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32) -> bool { false }
-}
-
-const DROPDOWN_ITEM_H: f32 = 22.0;
-const CHECKBOX_WIDTH: f32 = 16.0;
-pub struct MenuBar {
- x: f32, y: f32, w: f32, h: f32,
- hovering: bool,
- pub title: String,
- pub menus: Vec<Box<Menu>>,
- pub menu_items: Vec<String>,
- pub vertical_items: Vec<String>,
- pub menu_dropdowns: Vec<Vec<String>>,
- pub menu_dropdown_checked: Vec<Vec<Option<bool>>>,
- pub hovered_menu: Option<usize>,
- pub open_menu: Option<usize>,
- pub hovered_dropdown: Option<usize>,
- pub clicked_dropdown: Option<(usize, usize)>,
- pub was_open: Option<usize>,
- pub vertical: bool,
- pub visible: bool,
- pub focused: bool,
- pub z_level: i32,
- pub center_items: bool,
- pub curved_circle: Option<(f32, f32, f32)>,
- pub title_pos: Option<(f32, f32)>,
- pub title_buf: Option<glyphon::Buffer>,
- pub curved_title_char_bufs: Vec<glyphon::Buffer>,
- pub network_opacity: f32,
-}
-
-impl MenuBar {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- x, y, w, h, hovering: false,
- title: String::new(),
- menus: Vec::new(),
- menu_items: Vec::new(),
- vertical_items: Vec::new(),
- menu_dropdowns: Vec::new(),
- menu_dropdown_checked: Vec::new(),
- hovered_menu: None,
- open_menu: None,
- hovered_dropdown: None,
- clicked_dropdown: None,
- was_open: None,
- vertical: false,
- visible: true,
- focused: false,
- z_level: 100,
- center_items: false,
- curved_circle: None,
- title_pos: None,
- title_buf: None,
- curved_title_char_bufs: Vec::new(),
- network_opacity: 1.0,
- }
- }
-
- pub fn with_center_items(mut self, center: bool) -> Self {
- self.center_items = center;
- self
- }
-
- pub fn with_title(mut self, title: &str) -> Self {
- self.title = title.to_string();
- self
- }
-
- pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
- self.menu_items.push(label.to_string());
- self.vertical_items.push(label.to_string());
- self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
- self.menu_dropdown_checked.push(vec![None; items.len()]);
-
- let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
- let mut menu = Menu::new(label, label, &item_strs);
- menu.vertical = self.vertical;
- self.menus.push(Box::new(menu));
- self
- }
-
- pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
- self.menu_items.push(horizontal_label.to_string());
- self.vertical_items.push(vertical_label.to_string());
- self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
- self.menu_dropdown_checked.push(vec![None; items.len()]);
-
- let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
- let mut menu = Menu::new(horizontal_label, vertical_label, &item_strs);
- menu.vertical = self.vertical;
- self.menus.push(Box::new(menu));
- self
- }
-
- pub fn with_vertical(mut self, vertical: bool) -> Self {
- self.vertical = vertical;
- for menu in &mut self.menus {
- menu.vertical = vertical;
- }
- self
- }
-
- pub fn with_z_index(mut self, z: i32) -> Self {
- self.z_level = z;
- self
- }
-
- fn item_y_vertical(&self, idx: usize) -> f32 {
- let mut y = 8.0;
- if !self.title.is_empty() {
- y += 24.0;
- }
- y + idx as f32 * 24.0
- }
-
- fn item_h_vertical(&self) -> f32 {
- 24.0
- }
-}
-
-impl Widget for MenuBar {
- fn rect(&self) -> (f32, f32, f32, f32) {
- if !self.visible {
- return (0.0, 0.0, 0.0, 0.0);
- }
- if self.vertical {
- let total_h = if self.menus.is_empty() {
- self.h
- } else {
- let last_idx = self.menus.len() - 1;
- self.item_y_vertical(last_idx) + self.item_h_vertical()
- };
- (self.x, self.y, self.w, total_h)
- } else {
- (self.x, self.y, self.w, self.h)
- }
- }
-
- fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
- self.curved_circle = circle;
- if circle.is_none() {
- for menu in &mut self.menus {
- menu.curved_arc = None;
- }
- }
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
-
- let parent_ptr = self as *mut MenuBar as *mut (dyn Widget + 'static);
-
- if self.vertical {
- let mut cy = 8.0;
- if !self.title.is_empty() {
- cy += 24.0;
- }
- for menu in &mut self.menus {
- let ih = 24.0;
- menu.set_rect(x, y + cy, w, ih);
- menu.set_parent(Some(parent_ptr));
- cy += ih;
- }
- } else {
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
-
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let mut current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = self.title.len() as f32 * 7.5 + 24.0;
- let dtheta_title = title_w / r_mid;
- let theta_title = current_angle + dtheta_title / 2.0;
-
- let tx = ccx + r_mid * theta_title.cos() - title_w / 2.0 + 8.0;
- let ty = ccy + r_mid * theta_title.sin() - h / 2.0;
- self.title_pos = Some((tx, ty));
- current_angle += dtheta_title;
- } else {
- self.title_pos = None;
- }
-
- for menu in &mut self.menus {
- let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
- let dtheta_menu = iw / r_mid;
- let theta_menu = current_angle + dtheta_menu / 2.0;
-
- let mx = ccx + r_mid * theta_menu.cos() - iw / 2.0;
- let my = ccy + r_mid * theta_menu.sin() - h / 2.0;
-
- menu.set_rect(mx, my, iw, h);
- menu.set_parent(Some(parent_ptr));
- menu.curved_arc = Some((ccx, ccy, ccr, h, current_angle, current_angle + dtheta_menu));
- current_angle += dtheta_menu;
- }
- } else {
- self.title_pos = None;
- let mut cx = 8.0;
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- if self.w > total_width {
- cx = (self.w - total_width) / 2.0;
- }
- }
- if !self.title.is_empty() {
- cx += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &mut self.menus {
- let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
- menu.set_rect(x + cx, y, iw, h);
- menu.set_parent(Some(parent_ptr));
- cx += iw;
- }
- }
- }
- }
-
- fn set_network_opacity(&mut self, opacity: f32) {
- self.network_opacity = opacity;
- }
-
- fn color(&self) -> [f32; 4] {
- if !self.visible {
- [0.0, 0.0, 0.0, 0.0]
- } else if self.focused {
- let mut c = colors::PANEL_MENU_FOCUSED;
- c[3] *= self.network_opacity;
- c
- } else {
- let mut c = colors::PANEL_MENU_BG;
- c[3] *= self.network_opacity;
- c
- }
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.hovering = v;
- }
-
- fn hovered(&self) -> bool {
- self.hovering
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let dx = px - ccx;
- let dy = py - ccy;
- let dist = (dx * dx + dy * dy).sqrt();
- if dist >= ccr - self.h && dist <= ccr {
- let angle = dy.atan2(dx);
- let mut norm_angle = angle;
- if norm_angle < 0.0 {
- norm_angle += 2.0 * std::f32::consts::PI;
- }
-
- let r_mid = ccr - self.h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let end_angle = 1.5 * std::f32::consts::PI + total_angular_width / 2.0;
-
- if norm_angle >= start_angle && norm_angle <= end_angle {
- return true;
- }
- }
- for menu in &self.menus {
- if menu.hit_test(px, py) {
- return true;
- }
- }
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
- return true;
- }
- for menu in &self.menus {
- if menu.hit_test(px, py) {
- return true;
- }
- }
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- self.set_rect(rx, ry, rw, rh);
-
- if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/home/lsgalante/Dropbox/Clear/debug.txt") {
- use std::io::Write;
- let _ = writeln!(f, "MenuBar::on_cursor_moved px={}, py={} curved={:?} rect={:?}", px, py, self.curved_circle, (rx, ry, rw, rh));
- }
-
- let mut changed = false;
- self.hovered_menu = None;
- for (idx, menu) in self.menus.iter_mut().enumerate() {
- if menu.cursor_moved(px, py) {
- changed = true;
- }
- if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/home/lsgalante/Dropbox/Clear/debug.txt") {
- use std::io::Write;
- let _ = writeln!(f, " Menu[{}] active_title={} curved={:?} hovered={} hit={}", idx, menu.active_title(), menu.curved_arc, menu.hovered(), menu.hit_test(px, py));
- }
- if menu.hovered() {
- self.hovered_menu = Some(idx);
- }
- }
- changed
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- self.set_rect(rx, ry, rw, rh);
-
- let mut changed = false;
- for menu in &mut self.menus {
- let res = menu.mouse_input(button, state, px, py);
- if res {
- changed = true;
- }
- }
- if !self.is_menu_open() {
- self.unfocus();
- }
- changed
- }
-
- fn focus(&mut self) {
- if self.is_menu_open() {
- self.focused = true;
- for menu in &mut self.menus {
- if menu.is_menu_open() {
- menu.focus();
- return;
- }
- }
- } else {
- self.focused = false;
- focus::clear_if_matches(self);
- return;
- }
- self.focused = true;
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- self.focused = false;
- focus::clear_if_matches(self);
- for menu in &mut self.menus {
- menu.unfocus();
- }
- }
-
- fn focused(&self) -> bool {
- self.focused || self.is_menu_open()
- }
-
- fn set_selected(&mut self, selected: bool) {
- self.focused = selected;
- if !selected {
- for menu in &mut self.menus {
- menu.set_selected(false);
- }
- }
- }
-
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- for menu in &mut self.menus {
- menu.set_modifiers(ctrl, shift, alt);
- }
- }
-
- fn menu_names(&self) -> Vec<String> {
- self.menu_items.clone()
- }
-
- fn menu_click(&mut self) -> Option<(usize, usize)> {
- for (idx, menu) in self.menus.iter_mut().enumerate() {
- if let Some((_, item_idx)) = menu.menu_click() {
- return Some((idx, item_idx));
- }
- }
- 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);
- }
- }
- if let Some(menu) = self.menus.get_mut(menu_idx) {
- menu.set_item_checked(0, item_idx, 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()];
- }
- if let Some(menu) = self.menus.get_mut(menu_idx) {
- menu.items = items.to_vec();
- menu.item_checked = vec![Some(false); items.len()];
- menu.item_bufs.clear();
- }
- }
-
- fn is_menu_bar(&self) -> bool {
- self.visible
- }
-
- 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 (idx, menu) in self.menus.iter().enumerate() {
- if menu.hit_test(px, py) {
- let mut formatted_items = Vec::new();
- for (i, item) in menu.items.iter().enumerate() {
- let checked = menu.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "✓ ",
- Some(false) => " ",
- None => "",
- };
- formatted_items.push(format!("{}{}", prefix, item));
- }
- return Some((idx, menu.active_title().to_string(), formatted_items, menu.base.x, menu.base.y, menu.base.w, menu.base.h));
- }
- }
- None
- }
-
- fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
- if let Some(menu) = self.menus.get_mut(menu_idx) {
- menu.clicked_item = Some(item_idx);
- }
- }
-
- fn is_menu_open(&self) -> bool {
- self.visible && self.menus.iter().any(|m| m.is_menu_open())
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
- for menu in &self.menus {
- quads.extend(menu.extra_quads());
- }
- quads
- }
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut arcs = Vec::new();
- for menu in &self.menus {
- arcs.extend(menu.extra_arcs());
- }
- arcs
- }
-
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- if !self.visible {
- return;
- }
- if !self.title.is_empty() {
- if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
- if self.curved_title_char_bufs.len() != self.title.chars().count() {
- self.curved_title_char_bufs = self.title.chars()
- .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
- .collect();
- }
- self.title_buf = None;
- } else {
- if self.title_buf.is_none() {
- self.title_buf = Some(make_widget_text_buffer(fs, &self.title, 12.0, "Outfit"));
- }
- self.curved_title_char_bufs.clear();
- }
- } else {
- self.title_buf = None;
- self.curved_title_char_bufs.clear();
- }
- for menu in &mut self.menus {
- menu.prepare_text(fs);
- }
- }
-
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- if !self.visible {
- return Vec::new();
- }
- let mut items = Vec::new();
- let color = glyphon::Color::rgb(0xaa, 0xaa, 0xbb);
-
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - self.h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let mut current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = self.title.len() as f32 * 7.5 + 24.0;
- let dtheta_title = title_w / r_mid;
-
- let char_widths: Vec<f32> = self.title.chars().map(|c| {
- TextLabel::estimate_width(&c.to_string(), 12.0)
- }).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() {
- 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() - 12.0 / 2.0;
-
- items.push((c_buf, tx, ty, color));
- cur_char_angle += dtheta;
- }
- current_angle += dtheta_title;
- }
- } else {
- let mut start_x = 8.0;
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- if self.w > total_width {
- start_x = (self.w - total_width) / 2.0;
- }
- }
- if let Some(ref title_buf) = self.title_buf {
- items.push((title_buf, self.x + start_x, self.y + 7.0, color));
- }
- }
-
- for menu in &self.menus {
- items.extend(menu.get_text_items());
- }
- items
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let mut labels = Vec::new();
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - self.h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let mut current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = self.title.len() as f32 * 7.5 + 24.0;
- let dtheta_title = title_w / r_mid;
- labels.extend(TextLabel::curved_layout(
- &self.title,
- ccx, ccy, r_mid,
- current_angle, current_angle + dtheta_title,
- 12.0,
- [0xaa, 0xaa, 0xbb],
- ));
- current_angle += dtheta_title;
- }
- } else {
- let mut start_x = 8.0;
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- if self.w > total_width {
- start_x = (self.w - total_width) / 2.0;
- }
- }
- if !self.title.is_empty() {
- labels.push(TextLabel {
- text: self.title.clone(),
- x: self.x + start_x,
- y: self.y + 7.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- }
- }
- for menu in &self.menus {
- labels.extend(menu.text_labels());
- }
- labels
- }
-
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- for menu in &mut self.menus {
- menu.set_visible(visible);
- }
- }
-
- fn visible(&self) -> bool {
- self.visible
- }
-
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> {
- self.menus.iter().map(|m| {
- let ptr: *const dyn Widget = &**m as &dyn Widget;
- ptr as *mut (dyn Widget + 'static)
- }).collect()
- }
-
- fn z_index(&self) -> i32 {
- self.z_level
- }
-
- fn set_center_items(&mut self, center: bool) {
- self.center_items = center;
- }
-
- 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()
- }
-}
-
-impl Drop for MenuBar {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Menu {
- pub base: WidgetBase,
- pub title: String,
- pub vertical_title: String,
- pub items: Vec<String>,
- pub item_checked: Vec<Option<bool>>,
- pub open: bool,
- pub vertical: bool,
- hovered_item: Option<usize>,
- clicked_item: Option<usize>,
- was_open: Option<usize>,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
- pub curved_arc: Option<(f32, f32, f32, f32, f32, f32)>,
- pub title_buf: Option<glyphon::Buffer>,
- pub item_bufs: Vec<glyphon::Buffer>,
- pub check_buf: Option<glyphon::Buffer>,
- pub curved_char_bufs: Vec<glyphon::Buffer>,
-}
-
-impl Menu {
- pub fn new(title: &str, vertical_title: &str, items: &[String]) -> Self {
- Self {
- base: WidgetBase::new(),
- title: title.to_string(),
- vertical_title: vertical_title.to_string(),
- items: items.to_vec(),
- item_checked: vec![None; items.len()],
- open: false,
- vertical: false,
- hovered_item: None,
- clicked_item: None,
- was_open: None,
- parent: None,
- children: Vec::new(),
- curved_arc: None,
- title_buf: None,
- item_bufs: Vec::new(),
- check_buf: None,
- curved_char_bufs: Vec::new(),
- }
- }
-
- pub fn active_title(&self) -> &str {
- if self.vertical {
- &self.vertical_title
- } else {
- &self.title
- }
- }
-
- fn dropdown_rect(&self) -> (f32, f32, f32, f32) {
- let dh = self.items.len() as f32 * DROPDOWN_ITEM_H;
- let mut max_len = 0;
- for item in &self.items {
- max_len = max_len.max(item.len());
- }
- let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
- let dx = if self.vertical {
- self.base.x + self.base.w
- } else {
- self.base.x
- };
- let dy = if self.vertical {
- self.base.y
- } else {
- self.base.y + self.base.h
- };
- (dx, dy, dw, dh)
- }
-}
-impl Widget for Menu {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn label(&self) -> Option<String> {
- Some(self.active_title().to_string())
- }
-
- fn color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let dx = px - cx;
- let dy = py - cy;
- let dist = (dx * dx + dy * dy).sqrt();
- if dist >= r - thickness && dist <= r {
- let angle = dy.atan2(dx);
- let mut norm_angle = angle;
- if norm_angle < 0.0 {
- norm_angle += 2.0 * std::f32::consts::PI;
- }
- if norm_angle >= start_angle && norm_angle <= end_angle {
- return true;
- }
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
- return true;
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.was_open = None;
- let was_hovering = self.base.hovered;
- self.base.hovered = self.hit_test(px, py);
- let old_item = self.hovered_item;
- self.hovered_item = None;
-
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.items.len() {
- self.hovered_item = Some(di);
- }
- }
- }
-
- was_hovering != self.base.hovered || old_item != self.hovered_item
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left || state != ElementState::Pressed {
- return false;
- }
- if !self.hit_test(px, py) {
- return false;
- }
-
- // Check dropdown click if open
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.items.len() {
- self.clicked_item = Some(di);
- self.open = false;
- return true;
- }
- }
- }
-
- // Since we passed hit_test and didn't click dropdown, it's a click on the header title
- if self.was_open == Some(0) || self.open {
- self.open = false;
- self.was_open = None;
- } else {
- self.open = true;
- self.was_open = None;
- focus::set_focused(self);
- }
- true
- }
-
- fn focus(&mut self) {
- self.open = true;
- self.base.focused = true;
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- if self.open {
- self.was_open = Some(0);
- }
- self.open = false;
- self.base.focused = false;
- focus::clear_if_matches(self);
- self.hovered_item = None;
- }
-
- fn set_selected(&mut self, selected: bool) {
- self.base.focused = selected;
- if !selected {
- self.open = false;
- self.was_open = None;
- self.hovered_item = None;
- focus::clear_if_matches(self);
- }
- }
-
- 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 highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- if self.curved_arc.is_some() {
- None
- } else {
- let hc = self.highlight_color()?;
- Some((self.base.x, self.base.y, self.base.w, self.base.h, hc))
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 {
- quads.push((dx, dy, dw, dh, colors::PANEL_MENU_BG));
- if let Some(di) = self.hovered_item {
- quads.push((dx, dy + di as f32 * DROPDOWN_ITEM_H, dw, DROPDOWN_ITEM_H, colors::PANEL_MENU_HOVER));
- }
- }
- }
- quads
- }
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- let mut arcs = Vec::new();
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- if self.base.hovered && !self.open {
- arcs.push((cx, cy, r, thickness, start_angle, end_angle, colors::PANEL_MENU_HOVER));
- }
- }
- arcs
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let r_mid = r - thickness / 2.0;
- labels.extend(TextLabel::curved_layout(
- &self.active_title(),
- cx, cy, r_mid,
- start_angle, end_angle,
- 12.0,
- [0xcc, 0xcc, 0xd4],
- ));
- } else {
- labels.push(TextLabel {
- text: self.active_title().to_string(),
- x: self.base.x + 8.0,
- y: self.base.y + 7.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- }
- if self.open {
- let (dx, dy, _, _) = self.dropdown_rect();
- for (i, item) in self.items.iter().enumerate() {
- let checked = self.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "\u{2713} ",
- Some(false) => " ",
- None => "",
- };
- labels.push(TextLabel {
- text: format!("{}{}", prefix, item),
- x: dx + 8.0,
- y: dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- }
- }
- labels
- }
-
- fn set_visible(&mut self, visible: bool) {
- self.base.hovered = false;
- if !visible {
- self.open = false;
- self.was_open = None;
- self.hovered_item = None;
- focus::clear_if_matches(self);
- }
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) {
- self.parent = parent;
- }
-
- fn z_index(&self) -> i32 {
- 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) -> bool {
- self.base.focused
- }
-
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- let title_text = self.active_title();
- if let Some((_cx, _cy, _r, _thickness, _start_angle, _end_angle)) = self.curved_arc {
- if self.curved_char_bufs.len() != title_text.chars().count() {
- self.curved_char_bufs = title_text.chars()
- .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
- .collect();
- }
- self.title_buf = None;
- } else {
- if self.title_buf.is_none() {
- self.title_buf = Some(make_widget_text_buffer(fs, title_text, 12.0, "Outfit"));
- }
- self.curved_char_bufs.clear();
- }
-
- if self.open {
- if self.item_bufs.len() != self.items.len() {
- self.item_bufs = self.items.iter().enumerate().map(|(i, item)| {
- let checked = self.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "\u{2713} ",
- Some(false) => " ",
- None => "",
- };
- let text = format!("{}{}", prefix, item);
- make_widget_text_buffer(fs, &text, 12.0, "Outfit")
- }).collect();
- }
- } else {
- self.item_bufs.clear();
- }
- }
-
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- let mut items = Vec::new();
- let color = glyphon::Color::rgb(0xcc, 0xcc, 0xd4);
-
- 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(), 12.0)
- }).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() - 12.0 / 2.0;
-
- items.push((c_buf, tx, ty, color));
- cur_char_angle += dtheta;
- }
- }
- } else {
- if let Some(ref title_buf) = self.title_buf {
- items.push((title_buf, self.base.x + 8.0, self.base.y + 7.0, color));
- }
- }
-
- 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,
- ));
- }
- }
-
- items
- }
-}
-
-unsafe impl Send for Menu {}
-unsafe impl Sync for Menu {}
-
-impl Drop for Menu {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ButtonKind {
- Primary,
- Reset,
- ListRow,
- CopyIcon,
-}
-
-#[derive(Debug, Clone)]
-pub struct Button {
- base: WidgetBase,
- pressed: bool,
- just_clicked: bool,
- kind: ButtonKind,
- pub selected: bool,
-}
-
-impl Button {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: WidgetBase::new_rect(x, y, w, h),
- pressed: false,
- just_clicked: false,
- kind: ButtonKind::Primary,
- selected: false,
- }
- }
-
- pub fn new_reset(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: WidgetBase::new_rect(x, y, w, h),
- pressed: false,
- just_clicked: false,
- kind: ButtonKind::Reset,
- selected: false,
- }
- }
-
- pub fn new_list_row(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: WidgetBase::new_rect(x, y, w, h),
- pressed: false,
- just_clicked: false,
- kind: ButtonKind::ListRow,
- selected: false,
- }
- }
-
- pub fn new_copy_icon(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: WidgetBase::new_rect(x, y, w, h),
- pressed: false,
- just_clicked: false,
- kind: ButtonKind::CopyIcon,
- selected: false,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_selected(mut self, selected: bool) -> Self {
- self.selected = selected;
- self
- }
-}
-
-impl Widget for Button {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn top_room(&self) -> f32 { 0.0 }
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
-
- fn color(&self) -> [f32; 4] {
- match self.kind {
- ButtonKind::Primary => {
- if self.pressed { colors::BUTTON_PRESS }
- else if self.base.hovered { colors::BUTTON_HOVER }
- else { colors::BUTTON_IDLE }
- }
- ButtonKind::Reset => {
- if self.pressed { colors::RESET_BTN_PRESS }
- else if self.base.hovered { colors::RESET_BTN_HOVER }
- else { colors::RESET_BTN_IDLE }
- }
- ButtonKind::ListRow => {
- if self.selected {
- if self.pressed { [0.30, 0.52, 0.78, 0.6] }
- else if self.base.hovered { [0.30, 0.52, 0.78, 0.5] }
- else { [0.20, 0.40, 0.65, 0.4] }
- } else {
- if self.pressed { [0.20, 0.20, 0.25, 0.25] }
- else if self.base.hovered { [0.20, 0.20, 0.25, 0.15] }
- else { [0.0, 0.0, 0.0, 0.0] }
- }
- }
- ButtonKind::CopyIcon => {
- if self.selected {
- if self.pressed { [0.30, 0.52, 0.78, 0.5] }
- else if self.base.hovered { [0.30, 0.52, 0.78, 0.5] }
- else { [0.20, 0.40, 0.65, 0.2] }
- } else {
- if self.pressed { [0.20, 0.20, 0.25, 0.25] }
- else if self.base.hovered { [0.20, 0.20, 0.25, 0.25] }
- else { [0.0, 0.0, 0.0, 0.0] }
- }
- }
- }
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- self.pressed = true;
- return true;
- }
- }
- ElementState::Released => {
- if self.pressed && self.hit_test(px, py) {
- self.just_clicked = true;
- }
- let was = self.pressed;
- self.pressed = false;
- return was;
- }
- }
- false
- }
-
- fn take_click(&mut self) -> bool {
- if self.just_clicked { self.just_clicked = false; true } else { false }
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
- let font_size = 12.0;
- let est_w = if label == "📋" {
- 12.0
- } else {
- TextLabel::estimate_width(label, font_size)
- };
- let color = match self.kind {
- ButtonKind::ListRow | ButtonKind::CopyIcon => {
- if self.selected { [230, 230, 242] }
- else { [178, 178, 191] }
- }
- _ => [0xcc, 0xcc, 0xd4]
- };
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + (self.base.w - est_w) / 2.0,
- y: self.base.y + (self.base.h - font_size) / 2.0 - 1.0,
- font_size,
- color,
- });
- }
- labels
- }
- fn set_selected(&mut self, selected: bool) {
- self.selected = selected;
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- vec![(self.base.x, self.base.y, self.base.w, self.base.h, self.color())]
- }
-}
-
-pub enum PageButton {
- Active,
- Inactive,
-}
-
-pub struct Sidebar {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl Sidebar {
- pub fn new(w: f32) -> Self { Self { x: 0.0, y: 0.0, w, h: 0.0, hovered: false } }
-}
-
-impl Widget for Sidebar {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::sidebar_bg_color() }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-}
-
-pub struct Panel {
- base: WidgetBase,
- dragging: bool,
- drag_ox: f32, drag_oy: f32,
- drag_start_x: f32, drag_start_y: f32,
- bounds: Option<(f32, f32, f32, f32)>,
-}
-
-impl Panel {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: WidgetBase::new_rect(x, y, w, h),
- dragging: false,
- drag_ox: 0.0,
- drag_oy: 0.0,
- drag_start_x: 0.0,
- drag_start_y: 0.0,
- bounds: None,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-}
-
-impl Widget for Panel {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn top_room(&self) -> f32 { 0.0 }
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
-
- fn color(&self) -> [f32; 4] { if self.dragging { colors::PANEL_DRAG } else { colors::PANEL_IDLE } }
-
- fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- self.drag_begin(px, py);
- return true;
- }
- }
- ElementState::Released => {
- if self.dragging { self.drag_end(); return true; }
- }
- }
- false
- }
-
- fn is_dragging(&self) -> bool { self.dragging }
- fn draggable(&self) -> bool { true }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- 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))
- } else {
- (nx, ny)
- };
- if (nx - self.base.x).abs() > 0.01 || (ny - self.base.y).abs() > 0.01 {
- self.base.x = nx;
- self.base.y = ny;
- return true;
- }
- false
- }
-
- 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;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-}
-
-pub struct Node {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- selected: bool,
- dragging: bool,
- drag_ox: f32, drag_oy: f32,
- bounds: Option<(f32, f32, f32, f32)>,
- grid_snap_x: f32,
- grid_snap_y: f32,
- grid_origin_x: f32,
- grid_origin_y: f32,
- name: String,
- pub parameters: Vec<(String, String, String)>,
- geom_visible: bool,
- geom_toggled: bool,
- toggle_hovered: bool,
-}
-
-impl Node {
- pub fn new(x: f32, y: f32, w: f32, h: f32, name: &str) -> Self {
- Self {
- x, y, w, h,
- hovered: false, selected: false, dragging: false, drag_ox: 0.0, drag_oy: 0.0,
- bounds: None, grid_snap_x: 0.0, grid_snap_y: 0.0,
- grid_origin_x: 0.0, grid_origin_y: 0.0,
- name: name.to_string(), parameters: Vec::new(),
- geom_visible: true, geom_toggled: false, toggle_hovered: false,
- }
- }
-
- pub fn with_params(mut self, params: &[(&str, &str)]) -> Self {
- self.parameters = params.iter().map(|(k, v)| (k.to_string(), v.to_string(), "string".to_string())).collect();
- self
- }
-
- pub fn with_grid_snap(mut self, gx: f32, gy: f32) -> Self {
- self.grid_snap_x = gx;
- self.grid_snap_y = gy;
- self
- }
-
- 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)
- }
-}
-
-impl Widget for Node {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] {
- if self.dragging { colors::node_drag_color() }
- else if self.selected { colors::node_selected_color() }
- else { colors::node_color() }
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-
- fn focus(&mut self) {
- self.selected = true;
- focus::set_focused(self);
- }
- 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 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,
- font_size: 14.0,
- color: [0xcc, 0xcc, 0xd4],
- }]
- }
-
- fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was_hovered = self.hovered;
- self.hovered = self.hit_test(px, py);
-
- let was_toggle_hovered = self.toggle_hovered;
- let (tx, ty, tw, th) = self.toggle_rect();
- self.toggle_hovered = px >= tx && px < tx + tw && py >= ty && py < ty + th;
-
- was_hovered != self.hovered || was_toggle_hovered != self.toggle_hovered
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- let (tx, ty, tw, th) = self.toggle_rect();
- if px >= tx && px < tx + tw && py >= ty && py < ty + th {
- self.geom_visible = !self.geom_visible;
- self.geom_toggled = true;
- return true;
- }
- self.drag_begin(px, py);
- return true;
- }
- }
- ElementState::Released => {
- if self.dragging { self.drag_end(); return true; }
- }
- }
- false
- }
-
- 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;
- let (nx, ny) = if let Some((bx, by, bw, bh)) = self.bounds {
- (nx.clamp(bx, bx + bw - self.w), ny.clamp(by, by + bh - self.h))
- } else {
- (nx, ny)
- };
- let nx = if self.grid_snap_x > 0.0 {
- let relative = nx - self.grid_origin_x;
- let snapped = (relative / self.grid_snap_x).round() * self.grid_snap_x;
- snapped + self.grid_origin_x
- } else { nx };
- let ny = if self.grid_snap_y > 0.0 {
- let relative = ny - self.grid_origin_y;
- let snapped = (relative / self.grid_snap_y).round() * self.grid_snap_y;
- snapped + self.grid_origin_y
- } else { ny };
- if (nx - self.x).abs() > 0.01 || (ny - self.y).abs() > 0.01 {
- self.x = nx;
- self.y = ny;
- return true;
- }
- false
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- self.dragging = true;
- self.drag_ox = px - self.x;
- self.drag_oy = py - self.y;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let (tx, ty, tw, th) = self.toggle_rect();
- let bg_color = if self.toggle_hovered {
- colors::TOGGLE_HOVER
- } else {
- colors::TOGGLE_OFF
- };
-
- let mut quads = vec![
- (tx, ty, tw, th, bg_color)
- ];
-
- if self.geom_visible {
- let inset = 3.0;
- quads.push((tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, colors::TOGGLE_ON));
- }
-
- quads
- }
-
- 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) }
-}
-
-impl Drop for Node {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-fn menu_item_x(title: &str, menu_items: &[String], idx: usize) -> f32 {
- let mut x = 8.0;
- if !title.is_empty() {
- x += title.len() as f32 * 7.5 + 24.0;
- }
- for i in 0..idx {
- x += menu_items[i].len() as f32 * 7.5 + 16.0;
- }
- x
-}
-
-fn menu_item_w(menu_items: &[String], idx: usize) -> f32 {
- menu_items[idx].len() as f32 * 7.5 + 16.0
-}
-
-pub struct Checkbox {
- base: WidgetBase,
- checked: bool,
- just_clicked: bool,
-}
-
-impl Checkbox {
- pub fn new() -> Self {
- Self {
- base: WidgetBase::new(),
- checked: false,
- just_clicked: false,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_checked(&mut self, checked: bool) {
- self.checked = checked;
- }
-
- pub fn checked(&self) -> bool {
- self.checked
- }
-}
-
-impl Widget for Checkbox {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color(&self) -> [f32; 4] { if self.checked { colors::CHECKBOX_CHECKED } else { colors::CHECKBOX_BG } }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Released => {
- if self.hit_test(px, py) {
- self.checked = !self.checked;
- self.just_clicked = true;
- return true;
- }
- }
- _ => {}
- }
- false
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.checked {
- let (x, y, w, h) = self.rect();
- let pad_x = w * 0.25;
- let pad_y = h * 0.25;
- quads.push((x + pad_x, y + pad_y, w - 2.0 * pad_x, h - 2.0 * pad_y, [1.0, 1.0, 1.0, 0.9]));
- }
- quads
- }
-
- fn take_click(&mut self) -> bool {
- if self.just_clicked { self.just_clicked = false; true } else { false }
- }
- fn value(&self) -> i32 { if self.checked { 1 } else { 0 } }
-}
-
-#[derive(Debug, Clone)]
-pub struct Toggle {
- base: WidgetBase,
- toggled: bool,
- just_toggled: bool,
-}
-
-impl Toggle {
- pub fn new() -> Self {
- Self {
- base: WidgetBase::new(),
- toggled: false,
- just_toggled: false,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_label(&mut self, label: &str) {
- self.base.label = Some(label.to_string());
- }
-
- pub fn set_toggled(&mut self, v: bool) {
- self.toggled = v;
- }
-
- pub fn toggled(&self) -> bool {
- self.toggled
- }
-}
-
-impl Widget for Toggle {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color(&self) -> [f32; 4] { if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF } }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Released => {
- if self.hit_test(px, py) {
- self.toggled = !self.toggled;
- self.just_toggled = true;
- return true;
- }
- }
- _ => {}
- }
- false
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let bg = if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF };
- vec![(self.base.x, self.base.y, self.base.w, self.base.h, bg)]
- }
-
- fn take_click(&mut self) -> bool {
- if self.just_toggled { self.just_toggled = false; true } else { false }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Label {
- base: WidgetBase,
- font_size: f32,
- color: [u8; 3],
-}
-
-impl Label {
- pub fn new(text: &str) -> Self {
- let mut base = WidgetBase::new();
- base.label = Some(text.to_string());
- Self {
- base,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- }
- }
-
- pub fn with_font_size(mut self, size: f32) -> Self {
- self.font_size = size;
- self
- }
-
- pub fn with_color(mut self, color: [u8; 3]) -> Self {
- self.color = color;
- self
- }
-
- pub fn set_text(&mut self, text: &str) {
- self.base.label = Some(text.to_string());
- }
-
- pub fn set_color(&mut self, color: [u8; 3]) {
- self.color = color;
- }
-}
-
-impl Widget for Label {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn top_room(&self) -> f32 { 0.0 }
-
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- vec![TextLabel {
- text: self.base.label.clone().unwrap_or_default(),
- x: self.base.x,
- y: self.base.y + (self.base.h - self.font_size) / 2.0,
- font_size: self.font_size,
- color: self.color,
- }]
- }
-}
-
-#[derive(Clone)]
-pub struct SectionHeader {
- base: WidgetBase,
-}
-
-impl SectionHeader {
- pub fn new(title: &str) -> Self {
- let mut base = WidgetBase::new();
- base.label = Some(title.to_string());
- Self { base }
- }
-}
-
-impl Widget for SectionHeader {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn top_room(&self) -> f32 { 10.0 }
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- vec![(self.base.x + 8.0, self.base.y + 22.0, self.base.w - 16.0, 1.0, [0.18, 0.18, 0.27, 1.0])]
- }
- fn text_labels(&self) -> Vec<TextLabel> {
- vec![TextLabel {
- text: self.base.label.clone().unwrap_or_default(),
- x: self.base.x + 12.0,
- y: self.base.y,
- font_size: 14.0,
- color: [212, 212, 212],
- }]
- }
-}
-
-
-#[derive(Debug, Clone)]
-pub struct Svg {
- pub x: f32,
- pub y: f32,
- pub w: f32,
- pub h: f32,
- pub quads: Vec<(f32, f32, f32, f32, [f32; 4])>,
-}
-
-impl Svg {
- pub fn new(svg_data: &[u8], x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
- let opt = resvg::usvg::Options::default();
- let fontdb = resvg::usvg::fontdb::Database::new();
- let tree = resvg::usvg::Tree::from_data(svg_data, &opt, &fontdb).ok()?;
-
- let target_w = w as u32;
- let target_h = h as u32;
- if target_w == 0 || target_h == 0 {
- return None;
- }
- let mut pixmap = resvg::tiny_skia::Pixmap::new(target_w, target_h)?;
-
- let orig_w = tree.size().width();
- let orig_h = tree.size().height();
- let sx = target_w as f32 / orig_w;
- let sy = target_h as f32 / orig_h;
- let transform = resvg::tiny_skia::Transform::from_scale(sx, sy);
-
- resvg::render(&tree, transform, &mut pixmap.as_mut());
-
- let mut quads = Vec::new();
- let pixels = pixmap.data();
- for row in 0..target_h {
- for col in 0..target_w {
- let idx = ((row * target_w + 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;
- let g = pixels[idx + 1] as f32 / 255.0;
- let b = pixels[idx + 2] as f32 / 255.0;
- quads.push((
- x + col as f32,
- y + row as f32,
- 1.0,
- 1.0,
- [r, g, b, a],
- ));
- }
- }
- }
- }
-
- Some(Self { x, y, w, h, quads })
- }
-
- pub fn from_file<P: AsRef<std::path::Path>>(path: P, x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
- let data = std::fs::read(path).ok()?;
- Self::new(&data, x, y, w, h)
- }
-
- pub fn from_str(svg_str: &str, x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
- Self::new(svg_str.as_bytes(), x, y, w, h)
- }
-}
-
-impl Widget for Svg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- let dx = x - self.x;
- let dy = y - self.y;
- for quad in &mut self.quads {
- quad.0 += dx;
- quad.1 += dy;
- }
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- }
-
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- self.quads.clone()
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Slider {
- base: WidgetBase,
- dragging: bool,
- value: f32,
- drag_offset: f32,
- scroll_enabled: bool,
- show_readout: bool,
- editing: bool,
- edit_buffer: String,
- min: f32,
- max: f32,
-}
-
-impl Slider {
- pub fn new() -> Self {
- Self {
- base: WidgetBase::new(),
- dragging: false,
- value: 0.5,
- drag_offset: 0.0,
- scroll_enabled: false,
- show_readout: false,
- editing: false,
- edit_buffer: String::new(),
- min: 0.0,
- max: 1.0,
- }
- }
-
- pub fn with_range(mut self, min: f32, max: f32) -> Self {
- self.min = min;
- self.max = max;
- self
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_label(&mut self, label: &str) {
- self.base.label = Some(label.to_string());
- }
-
- pub fn set_range(&mut self, min: f32, max: f32) {
- self.min = min;
- self.max = max;
- }
-
- pub fn with_scroll(mut self, enabled: bool) -> Self {
- self.scroll_enabled = enabled;
- self
- }
-
- pub fn set_scroll(&mut self, enabled: bool) {
- self.scroll_enabled = enabled;
- }
-
- pub fn with_value(mut self, val: f32) -> Self {
- self.value = val.clamp(0.0, 1.0);
- self
- }
-
- pub fn set_value(&mut self, val: f32) {
- self.value = val.clamp(0.0, 1.0);
- }
-
- pub fn with_readout(mut self, enabled: bool) -> Self {
- self.show_readout = enabled;
- self
- }
-
- pub fn set_readout(&mut self, enabled: bool) {
- self.show_readout = enabled;
- }
-
- pub fn value(&self) -> f32 {
- self.value
- }
-
- pub fn get_scaled_value(&self) -> f32 {
- self.min + self.value * (self.max - self.min)
- }
-}
-
-impl Widget for Slider {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color(&self) -> [f32; 4] {
- if self.show_readout {
- [0.0, 0.0, 0.0, 0.0]
- } else {
- colors::slider_track()
- }
- }
-
- fn draggable(&self) -> bool { true }
- fn is_dragging(&self) -> bool { self.dragging }
-
- fn drag_update(&mut self, px: f32, _py: f32) -> bool {
- let (track_x, track_w) = if self.show_readout {
- let readout_w = 60.0;
- let gap = 8.0;
- let tw = (self.base.w - readout_w - gap).max(10.0);
- (self.base.x, tw)
- } else {
- (self.base.x, self.base.w)
- };
- let thumb_size = self.base.h * 0.9;
- let range = track_w - thumb_size;
- if range > 0.0 {
- let raw = (px - self.drag_offset - track_x) / range;
- let new_val = raw.clamp(0.0, 1.0);
- if (new_val - self.value).abs() > 0.001 {
- self.value = new_val;
- 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 drag_begin(&mut self, px: f32, _py: f32) {
- self.dragging = true;
- let (track_x, track_w) = if self.show_readout {
- let readout_w = 60.0;
- let gap = 8.0;
- let tw = (self.base.w - readout_w - gap).max(10.0);
- (self.base.x, tw)
- } else {
- (self.base.x, self.base.w)
- };
- let thumb_size = self.base.h * 0.9;
- let thumb_x = track_x + self.value * (track_w - thumb_size);
- self.drag_offset = px - thumb_x;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.scroll_enabled {
- return false;
- }
- let (sx, sy, sw, sh) = self.rect();
- let (track_x, track_w) = if self.show_readout {
- let readout_w = 60.0;
- let gap = 8.0;
- let tw = (sw - readout_w - gap).max(10.0);
- (sx, tw)
- } else {
- (sx, sw)
- };
- if px >= track_x && px <= track_x + track_w && py >= sy && py <= sy + sh {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y,
- MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
- };
- let step = 0.02;
- let new_val = (self.value - scroll_amount * step).clamp(0.0, 1.0);
- if (new_val - self.value).abs() > 0.0001 {
- self.value = new_val;
- 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 mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
-
- if self.show_readout {
- let readout_w = 60.0;
- let rx = self.base.x + self.base.w - readout_w;
-
- if px >= rx && px <= rx + readout_w && py >= self.base.y && py <= self.base.y + self.base.h {
- if state == ElementState::Pressed {
- if !self.editing {
- self.editing = true;
- let scaled_val = self.min + self.value * (self.max - self.min);
- self.edit_buffer = format!("{:.2}", scaled_val);
- focus::set_focused(self);
- }
- }
- return true;
- }
- }
-
- match state {
- ElementState::Pressed => {
- let (track_x, track_w) = if self.show_readout {
- let readout_w = 60.0;
- let gap = 8.0;
- let tw = (self.base.w - readout_w - gap).max(10.0);
- (self.base.x, tw)
- } else {
- (self.base.x, self.base.w)
- };
- let thumb_size = self.base.h * 0.9;
- let thumb_x = track_x + self.value * (track_w - thumb_size);
-
- if px >= track_x && px <= track_x + track_w && py >= self.base.y && py <= self.base.y + self.base.h {
- self.dragging = true;
- self.drag_offset = px - thumb_x;
- return true;
- }
- false
- }
- ElementState::Released => {
- if self.dragging {
- self.dragging = false;
- return true;
- }
- false
- }
- }
- }
-
- fn unfocus(&mut self) {
- if self.editing {
- self.editing = false;
- if let Ok(new_val) = self.edit_buffer.parse::<f32>() {
- 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;
- }
- }
- }
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !self.editing { return false; }
- if event.state != ElementState::Pressed { return false; }
-
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if !self.edit_buffer.is_empty() {
- self.edit_buffer.pop();
- return true;
- }
- }
- Key::Named(NamedKey::Enter) => {
- self.unfocus();
- return true;
- }
- Key::Named(NamedKey::Escape) => {
- self.editing = false;
- return true;
- }
- Key::Character(s) => {
- for ch in s.chars() {
- if ch.is_ascii_digit() || ch == '.' || (ch == '-' && self.edit_buffer.is_empty()) {
- self.edit_buffer.push(ch);
- }
- }
- return true;
- }
- _ => {}
- }
- false
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
-
- let (track_x, track_w) = if self.show_readout {
- let readout_w = 60.0;
- let gap = 8.0;
- let tw = (self.base.w - readout_w - gap).max(10.0);
-
- quads.push((self.base.x, self.base.y, tw, self.base.h, colors::slider_track()));
-
- let rx = self.base.x + self.base.w - readout_w;
- let bg_color = if self.editing {
- [0.06, 0.10, 0.18, 1.0]
- } else {
- [0.10, 0.10, 0.13, 1.0]
- };
- quads.push((rx, self.base.y, readout_w, self.base.h, bg_color));
-
- if self.base.focused || self.editing {
- let border_color = [0.20, 0.50, 0.85, 1.0];
- let border_t = 1.0;
- quads.push((rx, self.base.y, readout_w, border_t, border_color));
- quads.push((rx, self.base.y + self.base.h - border_t, readout_w, border_t, border_color));
- quads.push((rx, self.base.y, border_t, self.base.h, border_color));
- quads.push((rx + readout_w - border_t, self.base.y, border_t, self.base.h, border_color));
- }
-
- (self.base.x, tw)
- } else {
- (self.base.x, self.base.w)
- };
-
- let thumb_size = self.base.h * 0.9;
- let thumb_x = track_x + self.value * (track_w - thumb_size);
- let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
- let thumb_color = if self.dragging {
- colors::SLIDER_THUMB_DRAG
- } else {
- colors::SLIDER_THUMB
- };
- quads.push((thumb_x, thumb_y, thumb_size, thumb_size, thumb_color));
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
-
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x,
- y: self.base.y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
-
- if self.show_readout {
- let readout_w = 60.0;
- let rx = self.base.x + self.base.w - readout_w;
- let ry = self.base.y + (self.base.h - 12.0) / 2.0;
-
- let text = if self.editing {
- self.edit_buffer.clone()
- } else {
- let scaled_val = self.min + self.value * (self.max - self.min);
- format!("{:.2}", scaled_val)
- };
-
- labels.push(TextLabel {
- text,
- x: rx + 8.0,
- y: ry,
- font_size: 12.0,
- color: [0xee, 0xee, 0xf0],
- });
- }
-
- labels
- }
-
- fn value(&self) -> i32 { (self.value * 100.0) as i32 }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ActiveThumb {
- Low,
- High,
-}
-
-pub struct RangeSlider {
- base: WidgetBase,
- value_low: f32,
- value_high: f32,
- active_thumb: Option<ActiveThumb>,
- drag_offset: f32,
-}
-
-impl RangeSlider {
- pub fn new() -> Self {
- Self {
- base: WidgetBase::new(),
- value_low: 0.2,
- value_high: 0.8,
- active_thumb: None,
- drag_offset: 0.0,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_values(mut self, low: f32, high: f32) -> Self {
- self.value_low = low.clamp(0.0, 1.0);
- self.value_high = high.clamp(self.value_low, 1.0);
- self
- }
-
- pub fn set_values(&mut self, low: f32, high: f32) {
- self.value_low = low.clamp(0.0, 1.0);
- self.value_high = high.clamp(self.value_low, 1.0);
- }
-
- pub fn values(&self) -> (f32, f32) {
- (self.value_low, self.value_high)
- }
-}
-
-impl Widget for RangeSlider {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color(&self) -> [f32; 4] { colors::slider_track() }
-
- fn draggable(&self) -> bool { true }
- fn is_dragging(&self) -> bool { self.active_thumb.is_some() }
-
- fn drag_update(&mut self, px: f32, _py: f32) -> bool {
- let Some(active) = self.active_thumb else { return false; };
- let thumb_size = self.base.h * 0.9;
- let range = self.base.w - thumb_size;
- if range <= 0.0 { return false; }
-
- let new_val = ((px - self.drag_offset - self.base.x) / range).clamp(0.0, 1.0);
- match active {
- ActiveThumb::Low => {
- let constrained = new_val.min(self.value_high);
- if (constrained - self.value_low).abs() > 0.001 {
- self.value_low = constrained;
- return true;
- }
- }
- ActiveThumb::High => {
- let constrained = new_val.max(self.value_low);
- if (constrained - self.value_high).abs() > 0.001 {
- self.value_high = constrained;
- return true;
- }
- }
- }
- false
- }
-
- fn drag_begin(&mut self, px: f32, _py: f32) {
- let thumb_size = self.base.h * 0.9;
- let range = self.base.w - thumb_size;
- let thumb_low_x = self.base.x + self.value_low * range;
- let thumb_high_x = self.base.x + self.value_high * range;
- let center_low = thumb_low_x + thumb_size / 2.0;
- let center_high = thumb_high_x + thumb_size / 2.0;
-
- let active = if (self.value_low - self.value_high).abs() < 0.001 {
- if px < center_low {
- ActiveThumb::Low
- } else {
- ActiveThumb::High
- }
- } else {
- let dist_low = (px - center_low).abs();
- let dist_high = (px - center_high).abs();
- if dist_low < dist_high {
- ActiveThumb::Low
- } else {
- ActiveThumb::High
- }
- };
-
- self.active_thumb = Some(active);
- let active_x = match active {
- ActiveThumb::Low => thumb_low_x,
- ActiveThumb::High => thumb_high_x,
- };
- self.drag_offset = px - active_x;
- }
-
- fn drag_end(&mut self) {
- self.active_thumb = None;
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let thumb_size = self.base.h * 0.9;
- let range = self.base.w - thumb_size;
- let thumb_low_x = self.base.x + self.value_low * range;
- let thumb_high_x = self.base.x + self.value_high * range;
-
- let thumb_y = self.base.y + (self.base.h - thumb_size) / 2.0;
-
- // Highlighted track segment
- let highlight_x = thumb_low_x + thumb_size / 2.0;
- let highlight_w = thumb_high_x - thumb_low_x;
- let highlight_y = self.base.y + self.base.h * 0.35;
- let highlight_h = self.base.h * 0.3;
-
- let low_color = if self.active_thumb == Some(ActiveThumb::Low) {
- colors::SLIDER_THUMB_DRAG
- } else {
- colors::SLIDER_THUMB
- };
-
- let high_color = if self.active_thumb == Some(ActiveThumb::High) {
- colors::SLIDER_THUMB_DRAG
- } else {
- colors::SLIDER_THUMB
- };
-
- vec![
- (highlight_x, highlight_y, highlight_w, highlight_h, colors::PROGRESS_FILL),
- (thumb_low_x, thumb_y, thumb_size, thumb_size, low_color),
- (thumb_high_x, thumb_y, thumb_size, thumb_size, high_color),
- ]
- }
-
- fn value(&self) -> i32 {
- ((self.value_low * 100.0) as i32) | (((self.value_high * 100.0) as i32) << 16)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Float3 {
- base: WidgetBase,
- pub values: [f32; 3],
- mins: [f32; 3],
- maxs: [f32; 3],
- labels: [String; 3],
- dragging_idx: Option<usize>,
- drag_offset: f32,
- pub editing_idx: Option<usize>,
- pub edit_buffer: String,
-}
-
-impl Float3 {
- pub fn new() -> Self {
- Self {
- base: WidgetBase::new(),
- values: [0.5, 0.5, 0.5],
- mins: [0.0, 0.0, 0.0],
- maxs: [1.0, 1.0, 1.0],
- labels: ["X".to_string(), "Y".to_string(), "Z".to_string()],
- dragging_idx: None,
- drag_offset: 0.0,
- editing_idx: None,
- edit_buffer: String::new(),
- }
- }
-
- pub fn with_values(mut self, values: [f32; 3]) -> Self {
- self.values = values;
- self
- }
-
- pub fn with_range(mut self, min: f32, max: f32) -> Self {
- self.mins = [min, min, min];
- self.maxs = [max, max, max];
- self
- }
-
- pub fn set_values(&mut self, values: [f32; 3]) {
- self.values = values;
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn get_row_rects(&self) -> Vec<(f32, f32, f32, f32)> {
- let mut rects = Vec::new();
- let by = self.base.y + 20.0;
- for i in 0..3 {
- rects.push((self.base.x + 8.0, by + 6.0 + i as f32 * 26.0, self.base.w - 16.0, 20.0));
- }
- rects
- }
-}
-
-impl Widget for Float3 {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- 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 draggable(&self) -> bool { self.dragging_idx.is_some() }
- fn is_dragging(&self) -> bool { self.dragging_idx.is_some() }
-
- fn drag_begin(&mut self, _px: f32, _py: f32) {}
-
- fn drag_update(&mut self, px: f32, _py: f32) -> bool {
- if let Some(i) = self.dragging_idx {
- let track_x = self.base.x + 100.0;
- let track_w = self.base.w - 188.0;
- let thumb_size = 12.0 * 0.9;
- let range = track_w - thumb_size;
- if range > 0.0 {
- let raw = (px - self.drag_offset - track_x) / range;
- let new_val = raw.clamp(0.0, 1.0);
- if (new_val - self.values[i]).abs() > 0.001 {
- self.values[i] = new_val;
- if self.editing_idx == Some(i) {
- let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
- self.edit_buffer = format!("{:.2}", scaled_val);
- }
- return true;
- }
- }
- }
- false
- }
-
- fn drag_end(&mut self) {
- self.dragging_idx = None;
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- let rects = self.get_row_rects();
-
- for i in 0..3 {
- let r = rects[i];
- let rx = self.base.x + self.base.w - 68.0;
- let ry = r.1 + 4.0;
- let rh = 12.0;
- let readout_w = 60.0;
-
- if px >= rx && px <= rx + readout_w && py >= ry && py <= ry + rh {
- if state == ElementState::Pressed {
- if self.editing_idx != Some(i) {
- self.unfocus();
- self.editing_idx = Some(i);
- let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
- self.edit_buffer = format!("{:.2}", scaled_val);
- focus::set_focused(self);
- }
- }
- return true;
- }
- }
-
- if state == ElementState::Pressed {
- for i in 0..3 {
- let r = rects[i];
- let track_x = self.base.x + 100.0;
- let track_w = self.base.w - 188.0;
- let track_y = r.1 + 4.0;
- let track_h = 12.0;
- let thumb_size = track_h * 0.9;
- let range = track_w - thumb_size;
- let thumb_x = track_x + self.values[i] * range;
-
- if px >= track_x && px <= track_x + track_w && py >= track_y && py <= track_y + track_h {
- self.dragging_idx = Some(i);
- self.drag_offset = px - thumb_x;
- return true;
- }
- }
- } else if state == ElementState::Released {
- if self.dragging_idx.is_some() {
- self.dragging_idx = None;
- return true;
- }
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- let _idx = match self.editing_idx {
- Some(i) => i,
- None => return false,
- };
- if event.state != ElementState::Pressed { return false; }
-
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if !self.edit_buffer.is_empty() {
- self.edit_buffer.pop();
- return true;
- }
- }
- Key::Named(NamedKey::Enter) => {
- self.unfocus();
- return true;
- }
- Key::Named(NamedKey::Escape) => {
- self.editing_idx = None;
- return true;
- }
- Key::Character(s) => {
- for ch in s.chars() {
- if ch.is_ascii_digit() || ch == '.' || (ch == '-' && self.edit_buffer.is_empty()) {
- self.edit_buffer.push(ch);
- }
- }
- return true;
- }
- _ => {}
- }
- false
- }
-
- fn unfocus(&mut self) {
- if let Some(i) = self.editing_idx.take() {
- if let Ok(new_val) = self.edit_buffer.parse::<f32>() {
- let range = self.maxs[i] - self.mins[i];
- if range != 0.0 {
- self.values[i] = ((new_val - self.mins[i]) / range).clamp(0.0, 1.0);
- } else {
- self.values[i] = 0.0;
- }
- }
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
-
- // Outline border box
- let bx = self.base.x + 4.0;
- let bw = self.base.w - 8.0;
- let by = self.base.y + 20.0;
- let bh = 84.0;
- let border_color = [0.18, 0.18, 0.27, 1.0];
- 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));
-
- let rects = self.get_row_rects();
- for i in 0..3 {
- let r = rects[i];
- let track_x = self.base.x + 100.0;
- let track_w = self.base.w - 188.0;
- let track_y = r.1 + 4.0;
- let track_h = 12.0;
-
- quads.push((track_x, track_y, track_w, track_h, colors::slider_track()));
-
- let thumb_size = track_h * 0.9;
- let range = track_w - thumb_size;
- let thumb_x = track_x + self.values[i] * range;
- let thumb_color = if self.dragging_idx == Some(i) {
- colors::SLIDER_THUMB_DRAG
- } else {
- colors::SLIDER_THUMB
- };
- quads.push((thumb_x, track_y + (track_h - thumb_size)/2.0, thumb_size, thumb_size, thumb_color));
-
- let rx = self.base.x + self.base.w - 68.0;
- let bg_color = if self.editing_idx == Some(i) {
- [0.06, 0.10, 0.18, 1.0]
- } else {
- [0.10, 0.10, 0.13, 1.0]
- };
- quads.push((rx, track_y, 60.0, track_h, bg_color));
-
- if self.editing_idx == Some(i) {
- let border_color = [0.20, 0.50, 0.85, 1.0];
- quads.push((rx, track_y, 60.0, border_t, border_color));
- quads.push((rx, track_y + track_h - border_t, 60.0, border_t, border_color));
- quads.push((rx, track_y, border_t, track_h, border_color));
- quads.push((rx + 60.0 - border_t, track_y, border_t, track_h, border_color));
- }
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
-
- if let Some(ref l) = self.base.label {
- labels.push(TextLabel {
- text: l.clone(),
- x: self.base.x + 8.0,
- y: self.base.y + 2.0,
- font_size: 13.0,
- color: [0xee, 0xee, 0xf0],
- });
- }
-
- let rects = self.get_row_rects();
- for i in 0..3 {
- let r = rects[i];
- let track_y = r.1 + 4.0;
- let ry = track_y;
-
- labels.push(TextLabel {
- text: self.labels[i].clone(),
- x: self.base.x + 16.0,
- y: ry - 2.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
-
- let rx = self.base.x + self.base.w - 68.0;
- let text = if self.editing_idx == Some(i) {
- self.edit_buffer.clone()
- } else {
- let scaled_val = self.mins[i] + self.values[i] * (self.maxs[i] - self.mins[i]);
- format!("{:.2}", scaled_val)
- };
- labels.push(TextLabel {
- text,
- x: rx + 8.0,
- y: ry - 2.0,
- font_size: 12.0,
- color: [0xee, 0xee, 0xf0],
- });
- }
- labels
- }
-}
-
-
-pub struct ProgressBar {
- base: WidgetBase,
- value: f32,
-}
-
-impl ProgressBar {
- pub fn new(value: f32) -> Self {
- Self {
- base: WidgetBase::new(),
- value,
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-}
-
-impl Widget for ProgressBar {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
-
- fn color(&self) -> [f32; 4] { colors::PROGRESS_BG }
-}
-
-fn make_widget_text_buffer(fs: &mut glyphon::FontSystem, text: &str, size: f32, font_family: &str) -> glyphon::Buffer {
- let metrics = glyphon::Metrics::new(size, size * 1.4);
- let mut buf = glyphon::Buffer::new(fs, metrics);
- let attrs = glyphon::Attrs::new().family(glyphon::Family::Name(font_family));
- buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
- buf.shape_until_scroll(fs, true);
- buf
-}
-
-pub struct StatusBar {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- pub text: String,
- pub text_buf: Option<glyphon::Buffer>,
- pub text_offset_x: Option<f32>,
- pub text_color: Option<[f32; 4]>,
- pub bg_color: Option<[f32; 4]>,
-}
-
-impl StatusBar {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
- text: String::new(),
- text_buf: None,
- text_offset_x: None,
- text_color: None,
- bg_color: None,
- }
- }
- pub fn with_text(mut self, text: &str) -> Self {
- self.text = text.to_string();
- self
- }
- pub fn with_text_offset_x(mut self, offset: f32) -> Self {
- self.text_offset_x = Some(offset);
- self
- }
- pub fn with_text_color(mut self, color: [f32; 4]) -> Self {
- self.text_color = Some(color);
- self
- }
- pub fn with_bg_color(mut self, color: [f32; 4]) -> Self {
- self.bg_color = Some(color);
- self
- }
- pub fn set_text_offset_x(&mut self, offset: f32) {
- self.text_offset_x = Some(offset);
- }
- pub fn set_text_color(&mut self, color: [f32; 4]) {
- self.text_color = Some(color);
- }
- pub fn set_bg_color(&mut self, color: [f32; 4]) {
- self.bg_color = Some(color);
- }
-}
-
-impl Widget for StatusBar {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { self.bg_color.unwrap_or(colors::STATUS_BG) }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn set_text(&mut self, text: &str) {
- if self.text != text {
- self.text = text.to_string();
- self.text_buf = None;
- }
- }
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- if !self.text.is_empty() && self.text_buf.is_none() {
- self.text_buf = Some(make_widget_text_buffer(fs, &self.text, 12.0, "Outfit"));
- }
- }
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- if let Some(ref text_buf) = self.text_buf {
- let offset_x = self.text_offset_x.unwrap_or(12.0);
- let color = self.text_color.map(|c| glyphon::Color::rgb(
- (c[0] * 255.0) as u8,
- (c[1] * 255.0) as u8,
- (c[2] * 255.0) as u8,
- )).unwrap_or_else(|| glyphon::Color::rgb(0xaa, 0xaa, 0xbb));
- vec![(text_buf, self.x + offset_x, self.y + 4.0, color)]
- } else {
- Vec::new()
- }
- }
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.text.is_empty() {
- let offset_x = self.text_offset_x.unwrap_or(12.0);
- let color = self.text_color.map(|c| [
- (c[0] * 255.0) as u8,
- (c[1] * 255.0) as u8,
- (c[2] * 255.0) as u8,
- ]).unwrap_or([0xaa, 0xaa, 0xbb]);
- vec![TextLabel {
- text: self.text.clone(),
- x: self.x + offset_x,
- y: self.y + 4.0,
- font_size: 12.0,
- color,
- }]
- } else {
- Vec::new()
- }
- }
-}
-
-pub struct Splitter {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- dragging: bool,
- drag_ox: f32,
-}
-
-impl Splitter {
- pub fn new(w: f32) -> Self {
- Self { x: 0.0, y: 0.0, w, h: 0.0, hovered: false, dragging: false, drag_ox: 0.0 }
- }
-}
-
-impl Widget for Splitter {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] {
- if self.dragging { colors::SPLITTER_DRAG }
- else if self.hovered { colors::SPLITTER_HOVER }
- else { colors::SPLITTER_IDLE }
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovered;
- self.hovered = self.hit_test(px, py);
- was != self.hovered
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- self.drag_begin(px, py);
- return true;
- }
- }
- ElementState::Released => {
- if self.dragging { self.drag_end(); return true; }
- }
- }
- false
- }
-
- fn is_dragging(&self) -> bool { self.dragging }
- fn draggable(&self) -> bool { true }
-
- fn drag_update(&mut self, px: f32, _py: f32) -> bool {
- let new_x = px - self.drag_ox;
- if (new_x - self.x).abs() > 0.5 {
- self.x = new_x;
- return true;
- }
- false
- }
-
- fn drag_begin(&mut self, px: f32, _py: f32) {
- self.dragging = true;
- self.drag_ox = px - self.x;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-}
-
-#[derive(Debug, Clone)]
-pub struct Spinbox {
- base: WidgetBase,
- pub value: i32,
- min: i32, max: i32, step: i32,
- pub editing: bool,
- pub edit_buffer: String,
- pub cursor_idx: usize,
- hover_dec: bool,
- hover_inc: bool,
- unit: Option<String>,
- pub decimals: u32,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
-}
-
-impl Spinbox {
- pub fn new(value: i32, min: i32, max: i32, step: i32) -> Self {
- Self {
- base: WidgetBase::new(),
- value,
- min,
- max,
- step,
- editing: false,
- edit_buffer: String::new(),
- cursor_idx: 0,
- hover_dec: false,
- hover_inc: false,
- unit: None,
- decimals: 0,
- parent: None,
- children: Vec::new(),
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_label(&mut self, label: &str) {
- self.base.label = Some(label.to_string());
- }
-
- pub fn with_unit(mut self, unit: &str) -> Self {
- self.unit = Some(unit.to_string());
- self
- }
-
- pub fn set_unit(&mut self, unit: &str) {
- self.unit = Some(unit.to_string());
- }
-
- pub fn with_decimals(mut self, decimals: u32) -> Self {
- self.decimals = decimals;
- self
- }
-}
-
-impl Widget for Spinbox {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color(&self) -> [f32; 4] { colors::SPINBOX_BG }
- fn value(&self) -> i32 { self.value }
- fn widget_font(&self) -> Option<String> { Some("monospace".to_string()) }
-
-
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.base.hovered;
- self.base.hovered = self.hit_test(px, py);
- if !self.base.hovered {
- let changed = self.hover_dec || self.hover_inc;
- self.hover_dec = false;
- self.hover_inc = false;
- return changed || was != self.base.hovered;
- }
- let split = self.base.x + self.base.w * 0.55;
- let hd = px >= split && px < split + self.base.w * 0.225;
- let hi = px >= split + self.base.w * 0.225;
- let changed = hd != self.hover_dec || hi != self.hover_inc;
- self.hover_dec = hd;
- self.hover_inc = hi;
- changed || was != self.base.hovered
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- if !self.hit_test(px, py) { return false; }
- match state {
- ElementState::Pressed => {
- let split = self.base.x + self.base.w * 0.55;
- if px >= split && px < split + self.base.w * 0.225 {
- self.value = (self.value - self.step).max(self.min);
- true
- } else if px >= split + self.base.w * 0.225 {
- self.value = (self.value + self.step).min(self.max);
- true
- } else if px < split {
- self.editing = true;
- if self.decimals > 0 {
- let divisor = 10.0f32.powi(self.decimals as i32);
- self.edit_buffer = format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize);
- } else {
- self.edit_buffer = self.value.to_string();
- }
- let char_width = 8.4;
- let click_idx = (((px - (self.base.x + 4.0)) / char_width).round() as isize)
- .max(0)
- .min(self.edit_buffer.chars().count() as isize) as usize;
- self.cursor_idx = click_idx;
- focus::set_focused(self);
- true
- } else {
- false
- }
- }
- ElementState::Released => {
- false
- }
- }
- }
-
- fn focus(&mut self) {
- self.editing = true;
- if self.decimals > 0 {
- let divisor = 10.0f32.powi(self.decimals as i32);
- self.edit_buffer = format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize);
- } else {
- self.edit_buffer = self.value.to_string();
- }
- self.cursor_idx = self.edit_buffer.chars().count();
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- if self.editing {
- self.editing = false;
- if self.decimals > 0 {
- if let Ok(val_f) = self.edit_buffer.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) = self.edit_buffer.parse::<i32>() {
- self.value = val.clamp(self.min, self.max);
- }
- }
- }
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !self.editing { return false; }
- if event.state != ElementState::Pressed { return false; }
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if self.cursor_idx > 0 {
- let mut chars: Vec<char> = self.edit_buffer.chars().collect();
- chars.remove(self.cursor_idx - 1);
- self.edit_buffer = chars.into_iter().collect();
- self.cursor_idx -= 1;
- return true;
- }
- false
- }
- Key::Named(NamedKey::Delete) => {
- if self.cursor_idx < self.edit_buffer.chars().count() {
- let mut chars: Vec<char> = self.edit_buffer.chars().collect();
- chars.remove(self.cursor_idx);
- self.edit_buffer = chars.into_iter().collect();
- return true;
- }
- false
- }
- Key::Named(NamedKey::ArrowLeft) => {
- if self.cursor_idx > 0 {
- self.cursor_idx -= 1;
- true
- } else {
- false
- }
- }
- Key::Named(NamedKey::ArrowRight) => {
- if self.cursor_idx < self.edit_buffer.chars().count() {
- self.cursor_idx += 1;
- true
- } else {
- false
- }
- }
- Key::Named(NamedKey::Enter) => {
- if self.decimals > 0 {
- if let Ok(val_f) = self.edit_buffer.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) = self.edit_buffer.parse::<i32>() {
- self.value = val.clamp(self.min, self.max);
- }
- }
- self.editing = false;
- true
- }
- Key::Named(NamedKey::Escape) => {
- self.editing = false;
- true
- }
- _ => {
- if let Some(text) = &event.text {
- for ch in text.chars() {
- match ch {
- '-' if self.cursor_idx == 0 && !self.edit_buffer.starts_with('-') => {
- self.edit_buffer.insert(0, '-');
- self.cursor_idx += 1;
- }
- '.' if self.decimals > 0 && !self.edit_buffer.contains('.') => {
- let mut chars: Vec<char> = self.edit_buffer.chars().collect();
- chars.insert(self.cursor_idx, '.');
- self.edit_buffer = chars.into_iter().collect();
- self.cursor_idx += 1;
- }
- '0'..='9' => {
- let mut chars: Vec<char> = self.edit_buffer.chars().collect();
- chars.insert(self.cursor_idx, ch);
- self.edit_buffer = chars.into_iter().collect();
- self.cursor_idx += 1;
- }
- _ => {}
- }
- }
- }
- true
- }
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let split = self.base.x + self.base.w * 0.55;
- let btn_w = self.base.w * 0.225;
- let inc_col = if self.hover_inc { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
- let dec_col = if self.hover_dec { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
-
- let display_bg = if self.editing {
- [0.06, 0.10, 0.18, 1.0] // Focused dark-blue input field look
- } else {
- colors::SPINBOX_DISPLAY
- };
-
- quads.push((self.base.x, self.base.y, self.base.w * 0.55, self.base.h, display_bg));
- quads.push((split, self.base.y, btn_w, self.base.h, dec_col));
- quads.push((split + btn_w, self.base.y, btn_w, self.base.h, inc_col));
-
- if self.editing {
- let border_color = [0.20, 0.50, 0.85, 1.0]; // Bright focused blue border
- // Top border
- quads.push((self.base.x, self.base.y, self.base.w * 0.55, 1.0, border_color));
- // Bottom border
- quads.push((self.base.x, self.base.y + self.base.h - 1.0, self.base.w * 0.55, 1.0, border_color));
- // Left border
- quads.push((self.base.x, self.base.y, 1.0, self.base.h, border_color));
- // Right border
- quads.push((self.base.x + self.base.w * 0.55 - 1.0, self.base.y, 1.0, self.base.h, border_color));
-
- // Caret cursor
- let char_width = 8.4;
- let cursor_x = self.base.x + 4.0 + (self.cursor_idx as f32 * char_width);
- let max_cursor_x = split - 4.0;
- let final_cursor_x = cursor_x.min(max_cursor_x);
- let cursor_y = self.base.y + (self.base.h - 14.0) / 2.0;
- quads.push((final_cursor_x, cursor_y, 1.5, 14.0, [0.80, 0.80, 0.85, 1.0]));
- }
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + 4.0,
- y: self.base.y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
- let value_text = if self.editing {
- self.edit_buffer.clone()
- } else if self.decimals > 0 {
- let divisor = 10.0f32.powi(self.decimals as i32);
- format!("{:.width$}", self.value as f32 / divisor, width = self.decimals as usize)
- } else {
- self.value.to_string()
- };
- labels.push(TextLabel {
- text: value_text,
- x: self.base.x + 4.0,
- y: self.base.y + (self.base.h - 14.0) / 2.0 - 2.0,
- font_size: 14.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- if let Some(ref unit) = self.unit {
- labels.push(TextLabel {
- text: unit.clone(),
- x: self.base.x + 4.0 + 36.0,
- y: self.base.y + (self.base.h - 11.0) / 2.0 - 2.0,
- font_size: 11.0,
- color: [0x73, 0x73, 0x7a],
- });
- }
- let split = self.base.x + self.base.w * 0.55;
- labels.push(TextLabel {
- text: "-".to_string(),
- x: self.base.x + self.base.w * 0.6625 - 4.0,
- y: self.base.y + (self.base.h - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- labels.push(TextLabel {
- text: "+".to_string(),
- x: self.base.x + self.base.w * 0.8875 - 4.0,
- y: self.base.y + (self.base.h - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- labels
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for Spinbox {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-#[derive(Debug)]
-pub struct ColorSelector {
- base: WidgetBase,
- pub color: [u8; 3],
- just_clicked: bool,
- pub editing: bool,
- edit_buffer: String,
- pub command: String,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
- child: std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>,
-}
-
-impl Clone for ColorSelector {
- fn clone(&self) -> Self {
- Self {
- base: self.base.clone(),
- color: self.color,
- just_clicked: self.just_clicked,
- editing: self.editing,
- edit_buffer: self.edit_buffer.clone(),
- command: self.command.clone(),
- parent: self.parent,
- children: self.children.clone(),
- child: std::sync::Arc::new(std::sync::Mutex::new(None)),
- }
- }
-}
-
-impl ColorSelector {
- pub fn new(color: [u8; 3]) -> Self {
- Self {
- base: WidgetBase::new(),
- color,
- just_clicked: false,
- editing: false,
- edit_buffer: String::new(),
- command: "clear-color-interface".to_string(),
- parent: None,
- children: Vec::new(),
- child: std::sync::Arc::new(std::sync::Mutex::new(None)),
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_command(mut self, command: &str) -> Self {
- self.command = command.to_string();
- self
- }
-}
-
-impl Widget for ColorSelector {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color_u8(&self) -> Option<[u8; 4]> {
- Some([self.color[0], self.color[1], self.color[2], 255])
- }
-
-
-
- fn color(&self) -> [f32; 4] {
- colors::to_linear([
- self.color[0] as f32 / 255.0,
- self.color[1] as f32 / 255.0,
- self.color[2] as f32 / 255.0,
- 1.0,
- ])
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- if state != ElementState::Pressed { return false; }
- if !self.hit_test(px, py) { return false; }
- if px >= self.base.x + self.base.w * 0.65 {
- let hex = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
- let mut child_guard = self.child.lock().unwrap();
- if let Some(mut old_child) = child_guard.take() {
- let _ = old_child.kill();
- }
- if let Ok(child) = std::process::Command::new(&self.command)
- .arg(&hex)
- .stdout(std::process::Stdio::piped())
- .spawn()
- {
- *child_guard = Some(child);
- }
- return true;
- }
- self.focus();
- true
- }
-
- fn take_click(&mut self) -> bool {
- if self.just_clicked { self.just_clicked = false; true } else { false }
- }
-
- fn tick(&mut self, _dt: f32) -> bool {
- let mut child_opt = self.child.lock().unwrap();
- if let Some(ref mut child) = *child_opt {
- match child.try_wait() {
- Ok(Some(_status)) => {
- let child = child_opt.take().unwrap();
- if let Ok(output) = child.wait_with_output() {
- let stdout_str = String::from_utf8_lossy(&output.stdout);
- for line in stdout_str.lines().rev() {
- if let Some(c) = parse_hex(line.trim()) {
- self.color = c;
- self.just_clicked = true;
- return true;
- }
- }
- }
- }
- Ok(None) => {}
- Err(e) => {
- eprintln!("Error checking color selector child process: {:?}", e);
- *child_opt = None;
- }
- }
- }
- false
- }
-
- fn focus(&mut self) {
- self.editing = true;
- self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- if self.editing {
- self.editing = false;
- if let Some(c) = parse_hex(&self.edit_buffer) {
- self.color = c;
- }
- }
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !self.editing { return false; }
- if event.state != ElementState::Pressed { return false; }
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- self.edit_buffer.pop();
- true
- }
- Key::Named(NamedKey::Enter) => {
- if let Some(c) = parse_hex(&self.edit_buffer) {
- self.color = c;
- }
- self.editing = false;
- true
- }
- Key::Named(NamedKey::Escape) => {
- self.editing = false;
- true
- }
- _ => {
- if let Some(text) = &event.text {
- for ch in text.chars() {
- match ch {
- '#' if self.edit_buffer.is_empty() => self.edit_buffer.push('#'),
- '0'..='9' | 'a'..='f' | 'A'..='F' => {
- if self.edit_buffer.len() < 7 { self.edit_buffer.push(ch.to_ascii_lowercase()); }
- }
- _ => {}
- }
- }
- }
- true
- }
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let pick_x = self.base.x + self.base.w * 0.65;
- let pick_w = self.base.w * 0.35;
-
- // Draw the text box part background and border
- let bg_color = if self.editing {
- [0.06, 0.10, 0.18, 1.0]
- } else {
- [0.08, 0.08, 0.12, 1.0]
- };
- let border_color = if self.editing {
- [0.20, 0.50, 0.85, 1.0]
- } else if self.base.hovered {
- [0.25, 0.25, 0.35, 1.0]
- } else {
- [0.18, 0.18, 0.24, 1.0]
- };
-
- // Background & border for hex text box part
- quads.push((self.base.x, self.base.y, self.base.w * 0.65, self.base.h, border_color));
- quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w * 0.65 - 2.0, self.base.h - 2.0, bg_color));
-
- let linear_c = colors::to_linear([
- self.color[0] as f32 / 255.0,
- self.color[1] as f32 / 255.0,
- self.color[2] as f32 / 255.0,
- 1.0,
- ]);
- let border_w = 1.0;
- let border_c = colors::color_borders_color();
-
- let r = border_c[0];
- let g = border_c[1];
- let b = border_c[2];
- let steps = 6;
- for i in (1..=steps).rev() {
- let offset = i as f32 * 0.75;
- let rx = pick_x - offset;
- let ry = self.base.y - offset;
- let rw = pick_w + 2.0 * offset;
- let rh = self.base.h + 2.0 * offset;
- let alpha = 0.08 * (1.0 - (i as f32 / steps as f32).powf(1.5));
- if alpha > 0.001 {
- quads.push((rx, ry, rw, rh, [r, g, b, alpha]));
- }
- }
-
- quads.push((pick_x, self.base.y, pick_w, self.base.h, border_c));
- quads.push((
- pick_x + border_w,
- self.base.y + border_w,
- pick_w - 2.0 * border_w,
- self.base.h - 2.0 * border_w,
- linear_c,
- ));
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + 4.0,
- y: self.base.y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
- let hex = if self.editing { self.edit_buffer.clone() } else { format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]) };
- labels.push(TextLabel {
- text: hex,
- x: self.base.x + 4.0,
- y: self.base.y + 3.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- labels
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for ColorSelector {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-const BREADCRUMB_PADDING: f32 = 8.0;
-const SEGMENT_GAP: f32 = 4.0;
-
-#[derive(Debug, Clone)]
-pub struct Breadcrumb {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- path: Vec<String>,
- hovered_seg: Option<usize>,
- clicked_seg: Option<usize>,
- pub network_opacity: f32,
-}
-
-impl Breadcrumb {
- pub fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false,
- path: Vec::new(), hovered_seg: None, clicked_seg: None, network_opacity: 1.0 }
- }
-
- fn seg_at(&self, px: f32) -> Option<usize> {
- let mut cx = self.x + BREADCRUMB_PADDING;
- for (i, seg) in self.path.iter().enumerate() {
- let w = seg.len() as f32 * 7.5;
- if px >= cx && px < cx + w {
- return Some(i);
- }
- cx += w + SEGMENT_GAP;
- }
- None
- }
-}
-
-impl Widget for Breadcrumb {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn set_network_opacity(&mut self, opacity: f32) { self.network_opacity = opacity; }
- fn color(&self) -> [f32; 4] { [0.10, 0.10, 0.14, self.network_opacity] }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovered;
- self.hovered = self.hit_test(px, py);
- let old = self.hovered_seg;
- self.hovered_seg = if self.hovered { self.seg_at(px) } else { None };
- was != self.hovered || old != self.hovered_seg
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, _py: f32) -> bool {
- if button != MouseButton::Left || state != ElementState::Pressed { return false; }
- if let Some(i) = self.seg_at(px) {
- if i < self.path.len() - 1 {
- self.clicked_seg = Some(i);
- return true;
- }
- }
- 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 extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if let Some(i) = self.hovered_seg {
- let mut cx = self.x + BREADCRUMB_PADDING;
- for j in 0..i {
- let w = self.path[j].len() as f32 * 7.5;
- cx += w + SEGMENT_GAP;
- }
- let w = self.path[i].len() as f32 * 7.5;
- quads.push((cx, self.y, w, self.h, [1.0, 1.0, 1.0, 0.06]));
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- let mut cx = self.x + BREADCRUMB_PADDING;
- for (i, seg) in self.path.iter().enumerate() {
- labels.push(TextLabel {
- text: seg.clone(),
- x: cx,
- y: self.y + 6.0,
- font_size: 12.0,
- color: if i == self.path.len() - 1 { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] },
- });
- cx += seg.len() as f32 * 7.5 + SEGMENT_GAP;
- }
- labels
- }
-}
-
-fn parse_hex(s: &str) -> Option<[u8; 3]> {
- let s = s.trim_start_matches('#');
- if s.len() != 6 { return None; }
- let r = u8::from_str_radix(&s[0..2], 16).ok()?;
- let g = u8::from_str_radix(&s[2..4], 16).ok()?;
- let b = u8::from_str_radix(&s[4..6], 16).ok()?;
- Some([r, g, b])
-}
-
-pub struct Spreadsheet {
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- hovered: bool,
- visible: bool,
- headers: Vec<String>,
- rows: Vec<Vec<String>>,
- scroll_y: f32,
- scroll_velocity: f32,
- dragging_scrollbar: bool,
- drag_offset_y: f32,
- scrollbar_hovered: bool,
- scrollbar_thumb_hovered: bool,
-}
-
-impl Spreadsheet {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
- visible: false,
- headers: Vec::new(),
- rows: Vec::new(),
- scroll_y: 0.0,
- scroll_velocity: 0.0,
- dragging_scrollbar: false,
- drag_offset_y: 0.0,
- scrollbar_hovered: false,
- scrollbar_thumb_hovered: false,
- }
- }
-}
-
-impl Widget for Spreadsheet {
- fn rect(&self) -> (f32, f32, f32, f32) {
- if !self.visible {
- (0.0, 0.0, 0.0, 0.0)
- } else {
- (self.x, self.y, self.w, self.h)
- }
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- }
-
- fn color(&self) -> [f32; 4] {
- if !self.visible {
- [0.0, 0.0, 0.0, 0.0]
- } else {
- colors::PARAM_BG
- }
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.hovered = v;
- }
-
- fn hovered(&self) -> bool {
- self.hovered
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- px >= rx && px <= rx + rw && py >= ry && py <= ry + rh
- }
-
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- }
-
- fn visible(&self) -> bool {
- 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 on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was_hovered = self.hovered;
- self.hovered = self.hit_test(px, py);
-
- let was_sb_hovered = self.scrollbar_hovered;
- let was_thumb_hovered = self.scrollbar_thumb_hovered;
-
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scrollbar_w = 6.0;
- let scrollbar_padding = 2.0;
- let scrollbar_x = self.x + self.w - scrollbar_w - scrollbar_padding;
- let track_y = self.y + 24.0;
-
- self.scrollbar_hovered = px >= scrollbar_x - 2.0 && px <= self.x + self.w
- && py >= track_y && py <= self.y + self.h;
-
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let scroll_ratio = self.scroll_y / max_scroll_y;
- let track_scroll_range = visible_h - thumb_h;
- let thumb_y = track_y + scroll_ratio * track_scroll_range;
-
- self.scrollbar_thumb_hovered = px >= scrollbar_x - 2.0 && px <= self.x + self.w
- && py >= thumb_y && py <= thumb_y + thumb_h;
- } else {
- self.scrollbar_hovered = false;
- self.scrollbar_thumb_hovered = false;
- }
-
- was_hovered != self.hovered
- || was_sb_hovered != self.scrollbar_hovered
- || was_thumb_hovered != self.scrollbar_thumb_hovered
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if self.hit_test(px, py) {
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y * 24.0,
- MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
- };
- self.scroll_velocity += scroll_amount * 12.0;
- return true;
- }
- }
- false
- }
-
- fn draggable(&self) -> bool {
- if !self.visible {
- return false;
- }
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- visible_h > 0.0 && content_h > visible_h
- }
-
- fn is_dragging(&self) -> bool {
- self.dragging_scrollbar
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- self.scroll_velocity = 0.0;
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scrollbar_w = 6.0;
- let scrollbar_padding = 2.0;
- let scrollbar_x = self.x + self.w - scrollbar_w - scrollbar_padding;
- let track_y = self.y + 24.0;
-
- if px >= scrollbar_x - 4.0 && px <= self.x + self.w
- && py >= track_y && py <= self.y + self.h
- {
- self.dragging_scrollbar = true;
-
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let scroll_ratio = self.scroll_y / max_scroll_y;
- let track_scroll_range = visible_h - thumb_h;
- let thumb_y = track_y + scroll_ratio * track_scroll_range;
-
- if py >= thumb_y && py <= thumb_y + thumb_h {
- self.drag_offset_y = py - thumb_y;
- } else {
- self.drag_offset_y = thumb_h / 2.0;
- let new_thumb_y = py - self.drag_offset_y;
- let scroll_ratio = if track_scroll_range > 0.0 {
- ((new_thumb_y - track_y) / track_scroll_range).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- self.scroll_y = scroll_ratio * max_scroll_y;
- }
- }
- }
- }
-
- fn drag_update(&mut self, _px: f32, py: f32) -> bool {
- if self.dragging_scrollbar {
- self.scroll_velocity = 0.0;
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let track_y = self.y + 24.0;
- let track_scroll_range = visible_h - thumb_h;
-
- let new_thumb_y = py - self.drag_offset_y;
- let scroll_ratio = if track_scroll_range > 0.0 {
- ((new_thumb_y - track_y) / track_scroll_range).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- let old_scroll_y = self.scroll_y;
- self.scroll_y = scroll_ratio * max_scroll_y;
-
- return (self.scroll_y - old_scroll_y).abs() > 0.01;
- }
- }
- false
- }
-
- fn drag_end(&mut self) {
- self.dragging_scrollbar = false;
- self.scroll_velocity = 0.0;
- }
-
- fn tick(&mut self, dt: f32) -> bool {
- if self.scroll_velocity.abs() > 0.01 {
- 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);
- let old_scroll_y = self.scroll_y;
-
- self.scroll_y = (self.scroll_y + self.scroll_velocity * dt).clamp(0.0, max_scroll_y);
-
- // Decelerate with friction (exponential decay)
- let friction = 8.0;
- self.scroll_velocity *= (-friction * dt).exp();
-
- if self.scroll_y == 0.0 || self.scroll_y == max_scroll_y {
- self.scroll_velocity = 0.0;
- }
-
- if self.scroll_velocity.abs() < 5.0 {
- self.scroll_velocity = 0.0;
- }
-
- (self.scroll_y - old_scroll_y).abs() > 0.01
- } else {
- false
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
-
- // Header bg
- quads.push((self.x, self.y, self.w, 24.0, [0.12, 0.12, 0.16, 0.4]));
-
- // Zebra rows
- let row_h = 24.0;
- let body_top = self.y + 24.0;
- let body_bottom = self.y + self.h;
- for i in 0..self.rows.len() {
- let ry = self.y + 24.0 + i as f32 * row_h - self.scroll_y;
- if ry + row_h <= body_top || ry >= body_bottom {
- continue;
- }
- let draw_y = ry.max(body_top);
- let draw_h = (ry + row_h).min(body_bottom) - draw_y;
- if draw_h > 0.0 {
- let row_color = if i % 2 == 0 {
- [0.10, 0.10, 0.13, 0.15]
- } else {
- [0.08, 0.08, 0.11, 0.05]
- };
- quads.push((self.x, draw_y, self.w, draw_h, row_color));
-
- // Horizontal row separator
- let sep_y = ry + row_h;
- if sep_y >= body_top && sep_y < body_bottom {
- quads.push((self.x, sep_y, self.w, 1.0, [0.20, 0.20, 0.25, 0.15]));
- }
- }
- }
-
- // Header separator
- quads.push((self.x, self.y + 24.0, self.w, 1.0, [0.20, 0.20, 0.25, 0.25]));
-
- // Vertical separators
- let divider_h = self.h;
- if divider_h > 0.0 && !self.headers.is_empty() {
- let n_cols = self.headers.len();
- for i in 1..n_cols {
- let r = i as f32 / n_cols as f32;
- quads.push((self.x + self.w * r, self.y, 1.0, divider_h, [0.20, 0.20, 0.25, 0.15]));
- }
- }
-
- // Scrollbar track & thumb
- let content_h = self.rows.len() as f32 * row_h;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scrollbar_w = 6.0;
- let scrollbar_padding = 2.0;
- let scrollbar_x = self.x + self.w - scrollbar_w - scrollbar_padding;
- let track_y = self.y + 24.0;
- let track_h = visible_h;
-
- // Track BG
- quads.push((scrollbar_x, track_y, scrollbar_w, track_h, [0.05, 0.05, 0.08, 0.15]));
-
- // Thumb
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let scroll_ratio = self.scroll_y / max_scroll_y;
- let track_scroll_range = visible_h - thumb_h;
- let thumb_y = track_y + scroll_ratio * track_scroll_range;
-
- let thumb_color = if self.dragging_scrollbar {
- [0.40, 0.40, 0.48, 1.0]
- } else if self.scrollbar_thumb_hovered {
- [0.32, 0.32, 0.38, 1.0]
- } else if self.scrollbar_hovered {
- [0.24, 0.24, 0.30, 0.9]
- } else {
- [0.18, 0.18, 0.24, 0.7]
- };
-
- quads.push((scrollbar_x, thumb_y, scrollbar_w, thumb_h, thumb_color));
- }
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let mut labels = Vec::new();
- if self.headers.is_empty() {
- return labels;
- }
-
- let n_cols = self.headers.len();
- for (i, header) in self.headers.iter().enumerate() {
- let cx = self.x + self.w * (i as f32 / n_cols as f32) + 8.0;
- labels.push(TextLabel {
- text: header.clone(),
- x: cx,
- y: self.y + 6.0,
- font_size: 12.0,
- color: [0xdd, 0xdd, 0xee],
- });
- }
-
- let row_h = 24.0;
- let body_top = self.y + 24.0;
- let body_bottom = self.y + self.h;
- for (i, row) in self.rows.iter().enumerate() {
- let ry = self.y + 24.0 + i as f32 * row_h - self.scroll_y;
- // Only show text if the row is fully inside the spreadsheet body
- if ry < body_top || ry + row_h > body_bottom {
- continue;
- }
-
- for (col_idx, val) in row.iter().enumerate().take(n_cols) {
- let cx = self.x + self.w * (col_idx as f32 / n_cols as f32) + 8.0;
- labels.push(TextLabel {
- text: val.clone(),
- x: cx,
- y: ry + 6.0,
- font_size: 12.0,
- color: [0xbb, 0xbb, 0xcc],
- });
- }
- }
- labels
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ScrollBox {
- x: f32, y: f32, w: f32, h: f32,
- pub scroll_y: f32,
- pub content_h: f32,
- pub viewport_y: f32,
- pub viewport_h: f32,
- hovered: bool,
- pub show_border: bool,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
-}
-
-impl ScrollBox {
- pub fn new() -> Self {
- Self {
- x: 0.0, y: 0.0, w: 0.0, h: 0.0,
- scroll_y: 0.0,
- content_h: 0.0,
- viewport_y: 0.0,
- viewport_h: 0.0,
- hovered: false,
- show_border: true,
- parent: None,
- children: Vec::new(),
- }
- }
-
- pub fn update_bounds(&mut self, content_h: f32, viewport_y: f32, viewport_h: f32) {
- self.content_h = content_h;
- self.viewport_y = viewport_y;
- self.viewport_h = viewport_h;
- let max_scroll = (content_h - viewport_h).max(0.0);
- self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
- }
-
- pub fn get_item_draw_y(&self, virtual_y: f32, item_h: f32) -> Option<f32> {
- let draw_y = self.viewport_y + virtual_y - self.scroll_y;
- if draw_y >= self.viewport_y - 1.0 && draw_y + item_h <= self.viewport_y + self.viewport_h + 1.0 {
- Some(draw_y)
- } else {
- None
- }
- }
-}
-
-impl Widget for ScrollBox {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { [0.08, 0.08, 0.12, 0.3] }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn highlight_color(&self) -> Option<[f32; 4]> { None }
-
- fn focus(&mut self) {
- focus::set_focused(self);
- }
- fn unfocus(&mut self) {}
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button == MouseButton::Left && state == ElementState::Pressed {
- if self.hit_test(px, py) {
- self.focus();
- return true;
- }
- }
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovered;
- self.hovered = self.hit_test(px, py);
- was != self.hovered
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if self.hit_test(px, py) {
- let scroll_speed = 24.0;
- let dy = match delta {
- MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
- MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
- };
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y + dy).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- } else {
- false
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
-
- // Background
- quads.push((self.x, self.y, self.w, self.h, [0.08, 0.08, 0.12, 0.3]));
-
- // Border lines
- let box_border_color = if focus::is_focused(self) {
- [0.30, 0.50, 0.32, 1.0] // Focused green
- } else if self.hovered {
- [0.25, 0.25, 0.35, 1.0] // Hovered
- } else {
- [0.18, 0.18, 0.24, 1.0] // Default
- };
- quads.push((self.x, self.y, self.w, 1.0, box_border_color)); // Top
- quads.push((self.x, self.y + self.h - 1.0, self.w, 1.0, box_border_color)); // Bottom
- quads.push((self.x, self.y, 1.0, self.h, box_border_color)); // Left
- quads.push((self.x + self.w - 1.0, self.y, 1.0, self.h, box_border_color)); // Right
-
- // Scrollbar
- if self.content_h > self.viewport_h {
- let sb_x = self.x + self.w - 8.0;
- let sb_w = 4.0;
- let sb_track_h = self.viewport_h - 8.0;
- let sb_track_y = self.viewport_y + 4.0;
-
- // Track
- quads.push((sb_x, sb_track_y, sb_w, sb_track_h, [0.15, 0.15, 0.20, 0.3]));
-
- // Thumb
- let visible_ratio = self.viewport_h / self.content_h;
- let thumb_h = (sb_track_h * visible_ratio).clamp(20.0, sb_track_h);
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
- let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
-
- quads.push((sb_x, thumb_y, sb_w, thumb_h, [0.60, 0.60, 0.65, 0.4]));
- }
-
- quads
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !focus::is_focused(self) {
- return false;
- }
- if event.state != ElementState::Pressed {
- return false;
- }
- if event.ctrl {
- match &event.logical_key {
- Key::Character(c) if c == "n" || c == "N" => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- Key::Character(c) if c == "p" || c == "P" => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- _ => false,
- }
- } else {
- match &event.logical_key {
- Key::Named(NamedKey::ArrowDown) => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- Key::Named(NamedKey::ArrowUp) => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- _ => false,
- }
- }
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for ScrollBox {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_rangeslider_interaction() {
- let mut rs = RangeSlider::new();
- rs.set_rect(10.0, 10.0, 200.0, 20.0);
-
- // Low value: 0.2, High value: 0.8
- let (low, high) = rs.values();
- assert_eq!(low, 0.2);
- assert_eq!(high, 0.8);
-
- // Thumb size = h * 0.9 = 18.0
- // Range = w - thumb_size = 200.0 - 18.0 = 182.0
- // Thumb low center: x + 0.2 * 182.0 + 9.0 = 10.0 + 36.4 + 9.0 = 55.4
- // Thumb high center: x + 0.8 * 182.0 + 9.0 = 10.0 + 145.6 + 9.0 = 164.6
-
- // 1. Drag Low thumb from 0.2 to 0.45
- // Click at px = 55.4 (center of low thumb)
- rs.drag_begin(55.4, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
-
- // Drag to px = 100.9 (new low value = (100.9 - offset(9.0) - 10.0) / 182.0 = 81.9 / 182.0 = 0.45)
- let changed = rs.drag_update(100.9, 20.0);
- assert!(changed);
- assert!((rs.values().0 - 0.45).abs() < 0.01);
- assert_eq!(rs.values().1, 0.8); // High value unchanged
-
- rs.drag_end();
- assert_eq!(rs.active_thumb, None);
-
- // 2. Drag High thumb from 0.8 to 0.6
- // Click at px = 164.6 (center of high thumb)
- rs.drag_begin(164.6, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
-
- // Drag to px = 128.2 (new high value = (128.2 - offset(9.0) - 10.0) / 182.0 = 109.2 / 182.0 = 0.6)
- let changed = rs.drag_update(128.2, 20.0);
- assert!(changed);
- assert!((rs.values().1 - 0.6).abs() < 0.01);
-
- rs.drag_end();
- }
-
- #[test]
- fn test_rangeslider_overlap() {
- let mut rs = RangeSlider::new().with_values(0.5, 0.5);
- rs.set_rect(10.0, 10.0, 200.0, 20.0);
-
- // Both low and high are 0.5. Thumb center = 10.0 + 0.5 * 182.0 + 9.0 = 110.0
- // Click to the left of center should select Low thumb
- rs.drag_begin(109.0, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
- rs.drag_end();
-
- // Click to the right of center should select High thumb
- rs.drag_begin(111.0, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
- rs.drag_end();
-
- // Drag Low thumb past High value (0.5). It should be constrained to 0.5
- rs.drag_begin(110.0, 20.0); // selects low
- rs.drag_update(150.0, 20.0); // drag past high
- assert_eq!(rs.values().0, 0.5); // constrained
- rs.drag_end();
- }
-
-
- #[test]
- fn test_node_toggle_geometry_visibility() {
- let mut node = Node::new(100.0, 100.0, 200.0, 50.0, "Test Node");
-
- // 1. Initial state
- assert!(node.geom_visible());
- assert!(!node.take_geom_toggle());
- assert!(node.draggable());
-
- // Get the toggle rect
- let (tx, ty, tw, th) = node.toggle_rect();
-
- // 2. Hover toggle area
- // Move cursor inside toggle area
- let changed = node.cursor_moved(tx + tw / 2.0, ty + th / 2.0);
- assert!(changed);
- assert!(node.toggle_hovered);
- assert!(!node.draggable(), "Node should not be draggable when hovering over the toggle widget");
-
- // Move cursor outside toggle area but inside node
- let changed2 = node.cursor_moved(tx - 10.0, ty + th / 2.0);
- assert!(changed2);
- assert!(!node.toggle_hovered);
- assert!(node.draggable());
-
- // 3. Click toggle area
- // Move cursor back inside toggle area
- node.cursor_moved(tx + tw / 2.0, ty + th / 2.0);
- // Press Left button
- let input_changed = node.mouse_input(MouseButton::Left, ElementState::Pressed, tx + tw / 2.0, ty + th / 2.0);
- assert!(input_changed);
- assert!(!node.geom_visible(), "Geometry visibility should be toggled off");
- assert!(node.take_geom_toggle(), "take_geom_toggle should return true after toggle click");
- assert!(!node.take_geom_toggle(), "take_geom_toggle should clear state after being called once");
-
- // Click again to toggle back on
- let input_changed2 = node.mouse_input(MouseButton::Left, ElementState::Pressed, tx + tw / 2.0, ty + th / 2.0);
- assert!(input_changed2);
- assert!(node.geom_visible(), "Geometry visibility should be toggled back on");
- assert!(node.take_geom_toggle());
- }
-
- #[test]
- fn test_graph_interaction() {
- let mut graph = Graph::new();
- graph.set_rect(0.0, 0.0, 800.0, 600.0);
- graph.set_grid_sizes(100.0, 50.0);
- graph.set_skipped_sizes(10.0, 20.0);
- graph.set_grid_origin(0.0, 0.0);
-
- let nodes = vec![
- GraphNode {
- name: "Node A".to_string(),
- position: (0.0, 0.0),
- parameters: vec![],
- geom_visible: true,
- },
- GraphNode {
- name: "Node B".to_string(),
- position: (2.0, 1.0),
- parameters: vec![],
- geom_visible: true,
- },
- ];
- graph.set_nodes(&nodes);
-
- // 1. Initial State
- assert_eq!(graph.get_nodes().len(), 2);
- assert_eq!(graph.selected_node(), None);
-
- // 2. Select Node A
- // Node A screen rect: (0, 0, 100, 50)
- let clicked = graph.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 25.0);
- assert!(clicked);
- assert_eq!(graph.selected_node(), Some(0));
- assert!(graph.draggable());
-
- // 3. Drag Node A
- graph.drag_begin(50.0, 25.0);
- assert!(graph.is_dragging());
-
- // Enable snapping
- graph.set_grid_snap_enabled(true);
- graph.drag_update(170.0, 85.0); // drag offset from Node A center (50, 25): nx = 170 - 50 = 120, ny = 85 - 25 = 60
- assert_eq!(graph.drag_node_pos, Some((120.0, 60.0)));
-
- graph.drag_end();
- assert_eq!(graph.get_nodes()[0].position, (1.0, 1.0)); // Snapped grid position: (120/120, 60/60)
-
- // 4. Toggle geometry visibility of Node B
- // Node B screen rect: (2 * 120 = 240, 1 * 60 = 60, 100, 50)
- // Toggle button: tx = 240 + 100 - 30 = 310, ty = 60 + (50 - 18)/2 = 76, tw = 18, th = 18
- let clicked_toggle = graph.mouse_input(MouseButton::Left, ElementState::Pressed, 319.0, 85.0);
- assert!(clicked_toggle);
- assert_eq!(graph.take_node_geom_toggle(), Some((1, false)));
- }
-
-
- #[test]
- fn test_menubar_vertical_horizontal_labels() {
- // Create MenuBar with custom horizontal and vertical labels
- let mut menubar = MenuBar::new(0.0, 0.0, 120.0, 30.0)
- .with_title("App")
- .with_item_vh("FileH", "FileV", &["Open", "Save"])
- .with_item("Edit", &["Undo"]);
-
- // 1. Horizontal mode (default)
- assert!(!menubar.vertical);
- let labels_h = menubar.text_labels();
- // Title should be "App", item 0 should be "FileH", item 1 should be "Edit"
- assert_eq!(labels_h[0].text, "App");
- assert_eq!(labels_h[1].text, "FileH");
- assert_eq!(labels_h[2].text, "Edit");
-
- // Hover test in horizontal layout
- let ix = menu_item_x("App", &["FileH".to_string(), "Edit".to_string()], 0);
- let iw = menu_item_w(&["FileH".to_string(), "Edit".to_string()], 0);
-
- // Move cursor inside "FileH" bounds
- menubar.cursor_moved(ix + iw / 2.0, 15.0);
- assert_eq!(menubar.hovered_menu, Some(0));
-
- // 2. Vertical mode
- let mut menubar_v = menubar.with_vertical(true);
- assert!(menubar_v.vertical);
- let labels_v = menubar_v.text_labels();
- // Title should be "App", item 0 should be "FileV", item 1 should be "Edit"
- assert_eq!(labels_v[0].text, "App");
- assert_eq!(labels_v[1].text, "FileV");
- assert_eq!(labels_v[2].text, "Edit");
-
- // Hover test in vertical layout
- let iy = menubar_v.item_y_vertical(0);
- let ih = menubar_v.item_h_vertical();
-
- // Move cursor inside "FileV" bounds
- menubar_v.cursor_moved(20.0, iy + ih / 2.0);
- assert_eq!(menubar_v.hovered_menu, Some(0));
- }
-
- #[test]
- fn test_spreadsheet_dynamic() {
- let mut spreadsheet = Spreadsheet::new();
- spreadsheet.set_rect(10.0, 10.0, 100.0, 200.0);
- spreadsheet.set_visible(true);
-
- // Initially empty
- assert!(spreadsheet.headers.is_empty());
- assert!(spreadsheet.rows.is_empty());
- assert!(spreadsheet.text_labels().is_empty());
-
- // Set dynamic headers and rows
- let headers = vec!["ColA".to_string(), "ColB".to_string()];
- let rows = vec![
- vec!["Val1".to_string(), "Val2".to_string()],
- vec!["Val3".to_string(), "Val4".to_string()],
- ];
- spreadsheet.set_spreadsheet_data(headers, rows);
-
- assert_eq!(spreadsheet.headers.len(), 2);
- assert_eq!(spreadsheet.rows.len(), 2);
-
- // Verify labels generated
- let labels = spreadsheet.text_labels();
- // 2 headers + 4 cell values = 6 labels total
- assert_eq!(labels.len(), 6);
- assert_eq!(labels[0].text, "ColA");
- assert_eq!(labels[1].text, "ColB");
- assert_eq!(labels[2].text, "Val1");
- assert_eq!(labels[3].text, "Val2");
- assert_eq!(labels[4].text, "Val3");
- assert_eq!(labels[5].text, "Val4");
-
- // Verify positions are correct (col 0 starts at x = 10.0 + 8.0 = 18.0)
- assert_eq!(labels[0].x, 18.0);
- // Col 1 starts at x = 10.0 + 100.0 * 0.5 + 8.0 = 68.0
- assert_eq!(labels[1].x, 68.0);
- assert_eq!(labels[2].x, 18.0);
- assert_eq!(labels[3].x, 68.0);
- }
-
- #[test]
- fn test_spreadsheet_scrolling() {
- let mut spreadsheet = Spreadsheet::new();
- // Visible height is 100px. Header is 24px, so body is 76px.
- spreadsheet.set_rect(0.0, 0.0, 100.0, 100.0);
- spreadsheet.set_visible(true);
-
- let headers = vec!["ColA".to_string()];
- // Each row is 24px. With 10 rows, content_h = 240px.
- let mut rows = Vec::new();
- for i in 0..10 {
- rows.push(vec![format!("Row{}", i)]);
- }
- spreadsheet.set_spreadsheet_data(headers, rows);
-
- // Content height is 240px, visible height is 100px (body is 76px).
- // Since content height > visible body height, it should be draggable.
- assert!(spreadsheet.draggable());
-
- // Max scroll height = 240.0 - 76.0 = 164.0
-
- // Initial scroll position should be 0.0
- assert_eq!(spreadsheet.scroll_y, 0.0);
-
- // Scroll down via mouse wheel (positive delta scrolls content down, scroll_y increases via tick)
- let delta = MouseScrollDelta::LineDelta(0.0, 2.0);
- // Mouse over spreadsheet (50, 50)
- let changed = spreadsheet.mouse_wheel(&delta, 50.0, 50.0);
- assert!(changed);
- assert_eq!(spreadsheet.scroll_y, 0.0);
- assert!(spreadsheet.scroll_velocity > 0.0);
-
- // Tick to apply velocity
- let mut ticked_change = false;
- for _ in 0..100 {
- if spreadsheet.tick(0.016) {
- ticked_change = true;
- }
- }
- assert!(ticked_change);
- assert!(spreadsheet.scroll_y > 0.0);
- assert_eq!(spreadsheet.scroll_velocity, 0.0);
-
- // Scroll back to top
- let delta_up = MouseScrollDelta::LineDelta(0.0, -10.0);
- spreadsheet.mouse_wheel(&delta_up, 50.0, 50.0);
- assert!(spreadsheet.scroll_velocity < 0.0);
-
- // Tick back to top
- for _ in 0..100 {
- spreadsheet.tick(0.016);
- }
- assert_eq!(spreadsheet.scroll_y, 0.0);
- assert_eq!(spreadsheet.scroll_velocity, 0.0);
-
- // Drag test
- // Scrollbar width is 6px. Padding is 2px. Width is 100px.
- // Scrollbar track x is from 92px to 98px.
- // Let's drag. Click at (94, 50).
- spreadsheet.drag_begin(94.0, 50.0);
- assert!(spreadsheet.is_dragging());
-
- // Update drag to y = 80
- let changed_drag = spreadsheet.drag_update(94.0, 80.0);
- assert!(changed_drag);
- assert!(spreadsheet.scroll_y > 0.0);
-
- // End drag
- spreadsheet.drag_end();
- assert!(!spreadsheet.is_dragging());
- }
-
- #[test]
- fn test_spreadsheet_zero_height_no_panic() {
- let mut spreadsheet = Spreadsheet::new();
- // Visible height is set to 0.0
- spreadsheet.set_rect(0.0, 0.0, 100.0, 0.0);
- spreadsheet.set_visible(true);
-
- let headers = vec!["ColA".to_string()];
- let mut rows = Vec::new();
- for i in 0..10 {
- rows.push(vec![format!("Row{}", i)]);
- }
- // This should not panic
- spreadsheet.set_spreadsheet_data(headers, rows);
-
- // This should not panic
- spreadsheet.cursor_moved(50.0, 50.0);
-
- let delta = MouseScrollDelta::LineDelta(0.0, -2.0);
- // This should not panic
- spreadsheet.mouse_wheel(&delta, 50.0, 50.0);
-
- // This should not panic
- assert!(!spreadsheet.draggable());
-
- // This should not panic
- spreadsheet.drag_begin(94.0, 50.0);
- spreadsheet.drag_update(94.0, 80.0);
- spreadsheet.drag_end();
-
- // This should not panic and return empty quads for scrollbar
- let _quads = spreadsheet.extra_quads();
- // The header quad and divider (if any) are drawn, but scrollbar is not
- // Let's verify that the scrollbar was not drawn
- // (the last quad would be the scrollbar thumb with thumb_color if drawn,
- // but here scrollbar track & thumb shouldn't be added)
- assert_eq!(spreadsheet.scroll_y, 0.0);
-
- // Let's check text labels (should be empty because self.h is 0)
- let labels = spreadsheet.text_labels();
- // Headers labels are still generated since they don't depend on scroll/height,
- // but rows shouldn't be
- assert_eq!(labels.len(), 1); // Only header ColA
- }
-
- #[test]
- fn test_scroll_box_bounds_scrolling() {
- let mut sb = ScrollBox::new();
- sb.set_rect(10.0, 20.0, 100.0, 100.0);
-
- // 1. Initially scroll is 0
- assert_eq!(sb.scroll_y, 0.0);
-
- // 2. Update bounds: content_h = 150 (greater than viewport_h = 100)
- sb.update_bounds(150.0, 20.0, 100.0);
- assert_eq!(sb.scroll_y, 0.0);
- assert_eq!(sb.content_h, 150.0);
- assert_eq!(sb.viewport_h, 100.0);
-
- // 3. Scroll inside bounds
- let delta = MouseScrollDelta::LineDelta(0.0, -2.0); // scroll down by 2 lines (48px)
- let changed = sb.mouse_wheel(&delta, 50.0, 50.0);
- assert!(changed);
- assert_eq!(sb.scroll_y, 48.0);
-
- // 4. Clamps at max scroll: 150 - 100 = 50
- let delta_large = MouseScrollDelta::LineDelta(0.0, -10.0);
- sb.mouse_wheel(&delta_large, 50.0, 50.0);
- assert_eq!(sb.scroll_y, 50.0);
-
- // 5. Test item draw coordinates
- // Virtual item at virtual_y = 10, item_h = 24
- // Screen draw y = viewport_y + virtual_y - scroll_y = 20 + 10 - 50 = -20
- // -20 < viewport_y + 2.0 (22.0), so it should return None (not visible)
- assert!(sb.get_item_draw_y(10.0, 24.0).is_none());
-
- // Virtual item at virtual_y = 60, item_h = 24
- // Screen draw y = 20 + 60 - 50 = 30
- // 30 >= 22.0 and 30 + 24 <= 118.0, so it should return Some(30.0)
- assert_eq!(sb.get_item_draw_y(60.0, 24.0), Some(30.0));
- }
-
- #[test]
- fn test_dropdown_widget_interaction() {
- let options = vec!["Option A".to_string(), "Option B".to_string(), "Option C".to_string()];
- let mut dd = Dropdown::new(options, 0);
- dd.set_rect(10.0, 10.0, 100.0, 24.0);
-
- // 1. Initial State
- assert!(!dd.open);
- assert_eq!(dd.selected, 0);
-
- // 2. Click trigger area opens dropdown
- let input_changed = dd.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
- assert!(input_changed);
- assert!(dd.open);
-
- // 3. Hovering options inside popover
- // Popover starts at y = 10 + 24 = 34. Options are of height 24 each.
- // Hover option B at y = 34 + 24 + 12 = 70.0
- let move_changed = dd.cursor_moved(50.0, 70.0);
- assert!(move_changed);
- assert_eq!(dd.hovered_item, Some(1));
-
- // 4. Click option B selects it and closes dropdown
- let select_changed = dd.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 70.0);
- assert!(select_changed);
- assert!(!dd.open);
- assert_eq!(dd.selected, 1);
- assert!(dd.take_change());
- }
-
- #[test]
- fn test_textbox_selection_highlight() {
- let mut tb = TextBox::new("Initial Text".to_string());
- tb.set_rect(10.0, 10.0, 200.0, 30.0);
-
- // 1. Initial state
- assert!(!tb.editing);
- assert!(!tb.all_selected);
-
- // 2. Click focuses and triggers highlighting
- let clicked = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
- assert!(clicked);
- assert!(tb.editing);
- assert!(tb.all_selected);
- assert_eq!(tb.edit_buffer, "Initial Text");
-
- // 3. Typing a key replaces all text
- let key_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Character("A".to_string()),
- text: Some("A".to_string()),
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled = tb.keyboard_input(&key_ev);
- assert!(handled);
- assert!(!tb.all_selected);
- assert_eq!(tb.edit_buffer, "A");
-
- // 4. Pressing Enter commits change
- let enter_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::Enter),
- text: None,
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled_enter = tb.keyboard_input(&enter_ev);
- assert!(handled_enter);
- assert!(!tb.editing);
- assert_eq!(tb.text, "A");
- assert!(tb.take_change());
- }
-
- #[test]
- fn test_textbox_drag_and_modifier_selection() {
- let mut tb = TextBox::new("Hello World".to_string());
- tb.set_rect(10.0, 10.0, 200.0, 30.0);
-
- // 1. Initial click focuses and selects all
- let pressed = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
- assert!(pressed);
- let released = tb.mouse_input(MouseButton::Left, ElementState::Released, 50.0, 20.0);
- assert!(released);
- assert!(tb.editing);
- assert!(tb.all_selected);
- assert_eq!(tb.cursor_idx, 11);
- assert_eq!(tb.select_anchor, Some(0));
-
- // 2. Click inside placed caret at index 5 (x = 10 + 8 + 5 * 7.2 = 54)
- let pressed_inside = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 54.0, 20.0);
- assert!(pressed_inside);
- assert_eq!(tb.cursor_idx, 5);
- assert_eq!(tb.select_anchor, Some(5));
- assert!(!tb.all_selected);
-
- // 3. Drag to index 11 (x = 10 + 8 + 11 * 7.2 = 97.2)
- tb.drag_begin(54.0, 20.0);
- let updated = tb.drag_update(97.2, 20.0);
- assert!(updated);
- assert_eq!(tb.cursor_idx, 11);
- assert_eq!(tb.select_anchor, Some(5));
- tb.drag_end();
-
- // 4. Keyboard ArrowLeft with Shift shrinks selection from 11 to 10
- let left_shift_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowLeft),
- text: None,
- repeat: false,
- ctrl: false,
- shift: true,
- };
- let handled = tb.keyboard_input(&left_shift_ev);
- assert!(handled);
- assert_eq!(tb.cursor_idx, 10);
- assert_eq!(tb.select_anchor, Some(5));
-
- // 5. Keyboard ArrowLeft without Shift collapses selection to start (index 5)
- let left_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowLeft),
- text: None,
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled = tb.keyboard_input(&left_ev);
- assert!(handled);
- assert_eq!(tb.cursor_idx, 5);
- assert_eq!(tb.select_anchor, None);
-
- // 6. Keyboard Shift+Up highlights to beginning (cursor 0, anchor 5)
- let up_shift_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowUp),
- text: None,
- repeat: false,
- ctrl: false,
- shift: true,
- };
- let handled = tb.keyboard_input(&up_shift_ev);
- assert!(handled);
- assert_eq!(tb.cursor_idx, 0);
- assert_eq!(tb.select_anchor, Some(5));
-
- // 7. Typing a key replaces selected range "Hello" with "Rust"
- let rust_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Character("Rust".to_string()),
- text: Some("Rust".to_string()),
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled = tb.keyboard_input(&rust_ev);
- assert!(handled);
- assert_eq!(tb.edit_buffer, "Rust World");
- assert_eq!(tb.cursor_idx, 4);
- assert_eq!(tb.select_anchor, None);
- }
-
- #[test]
- fn test_paginator_rotated_tabs() {
- let pages = vec!["📁 Browse".to_string(), "🌐 Network".to_string()];
- let mut paginator = Paginator::new(56.0, pages)
- .with_tab_y_offset(100.0)
- .with_tabs_rotated(true);
-
- paginator.set_rect(0.0, 0.0, 1000.0, 600.0);
-
- // Verify target_y is updated correctly
- // selected_page = 0: tab_y_offset = 100.0
- assert_eq!(paginator.target_y, 100.0);
-
- paginator.set_selected_page(1);
- // selected_page = 1: tab_y_offset + 1 * (120.0 + 10.0) = 230.0
- assert_eq!(paginator.target_y, 230.0);
-
- // Verify hover coordinates
- // Tab 0 bx/by bounds:
- // tab_w = 32.0, tab_h = 120.0, spacing = 10.0, sidebar_w = 48.0
- // bx = self.x + (sidebar_w - tab_w) / 2 = 8.0
- // by = self.y + tab_y_offset + i * 130.0 = 100.0
- // bx range: [8.0, 40.0], by range: [100.0, 220.0]
-
- // Hover at (24.0, 150.0) should hit Tab 0
- let hover_tab0 = paginator.on_cursor_moved(24.0, 150.0);
- assert!(hover_tab0);
- assert_eq!(paginator.hovered_tab, Some(0));
-
- // Hover at (24.0, 280.0) should hit Tab 1 (by range: [230.0, 350.0])
- let hover_tab1 = paginator.on_cursor_moved(24.0, 280.0);
- assert!(hover_tab1);
- assert_eq!(paginator.hovered_tab, Some(1));
-
- // Clicking Tab 0 selects it
- let click_tab0 = paginator.mouse_input(MouseButton::Left, ElementState::Pressed, 24.0, 150.0);
- assert!(click_tab0);
- assert_eq!(paginator.pressed_tab, Some(0));
-
- let release_tab0 = paginator.mouse_input(MouseButton::Left, ElementState::Released, 24.0, 150.0);
- assert!(release_tab0);
- assert_eq!(paginator.selected_page, 0);
-
- // Check vertical text formatting
- let labels = paginator.text_labels();
- assert_eq!(labels.len(), 2);
- assert_eq!(labels[0].text, "📁");
- assert_eq!(labels[1].text, "🌐");
-
- // Check that rotated text quads were generated
- assert!(!paginator.tab_text_quads[0].is_empty());
- assert!(!paginator.tab_text_quads[1].is_empty());
- }
-
- #[test]
- fn test_svg_text_rendering() {
- let svg_data = r##"<svg width="32" height="120" xmlns="http://www.w3.org/2000/svg">
- <text x="16" y="60" font-family="sans-serif" font-size="12" fill="#E6E6F2" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 16 60)">Audio</text>
-</svg>"##.as_bytes();
-
- let opt = resvg::usvg::Options::default();
- let mut fontdb = resvg::usvg::fontdb::Database::new();
- fontdb.load_system_fonts();
- let tree = resvg::usvg::Tree::from_data(svg_data, &opt, &fontdb).unwrap();
-
- let mut pixmap = resvg::tiny_skia::Pixmap::new(32, 120).unwrap();
- resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
-
- pixmap.save_png("/home/lsgalante/Dropbox/Clear/scratch/test_svg.png").unwrap();
-
- // Check that some pixels were drawn (are non-transparent)
- let pixels = pixmap.data();
- let mut non_transparent = 0;
- for i in (3..pixels.len()).step_by(4) {
- if pixels[i] > 0 {
- non_transparent += 1;
- }
- }
- assert!(non_transparent > 0, "Should have rendered some text pixels");
- }
-}
-
-// Generic text item layout wrapper
-#[derive(Debug)]
-pub struct TextItem {
- pub buffer: glyphon::Buffer,
- pub x: f32,
- pub y: f32,
- pub color: glyphon::Color,
- pub bounds: Option<[f32; 4]>,
-}
-
-// Styled label builder with optional strikethrough
-#[derive(Debug)]
-pub struct StyledLabel {
- pub buffer: glyphon::Buffer,
- pub w: f32,
- pub color: [f32; 4],
- pub g_color: glyphon::Color,
- pub strikethrough: bool,
- pub strikethrough_color: Option<[f32; 4]>,
-}
-
-impl StyledLabel {
- pub fn new(fs: &mut glyphon::FontSystem, text: &str, size: f32, color: [f32; 4]) -> Self {
- Self::new_with_family(fs, text, size, color, "sans-serif")
- }
-
- pub fn new_with_family(fs: &mut glyphon::FontSystem, text: &str, size: f32, color: [f32; 4], family: &str) -> Self {
- let metrics = glyphon::Metrics::new(size, size * 1.4);
- let mut buffer = glyphon::Buffer::new(fs, metrics);
- let attrs = glyphon::Attrs::new().family(glyphon::Family::Name(family));
- buffer.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
- buffer.shape_until_scroll(fs, true);
- let w = buffer.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
- let g_color = glyphon::Color::rgb(
- (color[0] * 255.0) as u8,
- (color[1] * 255.0) as u8,
- (color[2] * 255.0) as u8,
- );
- Self {
- buffer,
- w,
- color,
- g_color,
- strikethrough: false,
- strikethrough_color: None,
- }
- }
-
- pub fn with_strikethrough(mut self, enabled: bool) -> Self {
- self.strikethrough = enabled;
- self
- }
-
- pub fn with_strikethrough_color(mut self, color: [f32; 4]) -> Self {
- self.strikethrough_color = Some(color);
- self
- }
-
- pub fn draw(self, text_items: &mut Vec<TextItem>, x: f32, y: f32) -> f32 {
- let w = self.w;
- text_items.push(TextItem {
- buffer: self.buffer,
- x,
- y,
- color: self.g_color,
- bounds: None,
- });
- w
- }
-
- pub fn strikethrough_rect(&self, x: f32, y: f32, scale: f32) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- if self.strikethrough {
- let col = self.strikethrough_color.unwrap_or(self.color);
- let font_size = self.buffer.metrics().font_size;
- let line_y = self.buffer.layout_runs().next().map(|r| r.line_y).unwrap_or(font_size * 1.05);
- let offset_y = line_y - 0.28 * font_size;
- Some((
- x,
- y + offset_y * scale,
- self.w,
- 1.0 * scale,
- col,
- ))
- } else {
- None
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ScrollingList {
- pub scroll_box: ScrollBox,
- pub item_height: f32,
- pub item_gap: f32,
-}
-
-impl ScrollingList {
- pub fn new(item_height: f32, item_gap: f32) -> Self {
- Self {
- scroll_box: ScrollBox::new(),
- item_height,
- item_gap,
- }
- }
-
- pub fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
- let item_height_full = self.item_height + self.item_gap;
- let content_h = count as f32 * item_height_full;
- self.scroll_box.update_bounds(content_h, viewport_y, viewport_h);
- }
-
- pub fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
- let item_height_full = self.item_height + self.item_gap;
- let virtual_y = idx as f32 * item_height_full + offset;
- self.scroll_box.get_item_draw_y(virtual_y, self.item_height)
- }
-
- pub fn scroll_y(&self) -> f32 {
- self.scroll_box.scroll_y
- }
-
- pub fn set_scroll_y(&mut self, val: f32) {
- self.scroll_box.scroll_y = val;
- }
-}
-
-impl Default for ScrollingList {
- fn default() -> Self {
- Self::new(24.0, 4.0)
- }
-}
-
-impl Widget for ScrollingList {
- fn rect(&self) -> (f32, f32, f32, f32) {
- self.scroll_box.rect()
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.scroll_box.set_rect(x, y, w, h);
- }
-
- fn color(&self) -> [f32; 4] {
- self.scroll_box.color()
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.scroll_box.set_hovered(v);
- }
-
- fn hovered(&self) -> bool {
- self.scroll_box.hovered()
- }
-
- fn highlight_color(&self) -> Option<[f32; 4]> {
- self.scroll_box.highlight_color()
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.scroll_box.cursor_moved(px, py)
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- self.scroll_box.mouse_wheel(delta, px, py)
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- self.scroll_box.mouse_input(button, state, px, py)
- }
-
- fn focus(&mut self) {
- self.scroll_box.focus();
- }
-
- fn unfocus(&mut self) {
- self.scroll_box.unfocus();
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- self.scroll_box.extra_quads()
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- self.scroll_box.keyboard_input(event)
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.scroll_box.parent() }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.scroll_box.set_parent(parent); }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.scroll_box.children() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.scroll_box.add_child(child); }
- fn clear_children(&mut self) { self.scroll_box.clear_children(); }
-
- fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
- self.update_bounds(count, viewport_y, viewport_h);
- }
-
- fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
- self.get_item_draw_y(idx, offset)
- }
-}
-
-unsafe impl Send for Container {}
-unsafe impl Sync for Container {}
-unsafe impl Send for ScrollBox {}
-unsafe impl Sync for ScrollBox {}
-unsafe impl Send for Spinbox {}
-unsafe impl Sync for Spinbox {}
-unsafe impl Send for ColorSelector {}
-unsafe impl Sync for ColorSelector {}
-unsafe impl Send for ScrollingList {}
-unsafe impl Sync for ScrollingList {}
-
-// ── Dropdown Widget ──
-
-#[derive(Debug, Clone)]
-pub struct Dropdown {
- base: WidgetBase,
- pub options: Vec<String>,
- pub selected: usize,
- pub open: bool,
- hovered_item: Option<usize>,
- just_changed: bool,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
-}
-
-impl Dropdown {
- pub fn new(options: Vec<String>, selected: usize) -> Self {
- Self {
- base: WidgetBase::new(),
- options,
- selected,
- open: false,
- hovered_item: None,
- just_changed: false,
- parent: None,
- children: Vec::new(),
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_label(&mut self, label: &str) {
- self.base.label = Some(label.to_string());
- }
-
- pub fn take_change(&mut self) -> bool {
- let changed = self.just_changed;
- self.just_changed = false;
- changed
- }
-
- pub fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- if !self.open { return; }
-
- let dy = self.base.y + self.base.h;
- let dh = self.options.len() as f32 * 24.0;
-
- // 1. Soft layered drop shadows
- pc.rect([0.02, 0.02, 0.05, 0.15], self.base.x + 1.0, dy + 1.0, self.base.w, dh);
- pc.rect([0.02, 0.02, 0.05, 0.08], self.base.x + 3.0, dy + 3.0, self.base.w, dh);
- pc.rect([0.02, 0.02, 0.05, 0.04], self.base.x + 5.0, dy + 5.0, self.base.w, dh);
-
- // 2. High-contrast premium outer border
- pc.rect([0.25, 0.35, 0.50, 0.40], self.base.x, dy, self.base.w, dh);
-
- // 3. Frosted glass background (matching magic alpha 0.699 in shader)
- pc.rect([0.06, 0.06, 0.09, 0.699], self.base.x + 1.0, dy + 1.0, self.base.w - 2.0, dh - 2.0); // bg
-
- if let Some(h_idx) = self.hovered_item {
- let iy = dy + h_idx as f32 * 24.0;
- // 4. Vibrantly colored translucent selection highlight
- pc.rect([0.20, 0.45, 0.85, 0.50], self.base.x + 2.0, iy + 2.0, self.base.w - 4.0, 20.0);
- }
-
- for (idx, opt) in self.options.iter().enumerate() {
- let iy = dy + idx as f32 * 24.0 + (24.0 - 12.0) / 2.0;
- let text_color = if self.hovered_item == Some(idx) {
- [0xff, 0xff, 0xff]
- } else if self.selected == idx {
- [0x3a, 0x9a, 0xff]
- } else {
- [0xcc, 0xcc, 0xd4]
- };
-
- pc.text(
- opt,
- self.base.x + 8.0,
- iy,
- 12.0,
- [
- text_color[0] as f32 / 255.0,
- text_color[1] as f32 / 255.0,
- text_color[2] as f32 / 255.0,
- 1.0,
- ],
- );
- }
- }
-}
-
-impl Default for Dropdown {
- fn default() -> Self {
- Self::new(Vec::new(), 0)
- }
-}
-
-impl Widget for Dropdown {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- fn color(&self) -> [f32; 4] {
- [0.08, 0.08, 0.12, 1.0]
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- let (x, y, w, h) = self.rect();
- let hx = if self.base.row_w > 0.0 { self.base.row_x } else { x };
- let hw = if self.base.row_w > 0.0 { self.base.row_w } else { w };
- let top = self.top_room();
- let hy = y - top;
- let hh = h + top;
- if self.open {
- let dy = y + h;
- let dh = self.options.len() as f32 * 24.0;
- let hit_trigger = px >= hx && px <= hx + hw && py >= hy && py <= hy + hh;
- let hit_popover = px >= x && px <= x + w && py >= dy && py <= dy + dh;
- hit_trigger || hit_popover
- } else {
- px >= hx && px <= hx + hw && py >= hy && py <= hy + hh
- }
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was_hovered = self.base.hovered;
- let was_hovered_item = self.hovered_item;
-
- self.base.hovered = self.hit_test(px, py);
- self.hovered_item = None;
-
- if self.open {
- let (x, y, w, h) = self.rect();
- let dy = y + h;
- let dh = self.options.len() as f32 * 24.0;
- if px >= x && px <= x + w && py >= dy && py <= dy + dh {
- let idx = ((py - dy) / 24.0) as usize;
- if idx < self.options.len() {
- self.hovered_item = Some(idx);
- }
- }
- }
-
- self.base.hovered != was_hovered || self.hovered_item != was_hovered_item
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left || state != ElementState::Pressed { return false; }
-
- let (x, y, w, h) = self.rect();
- let (hy, hh) = if self.base.label.is_some() {
- (y - 18.0, h + 18.0)
- } else {
- (y, h)
- };
- let dy = y + h;
- let dh = self.options.len() as f32 * 24.0;
-
- let inside_trigger = px >= x && px <= x + w && py >= hy && py <= hy + hh;
- let inside_popover = self.open && px >= x && px <= x + w && py >= dy && py <= dy + dh;
-
- if inside_popover {
- let idx = ((py - dy) / 24.0) as usize;
- if idx < self.options.len() {
- if self.selected != idx {
- self.selected = idx;
- self.just_changed = true;
- }
- }
- self.open = false;
- return true;
- }
-
- if inside_trigger {
- self.open = !self.open;
- if self.open {
- self.focus();
- } else {
- self.unfocus();
- }
- return true;
- }
-
- if self.open {
- self.open = false;
- return true;
- }
-
- false
- }
-
- fn focus(&mut self) {
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- self.open = false;
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if event.state != ElementState::Pressed { return false; }
- if !self.open {
- if let Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) = event.logical_key {
- self.open = true;
- self.hovered_item = Some(self.selected);
- return true;
- }
- return false;
- }
-
- match event.logical_key {
- Key::Named(NamedKey::ArrowDown) => {
- let current = self.hovered_item.unwrap_or(self.selected);
- if current + 1 < self.options.len() {
- self.hovered_item = Some(current + 1);
- } else {
- self.hovered_item = Some(0);
- }
- true
- }
- Key::Named(NamedKey::ArrowUp) => {
- let current = self.hovered_item.unwrap_or(self.selected);
- if current > 0 {
- self.hovered_item = Some(current - 1);
- } else {
- self.hovered_item = Some(self.options.len() - 1);
- }
- true
- }
- Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
- if let Some(idx) = self.hovered_item {
- if self.selected != idx {
- self.selected = idx;
- self.just_changed = true;
- }
- }
- self.open = false;
- true
- }
- Key::Named(NamedKey::Escape) => {
- self.open = false;
- true
- }
- _ => false
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
-
- let bg_color = [0.08, 0.08, 0.12, 1.0];
- let border_color = if self.open {
- [0.30, 0.50, 0.32, 1.0]
- } else if self.base.hovered {
- [0.25, 0.25, 0.35, 1.0]
- } else {
- [0.18, 0.18, 0.24, 1.0]
- };
-
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, border_color));
- quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w - 2.0, self.base.h - 2.0, bg_color));
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
-
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + 4.0,
- y: self.base.y - 14.0,
- font_size: 11.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
-
- let selected_text = self.options.get(self.selected).cloned().unwrap_or_default();
- labels.push(TextLabel {
- text: selected_text,
- x: self.base.x + 8.0,
- y: self.base.y + (self.base.h - 12.0) / 2.0,
- font_size: 12.0,
- color: [0xdd, 0xdd, 0xe2],
- });
-
- labels.push(TextLabel {
- text: "▼".to_string(),
- x: self.base.x + self.base.w - 18.0,
- y: self.base.y + (self.base.h - 10.0) / 2.0,
- font_size: 10.0,
- color: [0x83, 0x83, 0x8a],
- });
-
- labels
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
- fn value(&self) -> i32 { self.selected as i32 }
- fn take_click(&mut self) -> bool { self.take_change() }
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- if self.open {
- Some((self.base.x, self.base.y + self.base.h, self.base.w, self.options.len() as f32 * 24.0))
- } else {
- None
- }
- }
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- Dropdown::render_popover(self, pc);
- }
-}
-
-unsafe impl Send for Dropdown {}
-unsafe impl Sync for Dropdown {}
-
-// ── TextBox Widget ──
-
-#[derive(Debug, Clone)]
-pub struct TextBox {
- base: WidgetBase,
- pub text: String,
- pub editing: bool,
- pub edit_buffer: String,
- just_changed: bool,
- pub disabled: bool,
- pub all_selected: bool,
- pub cursor_idx: usize,
- pub select_anchor: Option<usize>,
- pub dragging: bool,
- pub just_focused: bool,
- pub drag_start_idx: Option<usize>,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub children: Vec<*mut (dyn Widget + 'static)>,
- pub max_width: Option<f32>,
- pub width: Option<f32>,
- pub is_password: bool,
- pub multiline: bool,
- pub draw_bg_border: bool,
- pub text_color: Option<[u8; 3]>,
- pub font_size: f32,
- pub font_family: String,
-}
-
-impl TextBox {
- pub fn new(text: String) -> Self {
- Self {
- base: WidgetBase::new(),
- text,
- editing: false,
- edit_buffer: String::new(),
- just_changed: false,
- disabled: false,
- all_selected: false,
- cursor_idx: 0,
- select_anchor: None,
- dragging: false,
- just_focused: false,
- drag_start_idx: None,
- parent: None,
- children: Vec::new(),
- max_width: Some(300.0),
- width: None,
- is_password: false,
- multiline: false,
- draw_bg_border: true,
- text_color: None,
- font_size: 12.0,
- font_family: "monospace".to_string(),
- }
- }
-
- pub fn with_multiline(mut self, multiline: bool) -> Self {
- self.multiline = multiline;
- self
- }
-
- pub fn with_draw_bg_border(mut self, draw: bool) -> Self {
- self.draw_bg_border = draw;
- self
- }
-
- pub fn with_text_color(mut self, color: Option<[u8; 3]>) -> Self {
- self.text_color = color;
- self
- }
-
- pub fn with_font_size(mut self, size: f32) -> Self {
- self.font_size = size;
- self
- }
-
- pub fn with_font_family(mut self, family: String) -> Self {
- self.font_family = family;
- self
- }
-
- pub fn wrap_text(&self, max_chars_per_line: usize) -> (Vec<String>, Vec<(usize, usize)>) {
- let text_src = if self.editing { &self.edit_buffer } else { &self.text };
- let chars: Vec<char> = text_src.chars().collect();
- let mut lines = Vec::new();
- let mut current_line = Vec::new();
- let mut index_map = vec![(0, 0); chars.len() + 1];
-
- let max_chars = max_chars_per_line.max(1);
-
- let mut i = 0;
- while i < chars.len() {
- let ch = chars[i];
-
- if ch == '\n' {
- index_map[i] = (lines.len(), current_line.len());
- lines.push(current_line.iter().collect::<String>());
- current_line.clear();
- i += 1;
- continue;
- }
-
- current_line.push(ch);
- index_map[i] = (lines.len(), current_line.len() - 1);
-
- if current_line.len() > max_chars {
- let mut space_idx = None;
- for (s_idx, &c) in current_line.iter().enumerate().rev() {
- if c.is_whitespace() {
- space_idx = Some(s_idx);
- break;
- }
- }
-
- if let Some(s_idx) = space_idx {
- let line_to_push: Vec<char> = current_line[0..s_idx + 1].to_vec();
- let remaining: Vec<char> = current_line[s_idx + 1..].to_vec();
-
- let line_idx = lines.len();
- lines.push(line_to_push.iter().collect::<String>());
-
- current_line = remaining;
- let start_orig = i - current_line.len() + 1;
- for c_idx in 0..current_line.len() {
- index_map[start_orig + c_idx] = (line_idx + 1, c_idx);
- }
- } else {
- let line_to_push: Vec<char> = current_line[0..max_chars].to_vec();
- let remaining: Vec<char> = current_line[max_chars..].to_vec();
-
- let line_idx = lines.len();
- lines.push(line_to_push.iter().collect::<String>());
-
- current_line = remaining;
- let start_orig = i - current_line.len() + 1;
- for c_idx in 0..current_line.len() {
- index_map[start_orig + c_idx] = (line_idx + 1, c_idx);
- }
- }
- }
- i += 1;
- }
-
- index_map[chars.len()] = (lines.len(), current_line.len());
- lines.push(current_line.iter().collect::<String>());
-
- (lines, index_map)
- }
-
- pub fn map_2d_to_1d(&self, index_map: &[(usize, usize)], target_line: usize, target_col: usize, max_line_idx: usize) -> usize {
- let line = target_line.min(max_line_idx);
- let mut best_idx = 0;
- let mut best_dist = usize::MAX;
-
- for (i, &(l, c)) in index_map.iter().enumerate() {
- if l == line {
- let dist = (c as isize - target_col as isize).abs() as usize;
- if dist < best_dist {
- best_dist = dist;
- best_idx = i;
- }
- }
- }
- best_idx
- }
-
- pub fn with_password(mut self, is_password: bool) -> Self {
- self.is_password = is_password;
- self
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_label(&mut self, label: &str) {
- self.base.label = Some(label.to_string());
- }
-
- pub fn take_change(&mut self) -> bool {
- let changed = self.just_changed;
- self.just_changed = false;
- changed
- }
-
- pub fn with_max_width(mut self, max_w: Option<f32>) -> Self {
- self.max_width = max_w;
- self
- }
-
- pub fn set_max_width(&mut self, max_w: Option<f32>) {
- self.max_width = max_w;
- }
-
- pub fn with_width(mut self, w: f32) -> Self {
- self.width = Some(w);
- self
- }
-
- pub fn set_width(&mut self, w: f32) {
- self.width = Some(w);
- }
-
- pub fn copy_selection(&self) {
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- if start != end {
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- let selected_text: String = chars[start..end].iter().collect();
- clipboard::copy_to_clipboard(&selected_text);
- }
- }
-
- pub fn cut_selection(&mut self) -> bool {
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- if start != end {
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- let selected_text: String = chars[start..end].iter().collect();
- clipboard::copy_to_clipboard(&selected_text);
-
- let mut new_buf = String::new();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start;
- self.select_anchor = None;
- self.all_selected = false;
- return true;
- }
- false
- }
-
- pub fn paste_from_clipboard(&mut self) -> bool {
- if let Some(text) = clipboard::read_from_clipboard() {
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- let mut new_buf = String::new();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- let mut inserted_count = 0;
- for ch in text.chars() {
- if !ch.is_control() && ch != '\n' && ch != '\r' {
- new_buf.push(ch);
- inserted_count += 1;
- }
- }
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start + inserted_count;
- self.select_anchor = None;
- self.all_selected = false;
- true
- } else {
- false
- }
- }
-
- pub fn select_all(&mut self) {
- let len = self.edit_buffer.chars().count();
- self.select_anchor = Some(0);
- self.cursor_idx = len;
- self.all_selected = len > 0;
- }
-}
-
-impl Default for TextBox {
- fn default() -> Self {
- Self::new(String::new())
- }
-}
-
-impl Widget for TextBox {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
-
- 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 final_w = if let Some(explicit_w) = self.width {
- explicit_w
- } else if let Some(max_w) = self.max_width {
- w.min(max_w)
- } else {
- w
- };
- if let Some(b) = self.base_mut() {
- b.x = x;
- b.y = y;
- b.w = final_w;
- b.h = h;
- }
- }
- fn set_row_rect(&mut self, x: f32, w: f32) {
- let final_w = if let Some(explicit_w) = self.width {
- explicit_w
- } else if let Some(max_w) = self.max_width {
- w.min(max_w)
- } else {
- w
- };
- if let Some(b) = self.base_mut() {
- b.row_x = x;
- b.row_w = final_w;
- }
- }
-
- fn color(&self) -> [f32; 4] {
- [0.10, 0.10, 0.16, 1.0]
- }
-
-
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- if self.disabled {
- let was = self.base.hovered;
- self.base.hovered = false;
- return was;
- }
- let mut changed = false;
- if self.dragging && self.editing {
- let char_width = self.font_size * 0.6;
- let drag_idx = if self.multiline {
- let line_height = self.font_size * 1.333;
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, index_map) = self.wrap_text(max_chars);
- let click_line = (((py - (self.base.y + 8.0)) / line_height).floor() as isize).max(0) as usize;
- let click_col = (((px - (self.base.x + 8.0)) / char_width).round() as isize).max(0) as usize;
- self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
- } else {
- (((px - (self.base.x + 8.0)) / char_width).round() as isize)
- .max(0)
- .min(self.edit_buffer.chars().count() as isize) as usize
- };
- if self.cursor_idx != drag_idx {
- self.cursor_idx = drag_idx;
- self.just_focused = false;
- let len = self.edit_buffer.chars().count();
- let start = self.select_anchor.unwrap_or(0).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(0).max(self.cursor_idx);
- self.all_selected = start == 0 && end == len && len > 0;
- changed = true;
- }
- }
- let was = self.base.hovered;
- self.base.hovered = self.hit_test(px, py);
- if was != self.base.hovered {
- changed = true;
- }
- changed
- }
-
- fn draggable(&self) -> bool { !self.disabled }
- fn is_dragging(&self) -> bool { self.dragging }
- fn widget_font(&self) -> Option<String> { Some(self.font_family.clone()) }
-
- fn drag_begin(&mut self, _px: f32, _py: f32) {
- if self.disabled || !self.editing { return; }
- self.dragging = true;
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- if self.disabled || !self.editing { return false; }
- let char_width = self.font_size * 0.6;
- let drag_idx = if self.multiline {
- let line_height = self.font_size * 1.333;
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, index_map) = self.wrap_text(max_chars);
- let click_line = (((py - (self.base.y + 8.0)) / line_height).floor() as isize).max(0) as usize;
- let click_col = (((px - (self.base.x + 8.0)) / char_width).round() as isize).max(0) as usize;
- self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
- } else {
- (((px - (self.base.x + 8.0)) / char_width).round() as isize)
- .max(0)
- .min(self.edit_buffer.chars().count() as isize) as usize
- };
- if self.cursor_idx != drag_idx {
- self.cursor_idx = drag_idx;
- self.just_focused = false;
- let len = self.edit_buffer.chars().count();
- let start = self.select_anchor.unwrap_or(0).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(0).max(self.cursor_idx);
- self.all_selected = start == 0 && end == len && len > 0;
- return true;
- }
- false
- }
-
- fn drag_end(&mut self) {
- self.dragging = false;
- }
-
- fn value(&self) -> i32 { 0 }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if self.disabled { return false; }
- if button != MouseButton::Left { return false; }
- if !self.hit_test(px, py) { return false; }
- match state {
- ElementState::Pressed => {
- if !self.editing {
- self.focus();
- } else {
- let char_width = self.font_size * 0.6;
- let idx = if self.multiline {
- let line_height = self.font_size * 1.333;
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, index_map) = self.wrap_text(max_chars);
- let click_line = (((py - (self.base.y + 8.0)) / line_height).floor() as isize).max(0) as usize;
- let click_col = (((px - (self.base.x + 8.0)) / char_width).round() as isize).max(0) as usize;
- self.map_2d_to_1d(&index_map, click_line, click_col, lines.len() - 1)
- } else {
- (((px - (self.base.x + 8.0)) / char_width).round() as isize)
- .max(0)
- .min(self.edit_buffer.chars().count() as isize) as usize
- };
- self.cursor_idx = idx;
- self.select_anchor = Some(idx);
- self.all_selected = false;
- }
- true
- }
- ElementState::Released => {
- if self.dragging {
- self.drag_end();
- }
- if self.select_anchor == Some(self.cursor_idx) {
- self.select_anchor = None;
- }
- true
- }
- }
- }
-
- fn focus(&mut self) {
- if self.disabled { return; }
- self.editing = true;
- self.edit_buffer = self.text.clone();
- let len = self.edit_buffer.chars().count();
- self.cursor_idx = len;
- self.select_anchor = Some(0);
- self.all_selected = len > 0;
- self.just_focused = true;
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- if self.editing {
- self.editing = false;
- if self.text != self.edit_buffer {
- self.text = self.edit_buffer.clone();
- self.just_changed = true;
- }
- self.select_anchor = None;
- self.all_selected = false;
- }
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !self.editing || self.disabled { return false; }
- if event.state != ElementState::Pressed { return false; }
-
- let control = event.ctrl;
-
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if self.all_selected {
- self.edit_buffer.clear();
- self.cursor_idx = 0;
- self.select_anchor = None;
- self.all_selected = false;
- return true;
- }
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- if start != end {
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- let mut new_buf = String::new();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start;
- self.select_anchor = None;
- return true;
- }
- if self.cursor_idx > 0 {
- let mut chars: Vec<char> = self.edit_buffer.chars().collect();
- chars.remove(self.cursor_idx - 1);
- self.edit_buffer = chars.into_iter().collect();
- self.cursor_idx -= 1;
- self.select_anchor = None;
- return true;
- }
- false
- }
- Key::Named(NamedKey::Delete) => {
- if self.all_selected {
- self.edit_buffer.clear();
- self.cursor_idx = 0;
- self.select_anchor = None;
- self.all_selected = false;
- return true;
- }
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- if start != end {
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- let mut new_buf = String::new();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start;
- self.select_anchor = None;
- return true;
- }
- if self.cursor_idx < self.edit_buffer.chars().count() {
- let mut chars: Vec<char> = self.edit_buffer.chars().collect();
- chars.remove(self.cursor_idx);
- self.edit_buffer = chars.into_iter().collect();
- self.select_anchor = None;
- return true;
- }
- false
- }
- Key::Named(NamedKey::ArrowLeft) => {
- let shift = event.shift;
- if shift {
- if self.select_anchor.is_none() {
- self.select_anchor = Some(self.cursor_idx);
- }
- if self.cursor_idx > 0 {
- self.cursor_idx -= 1;
- true
- } else {
- false
- }
- } else {
- if let Some(anchor) = self.select_anchor {
- self.cursor_idx = anchor.min(self.cursor_idx);
- self.select_anchor = None;
- self.all_selected = false;
- true
- } else if self.cursor_idx > 0 {
- self.cursor_idx -= 1;
- true
- } else {
- false
- }
- }
- }
- Key::Named(NamedKey::ArrowRight) => {
- let shift = event.shift;
- if shift {
- if self.select_anchor.is_none() {
- self.select_anchor = Some(self.cursor_idx);
- }
- if self.cursor_idx < self.edit_buffer.chars().count() {
- self.cursor_idx += 1;
- true
- } else {
- false
- }
- } else {
- if let Some(anchor) = self.select_anchor {
- self.cursor_idx = anchor.max(self.cursor_idx);
- self.select_anchor = None;
- self.all_selected = false;
- true
- } else if self.cursor_idx < self.edit_buffer.chars().count() {
- self.cursor_idx += 1;
- true
- } else {
- false
- }
- }
- }
- Key::Named(NamedKey::ArrowUp) => {
- let shift = event.shift;
- if shift {
- if self.select_anchor.is_none() {
- self.select_anchor = Some(self.cursor_idx);
- }
- } else {
- self.select_anchor = None;
- self.all_selected = false;
- }
- if self.multiline {
- let char_width = self.font_size * 0.6;
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, index_map) = self.wrap_text(max_chars);
- let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
- if cursor_l > 0 {
- self.cursor_idx = self.map_2d_to_1d(&index_map, cursor_l - 1, cursor_c, lines.len() - 1);
- } else {
- self.cursor_idx = 0;
- }
- } else {
- self.cursor_idx = 0;
- }
- true
- }
- Key::Named(NamedKey::ArrowDown) => {
- let shift = event.shift;
- if shift {
- if self.select_anchor.is_none() {
- self.select_anchor = Some(self.cursor_idx);
- }
- } else {
- self.select_anchor = None;
- self.all_selected = false;
- }
- if self.multiline {
- let char_width = self.font_size * 0.6;
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, index_map) = self.wrap_text(max_chars);
- let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
- if cursor_l < lines.len() - 1 {
- self.cursor_idx = self.map_2d_to_1d(&index_map, cursor_l + 1, cursor_c, lines.len() - 1);
- } else {
- self.cursor_idx = self.edit_buffer.chars().count();
- }
- } else {
- self.cursor_idx = self.edit_buffer.chars().count();
- }
- true
- }
- Key::Named(NamedKey::Home) => {
- let shift = event.shift;
- if shift {
- if self.select_anchor.is_none() {
- self.select_anchor = Some(self.cursor_idx);
- }
- } else {
- self.select_anchor = None;
- self.all_selected = false;
- }
- self.cursor_idx = 0;
- true
- }
- Key::Named(NamedKey::End) => {
- let shift = event.shift;
- if shift {
- if self.select_anchor.is_none() {
- self.select_anchor = Some(self.cursor_idx);
- }
- } else {
- self.select_anchor = None;
- self.all_selected = false;
- }
- self.cursor_idx = self.edit_buffer.chars().count();
- true
- }
- Key::Named(NamedKey::Enter) => {
- if self.multiline {
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- let mut new_buf = String::new();
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- new_buf.push('\n');
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start + 1;
- self.select_anchor = None;
- self.all_selected = false;
- } else {
- self.unfocus();
- }
- true
- }
- Key::Named(NamedKey::Escape) => {
- self.editing = false;
- self.edit_buffer = self.text.clone();
- self.select_anchor = None;
- self.all_selected = false;
- true
- }
- Key::Character(ref ch_str) if control && (ch_str == "a" || ch_str == "A") => {
- self.cursor_idx = self.edit_buffer.chars().count();
- self.select_anchor = Some(0);
- self.all_selected = true;
- true
- }
- Key::Character(ref ch_str) if control && (ch_str == "c" || ch_str == "C") => {
- self.copy_selection();
- true
- }
- Key::Character(ref ch_str) if control && (ch_str == "x" || ch_str == "X") => {
- self.cut_selection();
- true
- }
- Key::Character(ref ch_str) if control && (ch_str == "v" || ch_str == "V") => {
- if let Some(pasted) = clipboard::read_from_clipboard() {
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- let mut new_buf = String::new();
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- let inserted_count = pasted.chars().count();
- new_buf.push_str(&pasted);
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start + inserted_count;
- self.select_anchor = None;
- self.all_selected = false;
- } else {
- self.select_anchor = None;
- self.all_selected = false;
- }
- true
- }
- _ => {
- if let Some(text) = &event.text {
- if !control {
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
- let mut new_buf = String::new();
- let chars: Vec<char> = self.edit_buffer.chars().collect();
- for i in 0..start {
- new_buf.push(chars[i]);
- }
- let inserted_count = text.chars().count();
- new_buf.push_str(text);
- for i in end..chars.len() {
- new_buf.push(chars[i]);
- }
- self.edit_buffer = new_buf;
- self.cursor_idx = start + inserted_count;
- self.select_anchor = None;
- self.all_selected = false;
- return true;
- }
- }
- false
- }
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.disabled {
- if self.draw_bg_border {
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, [0.12, 0.12, 0.16, 1.0]));
- quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w - 2.0, self.base.h - 2.0, [0.06, 0.06, 0.08, 1.0]));
- }
- return quads;
- }
-
- if self.draw_bg_border {
- let bg_color = if self.editing {
- [0.06, 0.10, 0.18, 1.0]
- } else {
- [0.08, 0.08, 0.12, 1.0]
- };
- let border_color = if self.editing {
- [0.20, 0.50, 0.85, 1.0]
- } else if self.base.hovered {
- [0.25, 0.25, 0.35, 1.0]
- } else {
- [0.18, 0.18, 0.24, 1.0]
- };
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, border_color));
- quads.push((self.base.x + 1.0, self.base.y + 1.0, self.base.w - 2.0, self.base.h - 2.0, bg_color));
- }
-
- if self.editing {
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
-
- let highlight_color = [0.20, 0.50, 0.85, 0.3];
- let cursor_color = if self.draw_bg_border {
- [0.80, 0.80, 0.85, 1.0]
- } else {
- [0.10, 0.10, 0.15, 1.0]
- };
-
- let start = self.select_anchor.unwrap_or(self.cursor_idx).min(self.cursor_idx);
- let end = self.select_anchor.unwrap_or(self.cursor_idx).max(self.cursor_idx);
-
- if self.multiline {
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, index_map) = self.wrap_text(max_chars);
-
- if start != end {
- let start_pos = index_map[start.min(index_map.len() - 1)];
- let end_pos = index_map[end.min(index_map.len() - 1)];
-
- for line_idx in start_pos.0..=end_pos.0 {
- let mut line_start_col = None;
- let mut line_end_col = None;
- for idx in start..end {
- if idx < index_map.len() {
- let (l, c) = index_map[idx];
- if l == line_idx {
- if line_start_col.is_none() || c < line_start_col.unwrap() {
- line_start_col = Some(c);
- }
- if line_end_col.is_none() || c > line_end_col.unwrap() {
- line_end_col = Some(c);
- }
- }
- }
- }
- if let (Some(sc), Some(ec)) = (line_start_col, line_end_col) {
- let highlight_x = self.base.x + 8.0 + (sc as f32 * char_width);
- let highlight_w = ((ec - sc + 1) as f32 * char_width);
- let highlight_y = self.base.y + 8.0 + (line_idx as f32 * line_height);
- quads.push((
- highlight_x,
- highlight_y,
- highlight_w,
- line_height,
- highlight_color,
- ));
- }
- }
- }
-
- let caret_h = self.font_size * 1.15;
- let (cursor_l, cursor_c) = index_map[self.cursor_idx.min(index_map.len() - 1)];
- let cursor_x = self.base.x + 8.0 + (cursor_c as f32 * char_width);
- let cursor_y = self.base.y + 8.0 + (cursor_l as f32 * line_height) + (line_height - caret_h) / 2.0;
- quads.push((cursor_x, cursor_y, 1.5, caret_h, cursor_color));
- } else {
- let caret_h = self.font_size * 1.15;
- let line_h = self.font_size * 1.333;
- if start != end {
- let highlight_x = self.base.x + 8.0 + (start as f32 * char_width);
- let max_x = self.base.x + self.base.w - 6.0;
- let highlight_w = ((end - start) as f32 * char_width).min(max_x - highlight_x).max(0.0);
- quads.push((
- highlight_x,
- self.base.y + (self.base.h - line_h) / 2.0,
- highlight_w,
- line_h,
- highlight_color,
- ));
- }
-
- let cursor_x = self.base.x + 8.0 + (self.cursor_idx as f32 * char_width);
- let max_cursor_x = self.base.x + self.base.w - 6.0;
- let final_cursor_x = cursor_x.min(max_cursor_x);
- let cursor_y = self.base.y + (self.base.h - caret_h) / 2.0;
- quads.push((final_cursor_x, cursor_y, 1.5, caret_h, cursor_color));
- }
- }
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x + 4.0,
- y: self.base.y - 14.0,
- font_size: 11.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
- let mut val_text = if self.editing {
- self.edit_buffer.clone()
- } else {
- self.text.clone()
- };
- if self.is_password {
- val_text = "•".repeat(val_text.chars().count());
- }
-
- let label_color = if let Some(custom_color) = self.text_color {
- custom_color
- } else if self.disabled {
- [0x53, 0x53, 0x5a]
- } else if self.all_selected {
- [0xff, 0xff, 0xff]
- } else if self.editing {
- [0xee, 0xee, 0xf5]
- } else {
- [0xcc, 0xcc, 0xd4]
- };
-
- if self.multiline {
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
- let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
- let (lines, _) = self.wrap_text(max_chars);
- for (line_idx, line_text) in lines.iter().enumerate() {
- labels.push(TextLabel {
- text: line_text.clone(),
- x: self.base.x + 8.0,
- y: self.base.y + 8.0 + (line_idx as f32 * line_height) + (line_height - self.font_size) / 2.0,
- font_size: self.font_size,
- color: label_color,
- });
- }
- } else {
- labels.push(TextLabel {
- text: val_text,
- x: self.base.x + 8.0,
- y: self.base.y + (self.base.h - self.font_size) / 2.0,
- font_size: self.font_size,
- color: label_color,
- });
- }
- labels
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for TextBox {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-unsafe impl Send for TextBox {}
-unsafe impl Sync for TextBox {}
-
-use std::sync::OnceLock;
-static FONT_DB: OnceLock<resvg::usvg::fontdb::Database> = OnceLock::new();
-
-pub fn get_font_db() -> &'static resvg::usvg::fontdb::Database {
- FONT_DB.get_or_init(|| {
- let mut db = resvg::usvg::fontdb::Database::new();
- db.load_system_fonts();
- db
- })
-}
-
-pub struct Plate {
- pub base: WidgetBase,
- pub dragging: bool,
- pub drag_ox: f32,
- pub drag_oy: f32,
- pub drag_start_x: f32,
- pub drag_start_y: f32,
- pub bounds: Option<(f32, f32, f32, f32)>,
- pub color: Option<[f32; 4]>,
- pub curved_circle: Option<(f32, f32, f32)>,
- pub network_opacity: f32,
- pub blur: bool,
- pub children: Vec<*mut (dyn Widget + 'static)>,
- pub parent: Option<*mut (dyn Widget + 'static)>,
- pub visible: bool,
- pub column_layout: bool,
-}
-
-impl Plate {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: WidgetBase::new_rect(x, y, w, h),
- dragging: false,
- drag_ox: 0.0,
- drag_oy: 0.0,
- drag_start_x: 0.0,
- drag_start_y: 0.0,
- bounds: None,
- color: None,
- curved_circle: None,
- network_opacity: 1.0,
- blur: true,
- children: Vec::new(),
- parent: None,
- visible: true,
- column_layout: false,
- }
- }
-
- pub fn with_color(mut self, color: [f32; 4]) -> Self {
- self.color = Some(color);
- self
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_blur(mut self, blur: bool) -> Self {
- self.blur = blur;
- self
- }
-
- pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-}
-
-impl Widget for Plate {
- fn base(&self) -> Option<&WidgetBase> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut WidgetBase> { Some(&mut self.base) }
- fn is_plate(&self) -> bool { true }
- fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
- fn top_room(&self) -> f32 { 0.0 }
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
-
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- for &child_ptr in &self.children {
- unsafe {
- (*child_ptr).set_modifiers(ctrl, shift, alt);
- }
- }
- }
-
- fn visible(&self) -> bool {
- self.visible
- }
-
- fn color(&self) -> [f32; 4] {
- let mut c = if let Some(c) = self.color {
- c
- } else if self.dragging {
- colors::PANEL_DRAG
- } else {
- colors::PANEL_IDLE
- };
- c[3] *= self.network_opacity;
- if self.blur {
- c[3] = -c[3].abs();
- }
- c
- }
-
- fn set_network_opacity(&mut self, opacity: f32) {
- self.network_opacity = opacity;
- }
-
- fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-
- fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
- self.curved_circle = circle;
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((cx, cy, r)) = self.curved_circle {
- let dx = px - cx;
- let dy = py - cy;
- return dx * dx + dy * dy <= r * r;
- }
-
- let (x, y, w, h) = self.rect();
- if px < x || px >= x + w || py < y || py >= y + h {
- return false;
- }
-
- let r = 12.0f32.min(w * 0.5).min(h * 0.5);
- if r <= 0.1 {
- return true;
- }
-
- // Check corners
- if px < x + r && py < y + r {
- let dx = px - (x + r);
- let dy = py - (y + r);
- return dx * dx + dy * dy <= r * r;
- }
- if px >= x + w - r && py < y + r {
- let dx = px - (x + w - r);
- let dy = py - (y + r);
- return dx * dx + dy * dy <= r * r;
- }
- if px >= x + w - r && py >= y + h - r {
- let dx = px - (x + w - r);
- let dy = py - (y + h - r);
- return dx * dx + dy * dy <= r * r;
- }
- if px < x + r && py >= y + h - r {
- let dx = px - (x + r);
- let dy = py - (y + h - r);
- return dx * dx + dy * dy <= r * r;
- }
-
- true
- }
-
- 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;
- }
-
- if !self.visible {
- return;
- }
-
- let padding_x = 20.0;
- let padding_y = 20.0;
- 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 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 {
- let w = unsafe { &*w_ptr };
- if !w.layout_ignore() {
- active_widgets.push(w_ptr);
- }
- }
-
- if self.column_layout {
- let mut current_y = start_y;
- let spacing = 12.0;
- for &w_ptr in &active_widgets {
- let w = unsafe { &mut *w_ptr };
- let (_, _, ww, wh) = w.rect();
- let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
- let top = w.top_room();
- 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;
- }
- } else {
- let mut total_diagonal = 0.0;
- let mut count = 0;
- for &w_ptr in &active_widgets {
- let w = unsafe { &*w_ptr };
- let (_, _, ww, wh) = w.rect();
- let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
- let use_h = if wh > 0.0 { wh } else { 50.0 };
- total_diagonal += (use_w * use_w + use_h * use_h).sqrt();
- count += 1;
- }
- let avg_diagonal = if count > 0 { total_diagonal / count as f32 } else { 100.0 };
- let base_spacing = (avg_diagonal * 0.55).max(60.0);
-
- for (i, &w_ptr) in active_widgets.iter().enumerate() {
- let w = unsafe { &mut *w_ptr };
- let (_, _, ww, wh) = w.rect();
- let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
- 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);
- } else {
- let mut ring = 1;
- let mut ring_start = 1;
- let mut placed = false;
- while !placed {
- let ring_capacity = ring * 6;
- if i < ring_start + ring_capacity {
- let pos_in_ring = i - ring_start;
- let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
- let radius = (ring as f32) * base_spacing;
-
- 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,
- );
- placed = true;
- } else {
- ring_start += ring_capacity;
- ring += 1;
- }
- }
- }
- }
- }
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) {
- self.parent = parent;
- }
-
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> {
- self.children.clone()
- }
-
- fn add_child(&mut self, child: *mut (dyn Widget + 'static)) {
- self.children.push(child);
- }
-
- fn clear_children(&mut self) {
- self.children.clear();
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
- let (px, py, pw, ph) = self.rect();
- quads.push((px, py, pw, ph, self.color()));
-
- 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());
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let mut labels = Vec::new();
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x: self.base.x,
- y: self.base.y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
- for &child_ptr in &self.children {
- let widget = unsafe { &*child_ptr };
- labels.extend(widget.text_labels());
- }
- labels
- }
-
- fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
- if !self.visible {
- return Vec::new();
- }
- let mut result = Vec::new();
- if let Some(ref label) = self.base.label {
- result.push((
- TextLabel {
- text: label.clone(),
- x: self.base.x,
- y: self.base.y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- },
- None,
- ));
- }
- for &child_ptr in &self.children {
- let widget = unsafe { &*child_ptr };
- result.extend(widget.text_labels_with_bounds());
- }
- result
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- let mut changed = false;
- for &widget_ptr in &self.children {
- let widget = unsafe { &mut *widget_ptr };
- if widget.is_dragging() {
- if widget.drag_update(px, py) {
- changed = true;
- }
- } else if widget.cursor_moved(px, py) {
- changed = true;
- }
- }
- changed
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- for &widget_ptr in self.children.iter().rev() {
- let widget = unsafe { &mut *widget_ptr };
- if widget.popover_rect().is_some() {
- if widget.mouse_input(button, state, px, py) {
- return true;
- }
- }
- }
- for &widget_ptr in self.children.iter().rev() {
- let widget = unsafe { &mut *widget_ptr };
- if widget.mouse_input(button, state, px, py) {
- return true;
- }
- if state == ElementState::Pressed && !widget.hit_test(px, py) {
- widget.unfocus();
- }
- }
-
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- self.drag_begin(px, py);
- return true;
- }
- }
- ElementState::Released => {
- if self.dragging { self.drag_end(); return true; }
- }
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !self.visible {
- return false;
- }
- for &widget_ptr in &self.children {
- let widget = unsafe { &mut *widget_ptr };
- if widget.keyboard_input(event) {
- return true;
- }
- }
- false
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- for &widget_ptr in self.children.iter().rev() {
- let widget = unsafe { &mut *widget_ptr };
- if widget.mouse_wheel(delta, px, py) {
- return true;
- }
- }
- false
- }
-
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- if !self.visible {
- return None;
- }
- for &widget_ptr in self.children.iter().rev() {
- let widget = unsafe { &*widget_ptr };
- if let Some(r) = widget.popover_rect() {
- return Some(r);
- }
- }
- None
- }
-
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- if !self.visible {
- return;
- }
- for &widget_ptr in self.children.iter().rev() {
- let widget = unsafe { &*widget_ptr };
- widget.render_popover(pc);
- }
- }
-
- fn tick(&mut self, dt: f32) -> bool {
- if !self.visible {
- return false;
- }
- let mut changed = false;
- for &widget_ptr in &self.children {
- let widget = unsafe { &mut *widget_ptr };
- if widget.tick(dt) {
- changed = true;
- }
- }
- changed
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- 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))
- } 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;
-
- for &child_ptr in &self.children {
- unsafe {
- let (cx, cy, cw, ch) = (*child_ptr).rect();
- (*child_ptr).set_rect(cx + dx, cy + dy, cw, ch);
- }
- }
- return true;
- }
- false
- }
-
- 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;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-}
-
-unsafe impl Send for Plate {}
-unsafe impl Sync for Plate {}
-
-pub struct Paginator {
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- hovered: bool,
- pub sidebar_menu: MenuBar,
- pub plates: Vec<Plate>,
- pub selected_page: usize,
- pub page_changed: bool,
- pub sidebar_label: Option<String>,
- pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
- pub sidebar_scroll_y: f32,
- pub scale_factor: f32,
- pub sidebar_mode: bool,
- pub page_hidden: bool,
- pub sidebar_w: f32,
- pub pages: Vec<String>,
- pub tabs_at_top: bool,
- pub tab_y_offset: f32,
- pub tabs_rotated: bool,
- pub hovered_tab: Option<usize>,
- pub pressed_tab: Option<usize>,
- pub target_y: f32,
- pub target_x: f32,
- pub current_y: Option<f32>,
- pub current_x: Option<f32>,
- parent: Option<*mut (dyn Widget + 'static)>,
-}
-
-impl Paginator {
- pub fn sidebar_w(&self) -> f32 {
- self.sidebar_w
- }
-
- pub fn new(sidebar_w: f32, pages: Vec<String>) -> Self {
- let num_pages = pages.len();
-
- let mut sidebar_menu = MenuBar::new(0.0, 0.0, sidebar_w, 0.0)
- .with_vertical(true);
- for page in &pages {
- sidebar_menu = sidebar_menu.with_item(page, &[]);
- }
-
- let mut plates = Vec::new();
- for _ in 0..num_pages {
- let mut plate = Plate::new(0.0, 0.0, 0.0, 0.0);
- plate.visible = false;
- plates.push(plate);
- }
- if num_pages > 0 {
- plates[0].visible = true;
- sidebar_menu.menus[0].set_selected(true);
- }
-
- let mut pag = Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
- sidebar_menu,
- plates,
- selected_page: 0,
- page_changed: false,
- sidebar_label: None,
- tab_text_quads: Vec::new(),
- sidebar_scroll_y: 0.0,
- scale_factor: 1.0,
- sidebar_mode: true,
- page_hidden: false,
- sidebar_w,
- pages,
- tabs_at_top: false,
- tab_y_offset: 10.0,
- tabs_rotated: true,
- hovered_tab: None,
- pressed_tab: None,
- target_y: 10.0,
- target_x: 0.0,
- current_y: Some(10.0),
- current_x: Some(0.0),
- parent: None,
- };
- pag.update_target_pos();
- pag
- }
-
- pub fn tab_rect(&self, idx: usize) -> (f32, f32, f32, f32) {
- if idx >= self.pages.len() {
- return (0.0, 0.0, 0.0, 0.0);
- }
- if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + idx as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + idx as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + idx as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- }
- }
-
- pub fn tab_size(&self, idx: usize) -> (f32, f32) {
- let r = self.tab_rect(idx);
- (r.2, r.3)
- }
-
- pub fn with_column_layout(mut self, enabled: bool) -> Self {
- for plate in &mut self.plates {
- plate.column_layout = enabled;
- }
- self
- }
-
- pub fn with_sidebar_mode(mut self, enabled: bool) -> Self {
- self.sidebar_mode = enabled;
- self
- }
-
- pub fn with_sidebar_label(mut self, label: &str) -> Self {
- self.sidebar_label = Some(label.to_string());
- self
- }
-
- pub fn sidebar_label_height(&self) -> f32 {
- if self.sidebar_label.is_some() {
- 24.0
- } else {
- 0.0
- }
- }
-
- pub fn is_page_hidden(&self) -> bool {
- self.page_hidden
- }
-
- pub fn set_page_hidden(&mut self, hidden: bool) {
- self.page_hidden = hidden;
- }
-
- pub fn set_sidebar_mode(&mut self, enabled: bool) {
- self.sidebar_mode = enabled;
- }
-
- pub fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Widget + 'static)) {
- if page_idx < self.plates.len() {
- self.plates[page_idx].add_child(widget);
- unsafe {
- (*widget).set_parent(Some(&mut self.plates[page_idx] as *mut _));
- }
- }
- }
-
- pub fn clear_page_widgets(&mut self, page_idx: usize) {
- if page_idx < self.plates.len() {
- self.plates[page_idx].clear_children();
- }
- }
-
- pub fn with_tabs_at_top(mut self, top: bool) -> Self {
- self.tabs_at_top = top;
- self.update_target_pos();
- self
- }
-
- pub fn with_tab_y_offset(mut self, offset: f32) -> Self {
- self.tab_y_offset = offset;
- if self.current_y == Some(10.0) {
- self.current_y = Some(offset);
- }
- self.update_target_pos();
- self
- }
-
- pub fn with_tabs_rotated(mut self, rotated: bool) -> Self {
- self.tabs_rotated = rotated;
- self.update_target_pos();
- self
- }
-
- pub fn selected_page(&self) -> usize {
- self.selected_page
- }
-
- pub fn set_selected_page(&mut self, page: usize) {
- if page < self.plates.len() {
- if self.selected_page != page {
- self.plates[self.selected_page].visible = false;
- self.selected_page = page;
- self.plates[self.selected_page].visible = true;
- self.update_target_pos();
-
- // Update MenuBar focus/selection
- for (i, menu) in self.sidebar_menu.menus.iter_mut().enumerate() {
- menu.set_selected(i == page);
- }
- }
- }
- }
-
- pub fn set_pages(&mut self, pages: Vec<String>) {
- self.pages = pages.clone();
- let num_pages = pages.len();
-
- let mut sidebar_menu = MenuBar::new(0.0, 0.0, self.sidebar_w, 0.0)
- .with_vertical(true);
- for page in &pages {
- sidebar_menu = sidebar_menu.with_item(page, &[]);
- }
- self.sidebar_menu = sidebar_menu;
-
- let mut plates = Vec::new();
- for _ in 0..num_pages {
- let mut plate = Plate::new(0.0, 0.0, 0.0, 0.0);
- plate.visible = false;
- plates.push(plate);
- }
- self.plates = plates;
- if self.selected_page >= num_pages {
- self.selected_page = 0;
- }
- if !self.plates.is_empty() {
- self.plates[self.selected_page].visible = true;
- self.sidebar_menu.menus[self.selected_page].set_selected(true);
- }
- self.update_target_pos();
- }
-
- pub fn set_scale_factor(&mut self, scale: f32) {
- self.scale_factor = scale;
- }
-
- pub fn vertical_tab_size(&self) -> (f32, f32) {
- if self.tabs_rotated {
- ((self.sidebar_w - 16.0).clamp(24.0, 120.0), 120.0)
- } else {
- (self.sidebar_w - 10.0, 40.0)
- }
- }
-
- fn total_sidebar_height(&self) -> f32 {
- let (_, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let step = if self.tabs_rotated { tab_h + spacing } else { 50.0 };
- self.tab_y_offset + self.pages.len() as f32 * step - spacing
- }
-
- fn update_target_pos(&mut self) {
- if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- self.target_x = self.selected_page as f32 * tab_w;
- if self.current_x.is_none() {
- self.current_x = Some(self.target_x);
- }
- } else if self.tabs_rotated {
- let (_, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let target = self.tab_y_offset + self.selected_page as f32 * (tab_h + spacing);
- self.target_y = target;
- if self.current_y.is_none() {
- self.current_y = Some(target);
- }
- } else {
- let target = self.tab_y_offset + self.selected_page as f32 * 50.0;
- self.target_y = target;
- if self.current_y.is_none() {
- self.current_y = Some(target);
- }
- }
- self.generate_tab_quads();
- }
-
- fn generate_tab_quads(&mut self) {
- self.tab_text_quads.clear();
- let (tab_w, tab_h) = self.vertical_tab_size();
- 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;
-
- for (i, page_name) in self.pages.iter().enumerate() {
- let color = if self.selected_page == 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 w_px = tab_w as u32;
- let h_px = if has_icon { 80 } else { 120 };
-
- if w_px == 0 || h_px == 0 {
- self.tab_text_quads.push(Vec::new());
- continue;
- }
-
- let svg_data = format!(
- r##"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
- <text x="{}" y="{}" font-family="sans-serif" font-size="12" fill="{}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 {} {})">{}</text>
-</svg>"##,
- w_px, h_px,
- w_px as f32 / 2.0, h_px as f32 / 2.0,
- hex_color,
- w_px as f32 / 2.0, h_px as f32 / 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;
- let g = pixels[idx + 1] as f32 / 255.0;
- let b = pixels[idx + 2] as f32 / 255.0;
- page_quads.push((
- col as f32,
- row as f32,
- 1.0,
- 1.0,
- [r, g, b, a],
- ));
- }
- }
- }
- }
- }
- }
- self.tab_text_quads.push(page_quads);
- }
- }
-}
-
-impl Widget for Paginator {
- fn rect(&self) -> (f32, f32, f32, f32) {
- (self.x, self.y, self.w, self.h)
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- self.update_target_pos();
-
- let self_ptr = self as *mut Paginator;
- self.sidebar_menu.set_parent(Some(self_ptr));
- for plate in &mut self.plates {
- plate.set_parent(Some(self_ptr));
- }
-
- let tabs_at_top = self.tabs_at_top;
- if tabs_at_top {
- self.sidebar_menu.set_rect(self.x, self.y, self.w, 40.0);
- for plate in &mut self.plates {
- plate.set_rect(self.x, self.y + 40.0, self.w, (self.h - 40.0).max(0.0));
- }
- } else {
- let sidebar_w = self.sidebar_w;
- self.sidebar_menu.set_rect(self.x, self.y, sidebar_w, self.h);
- for plate in &mut self.plates {
- plate.set_rect(self.x + sidebar_w, self.y, (self.w - sidebar_w).max(0.0), self.h);
- }
- }
- }
-
- fn color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.hovered = v;
- }
-
- fn hovered(&self) -> bool {
- self.hovered
- }
-
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- if let Some(i) = self.hovered_tab {
- if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- let bx = self.x + i as f32 * tab_w + 2.0;
- let by = self.y + 2.0;
- let bw = tab_w - 4.0;
- let bh = 40.0 - 4.0;
- let scroll_offset = hover_animation::get_scroll_offset();
- Some((bx, by + scroll_offset, bw, bh, colors::HIGHLIGHT_SECONDARY))
- } else {
- let (bx, by, bw, bh) = if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (
- self.x + 5.0,
- self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y,
- bw,
- 40.0,
- )
- };
- let scroll_offset = hover_animation::get_scroll_offset();
- let qy = by + scroll_offset;
- let qh = bh;
-
- let min_y = self.y;
- let max_y = self.y + self.h;
- let ry1 = qy.max(min_y);
- let ry2 = (qy + qh).min(max_y);
- let rh = ry2 - ry1;
- if rh > 0.0 {
- Some((bx, ry1, bw, rh, colors::HIGHLIGHT_SECONDARY))
- } else {
- None
- }
- }
- } else {
- None
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.tabs_rotated {
- quads.push((self.x, self.y, self.sidebar_w, self.h, colors::sidebar_bg_color()));
-
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let min_y = self.y;
- let max_y = self.y + self.h;
-
- for (i, page_name) in self.pages.iter().enumerate() {
- let bx = self.x + (self.sidebar_w - tab_w) / 2.0;
- let by = self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y;
-
- let trimmed = page_name.trim();
- let has_icon = trimmed.find(' ').is_some();
- let y_offset = if has_icon { 40.0 } else { 0.0 };
-
- if i < self.tab_text_quads.len() {
- for &(qx, qy, qw, qh, qc) in &self.tab_text_quads[i] {
- let absolute_x = bx + qx;
- let absolute_y = by + 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));
- }
- }
- }
- }
- } else {
- quads.extend(self.sidebar_menu.extra_quads());
- }
-
- if self.selected_page < self.plates.len() {
- quads.extend(self.plates[self.selected_page].extra_quads());
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- 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;
-
- for (i, page_name) in self.pages.iter().enumerate() {
- let color = if self.selected_page == i {
- [active_r, active_g, active_b]
- } else {
- [inactive_r, inactive_g, inactive_b]
- };
- let bx = self.x + (self.sidebar_w - tab_w) / 2.0;
- let by = self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y;
- let bw = tab_w;
-
- let trimmed = page_name.trim();
- let has_icon = trimmed.find(' ').is_some();
- if has_icon {
- 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 icon_y = by + 12.0;
- if icon_y >= self.y && icon_y + icon_font_size <= self.y + self.h {
- labels.push(TextLabel {
- text: icon.to_string(),
- x: bx + (bw - est_icon_w) / 2.0,
- y: icon_y,
- font_size: icon_font_size,
- color,
- });
- }
- }
- }
- }
- }
- } else {
- labels.extend(self.sidebar_menu.text_labels());
- }
-
- if self.selected_page < self.plates.len() {
- labels.extend(self.plates[self.selected_page].text_labels());
- }
- labels
- }
-
- fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
- let mut labels = Vec::new();
- for l in self.text_labels() {
- labels.push((l, None));
- }
- labels
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let mut changed = false;
- let was_hovered_tab = self.hovered_tab;
- self.hovered_tab = None;
- for i in 0..self.pages.len() {
- let (bx, by, bw, bh) = if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + i as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- };
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
- self.hovered_tab = Some(i);
- break;
- }
- }
- if was_hovered_tab != self.hovered_tab {
- changed = true;
- }
-
- if self.sidebar_menu.cursor_moved(px, py) {
- changed = true;
- }
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].cursor_moved(px, py) {
- changed = true;
- }
- }
-
- changed
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].popover_rect().is_some() {
- if self.plates[self.selected_page].mouse_input(button, state, px, py) {
- return true;
- }
- }
- }
-
- let mut clicked_tab = false;
- if button == MouseButton::Left {
- match state {
- ElementState::Pressed => {
- self.pressed_tab = None;
- for i in 0..self.pages.len() {
- let (bx, by, bw, bh) = if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + i as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- };
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
- self.pressed_tab = Some(i);
- clicked_tab = true;
- break;
- }
- }
- }
- ElementState::Released => {
- if let Some(i) = self.pressed_tab.take() {
- let (bx, by, bw, bh) = if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + i as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- };
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
- if self.selected_page != i {
- self.set_selected_page(i);
- self.page_changed = true;
- }
- clicked_tab = true;
- }
- }
- }
- }
- }
-
- if clicked_tab {
- return true;
- }
-
- if self.sidebar_menu.mouse_input(button, state, px, py) {
- return true;
- }
-
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].mouse_input(button, state, px, py) {
- return true;
- }
- }
-
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if self.sidebar_menu.keyboard_input(event) {
- return true;
- }
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].keyboard_input(event) {
- return true;
- }
- }
- false
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.tabs_at_top {
- let bx = self.x;
- let by = self.y;
- let bw = self.sidebar_w;
- let bh = self.h;
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
- let scroll_speed = 24.0;
- let dy = match delta {
- MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
- MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
- };
- let old_scroll = self.sidebar_scroll_y;
- let max_scroll = (self.total_sidebar_height() - self.h).max(0.0);
- self.sidebar_scroll_y = (self.sidebar_scroll_y + dy).clamp(0.0, max_scroll);
- if (self.sidebar_scroll_y - old_scroll).abs() > 0.01 {
- self.update_target_pos();
- return true;
- }
- }
- }
-
- if self.sidebar_menu.mouse_wheel(delta, px, py) {
- return true;
- }
-
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].mouse_wheel(delta, px, py) {
- return true;
- }
- }
- false
- }
-
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- if let Some(r) = self.sidebar_menu.popover_rect() {
- return Some(r);
- }
- if self.selected_page < self.plates.len() {
- if let Some(r) = self.plates[self.selected_page].popover_rect() {
- return Some(r);
- }
- }
- None
- }
-
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- self.sidebar_menu.render_popover(pc);
- if self.selected_page < self.plates.len() {
- self.plates[self.selected_page].render_popover(pc);
- }
- }
-
- fn take_click(&mut self) -> bool {
- if self.page_changed {
- self.page_changed = false;
- true
- } else {
- false
- }
- }
-
- fn value(&self) -> i32 {
- self.selected_page as i32
- }
-
- fn tick(&mut self, dt: f32) -> bool {
- let mut changed = false;
- if let Some(current) = self.current_y {
- let diff = self.target_y - current;
- if diff.abs() > 0.1 {
- let decay = 15.0;
- let next = current + diff * (1.0 - (-decay * dt).exp());
- self.current_y = Some(next);
- changed = true;
- } else {
- self.current_y = Some(self.target_y);
- }
- }
- if let Some(current) = self.current_x {
- let diff = self.target_x - current;
- if diff.abs() > 0.1 {
- let decay = 15.0;
- let next = current + diff * (1.0 - (-decay * dt).exp());
- self.current_x = Some(next);
- changed = true;
- } else {
- self.current_x = Some(self.target_x);
- }
- }
-
- let self_ptr = self as *mut Paginator;
- self.sidebar_menu.set_parent(Some(self_ptr));
- for plate in &mut self.plates {
- plate.set_parent(Some(self_ptr));
- }
-
- if self.sidebar_menu.tick(dt) {
- changed = true;
- }
-
- for plate in &mut self.plates {
- if plate.tick(dt) {
- changed = true;
- }
- }
-
- changed
- }
-
- fn parent(&self) -> Option<*mut (dyn Widget + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Widget + 'static)>) {
- self.parent = parent;
- }
-
- fn children(&self) -> Vec<*mut (dyn Widget + 'static)> {
- let mut list = Vec::new();
- list.push(&self.sidebar_menu as *const dyn Widget as *mut dyn Widget);
- for plate in &self.plates {
- list.push(plate as *const dyn Widget as *mut dyn Widget);
- }
- list
- }
-
- fn add_child(&mut self, _child: *mut (dyn Widget + 'static)) {}
- fn clear_children(&mut self) {}
-
- 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 menu_names(&self) -> Vec<String> {
- self.pages.clone()
- }
- fn selected_page(&self) -> usize {
- self.selected_page()
- }
- fn set_selected_page(&mut self, page: usize) {
- self.set_selected_page(page);
- }
- fn is_page_hidden(&self) -> bool {
- self.is_page_hidden()
- }
- fn set_page_hidden(&mut self, hidden: bool) {
- self.set_page_hidden(hidden);
- }
- fn set_pages(&mut self, pages: Vec<String>) {
- self.set_pages(pages);
- }
- fn sidebar_w(&self) -> f32 {
- self.sidebar_w()
- }
- fn set_sidebar_mode(&mut self, enabled: bool) {
- self.set_sidebar_mode(enabled);
- }
- fn set_sidebar_label(&mut self, label: Option<String>) {
- self.sidebar_label = label;
- self.update_target_pos();
- }
- fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Widget + 'static)) {
- self.add_widget_to_page(page_idx, widget);
- }
- fn clear_page_widgets(&mut self, page_idx: usize) {
- self.clear_page_widgets(page_idx);
- }
- 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()
- }
-}
-
-unsafe impl Send for Paginator {}
-unsafe impl Sync for Paginator {}
-
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
-pub struct Finger {
- pub slot: usize,
- pub x: f32,
- pub y: f32,
-}
-
-#[derive(Debug, Clone)]
-pub struct Trackpad {
- base: WidgetBase,
- pub fingers: Vec<Finger>,
-}
-
-impl Trackpad {
- pub fn new() -> Self {
- Self {
- base: WidgetBase::new(),
- fingers: Vec::new(),
- }
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_fingers(&mut self, fingers: Vec<Finger>) {
- self.fingers = fingers;
- }
-}
-
-impl Widget for Trackpad {
- fn base(&self) -> Option<&WidgetBase> {
- Some(&self.base)
- }
-
- fn base_mut(&mut self) -> Option<&mut WidgetBase> {
- Some(&mut self.base)
- }
-
- fn color(&self) -> [f32; 4] {
- [0.11, 0.11, 0.16, 0.85]
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let (x, y, w, h) = self.rect();
- let mut quads = Vec::new();
-
- // 1. Background
- quads.push((x, y, w, h, [0.11, 0.11, 0.16, 0.85]));
-
- // 2. Borders
- let border_color = [0.28, 0.28, 0.38, 1.0];
- quads.push((x, y, w, 1.0, border_color)); // Top
- quads.push((x, y + h - 1.0, w, 1.0, border_color)); // Bottom
- quads.push((x, y, 1.0, h, border_color)); // Left
- quads.push((x + w - 1.0, y, 1.0, h, border_color)); // Right
-
- // 3. Fingers
- for finger in &self.fingers {
- let rx = finger.x.clamp(0.0, 1.0);
- let ry = finger.y.clamp(0.0, 1.0);
- let fx = x + rx * w;
- let fy = y + ry * h;
- let dot_size = 12.0;
-
- // Render glow (outer light blue rectangle)
- quads.push((
- fx - (dot_size + 6.0) / 2.0,
- fy - (dot_size + 6.0) / 2.0,
- dot_size + 6.0,
- dot_size + 6.0,
- [0.35, 0.55, 0.95, 0.4],
- ));
- // Render core (solid blue/purple rectangle)
- quads.push((
- fx - dot_size / 2.0,
- fy - dot_size / 2.0,
- dot_size,
- dot_size,
- [0.45, 0.65, 1.0, 1.0],
- ));
- }
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let (x, y, _w, h) = self.rect();
- let mut labels = Vec::new();
-
- // Render "Touchpad Area" label
- labels.push(TextLabel {
- text: "Touchpad Area".to_string(),
- x: x + 12.0,
- y: y + h - 22.0,
- font_size: 11.0,
- color: [0x73, 0x73, 0x8c],
- });
-
- // Optional widget-base label on top
- if let Some(ref label) = self.base.label {
- labels.push(TextLabel {
- text: label.clone(),
- x,
- y: y - 18.0,
- font_size: 12.0,
- color: [0x83, 0x83, 0x8a],
- });
- }
-
- labels
- }
-
- fn top_room(&self) -> f32 {
- if self.base.label.is_some() {
- 16.0
- } else {
- 0.0
- }
- }
-
- fn draggable(&self) -> bool { true }
- fn is_dragging(&self) -> bool { !self.fingers.is_empty() }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- let (x, y, w, h) = self.rect();
- if w > 0.0 && h > 0.0 {
- let rx = ((px - x) / w).clamp(0.0, 1.0);
- let ry = ((py - y) / h).clamp(0.0, 1.0);
- self.fingers = vec![Finger { slot: 0, x: rx, y: ry }];
- }
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- let (x, y, w, h) = self.rect();
- if w > 0.0 && h > 0.0 {
- let rx = ((px - x) / w).clamp(0.0, 1.0);
- let ry = ((py - y) / h).clamp(0.0, 1.0);
- self.fingers = vec![Finger { slot: 0, x: rx, y: ry }];
- true
- } else {
- false
- }
- }
-
- fn drag_end(&mut self) {
- self.fingers.clear();
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button == MouseButton::Left {
- let (x, y, w, h) = self.rect();
- if px >= x && px <= x + w && py >= y && py <= y + h {
- if state == ElementState::Pressed {
- let rx = ((px - x) / w).clamp(0.0, 1.0);
- let ry = ((py - y) / h).clamp(0.0, 1.0);
- self.fingers = vec![Finger { slot: 0, x: rx, y: ry }];
- return true;
- } else {
- self.fingers.clear();
- return true;
- }
- }
- }
- false
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Separator {
- pub x: f32,
- pub y: f32,
- pub w: f32,
- pub h: f32,
- pub color: [f32; 4],
-}
-
-impl Separator {
- pub fn new(x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) -> Self {
- Self { x, y, w, h, color }
- }
-}
-
-impl Widget for Separator {
- fn rect(&self) -> (f32, f32, f32, f32) {
- (self.x, self.y, self.w, self.h)
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- }
-
- fn color(&self) -> [f32; 4] {
- self.color
- }
-}
-
-fn serialize_single_widget(w: &dyn Widget, json: &mut String) {
- let (x, y, width, height) = w.rect();
- let label = w.label().or_else(|| w.base().and_then(|b| b.label.clone())).unwrap_or_default();
- let focused = w.base().map_or(false, |b| b.focused);
- let hovered = w.hovered();
- let value = w.value();
- let type_name = w.type_name();
-
- // Escape JSON label
- let escaped_label = label.replace('\\', "\\\\").replace('"', "\\\"");
-
- json.push_str(&format!(
- "{{\"type\":\"{}\",\"label\":\"{}\",\"rect\":[{},{},{},{}],\"focused\":{},\"hovered\":{},\"value\":{}",
- type_name, escaped_label, x, y, width, height, focused, hovered, value
- ));
-
- // Handle children
- let children = w.children();
- let menu_items = w.menu_items();
-
- if type_name == "Menu" && w.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 checked_states = w.menu_item_checked();
- for (i, item) in menu_items.iter().enumerate() {
- if i > 0 {
- json.push(',');
- }
- let item_y = dy + i as f32 * DROPDOWN_ITEM_H;
- let checked = checked_states.get(i).copied().flatten().unwrap_or(false);
- let item_escaped = item.replace('\\', "\\\\").replace('"', "\\\"");
- json.push_str(&format!(
- "{{\"type\":\"MenuItem\",\"label\":\"{}\",\"rect\":[{},{},{},{}],\"focused\":false,\"hovered\":false,\"value\":{}}}",
- item_escaped, dx, item_y, dw, DROPDOWN_ITEM_H, if checked { 1 } else { 0 }
- ));
- }
- json.push_str("]}");
- } else if !children.is_empty() {
- json.push_str(",\"children\":[");
- for (i, child_ptr) in children.iter().enumerate() {
- if i > 0 {
- json.push(',');
- }
- unsafe {
- serialize_single_widget(&**child_ptr, json);
- }
- }
- json.push_str("]}");
- } else {
- json.push('}');
- }
-}
-
-pub fn serialize_widgets(widgets: &[Box<dyn Widget>]) -> String {
- let mut json = String::new();
- json.push('[');
- for (i, w) in widgets.iter().enumerate() {
- if i > 0 {
- json.push(',');
- }
- serialize_single_widget(&**w, &mut json);
- }
- json.push(']');
- json
-}
-
-pub struct Graph {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- show_network_grid: bool,
- grid_size_x: f32,
- grid_size_y: f32,
- grid_origin_x: f32,
- grid_origin_y: f32,
- skipped_row_h: f32,
- skipped_col_w: f32,
- nodes: Vec<GraphNode>,
- selected_idx: Option<usize>,
- double_clicked_idx: Option<usize>,
- double_click_timer: Option<(std::time::Instant, usize)>,
- grid_snap_enabled: bool,
- node_geom_toggled: Option<(usize, bool)>,
-
- // For dragging a node
- dragging_idx: Option<usize>,
- drag_ox: f32,
- drag_oy: f32,
- drag_node_pos: Option<(f32, f32)>,
-
- // Hover tracking
- toggle_hovered_idx: Option<usize>,
-
- uniform_background: bool,
- network_opacity: f32,
- cell_color: [f32; 3],
- gap_color: [f32; 3],
-}
-
-impl Graph {
- pub fn new() -> Self {
- Self {
- x: 0.0, y: 0.0, w: 0.0, h: 0.0,
- hovered: false,
- show_network_grid: false,
- grid_size_x: 150.0,
- grid_size_y: 75.0,
- grid_origin_x: 0.0,
- grid_origin_y: 0.0,
- skipped_row_h: 37.5,
- skipped_col_w: 37.5,
- nodes: Vec::new(),
- selected_idx: None,
- double_clicked_idx: None,
- double_click_timer: None,
- grid_snap_enabled: false,
- node_geom_toggled: None,
- dragging_idx: None,
- drag_ox: 0.0,
- drag_oy: 0.0,
- drag_node_pos: None,
- toggle_hovered_idx: None,
- uniform_background: false,
- network_opacity: 0.95,
- cell_color: [0.13, 0.13, 0.16],
- gap_color: [0.07, 0.07, 0.09],
- }
- }
-
- pub fn node_rect(&self, idx: usize) -> Option<(f32, f32, f32, f32)> {
- let node = self.nodes.get(idx)?;
- let (nx, ny) = if self.dragging_idx == Some(idx) {
- self.drag_node_pos.unwrap_or((
- node.position.0 * (self.grid_size_x + self.skipped_col_w) + self.grid_origin_x,
- node.position.1 * (self.grid_size_y + self.skipped_row_h) + self.grid_origin_y,
- ))
- } else {
- (
- node.position.0 * (self.grid_size_x + self.skipped_col_w) + self.grid_origin_x,
- node.position.1 * (self.grid_size_y + self.skipped_row_h) + self.grid_origin_y,
- )
- };
- Some((nx, ny, self.grid_size_x, self.grid_size_y))
- }
-
- 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))
- }
-
- fn find_empty_cell(&self, start_x: f32, start_y: f32, skip_idx: Option<usize>) -> (f32, f32) {
- let x = start_x;
- let mut y = start_y;
- loop {
- let occupied = self.nodes.iter().enumerate().any(|(idx, node)| {
- if Some(idx) == skip_idx {
- false
- } else {
- (node.position.0 - x).abs() < 0.01 && (node.position.1 - y).abs() < 0.01
- }
- });
- if occupied {
- y += 1.0;
- } else {
- break;
- }
- }
- (x, y)
- }
-}
-
-impl Widget for Graph {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn set_uniform_background(&mut self, uniform: bool) { self.uniform_background = uniform; }
- fn set_network_opacity(&mut self, opacity: f32) { self.network_opacity = opacity; }
- fn set_cell_color(&mut self, color: [f32; 3]) { self.cell_color = color; }
- fn set_gap_color(&mut self, color: [f32; 3]) { self.gap_color = color; }
- fn color(&self) -> [f32; 4] {
- if self.uniform_background {
- [0.10, 0.10, 0.13, self.network_opacity]
- } else {
- [0.0, 0.0, 0.0, 0.0]
- }
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- 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 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 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,
- color: [0xcc, 0xcc, 0xd4],
- });
- }
- }
- }
- labels
- }
-
- fn focus(&mut self) {
- focus::set_focused(self);
- }
-
- fn is_dragging(&self) -> bool { self.dragging_idx.is_some() }
- fn draggable(&self) -> bool { self.dragging_idx.is_some() }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- if let Some(idx) = self.dragging_idx {
- if let Some((nx, ny, _, _)) = self.node_rect(idx) {
- self.drag_ox = px - nx;
- self.drag_oy = py - ny;
- self.drag_node_pos = Some((nx, ny));
- }
- }
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- if self.dragging_idx.is_some() {
- let nx = px - self.drag_ox;
- let ny = py - self.drag_oy;
-
- let snap_x = if self.grid_snap_enabled { self.grid_size_x + self.skipped_col_w } else { 0.0 };
- let snap_y = if self.grid_snap_enabled { self.grid_size_y + self.skipped_row_h } else { 0.0 };
-
- let nx = if snap_x > 0.0 {
- let relative = nx - self.grid_origin_x;
- let snapped = (relative / snap_x).round() * snap_x;
- snapped + self.grid_origin_x
- } else { nx };
-
- let ny = if snap_y > 0.0 {
- let relative = ny - self.grid_origin_y;
- let snapped = (relative / snap_y).round() * snap_y;
- snapped + self.grid_origin_y
- } else { ny };
-
- self.drag_node_pos = Some((nx, ny));
- return true;
- }
- false
- }
-
- fn drag_end(&mut self) {
- if let Some((nx, ny)) = self.drag_node_pos.take() {
- let c = ((nx - self.grid_origin_x) / (self.grid_size_x + self.skipped_col_w)).round();
- let r = ((ny - self.grid_origin_y) / (self.grid_size_y + self.skipped_row_h)).round();
- if let Some(idx) = self.dragging_idx.take() {
- let (nx, ny) = self.find_empty_cell(c, r, Some(idx));
- self.nodes[idx].position = (nx, ny);
- }
- } else {
- self.dragging_idx = None;
- }
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was_toggle_hovered = self.toggle_hovered_idx;
- self.toggle_hovered_idx = None;
- for i in 0..self.nodes.len() {
- if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
- if px >= tx && px < tx + tw && py >= ty && py < ty + th {
- self.toggle_hovered_idx = Some(i);
- break;
- }
- }
- }
- was_toggle_hovered != self.toggle_hovered_idx
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- for i in (0..self.nodes.len()).rev() {
- if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
- if px >= tx && px < tx + tw && py >= ty && py < ty + th {
- self.nodes[i].geom_visible = !self.nodes[i].geom_visible;
- self.node_geom_toggled = Some((i, self.nodes[i].geom_visible));
- return true;
- }
- }
- if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
- if px >= nx && px < nx + nw && py >= ny && py < ny + nh {
- let now = std::time::Instant::now();
- if let Some((prev_time, prev_idx)) = self.double_click_timer {
- if prev_idx == i && now.duration_since(prev_time) < std::time::Duration::from_millis(500) {
- self.double_clicked_idx = Some(i);
- }
- }
- self.double_click_timer = Some((now, i));
- self.selected_idx = Some(i);
- self.dragging_idx = Some(i);
- self.drag_ox = px - nx;
- self.drag_oy = py - ny;
- self.drag_node_pos = Some((nx, ny));
- self.focus();
- return true;
- }
- }
- }
- self.selected_idx = None;
- false
- }
- ElementState::Released => {
- if self.dragging_idx.is_some() {
- self.drag_end();
- return true;
- }
- false
- }
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- let min_x = self.x;
- let min_y = self.y;
- let max_x = self.x + self.w;
- let max_y = self.y + self.h;
- let push_clipped = |qx: f32, qy: f32, qw: f32, qh: f32, qc: [f32; 4], q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
- let rx1 = qx.max(min_x);
- let ry1 = qy.max(min_y);
- let rx2 = (qx + qw).min(max_x);
- let ry2 = (qy + qh).min(max_y);
- let rw = rx2 - rx1;
- let rh = ry2 - ry1;
- if rw > 0.0 && rh > 0.0 {
- q.push((rx1, ry1, rw, rh, qc));
- }
- };
-
- // Draw connection wires
- let wire_color = [0.0, 0.75, 1.0, 0.7]; // Vibrant cyan glow
- let wire_thickness = 3.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")) {
- if let Some(src_idx) = self.nodes.iter().position(|n| n.name == *input_name) {
- if let (Some((sx, sy, sw, sh)), Some((ex, ey, ew, _eh))) = (self.node_rect(src_idx), self.node_rect(i)) {
- let start_x = sx + sw / 2.0;
- let start_y = sy + sh;
- let end_x = ex + ew / 2.0;
- let end_y = ey;
-
- let mid_y = start_y + (end_y - start_y) / 2.0;
-
- // Vertical segment 1
- let v1_min_y = start_y.min(mid_y);
- let v1_max_y = start_y.max(mid_y);
- push_clipped(
- start_x - wire_thickness / 2.0,
- v1_min_y,
- wire_thickness,
- v1_max_y - v1_min_y,
- wire_color,
- &mut quads,
- );
-
- // Horizontal segment
- let h_min_x = start_x.min(end_x);
- let h_max_x = start_x.max(end_x);
- push_clipped(
- h_min_x,
- mid_y - wire_thickness / 2.0,
- h_max_x - h_min_x,
- wire_thickness,
- wire_color,
- &mut quads,
- );
-
- // Vertical segment 2
- let v2_min_y = mid_y.min(end_y);
- let v2_max_y = mid_y.max(end_y);
- push_clipped(
- end_x - wire_thickness / 2.0,
- v2_min_y,
- wire_thickness,
- v2_max_y - v2_min_y,
- wire_color,
- &mut quads,
- );
- }
- }
- }
- }
-
- if self.show_network_grid && self.grid_size_x > 0.0 && self.grid_size_y > 0.0 && !self.uniform_background {
- let step_y = self.grid_size_y + self.skipped_row_h;
- let step_x = self.grid_size_x + self.skipped_col_w;
-
- 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 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);
-
- // 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]));
-
- // Draw filled cells with cell color
- for r in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (r as f32) * step_y;
- 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));
- }
- }
- }
- }
- }
-
- for i in 0..self.nodes.len() {
- if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
- let bg_color = if self.dragging_idx == Some(i) {
- colors::node_drag_color()
- } else if self.selected_idx == Some(i) {
- colors::node_selected_color()
- } else {
- colors::node_color()
- };
- push_clipped(nx, ny, nw, nh, bg_color, &mut quads);
-
- if let Some((tx, ty, tw, th)) = self.toggle_rect(i) {
- let btn_color = if self.toggle_hovered_idx == Some(i) {
- colors::TOGGLE_HOVER
- } else {
- colors::TOGGLE_OFF
- };
- push_clipped(tx, ty, tw, th, btn_color, &mut quads);
-
- if self.nodes[i].geom_visible {
- let inset = 3.0;
- push_clipped(tx + inset, ty + inset, tw - inset * 2.0, th - inset * 2.0, colors::TOGGLE_ON, &mut quads);
- }
- }
- }
- }
-
- 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) -> bool {
- if self.hit_test(px, py) {
- match delta {
- MouseScrollDelta::LineDelta(x, y) => {
- self.grid_origin_x += *x * 15.0;
- self.grid_origin_y += *y * 15.0;
- true
- }
- MouseScrollDelta::PixelDelta(pos) => {
- self.grid_origin_x += pos.x as f32;
- self.grid_origin_y += pos.y as f32;
- true
- }
- }
- } else {
- false
- }
- }
-}
-
-
-
-
diff --git a/src/widget/container.rs b/src/widget/container.rs
new file mode 100644
index 0000000..0daecb7
--- /dev/null
+++ b/src/widget/container.rs
@@ -0,0 +1,5076 @@
+use crate::colors;
+use crate::widget::*;
+use crate::widget::display::make_widget_text_buffer;
+use crate::widget::input::{BREADCRUMB_PADDING, SEGMENT_GAP};
+
+#[derive(Clone)]
+pub struct Container {
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+}
+
+impl Container {
+ pub fn new() -> Self {
+ Self { parent: None, children: Vec::new() }
+ }
+}
+
+impl Element for Container {
+ fn rect(&self) -> (f32, f32, f32, f32) { (0.0, 0.0, 0.0, 0.0) }
+ fn set_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
+ fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+
+ fn focus(&mut self) {
+ focus::set_focused(self);
+ }
+ fn unfocus(&mut self) {}
+
+ fn parent(&self) -> Option<*mut (dyn Element + 'static)> { self.parent }
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) { self.parent = parent; }
+ fn children(&self) -> Vec<*mut (dyn Element + 'static)> { self.children.clone() }
+ fn add_child(&mut self, child: *mut (dyn Element + 'static)) { self.children.push(child); }
+ fn clear_children(&mut self) { self.children.clear(); }
+}
+
+impl Drop for Container {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
+
+pub struct Header {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+}
+
+impl Header {
+ pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+}
+
+impl Element for Header {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::HEADER_BG }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+}
+
+pub struct ContentBg {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+ show_network_grid: bool,
+ grid_size_x: f32,
+ grid_size_y: f32,
+ grid_origin_x: f32,
+ grid_origin_y: f32,
+ skipped_row_h: f32,
+ skipped_col_w: f32,
+}
+
+impl ContentBg {
+ pub fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 }
+ }
+}
+
+impl Element for ContentBg {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] {
+ if self.show_network_grid {
+ [0.0, 0.0, 0.0, 0.0]
+ } else {
+ colors::CONTENT_BG
+ }
+ }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+ fn hit_test(&self, _px: f32, _py: f32) -> 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 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 {
+ return vec![];
+ }
+ let mut quads = Vec::new();
+ let grid_color = [0.0, 0.0, 0.0, 0.0];
+ let max_alpha = colors::CONTENT_BG[3]; // Peak opacity in the middle of gradient cells matches non-gradient cells
+ let steps = 20; // Silky-smooth gradient transition
+
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ 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 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);
+
+ // Draw individual cell backgrounds to avoid stacking with gradients
+ for ry in ry_start..=ry_end {
+ let y1 = self.grid_origin_y + (ry as f32) * step_y;
+ let draw_start_y = y1.max(self.y);
+ let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
+ if draw_start_y < draw_end_y {
+ for cx in cx_start..=cx_end {
+ let x1 = self.grid_origin_x + (cx as f32) * step_x;
+ let draw_start_x = x1.max(self.x);
+ let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
+ if draw_start_x < draw_end_x {
+ quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
+ }
+ }
+ }
+ }
+ }
+
+ // Draw interstitial row gradients (horizontal bands fading to 0 alpha at left and right sides)
+ if self.skipped_row_h > 0.0 {
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ let step_x = self.grid_size_x + self.skipped_col_w;
+ if step_y >= 4.0 && step_x >= 4.0 {
+ let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.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);
+
+ for k in k_start..=k_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 {
+ continue;
+ }
+ let draw_start_y = y2.max(self.y);
+ let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
+ if draw_start_y >= draw_end_y {
+ continue;
+ }
+
+ for cx in cx_start..=cx_end {
+ let x1 = self.grid_origin_x + (cx as f32) * step_x;
+ let x_mid = x1 + self.grid_size_x / 2.0;
+ let w_total = self.grid_size_x;
+ let sub_w = w_total / steps as f32;
+
+ for i in 0..steps {
+ let sx_start = x1 + i as f32 * sub_w;
+ let sx_end = sx_start + sub_w;
+ let draw_start_x = sx_start.max(self.x);
+ let draw_end_x = sx_end.min(self.x + self.w);
+ if draw_start_x < draw_end_x {
+ let sx_mid = (sx_start + sx_end) / 2.0;
+ let dist = (sx_mid - x_mid).abs();
+ let d = (dist / (w_total / 2.0)).min(1.0);
+
+ // Fade the cell background color from max_alpha in the middle to transparent at the edges
+ let alpha = max_alpha * (1.0 - d);
+ if alpha > 0.001 {
+ quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Draw interstitial column gradients (vertical bands fading to 0 alpha at top and bottom)
+ if self.skipped_col_w > 0.0 {
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ let step_x = self.grid_size_x + self.skipped_col_w;
+ if step_y >= 4.0 && step_x >= 4.0 {
+ let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+
+ 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);
+
+ for k in k_start..=k_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 {
+ continue;
+ }
+ let draw_start_x = x2.max(self.x);
+ let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
+ if draw_start_x >= draw_end_x {
+ continue;
+ }
+
+ for ry in ry_start..=ry_end {
+ let y1 = self.grid_origin_y + (ry as f32) * step_y;
+ let y_mid = y1 + self.grid_size_y / 2.0;
+ let h_total = self.grid_size_y;
+ let sub_h = h_total / steps as f32;
+
+ for i in 0..steps {
+ let sy_start = y1 + i as f32 * sub_h;
+ let sy_end = sy_start + sub_h;
+ let draw_start_y = sy_start.max(self.y);
+ let draw_end_y = sy_end.min(self.y + self.h);
+ if draw_start_y < draw_end_y {
+ let sy_mid = (sy_start + sy_end) / 2.0;
+ let dist = (sy_mid - y_mid).abs();
+ let d = (dist / (h_total / 2.0)).min(1.0);
+
+ // Fade the cell background color from max_alpha in the middle to transparent at the edges
+ let alpha = max_alpha * (1.0 - d);
+ if alpha > 0.001 {
+ quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Draw the grid borders
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ if step_y >= 4.0 {
+ let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+ for k in k_start..=k_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 {
+ continue;
+ }
+ if y1 >= self.y {
+ quads.push((self.x, y1, self.w, 1.0, grid_color));
+ }
+ if y2 >= self.y && y2 < self.y + self.h {
+ quads.push((self.x, y2, self.w, 1.0, grid_color));
+ }
+ }
+ }
+
+ let step_x = self.grid_size_x + self.skipped_col_w;
+ if step_x >= 4.0 {
+ let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+ for k in k_start..=k_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 {
+ continue;
+ }
+ if x1 >= self.x {
+ quads.push((x1, self.y, 1.0, self.h, grid_color));
+ }
+ if x2 >= self.x && x2 < self.x + self.w {
+ quads.push((x2, self.y, 1.0, self.h, grid_color));
+ }
+ }
+ }
+ quads
+ }
+}
+
+pub struct ViewportBg {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+}
+
+impl ViewportBg {
+ pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+}
+
+impl Element for ViewportBg {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::VIEWPORT_BG }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+ fn hit_test(&self, _px: f32, _py: f32) -> bool { false }
+}
+
+pub struct ParametersBg {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+ display_params: Vec<(String, String, String)>,
+ dragging_param: Option<usize>,
+ pub focused_param: Option<usize>,
+ mouse_pos: Option<(f32, f32)>,
+ sliders: Vec<Option<Slider>>,
+ float3s: Vec<Option<Float3>>,
+ spinboxes: Vec<Option<Spinbox>>,
+ visible: bool,
+}
+
+impl ParametersBg {
+ pub fn new() -> Self {
+ Self {
+ x: 0.0,
+ y: 0.0,
+ w: 0.0,
+ h: 0.0,
+ hovered: false,
+ display_params: Vec::new(),
+ dragging_param: None,
+ focused_param: None,
+ mouse_pos: None,
+ sliders: Vec::new(),
+ float3s: Vec::new(),
+ spinboxes: Vec::new(),
+ visible: true,
+ }
+ }
+
+ pub fn get_param_rects(&self) -> Vec<(f32, f32, f32, f32)> {
+ let mut rects = Vec::new();
+ let mut cur_y = self.y + 30.0;
+ for p in &self.display_params {
+ let h = if p.2 == "code" {
+ 200.0
+ } else if p.2 == "section" {
+ 24.0
+ } else if p.2.starts_with("float3") {
+ 108.0
+ } else if p.2 == "text" {
+ 24.0
+ } else {
+ 20.0
+ };
+ rects.push((self.x + 8.0, cur_y, self.w - 16.0, h));
+ cur_y += h + 8.0;
+ }
+ rects
+ }
+
+ fn update_slider_rects(&mut self) {
+ let rects = self.get_param_rects();
+ for (i, s_opt) in self.sliders.iter_mut().enumerate() {
+ if let Some(s) = s_opt {
+ let r = rects[i];
+ let track_x = self.x + 100.0;
+ let track_w = (self.w - 100.0 - 20.0).max(10.0);
+ let track_y = r.1 + 4.0;
+ let track_h = 12.0;
+ s.set_rect(track_x, track_y, track_w, track_h);
+ }
+ }
+ for (i, f_opt) in self.float3s.iter_mut().enumerate() {
+ if let Some(f) = f_opt {
+ let r = rects[i];
+ f.set_rect(r.0, r.1, r.2, r.3);
+ }
+ }
+ for (i, sb_opt) in self.spinboxes.iter_mut().enumerate() {
+ if let Some(sb) = sb_opt {
+ let r = rects[i];
+ let box_x = self.x + 100.0;
+ let box_w = (self.w - 100.0 - 16.0).max(10.0);
+ sb.set_rect(box_x, r.1, box_w, r.3);
+ }
+ }
+ }
+}
+
+fn parse_slider_range(ptype: &str) -> (f32, f32) {
+ if ptype.starts_with("slider:") || ptype.starts_with("float3:") {
+ let parts: Vec<&str> = ptype.split(':').collect();
+ if parts.len() >= 3 {
+ if let (Ok(min), Ok(max)) = (parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
+ return (min, max);
+ }
+ }
+ }
+ (0.0, 2.0)
+}
+
+fn parse_spinbox_range(ptype: &str) -> (i32, i32, i32) {
+ if ptype.starts_with("spinbox:") {
+ let parts: Vec<&str> = ptype.split(':').collect();
+ if parts.len() >= 4 {
+ if let (Ok(min), Ok(max), Ok(step)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>(), parts[3].parse::<i32>()) {
+ return (min, max, step);
+ }
+ } else if parts.len() == 3 {
+ if let (Ok(min), Ok(max)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>()) {
+ return (min, max, 1);
+ }
+ }
+ }
+ (0, 10000, 1)
+}
+
+fn parse_float3_value(val_str: &str, min: f32, max: f32) -> [f32; 3] {
+ let mut out = [0.5, 0.5, 0.5];
+ let parts: Vec<&str> = val_str
+ .split(|c| c == ':' || c == ',' || c == ' ')
+ .filter(|s| !s.is_empty())
+ .collect();
+ for i in 0..3 {
+ if i < parts.len() {
+ if let Ok(v) = parts[i].parse::<f32>() {
+ let range = max - min;
+ if range != 0.0 {
+ out[i] = ((v - min) / range).clamp(0.0, 1.0);
+ } else {
+ out[i] = 0.0;
+ }
+ }
+ }
+ }
+ out
+}
+
+impl Element for ParametersBg {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.x = x; self.y = y; self.w = w; self.h = h;
+ self.update_slider_rects();
+ }
+ fn color(&self) -> [f32; 4] {
+ if !self.visible {
+ return [0.0, 0.0, 0.0, 0.0];
+ }
+ colors::PARAM_BG
+ }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+ fn set_visible(&mut self, visible: bool) {
+ self.visible = visible;
+ }
+ fn visible(&self) -> bool {
+ self.visible
+ }
+ fn hit_test(&self, px: f32, py: f32) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.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 unfocus(&mut self) {
+ if let Some(idx) = self.focused_param {
+ if idx < self.display_params.len() {
+ let p = &mut self.display_params[idx];
+ if p.2.starts_with("spinbox") {
+ if let Some(sb) = &mut self.spinboxes[idx] {
+ sb.unfocus();
+ p.1 = sb.value.to_string();
+ }
+ } else if p.2.starts_with("slider") {
+ if let Some(s) = &mut self.sliders[idx] {
+ s.unfocus();
+ let (min, max) = parse_slider_range(&p.2);
+ let new_val = min + s.value * (max - min);
+ p.1 = format!("{:.2}", new_val);
+ }
+ } else if p.2.starts_with("float3") {
+ if let Some(f) = &mut self.float3s[idx] {
+ f.unfocus();
+ let (min, max) = parse_slider_range(&p.2);
+ let val0 = min + f.values[0] * (max - min);
+ let val1 = min + f.values[1] * (max - min);
+ let val2 = min + f.values[2] * (max - min);
+ p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+ }
+ }
+ }
+ }
+ self.focused_param = None;
+ }
+
+ 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"))
+ }
+
+ fn is_dragging(&self) -> bool {
+ self.dragging_param.is_some()
+ }
+
+ fn drag_begin(&mut self, px: f32, py: f32) {
+ let rects = self.get_param_rects();
+ for (i, p) in self.display_params.iter().enumerate() {
+ if p.2.starts_with("slider") {
+ let r = rects[i];
+ let row_y = r.1;
+ if py >= row_y - 2.0 && py <= row_y + 18.0 {
+ if let Some(s) = &mut self.sliders[i] {
+ s.drag_begin(px, py);
+ self.dragging_param = Some(i);
+ break;
+ }
+ }
+ } else if p.2.starts_with("float3") {
+ let r = rects[i];
+ if py >= r.1 && py <= r.1 + r.3 {
+ if let Some(f) = &mut self.float3s[i] {
+ if f.mouse_input(MouseButton::Left, ElementState::Pressed, px, py) {
+ self.dragging_param = Some(i);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn drag_update(&mut self, px: f32, py: f32) -> bool {
+ if let Some(i) = self.dragging_param {
+ if let Some(s) = &mut self.sliders[i] {
+ if s.drag_update(px, py) {
+ let (min, max) = parse_slider_range(&self.display_params[i].2);
+ let new_val = min + s.value * (max - min);
+ let old_val = &self.display_params[i].1;
+ let new_val_str = format!("{:.2}", new_val);
+ if *old_val != new_val_str {
+ self.display_params[i].1 = new_val_str;
+ return true;
+ }
+ }
+ } else if let Some(f) = &mut self.float3s[i] {
+ if f.drag_update(px, py) {
+ let (min, max) = parse_slider_range(&self.display_params[i].2);
+ let val0 = min + f.values[0] * (max - min);
+ let val1 = min + f.values[1] * (max - min);
+ let val2 = min + f.values[2] * (max - min);
+ let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+ let old_val = &self.display_params[i].1;
+ if *old_val != new_val_str {
+ self.display_params[i].1 = new_val_str;
+ return true;
+ }
+ }
+ }
+ }
+ false
+ }
+
+ fn drag_end(&mut self) {
+ if let Some(i) = self.dragging_param.take() {
+ if let Some(s) = &mut self.sliders[i] {
+ s.drag_end();
+ } else if let Some(f) = &mut self.float3s[i] {
+ f.drag_end();
+ }
+ }
+ }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ self.mouse_pos = Some((px, py));
+ true
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button == MouseButton::Left && state == ElementState::Pressed {
+ let rects = self.get_param_rects();
+ let mut clicked_any_focusable = false;
+ for (i, p) in self.display_params.iter_mut().enumerate() {
+ if p.2 == "code" {
+ let r = rects[i];
+ if px >= r.0 && px <= r.0 + r.2 && py >= r.1 + 18.0 && py <= r.1 + r.3 {
+ self.focused_param = Some(i);
+ clicked_any_focusable = true;
+ break;
+ }
+ } else if p.2 == "text" {
+ let box_x = self.x + 100.0;
+ let box_w = (self.w - 100.0 - 16.0).max(10.0);
+ let r = rects[i];
+ if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
+ self.focused_param = Some(i);
+ clicked_any_focusable = true;
+ break;
+ }
+ } else if p.2.starts_with("spinbox") {
+ if let Some(sb) = &mut self.spinboxes[i] {
+ if sb.mouse_input(button, state, px, py) {
+ p.1 = sb.value.to_string();
+ if sb.editing {
+ self.focused_param = Some(i);
+ } else {
+ self.unfocus();
+ }
+ return true;
+ }
+ }
+ } else if p.2.starts_with("slider") {
+ let r = rects[i];
+ if py >= r.1 && py <= r.1 + r.3 {
+ if let Some(s) = &mut self.sliders[i] {
+ if s.mouse_input(button, state, px, py) {
+ if s.editing {
+ self.focused_param = Some(i);
+ clicked_any_focusable = true;
+ }
+ break;
+ }
+ }
+ }
+ } else if p.2.starts_with("float3") {
+ let r = rects[i];
+ if py >= r.1 && py <= r.1 + r.3 {
+ if let Some(f) = &mut self.float3s[i] {
+ if f.mouse_input(button, state, px, py) {
+ if f.editing_idx.is_some() {
+ self.focused_param = Some(i);
+ clicked_any_focusable = true;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ if !clicked_any_focusable {
+ self.unfocus();
+ }
+ return true;
+ }
+ false
+ }
+
+ fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+ if let Some(idx) = self.focused_param {
+ if event.state == ElementState::Pressed {
+ let p = &mut self.display_params[idx];
+ if p.2 == "code" {
+ match &event.logical_key {
+ Key::Named(NamedKey::Backspace) => {
+ if !p.1.is_empty() {
+ p.1.pop();
+ return true;
+ }
+ }
+ Key::Named(NamedKey::Enter) => {
+ p.1.push('\n');
+ return true;
+ }
+ Key::Named(NamedKey::Escape) => {
+ self.focused_param = None;
+ return true;
+ }
+ Key::Character(s) => {
+ p.1.push_str(s);
+ return true;
+ }
+ _ => {}
+ }
+ } else if p.2 == "text" {
+ match &event.logical_key {
+ Key::Named(NamedKey::Backspace) => {
+ if !p.1.is_empty() {
+ p.1.pop();
+ return true;
+ }
+ }
+ Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Escape) => {
+ self.focused_param = None;
+ return true;
+ }
+ Key::Character(s) => {
+ p.1.push_str(s);
+ return true;
+ }
+ _ => {}
+ }
+ } else if p.2.starts_with("spinbox") {
+ if let Some(sb) = &mut self.spinboxes[idx] {
+ if sb.keyboard_input(event) {
+ if !sb.editing {
+ p.1 = sb.value.to_string();
+ self.focused_param = None;
+ } else {
+ p.1 = sb.edit_buffer.clone();
+ }
+ return true;
+ }
+ }
+ } else if p.2.starts_with("slider") {
+ if let Some(s) = &mut self.sliders[idx] {
+ if s.keyboard_input(event) {
+ let (min, max) = parse_slider_range(&p.2);
+ let new_val = min + s.value * (max - min);
+ p.1 = format!("{:.2}", new_val);
+ if !s.editing {
+ self.focused_param = None;
+ }
+ return true;
+ }
+ }
+ } else if p.2.starts_with("float3") {
+ if let Some(f) = &mut self.float3s[idx] {
+ if f.keyboard_input(event) {
+ let (min, max) = parse_slider_range(&p.2);
+ let val0 = min + f.values[0] * (max - min);
+ let val1 = min + f.values[1] * (max - min);
+ let val2 = min + f.values[2] * (max - min);
+ p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+ if f.editing_idx.is_none() {
+ self.focused_param = None;
+ }
+ return true;
+ }
+ }
+ }
+ }
+ }
+ false
+ }
+
+ fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
+ let mut changed = false;
+ let rects = self.get_param_rects();
+ for (i, p) in self.display_params.iter_mut().enumerate() {
+ if p.2.starts_with("slider") {
+ let r = rects[i];
+ let row_y = r.1;
+ if py >= row_y - 2.0 && py <= row_y + 18.0 && px >= self.x && px <= self.x + self.w {
+ if let Some(s) = &mut self.sliders[i] {
+ let was_scroll = s.scroll_enabled;
+ s.set_scroll(true);
+ if s.mouse_wheel(delta, px, py) {
+ let (min, max) = parse_slider_range(&p.2);
+ let new_val = min + s.value * (max - min);
+ let old_val = &p.1;
+ let new_val_str = format!("{:.2}", new_val);
+ if *old_val != new_val_str {
+ p.1 = new_val_str;
+ changed = true;
+ }
+ }
+ s.set_scroll(was_scroll);
+ }
+ }
+ } else if p.2.starts_with("float3") {
+ let r = rects[i];
+ let row_y = r.1;
+ if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
+ if let Some(f) = &mut self.float3s[i] {
+ let rects_inner = f.get_row_rects();
+ for j in 0..3 {
+ let r_inner = rects_inner[j];
+ if py >= r_inner.1 && py <= r_inner.1 + r_inner.3 {
+ let scroll_amount = match delta {
+ MouseScrollDelta::LineDelta(_x, y) => *y,
+ MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
+ };
+ let step = 0.02;
+ let new_val = (f.values[j] - scroll_amount * step).clamp(0.0, 1.0);
+ if (new_val - f.values[j]).abs() > 0.0001 {
+ f.values[j] = new_val;
+ if f.editing_idx == Some(j) {
+ let scaled_val = f.mins[j] + f.values[j] * (f.maxs[j] - f.mins[j]);
+ f.edit_buffer = format!("{:.2}", scaled_val);
+ }
+ let (min, max) = parse_slider_range(&p.2);
+ let val0 = min + f.values[0] * (max - min);
+ let val1 = min + f.values[1] * (max - min);
+ let val2 = min + f.values[2] * (max - min);
+ let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
+ if p.1 != new_val_str {
+ p.1 = new_val_str;
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ } else if p.2.starts_with("spinbox") {
+ let r = rects[i];
+ let row_y = r.1;
+ if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
+ if let Some(sb) = &mut self.spinboxes[i] {
+ let scroll_amount = match delta {
+ MouseScrollDelta::LineDelta(_x, y) => *y as i32,
+ MouseScrollDelta::PixelDelta(pos) => {
+ let dy = pos.y;
+ if dy > 0.0 { 1 } else if dy < 0.0 { -1 } else { 0 }
+ }
+ };
+ let new_val = (sb.value + scroll_amount * sb.step).clamp(sb.min, sb.max);
+ if sb.value != new_val {
+ sb.value = new_val;
+ p.1 = new_val.to_string();
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+ changed
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut quads = Vec::new();
+ let rects = self.get_param_rects();
+
+ // Find sections and their ranges
+ let mut sections = Vec::new();
+ let mut current_section: Option<(usize, usize)> = None;
+ let mut in_section = false;
+ for (i, p) in self.display_params.iter().enumerate() {
+ if p.2 == "section" {
+ if let Some((start, end)) = current_section {
+ sections.push((start, end));
+ }
+ current_section = None;
+ in_section = true;
+ } else {
+ if in_section {
+ if let Some((_, ref mut end)) = current_section {
+ *end = i;
+ } else {
+ current_section = Some((i, i));
+ }
+ }
+ }
+ }
+ if let Some((start, end)) = current_section {
+ sections.push((start, end));
+ }
+
+ // Draw section border boxes
+ for (start, end) in sections {
+ if start <= end && start < rects.len() && end < rects.len() {
+ let r_start = rects[start];
+ let r_end = rects[end];
+ let bx = self.x + 4.0;
+ let bw = self.w - 8.0;
+ let by = r_start.1 - 4.0;
+ let bh = (r_end.1 + r_end.3 + 4.0) - by;
+
+ let border_color = [0.18, 0.18, 0.27, 1.0];
+ let border_t = 1.0;
+
+ // Top border
+ quads.push((bx, by, bw, border_t, border_color));
+ // Bottom border
+ quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+ // Left border
+ quads.push((bx, by, border_t, bh, border_color));
+ // Right border
+ quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+ }
+ }
+
+ for (i, p) in self.display_params.iter().enumerate() {
+ let r = rects[i];
+ if p.2.starts_with("slider") {
+ if let Some(s) = &self.sliders[i] {
+ let (sx, sy, sw, sh) = s.rect();
+ quads.push((sx, sy, sw, sh, s.color()));
+ quads.extend(s.extra_quads());
+ }
+ } else if p.2 == "section" {
+ // Section header line is handled by the border box top border now
+ } else if p.2.starts_with("float3") {
+ if let Some(f) = &self.float3s[i] {
+ quads.extend(f.extra_quads());
+ }
+ } else if p.2 == "code" {
+ quads.push((r.0, r.1 + 18.0, r.2, r.3 - 18.0, [0.08, 0.08, 0.10, 1.0]));
+ let border_color = if self.focused_param == Some(i) {
+ [0.25, 0.45, 0.85, 1.0]
+ } else {
+ [0.20, 0.20, 0.25, 1.0]
+ };
+ let (bx, by, bw, bh) = (r.0, r.1 + 18.0, r.2, r.3 - 18.0);
+ quads.push((bx, by, bw, 1.0, border_color));
+ quads.push((bx, by + bh - 1.0, bw, 1.0, border_color));
+ quads.push((bx, by, 1.0, bh, border_color));
+ quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
+ } else if p.2 == "text" {
+ let box_x = self.x + 100.0;
+ let box_w = (self.w - 100.0 - 16.0).max(10.0);
+ quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
+ let border_color = if self.focused_param == Some(i) {
+ [0.25, 0.45, 0.85, 1.0]
+ } else {
+ [0.20, 0.20, 0.25, 1.0]
+ };
+ let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
+ let border_t = 1.0;
+ quads.push((bx, by, bw, border_t, border_color));
+ quads.push((bx, by + bh - border_t, bw, border_t, border_color));
+ quads.push((bx, by, border_t, bh, border_color));
+ quads.push((bx + bw - border_t, by, border_t, bh, border_color));
+ } else if p.2.starts_with("spinbox") {
+ if let Some(sb) = &self.spinboxes[i] {
+ quads.push((sb.base.x, sb.base.y, sb.base.w, sb.base.h, sb.color()));
+ quads.extend(sb.extra_quads());
+ }
+ }
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let rects = self.get_param_rects();
+ let mut labels = Vec::new();
+ for (i, (name, value, ptype)) in self.display_params.iter().enumerate() {
+ let r = rects[i];
+ if ptype.starts_with("slider") {
+ labels.push(TextLabel {
+ text: name.clone(),
+ x: self.x + 8.0,
+ y: r.1,
+ font_size: 12.0,
+ color: [0xaa, 0xaa, 0xbb],
+ });
+ if let Some(s) = &self.sliders[i] {
+ labels.extend(s.text_labels());
+ }
+ } else if ptype.starts_with("float3") {
+ if let Some(f) = &self.float3s[i] {
+ labels.extend(f.text_labels());
+ }
+ } else if ptype == "section" {
+ labels.push(TextLabel {
+ text: name.clone(),
+ x: self.x + 12.0,
+ y: r.1 + 2.0,
+ font_size: 13.0,
+ color: [0xee, 0xee, 0xf0],
+ });
+ } else if ptype == "code" {
+ labels.push(TextLabel {
+ text: format!("{}:\n{}", name, value),
+ x: self.x + 12.0,
+ y: r.1,
+ font_size: 12.0,
+ color: [0xaa, 0xaa, 0xbb],
+ });
+ } else if ptype.starts_with("spinbox") {
+ labels.push(TextLabel {
+ text: name.clone(),
+ x: self.x + 8.0,
+ y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+ font_size: 12.0,
+ color: [0xaa, 0xaa, 0xbb],
+ });
+ if let Some(sb) = &self.spinboxes[i] {
+ labels.extend(sb.text_labels());
+ }
+ } else if ptype == "text" {
+ labels.push(TextLabel {
+ text: name.clone(),
+ x: self.x + 8.0,
+ y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
+ font_size: 12.0,
+ color: [0xaa, 0xaa, 0xbb],
+ });
+ let val_text = if self.focused_param == Some(i) {
+ format!("{}|", value)
+ } else {
+ value.clone()
+ };
+ labels.push(TextLabel {
+ text: val_text,
+ x: self.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),
+ x: self.x + 8.0,
+ y: r.1,
+ font_size: 12.0,
+ color: [0xaa, 0xaa, 0xbb],
+ });
+ }
+ }
+ labels
+ }
+}
+
+pub struct MenuBar {
+ x: f32, y: f32, w: f32, h: f32,
+ hovering: bool,
+ pub title: String,
+ pub menus: Vec<Box<Menu>>,
+ pub menu_items: Vec<String>,
+ pub vertical_items: Vec<String>,
+ pub menu_dropdowns: Vec<Vec<String>>,
+ pub menu_dropdown_checked: Vec<Vec<Option<bool>>>,
+ pub hovered_menu: Option<usize>,
+ pub open_menu: Option<usize>,
+ pub hovered_dropdown: Option<usize>,
+ pub clicked_dropdown: Option<(usize, usize)>,
+ pub was_open: Option<usize>,
+ pub vertical: bool,
+ pub visible: bool,
+ pub focused: bool,
+ pub z_level: i32,
+ pub center_items: bool,
+ pub curved_circle: Option<(f32, f32, f32)>,
+ pub title_pos: Option<(f32, f32)>,
+ pub title_buf: Option<glyphon::Buffer>,
+ pub curved_title_char_bufs: Vec<glyphon::Buffer>,
+ pub network_opacity: f32,
+}
+
+impl MenuBar {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self {
+ x, y, w, h, hovering: false,
+ title: String::new(),
+ menus: Vec::new(),
+ menu_items: Vec::new(),
+ vertical_items: Vec::new(),
+ menu_dropdowns: Vec::new(),
+ menu_dropdown_checked: Vec::new(),
+ hovered_menu: None,
+ open_menu: None,
+ hovered_dropdown: None,
+ clicked_dropdown: None,
+ was_open: None,
+ vertical: false,
+ visible: true,
+ focused: false,
+ z_level: 100,
+ center_items: false,
+ curved_circle: None,
+ title_pos: None,
+ title_buf: None,
+ curved_title_char_bufs: Vec::new(),
+ network_opacity: 1.0,
+ }
+ }
+
+ pub fn with_center_items(mut self, center: bool) -> Self {
+ self.center_items = center;
+ self
+ }
+
+ pub fn with_title(mut self, title: &str) -> Self {
+ self.title = title.to_string();
+ self
+ }
+
+ pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
+ self.menu_items.push(label.to_string());
+ self.vertical_items.push(label.to_string());
+ self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
+ self.menu_dropdown_checked.push(vec![None; items.len()]);
+
+ let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
+ let mut menu = Menu::new(label, label, &item_strs);
+ menu.vertical = self.vertical;
+ self.menus.push(Box::new(menu));
+ self
+ }
+
+ pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
+ self.menu_items.push(horizontal_label.to_string());
+ self.vertical_items.push(vertical_label.to_string());
+ self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
+ self.menu_dropdown_checked.push(vec![None; items.len()]);
+
+ let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
+ let mut menu = Menu::new(horizontal_label, vertical_label, &item_strs);
+ menu.vertical = self.vertical;
+ self.menus.push(Box::new(menu));
+ self
+ }
+
+ pub fn with_vertical(mut self, vertical: bool) -> Self {
+ self.vertical = vertical;
+ for menu in &mut self.menus {
+ menu.vertical = vertical;
+ }
+ self
+ }
+
+ pub fn with_z_index(mut self, z: i32) -> Self {
+ self.z_level = z;
+ self
+ }
+
+ fn item_y_vertical(&self, idx: usize) -> f32 {
+ let mut y = 8.0;
+ if !self.title.is_empty() {
+ y += 24.0;
+ }
+ y + idx as f32 * 24.0
+ }
+
+ fn item_h_vertical(&self) -> f32 {
+ 24.0
+ }
+}
+
+impl Element for MenuBar {
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ if !self.visible {
+ return (0.0, 0.0, 0.0, 0.0);
+ }
+ if self.vertical {
+ let total_h = if self.menus.is_empty() {
+ self.h
+ } else {
+ let last_idx = self.menus.len() - 1;
+ self.item_y_vertical(last_idx) + self.item_h_vertical()
+ };
+ (self.x, self.y, self.w, total_h)
+ } else {
+ (self.x, self.y, self.w, self.h)
+ }
+ }
+
+ fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
+ self.curved_circle = circle;
+ if circle.is_none() {
+ for menu in &mut self.menus {
+ menu.curved_arc = None;
+ }
+ }
+ }
+
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.x = x;
+ self.y = y;
+ self.w = w;
+ self.h = h;
+
+ let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
+
+ if self.vertical {
+ let mut cy = 8.0;
+ if !self.title.is_empty() {
+ cy += 24.0;
+ }
+ for menu in &mut self.menus {
+ let ih = 24.0;
+ menu.set_rect(x, y + cy, w, ih);
+ menu.set_parent(Some(parent_ptr));
+ cy += ih;
+ }
+ } else {
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+
+ let total_angular_width = total_width / r_mid;
+ let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+ let mut current_angle = start_angle;
+
+ if !self.title.is_empty() {
+ let title_w = self.title.len() as f32 * 7.5 + 24.0;
+ let dtheta_title = title_w / r_mid;
+ let theta_title = current_angle + dtheta_title / 2.0;
+
+ let tx = ccx + r_mid * theta_title.cos() - title_w / 2.0 + 8.0;
+ let ty = ccy + r_mid * theta_title.sin() - h / 2.0;
+ self.title_pos = Some((tx, ty));
+ current_angle += dtheta_title;
+ } else {
+ self.title_pos = None;
+ }
+
+ for menu in &mut self.menus {
+ let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
+ let dtheta_menu = iw / r_mid;
+ let theta_menu = current_angle + dtheta_menu / 2.0;
+
+ let mx = ccx + r_mid * theta_menu.cos() - iw / 2.0;
+ let my = ccy + r_mid * theta_menu.sin() - h / 2.0;
+
+ menu.set_rect(mx, my, iw, h);
+ menu.set_parent(Some(parent_ptr));
+ menu.curved_arc = Some((ccx, ccy, ccr, h, current_angle, current_angle + dtheta_menu));
+ current_angle += dtheta_menu;
+ }
+ } else {
+ self.title_pos = None;
+ let mut cx = 8.0;
+ if self.center_items {
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+ if self.w > total_width {
+ cx = (self.w - total_width) / 2.0;
+ }
+ }
+ if !self.title.is_empty() {
+ cx += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &mut self.menus {
+ let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
+ menu.set_rect(x + cx, y, iw, h);
+ menu.set_parent(Some(parent_ptr));
+ cx += iw;
+ }
+ }
+ }
+ }
+
+ fn set_network_opacity(&mut self, opacity: f32) {
+ self.network_opacity = opacity;
+ }
+
+ fn color(&self) -> [f32; 4] {
+ if !self.visible {
+ [0.0, 0.0, 0.0, 0.0]
+ } else if self.focused {
+ let mut c = colors::PANEL_MENU_FOCUSED;
+ c[3] *= self.network_opacity;
+ c
+ } else {
+ let mut c = colors::PANEL_MENU_BG;
+ c[3] *= self.network_opacity;
+ c
+ }
+ }
+
+ fn set_hovered(&mut self, v: bool) {
+ self.hovering = v;
+ }
+
+ fn hovered(&self) -> bool {
+ self.hovering
+ }
+
+ fn hit_test(&self, px: f32, py: f32) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let dx = px - ccx;
+ let dy = py - ccy;
+ let dist = (dx * dx + dy * dy).sqrt();
+ if dist >= ccr - self.h && dist <= ccr {
+ let angle = dy.atan2(dx);
+ let mut norm_angle = angle;
+ if norm_angle < 0.0 {
+ norm_angle += 2.0 * std::f32::consts::PI;
+ }
+
+ let r_mid = ccr - self.h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+ let total_angular_width = total_width / r_mid;
+ let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+ let end_angle = 1.5 * std::f32::consts::PI + total_angular_width / 2.0;
+
+ if norm_angle >= start_angle && norm_angle <= end_angle {
+ return true;
+ }
+ }
+ for menu in &self.menus {
+ if menu.hit_test(px, py) {
+ return true;
+ }
+ }
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
+ return true;
+ }
+ for menu in &self.menus {
+ if menu.hit_test(px, py) {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ if !self.visible {
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ self.set_rect(rx, ry, rw, rh);
+
+ if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/home/lsgalante/Dropbox/Clear/debug.txt") {
+ use std::io::Write;
+ let _ = writeln!(f, "MenuBar::on_cursor_moved px={}, py={} curved={:?} rect={:?}", px, py, self.curved_circle, (rx, ry, rw, rh));
+ }
+
+ let mut changed = false;
+ self.hovered_menu = None;
+ for (idx, menu) in self.menus.iter_mut().enumerate() {
+ if menu.cursor_moved(px, py) {
+ changed = true;
+ }
+ if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/home/lsgalante/Dropbox/Clear/debug.txt") {
+ use std::io::Write;
+ let _ = writeln!(f, " Menu[{}] active_title={} curved={:?} hovered={} hit={}", idx, menu.active_title(), menu.curved_arc, menu.hovered(), menu.hit_test(px, py));
+ }
+ if menu.hovered() {
+ self.hovered_menu = Some(idx);
+ }
+ }
+ changed
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if !self.visible {
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ self.set_rect(rx, ry, rw, rh);
+
+ let mut changed = false;
+ for menu in &mut self.menus {
+ let res = menu.mouse_input(button, state, px, py);
+ if res {
+ changed = true;
+ }
+ }
+ if !self.is_menu_open() {
+ self.unfocus();
+ }
+ changed
+ }
+
+ fn focus(&mut self) {
+ if self.is_menu_open() {
+ self.focused = true;
+ for menu in &mut self.menus {
+ if menu.is_menu_open() {
+ menu.focus();
+ return;
+ }
+ }
+ } else {
+ self.focused = false;
+ focus::clear_if_matches(self);
+ return;
+ }
+ self.focused = true;
+ focus::set_focused(self);
+ }
+
+ fn unfocus(&mut self) {
+ self.focused = false;
+ focus::clear_if_matches(self);
+ for menu in &mut self.menus {
+ menu.unfocus();
+ }
+ }
+
+ fn focused(&self) -> bool {
+ self.focused || self.is_menu_open()
+ }
+
+ fn set_selected(&mut self, selected: bool) {
+ self.focused = selected;
+ if !selected {
+ for menu in &mut self.menus {
+ menu.set_selected(false);
+ }
+ }
+ }
+
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ for menu in &mut self.menus {
+ menu.set_modifiers(ctrl, shift, alt);
+ }
+ }
+
+ fn menu_names(&self) -> Vec<String> {
+ self.menu_items.clone()
+ }
+
+ fn menu_click(&mut self) -> Option<(usize, usize)> {
+ for (idx, menu) in self.menus.iter_mut().enumerate() {
+ if let Some((_, item_idx)) = menu.menu_click() {
+ return Some((idx, item_idx));
+ }
+ }
+ 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);
+ }
+ }
+ if let Some(menu) = self.menus.get_mut(menu_idx) {
+ menu.set_item_checked(0, item_idx, 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()];
+ }
+ if let Some(menu) = self.menus.get_mut(menu_idx) {
+ menu.items = items.to_vec();
+ menu.item_checked = vec![Some(false); items.len()];
+ menu.item_bufs.clear();
+ }
+ }
+
+ fn is_menu_bar(&self) -> bool {
+ self.visible
+ }
+
+ 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 (idx, menu) in self.menus.iter().enumerate() {
+ if menu.hit_test(px, py) {
+ let mut formatted_items = Vec::new();
+ for (i, item) in menu.items.iter().enumerate() {
+ let checked = menu.item_checked.get(i).and_then(|&v| v);
+ let prefix = match checked {
+ Some(true) => "✓ ",
+ Some(false) => " ",
+ None => "",
+ };
+ formatted_items.push(format!("{}{}", prefix, item));
+ }
+ return Some((idx, menu.active_title().to_string(), formatted_items, menu.base.x, menu.base.y, menu.base.w, menu.base.h));
+ }
+ }
+ None
+ }
+
+ fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+ if let Some(menu) = self.menus.get_mut(menu_idx) {
+ menu.clicked_item = Some(item_idx);
+ }
+ }
+
+ fn is_menu_open(&self) -> bool {
+ self.visible && self.menus.iter().any(|m| m.is_menu_open())
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut quads = Vec::new();
+ for menu in &self.menus {
+ quads.extend(menu.all_quads());
+ }
+ quads
+ }
+
+ fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut arcs = Vec::new();
+ for menu in &self.menus {
+ arcs.extend(menu.extra_arcs());
+ }
+ arcs
+ }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+ if !self.visible {
+ return;
+ }
+ if !self.title.is_empty() {
+ if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
+ if self.curved_title_char_bufs.len() != self.title.chars().count() {
+ self.curved_title_char_bufs = self.title.chars()
+ .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
+ .collect();
+ }
+ self.title_buf = None;
+ } else {
+ if self.title_buf.is_none() {
+ self.title_buf = Some(make_widget_text_buffer(fs, &self.title, 12.0, "Outfit"));
+ }
+ self.curved_title_char_bufs.clear();
+ }
+ } else {
+ self.title_buf = None;
+ self.curved_title_char_bufs.clear();
+ }
+ for menu in &mut self.menus {
+ menu.prepare_text(fs);
+ }
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut items = Vec::new();
+ let color = glyphon::Color::rgb(0xaa, 0xaa, 0xbb);
+
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - self.h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+ 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 = self.title.len() as f32 * 7.5 + 24.0;
+ let dtheta_title = title_w / r_mid;
+
+ let char_widths: Vec<f32> = self.title.chars().map(|c| {
+ TextLabel::estimate_width(&c.to_string(), 12.0)
+ }).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() {
+ 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() - 12.0 / 2.0;
+
+ items.push((c_buf, tx, ty, color));
+ cur_char_angle += dtheta;
+ }
+ }
+ } else {
+ let mut start_x = 8.0;
+ if self.center_items {
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+ if self.w > total_width {
+ start_x = (self.w - total_width) / 2.0;
+ }
+ }
+ if let Some(ref title_buf) = self.title_buf {
+ items.push((title_buf, self.x + start_x, self.y + 7.0, color));
+ }
+ }
+
+ for menu in &self.menus {
+ items.extend(menu.get_text_items());
+ }
+ items
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut labels = Vec::new();
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - self.h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+ 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 = self.title.len() as f32 * 7.5 + 24.0;
+ let dtheta_title = title_w / r_mid;
+ labels.extend(TextLabel::curved_layout(
+ &self.title,
+ ccx, ccy, r_mid,
+ current_angle, current_angle + dtheta_title,
+ 12.0,
+ [0xaa, 0xaa, 0xbb],
+ ));
+ }
+ } else {
+ let mut start_x = 8.0;
+ if self.center_items {
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * 7.5 + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
+ }
+ if self.w > total_width {
+ start_x = (self.w - total_width) / 2.0;
+ }
+ }
+ if !self.title.is_empty() {
+ labels.push(TextLabel {
+ text: self.title.clone(),
+ x: self.x + start_x,
+ y: self.y + 7.0,
+ font_size: 12.0,
+ color: [0xaa, 0xaa, 0xbb],
+ });
+ }
+ }
+ for menu in &self.menus {
+ labels.extend(menu.text_labels());
+ }
+ labels
+ }
+
+ fn set_visible(&mut self, visible: bool) {
+ self.visible = visible;
+ for menu in &mut self.menus {
+ menu.set_visible(visible);
+ }
+ }
+
+ fn visible(&self) -> bool {
+ self.visible
+ }
+
+ fn children(&self) -> Vec<*mut (dyn Element + 'static)> {
+ self.menus.iter().map(|m| {
+ let ptr: *const dyn Element = &**m as &dyn Element;
+ ptr as *mut (dyn Element + 'static)
+ }).collect()
+ }
+
+ fn z_index(&self) -> i32 {
+ self.z_level
+ }
+
+ fn set_center_items(&mut self, center: bool) {
+ self.center_items = center;
+ }
+
+ 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()
+ }
+}
+
+impl Drop for MenuBar {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Menu {
+ pub base: Widget,
+ pub title: String,
+ pub vertical_title: String,
+ pub items: Vec<String>,
+ pub item_checked: Vec<Option<bool>>,
+ pub open: bool,
+ pub vertical: bool,
+ hovered_item: Option<usize>,
+ clicked_item: Option<usize>,
+ was_open: Option<usize>,
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+ pub curved_arc: Option<(f32, f32, f32, f32, f32, f32)>,
+ pub title_buf: Option<glyphon::Buffer>,
+ pub item_bufs: Vec<glyphon::Buffer>,
+ pub check_buf: Option<glyphon::Buffer>,
+ pub curved_char_bufs: Vec<glyphon::Buffer>,
+}
+
+impl Menu {
+ pub fn new(title: &str, vertical_title: &str, items: &[String]) -> Self {
+ Self {
+ base: Widget::new(),
+ title: title.to_string(),
+ vertical_title: vertical_title.to_string(),
+ items: items.to_vec(),
+ item_checked: vec![None; items.len()],
+ open: false,
+ vertical: false,
+ hovered_item: None,
+ clicked_item: None,
+ was_open: None,
+ parent: None,
+ children: Vec::new(),
+ curved_arc: None,
+ title_buf: None,
+ item_bufs: Vec::new(),
+ check_buf: None,
+ curved_char_bufs: Vec::new(),
+ }
+ }
+
+ pub fn active_title(&self) -> &str {
+ if self.vertical {
+ &self.vertical_title
+ } else {
+ &self.title
+ }
+ }
+
+ fn dropdown_rect(&self) -> (f32, f32, f32, f32) {
+ let dh = self.items.len() as f32 * DROPDOWN_ITEM_H;
+ let mut max_len = 0;
+ for item in &self.items {
+ max_len = max_len.max(item.len());
+ }
+ let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
+ let dx = if self.vertical {
+ self.base.x + self.base.w
+ } else {
+ self.base.x
+ };
+ let dy = if self.vertical {
+ self.base.y
+ } else {
+ self.base.y + self.base.h
+ };
+ (dx, dy, dw, dh)
+ }
+}
+impl Element for Menu {
+ crate::impl_widget_base!(Menu);
+
+ fn label(&self) -> Option<String> {
+ Some(self.active_title().to_string())
+ }
+
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn hit_test(&self, px: f32, py: f32) -> bool {
+ if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
+ let dx = px - cx;
+ let dy = py - cy;
+ let dist = (dx * dx + dy * dy).sqrt();
+ if dist >= r - thickness && dist <= r {
+ let angle = dy.atan2(dx);
+ let mut norm_angle = angle;
+ if norm_angle < 0.0 {
+ norm_angle += 2.0 * std::f32::consts::PI;
+ }
+ if norm_angle >= start_angle && norm_angle <= end_angle {
+ return true;
+ }
+ }
+ if self.open {
+ let (dx, dy, dw, dh) = self.dropdown_rect();
+ if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ return true;
+ }
+ }
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
+ return true;
+ }
+ if self.open {
+ let (dx, dy, dw, dh) = self.dropdown_rect();
+ if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ self.was_open = None;
+ let was_hovering = self.base.hovered;
+ self.base.hovered = self.hit_test(px, py);
+ let old_item = self.hovered_item;
+ self.hovered_item = None;
+
+ if self.open {
+ let (dx, dy, dw, dh) = self.dropdown_rect();
+ if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+ if di < self.items.len() {
+ self.hovered_item = Some(di);
+ }
+ }
+ }
+
+ was_hovering != self.base.hovered || old_item != self.hovered_item
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left || state != ElementState::Pressed {
+ return false;
+ }
+ if !self.hit_test(px, py) {
+ return false;
+ }
+
+ // Check dropdown click if open
+ if self.open {
+ let (dx, dy, dw, dh) = self.dropdown_rect();
+ if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+ if di < self.items.len() {
+ self.clicked_item = Some(di);
+ self.open = false;
+ return true;
+ }
+ }
+ }
+
+ // Since we passed hit_test and didn't click dropdown, it's a click on the header title
+ if self.was_open == Some(0) || self.open {
+ self.open = false;
+ self.was_open = None;
+ } else {
+ self.open = true;
+ self.was_open = None;
+ focus::set_focused(self);
+ }
+ true
+ }
+
+ fn focus(&mut self) {
+ self.open = true;
+ self.base.focused = true;
+ focus::set_focused(self);
+ }
+
+ fn unfocus(&mut self) {
+ if self.open {
+ self.was_open = Some(0);
+ }
+ self.open = false;
+ self.base.focused = false;
+ focus::clear_if_matches(self);
+ self.hovered_item = None;
+ }
+
+ fn set_selected(&mut self, selected: bool) {
+ self.base.focused = selected;
+ if !selected {
+ self.open = false;
+ self.was_open = None;
+ self.hovered_item = None;
+ focus::clear_if_matches(self);
+ }
+ }
+
+ 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 highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+ if self.curved_arc.is_some() {
+ None
+ } else {
+ let hc = self.highlight_color()?;
+ Some((self.base.x, self.base.y, self.base.w, self.base.h, hc))
+ }
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ if self.open {
+ let (dx, dy, dw, dh) = self.dropdown_rect();
+ if dh > 0.0 {
+ quads.push((dx, dy, dw, dh, colors::PANEL_MENU_BG));
+ if let Some(di) = self.hovered_item {
+ quads.push((dx, dy + di as f32 * DROPDOWN_ITEM_H, dw, DROPDOWN_ITEM_H, colors::PANEL_MENU_HOVER));
+ }
+ }
+ }
+ quads
+ }
+
+ fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
+ let mut arcs = Vec::new();
+ if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
+ if self.base.hovered && !self.open {
+ arcs.push((cx, cy, r, thickness, start_angle, end_angle, colors::PANEL_MENU_HOVER));
+ }
+ }
+ arcs
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = Vec::new();
+ if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
+ let r_mid = r - thickness / 2.0;
+ labels.extend(TextLabel::curved_layout(
+ &self.active_title(),
+ cx, cy, r_mid,
+ start_angle, end_angle,
+ 12.0,
+ [0xcc, 0xcc, 0xd4],
+ ));
+ } else {
+ labels.push(TextLabel {
+ text: self.active_title().to_string(),
+ x: self.base.x + 8.0,
+ y: self.base.y + 7.0,
+ font_size: 12.0,
+ color: [0xcc, 0xcc, 0xd4],
+ });
+ }
+ if self.open {
+ let (dx, dy, _, _) = self.dropdown_rect();
+ for (i, item) in self.items.iter().enumerate() {
+ let checked = self.item_checked.get(i).and_then(|&v| v);
+ let prefix = match checked {
+ Some(true) => "\u{2713} ",
+ Some(false) => " ",
+ None => "",
+ };
+ labels.push(TextLabel {
+ text: format!("{}{}", prefix, item),
+ x: dx + 8.0,
+ y: dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
+ font_size: 12.0,
+ color: [0xcc, 0xcc, 0xd4],
+ });
+ }
+ }
+ labels
+ }
+
+ fn set_visible(&mut self, visible: bool) {
+ self.base.hovered = false;
+ if !visible {
+ self.open = false;
+ self.was_open = None;
+ self.hovered_item = None;
+ focus::clear_if_matches(self);
+ }
+ }
+
+ fn parent(&self) -> Option<*mut (dyn Element + 'static)> {
+ self.parent
+ }
+
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
+ self.parent = parent;
+ }
+
+ fn z_index(&self) -> i32 {
+ 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) -> bool {
+ self.base.focused
+ }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+ let title_text = self.active_title();
+ if let Some((_cx, _cy, _r, _thickness, _start_angle, _end_angle)) = self.curved_arc {
+ if self.curved_char_bufs.len() != title_text.chars().count() {
+ self.curved_char_bufs = title_text.chars()
+ .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
+ .collect();
+ }
+ self.title_buf = None;
+ } else {
+ if self.title_buf.is_none() {
+ self.title_buf = Some(make_widget_text_buffer(fs, title_text, 12.0, "Outfit"));
+ }
+ self.curved_char_bufs.clear();
+ }
+
+ if self.open {
+ if self.item_bufs.len() != self.items.len() {
+ self.item_bufs = self.items.iter().enumerate().map(|(i, item)| {
+ let checked = self.item_checked.get(i).and_then(|&v| v);
+ let prefix = match checked {
+ Some(true) => "\u{2713} ",
+ Some(false) => " ",
+ None => "",
+ };
+ let text = format!("{}{}", prefix, item);
+ make_widget_text_buffer(fs, &text, 12.0, "Outfit")
+ }).collect();
+ }
+ } else {
+ self.item_bufs.clear();
+ }
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ let mut items = Vec::new();
+ let color = glyphon::Color::rgb(0xcc, 0xcc, 0xd4);
+
+ 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(), 12.0)
+ }).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() - 12.0 / 2.0;
+
+ items.push((c_buf, tx, ty, color));
+ cur_char_angle += dtheta;
+ }
+ }
+ } else {
+ if let Some(ref title_bu
diff truncated