GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Add clear-inspector-v1 protocol and widget serialization support in clear-ui
Cargo.lock | 2 +
Cargo.toml | 2 +
protocol/clear-inspector-v1.xml | 52 ++++
src/color.rs | 121 +++++++++
src/layout.rs | 23 +-
src/lib.rs | 1 +
src/main.rs | 26 +-
src/protocol.rs | 17 ++
src/widget.rs | 555 +++++++++++++++++++++++++++++++++-------
9 files changed, 705 insertions(+), 94 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 22dc4d0..8d0aa75 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -180,7 +180,9 @@ dependencies = [
"serde",
"smithay-client-toolkit",
"tokio",
+ "wayland-backend",
"wayland-client",
+ "wayland-scanner",
"wgpu",
"xkeysym",
]
diff --git a/Cargo.toml b/Cargo.toml
index e555819..c106194 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,8 @@ smithay-client-toolkit = { version = "0.19.2", features = ["calloop"] }
calloop = "0.13.0"
calloop-wayland-source = "0.3.0"
wayland-client = { version = "0.31", features = ["system"] }
+wayland-backend = "0.3"
+wayland-scanner = "0.31"
xkeysym = "0.2"
resvg = "0.41.0"
serde = { version = "1.0", features = ["derive"] }
diff --git a/protocol/clear-inspector-v1.xml b/protocol/clear-inspector-v1.xml
new file mode 100644
index 0000000..b321965
--- /dev/null
+++ b/protocol/clear-inspector-v1.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="clear_inspector_v1">
+ <copyright>
+ Copyright © 2026 Antigravity
+ </copyright>
+
+ <interface name="zclear_inspector_v1" version="1">
+ <description summary="query information about client surface states and layouts">
+ This interface allows clear-ui client applications to register their surfaces
+ and publish their internal widget trees and state to the compositor.
+ It also allows inspector tools to request the list of active surfaces and their states.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the inspector object"/>
+ </request>
+
+ <request name="register_client">
+ <description summary="register a wl_surface as a clear-ui client window">
+ Associate a client wl_surface with this inspector.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ </request>
+
+ <request name="update_state">
+ <description summary="update the published state of a registered client surface">
+ The state is serialized as a JSON string containing widget layout and state data.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ <arg name="state" type="string" summary="JSON serialized state of the application"/>
+ </request>
+
+ <request name="get_inspected_surfaces">
+ <description summary="request the list of all registered surfaces from the compositor"/>
+ </request>
+
+ <event name="inspected_surface">
+ <description summary="reports a registered surface's state and absolute coordinates"/>
+ <arg name="title" type="string" summary="window title"/>
+ <arg name="app_id" type="string" summary="application ID"/>
+ <arg name="x" type="int" summary="absolute screen X coordinate"/>
+ <arg name="y" type="int" summary="absolute screen Y coordinate"/>
+ <arg name="width" type="int" summary="surface width"/>
+ <arg name="height" type="int" summary="surface height"/>
+ <arg name="state" type="string" summary="JSON state of the application"/>
+ </event>
+
+ <event name="inspected_surface_done">
+ <description summary="indicates all inspected surfaces have been reported"/>
+ </event>
+ </interface>
+</protocol>
diff --git a/src/color.rs b/src/color.rs
index 0924293..26fd7e6 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -22,11 +22,132 @@ pub const TOGGLE_OFF: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
pub const TOGGLE_ON: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
pub const TOGGLE_HOVER: [f32; 4] = [0.30, 0.30, 0.35, 1.0];
pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
+
+use std::sync::RwLock;
+
+static SLIDER_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_TRACK);
+static PAGE_LOW_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0600316, 0.0600316, 0.080219, 1.0]);
+static COLOR_BORDERS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.2039, 0.2039, 0.2530, 1.0]);
+
+pub fn page_low_color() -> [f32; 4] {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Ok(content) = std::fs::read_to_string("/home/lsgalante/.config/ccec/config.toml") {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("page_low_color") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let hex = rest.trim_end_matches('"').trim().trim_start_matches('#');
+ if hex.len() >= 6 {
+ if let (Ok(r), Ok(g), Ok(b)) = (
+ u8::from_str_radix(&hex[0..2], 16),
+ u8::from_str_radix(&hex[2..4], 16),
+ u8::from_str_radix(&hex[4..6], 16),
+ ) {
+ let r_f = srgb_to_linear(r as f32 / 255.0);
+ let g_f = srgb_to_linear(g as f32 / 255.0);
+ let b_f = srgb_to_linear(b as f32 / 255.0);
+ if let Ok(mut lock) = PAGE_LOW_COLOR.write() {
+ *lock = [r_f, g_f, b_f, 1.0];
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ *PAGE_LOW_COLOR.read().unwrap()
+}
+
+pub fn set_page_low_color(color: [f32; 4]) {
+ if let Ok(mut lock) = PAGE_LOW_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn color_borders_color() -> [f32; 4] {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Ok(content) = std::fs::read_to_string("/home/lsgalante/.config/ccec/config.toml") {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("color_borders_color") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let hex = rest.trim_end_matches('"').trim().trim_start_matches('#');
+ if hex.len() >= 6 {
+ if let (Ok(r), Ok(g), Ok(b)) = (
+ u8::from_str_radix(&hex[0..2], 16),
+ u8::from_str_radix(&hex[2..4], 16),
+ u8::from_str_radix(&hex[4..6], 16),
+ ) {
+ let r_f = srgb_to_linear(r as f32 / 255.0);
+ let g_f = srgb_to_linear(g as f32 / 255.0);
+ let b_f = srgb_to_linear(b as f32 / 255.0);
+ if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() {
+ *lock = [r_f, g_f, b_f, 1.0];
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ *COLOR_BORDERS_COLOR.read().unwrap()
+}
+
+pub fn set_color_borders_color(color: [f32; 4]) {
+ if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn slider_track() -> [f32; 4] {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Ok(content) = std::fs::read_to_string("/home/lsgalante/.config/ccec/config.toml") {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("slider_track_color") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let hex = rest.trim_end_matches('"').trim().trim_start_matches('#');
+ if hex.len() >= 6 {
+ if let (Ok(r), Ok(g), Ok(b)) = (
+ u8::from_str_radix(&hex[0..2], 16),
+ u8::from_str_radix(&hex[2..4], 16),
+ u8::from_str_radix(&hex[4..6], 16),
+ ) {
+ let r_f = srgb_to_linear(r as f32 / 255.0);
+ let g_f = srgb_to_linear(g as f32 / 255.0);
+ let b_f = srgb_to_linear(b as f32 / 255.0);
+ if let Ok(mut lock) = SLIDER_TRACK_COLOR.write() {
+ *lock = [r_f, g_f, b_f, 1.0];
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ *SLIDER_TRACK_COLOR.read().unwrap()
+}
+
+pub fn set_slider_track(color: [f32; 4]) {
+ if let Ok(mut lock) = SLIDER_TRACK_COLOR.write() {
+ *lock = color;
+ }
+}
+
pub const SLIDER_THUMB: [f32; 4] = [0.60, 0.60, 0.65, 1.0];
pub const SLIDER_THUMB_DRAG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
pub const PROGRESS_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
pub const PROGRESS_FILL: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
+pub const HIGHLIGHT_PRIMARY: [f32; 4] = [1.0, 1.0, 1.0, 0.12];
+pub const HIGHLIGHT_SECONDARY: [f32; 4] = [1.0, 1.0, 1.0, 0.06];
+
pub const SPINBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
pub const SPINBOX_BUTTON: [f32; 4] = [0.25, 0.25, 0.32, 1.0];
pub const SPINBOX_BUTTON_HOVER: [f32; 4] = [0.35, 0.35, 0.42, 1.0];
diff --git a/src/layout.rs b/src/layout.rs
index 136ecf5..8e0c5ff 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -10,13 +10,9 @@ pub trait RenderTarget {
pub fn render_widget<T: Widget>(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.extra_quads() {
+ for (qx, qy, qw, qh, qc) in w.all_quads() {
pc.rect(qc, qx, qy, qw, qh);
}
- // Render hover highlight on top of widget surfaces
- if let Some((hx, hy, hw, hh, hc)) = w.hover_highlight_quad() {
- pc.rect(hc, hx, hy, hw, hh);
- }
let font_opt = w.widget_font();
for label in w.text_labels() {
let color_f32 = [
@@ -33,6 +29,23 @@ pub fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y:
}
}
+pub struct UiFrame;
+
+impl UiFrame {
+ pub fn start(scroll_offset: f32) -> Self {
+ crate::widget::hover_animation::reset_frame_registration();
+ crate::widget::hover_animation::set_scroll_offset(scroll_offset);
+ Self
+ }
+
+ pub fn finish(self, pc: &mut dyn RenderTarget) {
+ crate::widget::hover_animation::post_render_check();
+ if let Some((qx, qy, qw, qh, qc)) = crate::widget::hover_animation::get_quad() {
+ pc.rect(qc, qx, qy, qw, qh);
+ }
+ }
+}
+
pub struct Column {
ox: f32,
oy: f32,
diff --git a/src/lib.rs b/src/lib.rs
index 84b7d9a..3a75d7b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@ pub mod color;
pub mod widget;
pub mod layout;
pub mod wayland;
+pub mod protocol;
pub mod colors {
pub use crate::color::*;
diff --git a/src/main.rs b/src/main.rs
index 1f37072..841006f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -145,7 +145,7 @@ impl State {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
- power_preference: wgpu::PowerPreference::HighPerformance,
+ power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
@@ -595,6 +595,7 @@ struct AppState {
ctrl_pressed: bool,
shift_pressed: bool,
pressed_key: Option<PressedKey>,
+ inspector: Option<clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1>,
}
impl CompositorHandler for AppState {
@@ -1071,6 +1072,17 @@ impl ProvidesRegistryState for AppState {
) {}
}
+impl wayland_client::Dispatch<clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1, ()> for AppState {
+ fn event(
+ _state: &mut Self,
+ _proxy: &clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1,
+ _event: clear_ui::protocol::zclear_inspector_v1::Event,
+ _data: &(),
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ ) {}
+}
+
delegate_compositor!(AppState);
delegate_xdg_shell!(AppState);
delegate_xdg_window!(AppState);
@@ -1091,6 +1103,7 @@ fn main() {
let shm_state = Shm::bind(&globals, &qh).unwrap();
let seat_state = SeatState::new(&globals, &qh);
let output_state = OutputState::new(&globals, &qh);
+ let inspector = globals.bind(&qh, 1..=1, ()).ok();
let mut app = AppState {
registry_state: RegistryState::new(&globals),
@@ -1110,6 +1123,7 @@ fn main() {
ctrl_pressed: false,
shift_pressed: false,
pressed_key: None,
+ inspector,
};
// Perform a roundtrip to populate output_state with active output scales
@@ -1129,6 +1143,10 @@ fn main() {
window.set_min_size(Some((pw, ph)));
window.commit();
+ if let Some(ref inspector) = app.inspector {
+ inspector.register_client(&surface);
+ }
+
let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
@@ -1189,6 +1207,12 @@ fn main() {
app.redraw = false;
if let Some(state) = &mut app.state {
state.render();
+ if let Some(ref inspector) = app.inspector {
+ if let Some(ref surface) = app.surface {
+ let json = clear_ui::widget::serialize_widgets(&state.widgets);
+ inspector.update_state(surface, json);
+ }
+ }
}
}
}
diff --git a/src/protocol.rs b/src/protocol.rs
new file mode 100644
index 0000000..de3fa5e
--- /dev/null
+++ b/src/protocol.rs
@@ -0,0 +1,17 @@
+#[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
+#[allow(non_upper_case_globals, non_snake_case, unused_imports, missing_docs, clippy::all)]
+pub mod zclear_inspector_v1 {
+ pub mod client {
+ use wayland_client;
+ use wayland_client::protocol::*;
+
+ pub mod __interfaces {
+ use wayland_client::protocol::__interfaces::*;
+ wayland_scanner::generate_interfaces!("protocol/clear-inspector-v1.xml");
+ }
+ use self::__interfaces::*;
+
+ wayland_scanner::generate_client_code!("protocol/clear-inspector-v1.xml");
+ }
+ pub use self::client::zclear_inspector_v1::*;
+}
diff --git a/src/widget.rs b/src/widget.rs
index 4305876..1ff1912 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -207,6 +207,183 @@ pub mod focus {
}
}
+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();
@@ -467,8 +644,22 @@ pub struct TextLabel {
}
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 is_covered_by(&self, px: f32, py: f32, pw: f32, ph: f32) -> bool {
- let text_w = self.text.chars().count() as f32 * self.font_size * 0.65;
+ 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
@@ -486,6 +677,7 @@ pub struct WidgetBase {
pub hovered: bool,
pub row_x: f32,
pub row_w: f32,
+ pub focused: bool,
}
impl WidgetBase {
@@ -499,6 +691,21 @@ impl WidgetBase {
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,
}
}
}
@@ -549,6 +756,11 @@ pub trait Widget {
}
fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ hover_animation::set_cursor_pos(px, py);
+ 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);
@@ -576,27 +788,32 @@ pub trait Widget {
}
}
- fn hover_highlight(&self) -> Option<[f32; 4]> {
- if self.hovered() { Some([1.0, 1.0, 1.0, 0.06]) } else { None }
+ 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)
+ } else if self.hovered() {
+ Some(colors::HIGHLIGHT_SECONDARY)
+ } else {
+ None
+ }
}
- fn hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+ fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+ let hc = self.highlight_color()?;
if let Some(b) = self.base() {
- if self.hovered() {
- 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, [1.0, 1.0, 1.0, 0.06]))
- } else {
- None
- }
- } else if let Some(hc) = self.hover_highlight() {
- let (x, y, w, h) = self.rect();
- Some((x, y, w, h, hc))
+ 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 {
- None
+ let (x, y, w, h) = self.rect();
+ let top_offset = self.top_room();
+ Some((x, y - top_offset, w, h + top_offset, hc))
}
}
@@ -610,6 +827,17 @@ pub trait Widget {
fn draggable(&self) -> bool { false }
fn extra_quads(&self) -> Vec<(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 {
@@ -626,6 +854,10 @@ pub trait Widget {
}
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() {
@@ -643,8 +875,16 @@ pub trait Widget {
fn set_drag_bounds(&mut self, _bx: f32, _by: f32, _bw: f32, _bh: f32) {}
- fn focus(&mut self) {}
- fn unfocus(&mut self) {}
+ 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 set_selected(&mut self, _selected: bool) {}
fn keyboard_input(&mut self, _event: &KeyEvent) -> bool { false }
@@ -1228,7 +1468,7 @@ impl Widget for ParametersBg {
let thumb_x = track_x + t * (track_w - thumb_size);
let thumb_y = r.1 + 3.0;
- quads.push((track_x, track_y, track_w, track_h, colors::SLIDER_TRACK));
+ quads.push((track_x, track_y, track_w, track_h, colors::slider_track()));
let thumb_color = if self.dragging_param == Some(i) {
colors::SLIDER_THUMB_DRAG
@@ -1491,7 +1731,7 @@ impl Widget for MenuBar {
false
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
if !self.visible {
return false;
}
@@ -1761,7 +2001,7 @@ impl Widget for Menu {
false
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
self.was_open = None;
let was_hovering = self.hovering;
self.hovering = self.hit_test(px, py);
@@ -1956,7 +2196,7 @@ pub struct Button {
impl Button {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new_rect(x, y, w, h),
pressed: false,
just_clicked: false,
kind: ButtonKind::Primary,
@@ -1966,7 +2206,7 @@ impl Button {
pub fn new_reset(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new_rect(x, y, w, h),
pressed: false,
just_clicked: false,
kind: ButtonKind::Reset,
@@ -1976,7 +2216,7 @@ impl Button {
pub fn new_list_row(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new_rect(x, y, w, h),
pressed: false,
just_clicked: false,
kind: ButtonKind::ListRow,
@@ -1986,7 +2226,7 @@ impl Button {
pub fn new_copy_icon(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: WidgetBase { x, y, w, h, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new_rect(x, y, w, h),
pressed: false,
just_clicked: false,
kind: ButtonKind::CopyIcon,
@@ -2009,7 +2249,7 @@ 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 hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+ fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
fn color(&self) -> [f32; 4] {
match self.kind {
@@ -2080,7 +2320,7 @@ impl Widget for Button {
let est_w = if label == "📋" {
12.0
} else {
- label.len() as f32 * 6.5
+ TextLabel::estimate_width(label, font_size)
};
let color = match self.kind {
ButtonKind::ListRow | ButtonKind::CopyIcon => {
@@ -2137,16 +2377,7 @@ pub struct Panel {
impl Panel {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- base: WidgetBase {
- x,
- y,
- w,
- h,
- label: None,
- hovered: false,
- row_x: 0.0,
- row_w: 0.0,
- },
+ base: WidgetBase::new_rect(x, y, w, h),
dragging: false,
drag_ox: 0.0,
drag_oy: 0.0,
@@ -2170,7 +2401,7 @@ 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 hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+ 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 } }
@@ -2306,7 +2537,7 @@ impl Widget for Node {
self.bounds = Some((bx, by, bw, bh));
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
let was_hovered = self.hovered;
self.hovered = self.hit_test(px, py);
@@ -2435,7 +2666,7 @@ pub struct Checkbox {
impl Checkbox {
pub fn new() -> Self {
Self {
- base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new(),
checked: false,
just_clicked: false,
}
@@ -2495,7 +2726,7 @@ pub struct Toggle {
impl Toggle {
pub fn new() -> Self {
Self {
- base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new(),
toggled: false,
just_toggled: false,
}
@@ -2559,17 +2790,10 @@ pub struct Label {
impl Label {
pub fn new(text: &str) -> Self {
+ let mut base = WidgetBase::new();
+ base.label = Some(text.to_string());
Self {
- base: WidgetBase {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- label: Some(text.to_string()),
- hovered: false,
- row_x: 0.0,
- row_w: 0.0,
- },
+ base,
font_size: 12.0,
color: [0x83, 0x83, 0x8a],
}
@@ -2701,6 +2925,7 @@ impl Widget for Svg {
}
}
+#[derive(Debug, Clone)]
pub struct Slider {
base: WidgetBase,
dragging: bool,
@@ -2712,7 +2937,7 @@ pub struct Slider {
impl Slider {
pub fn new() -> Self {
Self {
- base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new(),
dragging: false,
value: 0.5,
drag_offset: 0.0,
@@ -2733,13 +2958,22 @@ impl Slider {
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);
+ }
}
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] { colors::SLIDER_TRACK }
+ fn color(&self) -> [f32; 4] { colors::slider_track() }
fn draggable(&self) -> bool { true }
fn is_dragging(&self) -> bool { self.dragging }
@@ -2817,7 +3051,7 @@ pub struct RangeSlider {
impl RangeSlider {
pub fn new() -> Self {
Self {
- base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new(),
value_low: 0.2,
value_high: 0.8,
active_thumb: None,
@@ -2850,7 +3084,7 @@ 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 color(&self) -> [f32; 4] { colors::slider_track() }
fn draggable(&self) -> bool { true }
fn is_dragging(&self) -> bool { self.active_thumb.is_some() }
@@ -2963,7 +3197,7 @@ pub struct ProgressBar {
impl ProgressBar {
pub fn new(value: f32) -> Self {
Self {
- base: WidgetBase { x: 0.0, y: 0.0, w: 0.0, h: 0.0, label: None, hovered: false, row_x: 0.0, row_w: 0.0 },
+ base: WidgetBase::new(),
value,
}
}
@@ -2977,7 +3211,7 @@ impl ProgressBar {
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 hover_highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
+ fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
fn color(&self) -> [f32; 4] { colors::PROGRESS_BG }
}
@@ -3023,7 +3257,7 @@ impl Widget for Splitter {
fn set_hovered(&mut self, v: bool) { self.hovered = v; }
fn hovered(&self) -> bool { self.hovered }
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ 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
@@ -3133,7 +3367,7 @@ impl Widget for Spinbox {
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ 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 {
@@ -3500,7 +3734,33 @@ impl Widget for ColorSelector {
self.color[2] as f32 / 255.0,
1.0,
]);
- quads.push((pick_x, self.base.y, pick_w, self.base.h, linear_c));
+ 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
}
@@ -3576,7 +3836,7 @@ impl Widget for Breadcrumb {
fn set_hovered(&mut self, v: bool) { self.hovered = v; }
fn hovered(&self) -> bool { self.hovered }
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ 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;
@@ -3747,7 +4007,7 @@ impl Widget for Spreadsheet {
self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
let was_hovered = self.hovered;
self.hovered = self.hit_test(px, py);
@@ -4091,7 +4351,7 @@ impl Widget for ScrollBox {
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 hover_highlight(&self) -> Option<[f32; 4]> { None }
+ fn highlight_color(&self) -> Option<[f32; 4]> { None }
fn focus(&mut self) {
focus::set_focused(self);
@@ -4108,7 +4368,7 @@ impl Widget for ScrollBox {
false
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ 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
@@ -4892,11 +5152,11 @@ impl Widget for ScrollingList {
self.scroll_box.hovered()
}
- fn hover_highlight(&self) -> Option<[f32; 4]> {
- self.scroll_box.hover_highlight()
+ fn highlight_color(&self) -> Option<[f32; 4]> {
+ self.scroll_box.highlight_color()
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
self.scroll_box.cursor_moved(px, py)
}
@@ -5057,7 +5317,7 @@ impl Widget for Dropdown {
}
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
let was_hovered = self.base.hovered;
let was_hovered_item = self.hovered_item;
@@ -5486,7 +5746,7 @@ impl Widget for TextBox {
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
if self.disabled {
let was = self.base.hovered;
self.base.hovered = false;
@@ -5970,6 +6230,8 @@ pub struct Paginator {
hovered_tab: Option<usize>,
pressed_tab: Option<usize>,
page_changed: bool,
+ current_y: Option<f32>,
+ target_y: f32,
parent: Option<*mut (dyn Widget + 'static)>,
children: Vec<*mut (dyn Widget + 'static)>,
}
@@ -5985,6 +6247,8 @@ impl Paginator {
hovered_tab: None,
pressed_tab: None,
page_changed: false,
+ current_y: Some(10.0),
+ target_y: 10.0,
parent: None,
children: Vec::new(),
}
@@ -5996,7 +6260,17 @@ impl Paginator {
pub fn set_selected_page(&mut self, page: usize) {
if page < self.pages.len() {
- self.selected_page = page;
+ let target = 10.0 + page as f32 * 50.0;
+ if self.selected_page != page {
+ self.selected_page = page;
+ self.target_y = target;
+ if self.current_y.is_none() {
+ self.current_y = Some(target);
+ }
+ } else if self.current_y.is_none() {
+ self.target_y = target;
+ self.current_y = Some(target);
+ }
}
}
}
@@ -6008,10 +6282,46 @@ impl Widget for Paginator {
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 {
+ let bx = self.x + 5.0;
+ let by = self.y + 10.0 + i as f32 * 50.0;
+ let bw = self.sidebar_w - 10.0;
+ let bh = 40.0;
+ let scroll_offset = hover_animation::get_scroll_offset();
+ Some((bx, by + scroll_offset, bw, bh, colors::HIGHLIGHT_SECONDARY))
+ } else {
+ None
+ }
+ }
+
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = Vec::new();
quads.push((self.x, self.y, self.sidebar_w, self.h, colors::SIDEBAR_BG));
- quads.push((self.x + self.sidebar_w, self.y, self.w - self.sidebar_w, self.h, colors::CONTENT_BG));
+ quads.push((self.x + self.sidebar_w, self.y, self.w - self.sidebar_w, self.h, colors::page_low_color()));
+
+ if let Some(cy) = self.current_y {
+ let bx = self.x + 5.0;
+ let by = self.y + cy;
+ let bw = self.sidebar_w - 10.0;
+ let bh = 40.0;
+
+ // Draw nested rectangles to create a soft, faded-out edge/border effect
+ let steps = 16;
+ let start_alpha = 0.008;
+ let c = 0.000516;
+ for i in 0..steps {
+ let offset = i as f32 * 0.5;
+ let rx = bx + offset;
+ let ry = by + offset;
+ let rw = bw - 2.0 * offset;
+ let rh = bh - 2.0 * offset;
+ if rw > 0.0 && rh > 0.0 {
+ let step_alpha = start_alpha + c * (i * i) as f32;
+ quads.push((rx, ry, rw, rh, [0.20, 0.40, 0.65, step_alpha]));
+ }
+ }
+ }
for i in 0..self.pages.len() {
let bx = self.x + 5.0;
@@ -6019,9 +6329,7 @@ impl Widget for Paginator {
let bw = self.sidebar_w - 10.0;
let bh = 40.0;
- let c = if self.selected_page == i {
- [0.20, 0.40, 0.65, 0.4]
- } else if self.pressed_tab == Some(i) {
+ let c = if self.pressed_tab == Some(i) {
[0.20, 0.20, 0.25, 0.25]
} else if self.hovered_tab == Some(i) {
[0.20, 0.20, 0.25, 0.15]
@@ -6046,7 +6354,7 @@ impl Widget for Paginator {
let bh = 40.0;
let font_size = 12.0;
- let est_w = page_name.len() as f32 * 6.5;
+ let est_w = TextLabel::estimate_width(page_name, font_size);
let color = if self.selected_page == i {
[230, 230, 242]
} else {
@@ -6064,7 +6372,7 @@ impl Widget for Paginator {
labels
}
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
let was_hovered_tab = self.hovered_tab;
self.hovered_tab = None;
for i in 0..self.pages.len() {
@@ -6106,6 +6414,10 @@ impl Widget for Paginator {
if self.selected_page != i {
self.selected_page = i;
self.page_changed = true;
+ self.target_y = 10.0 + i as f32 * 50.0;
+ if self.current_y.is_none() {
+ self.current_y = Some(self.target_y);
+ }
}
return true;
}
@@ -6126,6 +6438,23 @@ impl Widget for Paginator {
fn value(&self) -> i32 { self.selected_page as i32 }
+ fn tick(&mut self, dt: f32) -> bool {
+ 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);
+ true
+ } else {
+ self.current_y = Some(self.target_y);
+ false
+ }
+ } else {
+ 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() }
@@ -6152,16 +6481,7 @@ pub struct Trackpad {
impl Trackpad {
pub fn new() -> Self {
Self {
- base: WidgetBase {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- label: None,
- hovered: false,
- row_x: 0.0,
- row_w: 0.0,
- },
+ base: WidgetBase::new(),
fingers: Vec::new(),
}
}
@@ -6314,5 +6634,64 @@ impl Widget for Trackpad {
}
}
+#[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
+ }
+}
+
+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(',');
+ }
+ let (x, y, width, height) = w.rect();
+ let label = 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
+ ));
+ }
+ json.push(']');
+ json
+}
+
+