GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: add scale module, custom vertex rendering, Column/Grid/Radial layout strategies, and Plate widget
Makefile | 14 +
src/color.rs | 120 +++-
src/engine.rs | 714 ++++++++++++++++++++-
src/layout.rs | 665 ++++++++++++++++++-
src/lib.rs | 1 +
src/main.rs | 631 ++++++++++++++----
src/scale.rs | 13 +
src/shader.wgsl | 15 -
src/widget.rs | 1569 ++++++++++++++++++++++++++++++---------------
src/widget/json_layout.rs | 142 +++-
10 files changed, 3178 insertions(+), 706 deletions(-)
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..72a0653
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+ cargo build --release
+
+install: build
+ mkdir -p ~/.local/bin
+ install -m 755 target/release/clear-ui ~/.local/bin/clear-ui
+
+run:
+ cargo run
+
+clean:
+ cargo clean
diff --git a/src/color.rs b/src/color.rs
index d47aef6..ed61384 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -32,6 +32,9 @@ static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
static SIDEBAR_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(SIDEBAR_BG);
static HIGHLIGHT_PRIMARY_COLOR: RwLock<[f32; 4]> = RwLock::new(HIGHLIGHT_PRIMARY);
static PAGINATOR_TAB_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]); // sRGB [230, 230, 242] linear
+static OPACITY: RwLock<Option<f32>> = RwLock::new(None);
+static TOGGLE_ON_COLOR: RwLock<[f32; 4]> = RwLock::new(TOGGLE_ON);
+static TOGGLE_OFF_COLOR: RwLock<[f32; 4]> = RwLock::new(TOGGLE_OFF);
pub fn node_color() -> [f32; 4] {
@@ -92,7 +95,11 @@ pub fn page_low_color() -> [f32; 4] {
}
}
});
- *PAGE_LOW_COLOR.read().unwrap()
+ let mut color = *PAGE_LOW_COLOR.read().unwrap();
+ if let Some(opacity) = read_opacity_if_configured() {
+ color[3] = opacity;
+ }
+ color
}
pub fn set_page_low_color(color: [f32; 4]) {
@@ -259,7 +266,11 @@ pub fn sidebar_bg_color() -> [f32; 4] {
}
}
});
- *SIDEBAR_BG_COLOR.read().unwrap()
+ let mut color = *SIDEBAR_BG_COLOR.read().unwrap();
+ if let Some(opacity) = read_opacity_if_configured() {
+ color[3] = opacity;
+ }
+ color
}
pub fn set_sidebar_bg_color(color: [f32; 4]) {
@@ -342,3 +353,108 @@ pub fn set_paginator_tab_label_color(color: [f32; 4]) {
}
}
+pub fn read_opacity_if_configured() -> Option<f32> {
+ 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") {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[transparency]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("opacity") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ if let Ok(o) = val.trim().parse::<f32>() {
+ if let Ok(mut lock) = OPACITY.write() {
+ *lock = Some(o.clamp(0.0, 1.0));
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ });
+ *OPACITY.read().unwrap()
+}
+
+pub fn toggle_on_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("toggle_enabled_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) = TOGGLE_ON_COLOR.write() {
+ *lock = [r_f, g_f, b_f, 1.0];
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ *TOGGLE_ON_COLOR.read().unwrap()
+}
+
+pub fn set_toggle_on_color(color: [f32; 4]) {
+ if let Ok(mut lock) = TOGGLE_ON_COLOR.write() {
+ *lock = color;
+ }
+}
+
+pub fn toggle_off_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("toggle_disabled_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) = TOGGLE_OFF_COLOR.write() {
+ *lock = [r_f, g_f, b_f, 1.0];
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ *TOGGLE_OFF_COLOR.read().unwrap()
+}
+
+pub fn set_toggle_off_color(color: [f32; 4]) {
+ if let Ok(mut lock) = TOGGLE_OFF_COLOR.write() {
+ *lock = color;
+ }
+}
+
diff --git a/src/engine.rs b/src/engine.rs
index d7904fd..f487f75 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -2,7 +2,7 @@ use std::time::Instant;
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
- delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
+ delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output, delegate_xdg_popup,
registry::{ProvidesRegistryState, RegistryState},
output::{OutputHandler, OutputState},
seat::{
@@ -24,13 +24,48 @@ use wayland_client::{
protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, wl_registry},
Connection, QueueHandle, Proxy,
};
+use smithay_client_toolkit::shell::xdg::popup::{Popup, PopupHandler, PopupConfigure};
+use smithay_client_toolkit::shell::xdg::{XdgPositioner, XdgSurface};
+use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity, ConstraintAdjustment};
+
use calloop::EventLoop;
use calloop_wayland_source::WaylandSource;
use glyphon::{
Cache, FontSystem, Resolution, SwashCache, TextArea, TextAtlas,
- TextBounds, TextRenderer, Viewport,
+ TextBounds, TextRenderer, Viewport, Buffer, Attrs, Metrics,
};
use crate::widget::{TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey};
+
+pub struct ActivePopup {
+ pub sctk_popup: Popup,
+ pub wgpu_surface: wgpu::Surface<'static>,
+ pub config: wgpu::SurfaceConfiguration,
+ pub logical_width: f32,
+ pub logical_height: f32,
+ pub configured: bool,
+ pub viewport: Viewport,
+ pub x: f32,
+ pub y: f32,
+ pub vertex_buffer: Option<wgpu::Buffer>,
+}
+
+fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+ let metrics = Metrics::new(size, size * 1.4);
+ let mut buf = Buffer::new(fs, metrics);
+ let mut attrs = Attrs::new();
+ if let Some(font_name) = font {
+ let family = match font_name {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ _ => glyphon::Family::Name(font_name),
+ };
+ attrs = attrs.family(family);
+ }
+ buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
+ buf.shape_until_scroll(fs, true);
+ buf
+}
use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
#[repr(C)]
@@ -110,9 +145,382 @@ pub fn quad_vertices_clipped(
quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, surface_w, surface_h, color, clip_circle).to_vec()
}
+pub fn line_vertices(
+ x1: f32, y1: f32, x2: f32, y2: f32,
+ thickness: f32,
+ sw: f32, sh: f32,
+ c: [f32; 4]
+) -> [Vertex; 6] {
+ let dx = x2 - x1;
+ let dy = y2 - y1;
+ let len = (dx * dx + dy * dy).sqrt();
+ if len < 0.001 {
+ return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c);
+ }
+ let ux = dx / len;
+ let uy = dy / len;
+ let nx = -uy;
+ let ny = ux;
+
+ let half_t = thickness * 0.5;
+ let p0x = x1 + nx * half_t;
+ let p0y = y1 + ny * half_t;
+ let p1x = x1 - nx * half_t;
+ let p1y = y1 - ny * half_t;
+ let p2x = x2 - nx * half_t;
+ let p2y = y2 - ny * half_t;
+ let p3x = x2 + nx * half_t;
+ let p3y = y2 + ny * half_t;
+
+ let ndc_p0x = (p0x / sw) * 2.0 - 1.0;
+ let ndc_p0y = 1.0 - (p0y / sh) * 2.0;
+ let ndc_p1x = (p1x / sw) * 2.0 - 1.0;
+ let ndc_p1y = 1.0 - (p1y / sh) * 2.0;
+ let ndc_p2x = (p2x / sw) * 2.0 - 1.0;
+ let ndc_p2y = 1.0 - (p2y / sh) * 2.0;
+ let ndc_p3x = (p3x / sw) * 2.0 - 1.0;
+ let ndc_p3y = 1.0 - (p3y / sh) * 2.0;
+
+ let clip_circle = [0.0, 0.0, 0.0];
+ [
+ Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
+ Vertex { position: [ndc_p1x, ndc_p1y], color: c, clip_circle },
+ Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
+ Vertex { position: [ndc_p0x, ndc_p0y], color: c, clip_circle },
+ Vertex { position: [ndc_p2x, ndc_p2y], color: c, clip_circle },
+ Vertex { position: [ndc_p3x, ndc_p3y], color: c, clip_circle },
+ ]
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
+pub enum LineCap {
+ Arrow,
+ Round,
+ Flat,
+}
+
+pub fn vector_vertices(
+ x1: f32, y1: f32, x2: f32, y2: f32,
+ thickness: f32,
+ sw: f32, sh: f32,
+ c: [f32; 4],
+ line_cap: LineCap,
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ let dx = x2 - x1;
+ let dy = y2 - y1;
+ let len = (dx * dx + dy * dy).sqrt();
+ if len < 0.001 {
+ return quad_vertices(x1 - thickness/2.0, y1 - thickness/2.0, thickness, thickness, sw, sh, c).to_vec();
+ }
+
+ match line_cap {
+ LineCap::Arrow => {
+ let ux = dx / len;
+ let uy = dy / len;
+ let nx = -uy;
+ let ny = ux;
+
+ let arrow_len = (thickness * 3.0).max(10.0).min(len);
+ let arrow_width = (thickness * 2.5).max(8.0);
+
+ let line_x2 = x2 - ux * arrow_len;
+ let line_y2 = y2 - uy * arrow_len;
+
+ if len > arrow_len {
+ verts.extend_from_slice(&line_vertices(x1, y1, line_x2, line_y2, thickness, sw, sh, c));
+ }
+
+ let bx = line_x2;
+ let by = line_y2;
+
+ let w1x = bx + nx * (arrow_width * 0.5);
+ let w1y = by + ny * (arrow_width * 0.5);
+ let w2x = bx - nx * (arrow_width * 0.5);
+ let w2y = by - ny * (arrow_width * 0.5);
+
+ let ndc_tip_x = (x2 / sw) * 2.0 - 1.0;
+ let ndc_tip_y = 1.0 - (y2 / sh) * 2.0;
+ let ndc_w1x = (w1x / sw) * 2.0 - 1.0;
+ let ndc_w1y = 1.0 - (w1y / sh) * 2.0;
+ let ndc_w2x = (w2x / sw) * 2.0 - 1.0;
+ let ndc_w2y = 1.0 - (w2y / sh) * 2.0;
+
+ let clip_circle = [0.0, 0.0, 0.0];
+ verts.push(Vertex { position: [ndc_tip_x, ndc_tip_y], color: c, clip_circle });
+ verts.push(Vertex { position: [ndc_w1x, ndc_w1y], color: c, clip_circle });
+ verts.push(Vertex { position: [ndc_w2x, ndc_w2y], color: c, clip_circle });
+ }
+ LineCap::Round => {
+ verts.extend_from_slice(&line_vertices(x1, y1, x2, y2, thickness, sw, sh, c));
+ let clip_circle = [0.0, 0.0, 0.0];
+ verts.extend(circle_vertices(x2, y2, thickness / 2.0, sw, sh, c, 16, clip_circle));
+ }
+ LineCap::Flat => {
+ verts.extend_from_slice(&line_vertices(x1, y1, x2, y2, thickness, sw, sh, c));
+ }
+ }
+
+ verts
+}
+
+pub fn rounded_rect_vertices_corners(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+ corners: (bool, bool, bool, bool),
+ clip_rect: Option<(f32, f32, f32, f32)>,
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ let r = r.min(ww * 0.5).min(h * 0.5);
+
+ let clamp_x = |val: f32| -> f32 {
+ if let Some((cx0, _, cx1, _)) = clip_rect {
+ val.max(cx0).min(cx1)
+ } else {
+ val
+ }
+ };
+ let clamp_y = |val: f32| -> f32 {
+ if let Some((_, cy0, _, cy1)) = clip_rect {
+ val.max(cy0).min(cy1)
+ } else {
+ val
+ }
+ };
+
+ let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
+ let x0 = clamp_x(qx);
+ let y0 = clamp_y(qy);
+ let x1 = clamp_x(qx + qw);
+ let y1 = clamp_y(qy + qh);
+
+ if x1 <= x0 || y1 <= y0 {
+ return;
+ }
+
+ let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+ let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+ let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+ let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+
+ verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+ };
+
+ if r <= 0.1 || (!corners.0 && !corners.1 && !corners.2 && !corners.3) {
+ push_quad(&mut verts, x, y, ww, h);
+ return verts;
+ }
+
+ // 1. Center rectangle
+ push_quad(&mut verts, x + r, y, ww - 2.0 * r, h);
+
+ // 2. Left rectangle
+ push_quad(&mut verts, x, y + r, r, h - 2.0 * r);
+
+ // 3. Right rectangle
+ push_quad(&mut verts, x + ww - r, y + r, r, h - 2.0 * r);
+
+ // 4. Four corners
+ let corner_configs = [
+ // Top-left
+ (corners.0, x, y, x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI),
+ // Top-right
+ (corners.1, x + ww - r, y, x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI),
+ // Bottom-right
+ (corners.2, x + ww - r, y + h - r, x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI),
+ // Bottom-left
+ (corners.3, x, y + h - r, x + r, y + h - r, 0.5 * std::f32::consts::PI, std::f32::consts::PI),
+ ];
+
+ let segments = 16;
+ for &(is_rounded, sqx, sqy, cx, cy, start, end) in &corner_configs {
+ if is_rounded {
+ for i in 0..segments {
+ let theta1 = start + (i as f32) * (end - start) / (segments as f32);
+ let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
+
+ let x0 = clamp_x(cx);
+ let y0 = clamp_y(cy);
+ let x1 = clamp_x(cx + r * theta1.cos());
+ let y1 = clamp_y(cy + r * theta1.sin());
+ let x2 = clamp_x(cx + r * theta2.cos());
+ let y2 = clamp_y(cy + r * theta2.sin());
+
+ let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+ let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+ let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+ let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+ let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
+ let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+
+ verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ }
+ } else {
+ push_quad(&mut verts, sqx, sqy, r, r);
+ }
+ }
+
+ verts
+}
+
+pub fn rounded_rect_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, clip_circle, (true, true, true, true), None)
+}
+
+pub fn plate_bevel_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ t: f32,
+ sw: f32, sh: f32,
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+
+ // 3D Bevel Colors
+ // Highlight (light) for top and left edges
+ let highlight_color = [1.0, 1.0, 1.0, 0.15];
+ // Shadow (dark) for bottom and right edges
+ let shadow_color = [0.0, 0.0, 0.0, 0.25];
+
+ // Straight borders on the inside edge
+ // Top border (light)
+ verts.extend(quad_vertices_with_clip(x + r, y, ww - 2.0 * r, t, sw, sh, highlight_color, clip_circle));
+ // Left border (light)
+ verts.extend(quad_vertices_with_clip(x, y + r, t, h - 2.0 * r, sw, sh, highlight_color, clip_circle));
+ // Bottom border (dark)
+ verts.extend(quad_vertices_with_clip(x + r, y + h - t, ww - 2.0 * r, t, sw, sh, shadow_color, clip_circle));
+ // Right border (dark)
+ verts.extend(quad_vertices_with_clip(x + ww - t, y + r, t, h - 2.0 * r, sw, sh, shadow_color, clip_circle));
+
+ // Corner arcs on the inside edge
+ let segments = 16;
+
+ // Top-left corner (fully light)
+ verts.extend(arc_background_vertices(
+ x + r, y + r, r, t,
+ std::f32::consts::PI, 1.5 * std::f32::consts::PI,
+ sw, sh, highlight_color, segments, clip_circle,
+ ));
+
+ // Top-right corner (split at 45 degrees)
+ verts.extend(arc_background_vertices(
+ x + ww - r, y + r, r, t,
+ 1.5 * std::f32::consts::PI, 1.75 * std::f32::consts::PI,
+ sw, sh, highlight_color, segments / 2, clip_circle,
+ ));
+ verts.extend(arc_background_vertices(
+ x + ww - r, y + r, r, t,
+ 1.75 * std::f32::consts::PI, 2.0 * std::f32::consts::PI,
+ sw, sh, shadow_color, segments / 2, clip_circle,
+ ));
+
+ // Bottom-right corner (fully dark)
+ verts.extend(arc_background_vertices(
+ x + ww - r, y + h - r, r, t,
+ 0.0, 0.5 * std::f32::consts::PI,
+ sw, sh, shadow_color, segments, clip_circle,
+ ));
+
+ // Bottom-left corner (split at 45 degrees)
+ verts.extend(arc_background_vertices(
+ x + r, y + h - r, r, t,
+ 0.5 * std::f32::consts::PI, 0.75 * std::f32::consts::PI,
+ sw, sh, shadow_color, segments / 2, clip_circle,
+ ));
+ verts.extend(arc_background_vertices(
+ x + r, y + h - r, r, t,
+ 0.75 * std::f32::consts::PI, std::f32::consts::PI,
+ sw, sh, highlight_color, segments / 2, clip_circle,
+ ));
+
+ verts
+}
+
pub fn widget_vertices(w: &dyn crate::widget::Widget, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
let (x, y, ww, h) = w.rect();
- quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle).to_vec()
+ let corners = w.rounded_corners();
+ let mut verts = if corners != (false, false, false, false) {
+ rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), clip_circle, corners, None)
+ } else {
+ quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle).to_vec()
+ };
+
+ if w.is_plate() {
+ verts.extend(plate_bevel_vertices(x, y, ww, h, 12.0, 1.5, sw, sh, clip_circle));
+ }
+
+ verts
+}
+
+pub fn extra_quad_vertices(
+ w: &dyn crate::widget::Widget,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+ sw: f32, sh: f32,
+ qc: [f32; 4],
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let corners = w.rounded_corners();
+ if corners == (false, false, false, false) {
+ return quad_vertices_with_clip(qx, qy, qw, qh, sw, sh, qc, clip_circle).to_vec();
+ }
+
+ let (wx, wy, ww, wh) = w.rect();
+ let extra_corners = (
+ corners.0 && qx <= wx + 0.1 && qy <= wy + 0.1,
+ corners.1 && qx + qw >= wx + ww - 0.1 && qy <= wy + 0.1,
+ corners.2 && qx + qw >= wx + ww - 0.1 && qy + qh >= wy + wh - 0.1,
+ corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
+ );
+
+ rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, None)
+}
+
+pub fn extra_quad_vertices_clipped(
+ w: &dyn crate::widget::Widget,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+ sw: f32, sh: f32,
+ qc: [f32; 4],
+ clip: (f32, f32, f32, f32),
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let corners = w.rounded_corners();
+ if corners == (false, false, false, false) {
+ let (cx0, cy0, cx1, cy1) = clip;
+ let ix0 = qx.max(cx0);
+ let iy0 = qy.max(cy0);
+ let ix1 = (qx + qw).min(cx1);
+ let iy1 = (qy + qh).min(cy1);
+ if ix1 <= ix0 || iy1 <= iy0 {
+ return Vec::new();
+ }
+ return quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, sw, sh, qc, clip_circle).to_vec();
+ }
+
+ let (wx, wy, ww, wh) = w.rect();
+ let extra_corners = (
+ corners.0 && qx <= wx + 0.1 && qy <= wy + 0.1,
+ corners.1 && qx + qw >= wx + ww - 0.1 && qy <= wy + 0.1,
+ corners.2 && qx + qw >= wx + ww - 0.1 && qy + qh >= wy + wh - 0.1,
+ corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
+ );
+
+ rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, Some(clip))
}
pub fn circle_vertices(
@@ -273,18 +681,31 @@ pub trait Application: Sized + 'static {
fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64);
+ fn view_vectors(&mut self, _vectors: &mut Vec<(f32, f32, f32, f32, f32, [f32; 4], LineCap)>, _size: LogicalSize, _scale: f64) {}
fn overlay_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
fn text_items(&self) -> &[TextItem];
fn text_areas(&self, scale_f32: f32, bounds: TextBounds) -> Vec<TextArea<'_>> {
- self.text_items().iter().map(|ti| TextArea {
- buffer: &ti.buffer,
- left: ti.x * scale_f32,
- top: ti.y * scale_f32,
- scale: scale_f32,
- bounds,
- default_color: ti.color,
- custom_glyphs: &[],
+ self.text_items().iter().map(|ti| {
+ let item_bounds = if let Some([l, t, r, b]) = ti.bounds {
+ TextBounds {
+ left: (l * scale_f32).round() as i32,
+ top: (t * scale_f32).round() as i32,
+ right: (r * scale_f32).round() as i32,
+ bottom: (b * scale_f32).round() as i32,
+ }
+ } else {
+ bounds
+ };
+ TextArea {
+ buffer: &ti.buffer,
+ left: ti.x * scale_f32,
+ top: ti.y * scale_f32,
+ scale: scale_f32,
+ bounds: item_bounds,
+ default_color: ti.color,
+ custom_glyphs: &[],
+ }
}).collect()
}
@@ -336,6 +757,7 @@ pub struct EngineState<A: Application> {
pub inner: A,
+ pub instance: Option<wgpu::Instance>,
pub wgpu_surface: Option<wgpu::Surface<'static>>,
pub device: Option<wgpu::Device>,
pub queue: Option<wgpu::Queue>,
@@ -363,6 +785,8 @@ pub struct EngineState<A: Application> {
pub shift_pressed: bool,
pub pressed_key: Option<PressedKey>,
pub sender: calloop::channel::Sender<A::Message>,
+ pub active_popup: Option<ActivePopup>,
+ pub qh: QueueHandle<EngineState<A>>,
}
impl<A: Application> EngineState<A> {
@@ -382,6 +806,7 @@ impl<A: Application> EngineState<A> {
backends: wgpu::Backends::VULKAN,
..Default::default()
});
+ self.instance = Some(instance.clone());
let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create wgpu surface");
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
@@ -397,7 +822,16 @@ impl<A: Application> EngineState<A> {
memory_hints: wgpu::MemoryHints::MemoryUsage,
}, None).await.expect("failed to request device");
- let config = wgpu_surface.get_default_config(&adapter, pw.max(1), ph.max(1)).expect("failed to get surface configuration");
+ let mut config = wgpu_surface.get_default_config(&adapter, pw.max(1), ph.max(1)).expect("failed to get surface configuration");
+ let capabilities = wgpu_surface.get_capabilities(&adapter);
+ let alpha_mode = if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
+ wgpu::CompositeAlphaMode::PreMultiplied
+ } else if capabilities.alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
+ wgpu::CompositeAlphaMode::PostMultiplied
+ } else {
+ capabilities.alpha_modes[0]
+ };
+ config.alpha_mode = alpha_mode;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
@@ -498,6 +932,9 @@ impl<A: Application> EngineState<A> {
let mut quads = Vec::new();
self.inner.view(&mut quads, LogicalSize::new(logical_w, logical_h), scale_factor);
+ let mut vectors = Vec::new();
+ self.inner.view_vectors(&mut vectors, LogicalSize::new(logical_w, logical_h), scale_factor);
+
let device = self.device.as_ref().unwrap();
let queue = self.queue.as_ref().unwrap();
let wgpu_surface = self.wgpu_surface.as_ref().unwrap();
@@ -512,6 +949,9 @@ impl<A: Application> EngineState<A> {
for &(qx, qy, qw, qh, qc) in &quads {
verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
}
+ for &(vx1, vy1, vx2, vy2, vthickness, vcolor, vcap) in &vectors {
+ verts.extend(vector_vertices(vx1, vy1, vx2, vy2, vthickness, logical_w, logical_h, vcolor, vcap));
+ }
self.vertex_count = verts.len() as u32;
if self.vertex_count > 0 {
let data = bytemuck::cast_slice(&verts);
@@ -628,12 +1068,140 @@ impl<A: Application> EngineState<A> {
queue.submit(std::iter::once(encoder.finish()));
output.present();
+
+ if let Some(ref mut popup) = self.active_popup {
+ if popup.configured {
+ let mut collector = crate::layout::PopoverCollector::new();
+ crate::layout::render_popovers(&mut collector);
+
+ let mut verts = Vec::new();
+ for &(color, qx, qy, qw, qh) in &collector.rects {
+ let qx_local = qx - popup.x;
+ let qy_local = qy - popup.y;
+ verts.extend(quad_vertices(qx_local, qy_local, qw, qh, popup.logical_width, popup.logical_height, color));
+ }
+
+ let vertex_count = verts.len() as u32;
+ if vertex_count > 0 {
+ let data = bytemuck::cast_slice(&verts);
+ let needed = data.len() as wgpu::BufferAddress;
+ let mut vbuf = popup.vertex_buffer.as_ref();
+ if vbuf.map_or(true, |v| needed > v.size()) {
+ let new_vbuf = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Popup Vertex Buffer"),
+ size: needed,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+ popup.vertex_buffer = Some(new_vbuf);
+ vbuf = popup.vertex_buffer.as_ref();
+ }
+ queue.write_buffer(vbuf.unwrap(), 0, data);
+ }
+
+ let mut text_items = Vec::new();
+ for (content, size, tx, ty, color, font, bounds) in collector.texts {
+ let tx_local = tx - popup.x;
+ let ty_local = ty - popup.y;
+ text_items.push(TextItem {
+ buffer: make_text_buffer_with_font(&mut self.font_system, &content, size, font.as_deref()),
+ x: tx_local,
+ y: ty_local,
+ color: glyphon::Color::rgb(
+ (color[0] * 255.0).clamp(0.0, 255.0) as u8,
+ (color[1] * 255.0).clamp(0.0, 255.0) as u8,
+ (color[2] * 255.0).clamp(0.0, 255.0) as u8,
+ ),
+ bounds,
+ });
+ }
+
+ let scale_f32 = scale_factor as f32;
+ let bounds = TextBounds {
+ left: 0,
+ top: 0,
+ right: (popup.logical_width * scale_f32) as i32,
+ bottom: (popup.logical_height * scale_f32) as i32,
+ };
+ let areas: Vec<TextArea<'_>> = text_items.iter().map(|ti| TextArea {
+ buffer: &ti.buffer,
+ left: ti.x * scale_f32,
+ top: ti.y * scale_f32,
+ scale: scale_f32,
+ bounds,
+ default_color: ti.color,
+ custom_glyphs: &[],
+ }).collect();
+
+ popup.viewport.update(queue, Resolution {
+ width: (popup.logical_width * scale_f32) as u32,
+ height: (popup.logical_height * scale_f32) as u32,
+ });
+
+ text_renderer.prepare(
+ device,
+ queue,
+ &mut self.font_system,
+ text_atlas,
+ &popup.viewport,
+ areas,
+ &mut self.swash_cache,
+ ).unwrap();
+
+ let popup_output = match popup.wgpu_surface.get_current_texture() {
+ Ok(t) => t,
+ Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+ popup.wgpu_surface.configure(device, &popup.config);
+ return;
+ }
+ Err(wgpu::SurfaceError::Timeout) => return,
+ Err(e) => {
+ eprintln!("Popup surface error: {e:?}");
+ return;
+ }
+ };
+ let popup_view = popup_output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let mut popup_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("Popup Encoder"),
+ });
+
+ {
+ let mut pass = popup_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Popup Render Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &popup_view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ if vertex_count > 0 {
+ pass.set_pipeline(render_pipeline);
+ pass.set_vertex_buffer(0, popup.vertex_buffer.as_ref().unwrap().slice(..));
+ pass.draw(0..vertex_count, 0..1);
+ }
+
+ text_renderer.render(text_atlas, &popup.viewport, &mut pass).unwrap();
+ }
+
+ queue.submit(std::iter::once(popup_encoder.finish()));
+ popup_output.present();
+ }
+ }
+
text_atlas.trim();
}
}
impl<A: Application> Drop for EngineState<A> {
fn drop(&mut self) {
+ self.instance = None;
self.wgpu_surface = None;
self.device = None;
self.queue = None;
@@ -820,8 +1388,15 @@ impl<A: Application> PointerHandler for EngineState<A> {
use smithay_client_toolkit::seat::pointer::PointerEventKind;
for event in events {
let (x, y) = event.position;
- let lx = x as f32;
- let ly = y as f32;
+ let mut lx = x as f32;
+ let mut ly = y as f32;
+
+ if let Some(ref popup) = self.active_popup {
+ if event.surface == *popup.sctk_popup.wl_surface() {
+ lx += popup.x;
+ ly += popup.y;
+ }
+ }
match &event.kind {
PointerEventKind::Enter { .. } => {
@@ -1039,10 +1614,37 @@ impl<A: Application> wayland_client::Dispatch<crate::protocol::zclear_inspector_
) {}
}
+impl<A: Application> PopupHandler for EngineState<A> {
+ fn configure(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _popup: &Popup,
+ _config: PopupConfigure,
+ ) {
+ if let Some(ref mut p) = self.active_popup {
+ p.configured = true;
+ }
+ self.redraw = true;
+ }
+
+ 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);
+ popover.unfocus();
+ }
+ }
+ self.active_popup = None;
+ self.redraw = true;
+ }
+}
+
// Delegate macros for generic EngineState
delegate_compositor!(@<A: Application> EngineState<A>);
delegate_xdg_shell!(@<A: Application> EngineState<A>);
delegate_xdg_window!(@<A: Application> EngineState<A>);
+delegate_xdg_popup!(@<A: Application> EngineState<A>);
delegate_shm!(@<A: Application> EngineState<A>);
delegate_seat!(@<A: Application> EngineState<A>);
delegate_pointer!(@<A: Application> EngineState<A>);
@@ -1082,6 +1684,7 @@ pub fn run<A: Application>() {
window: None,
surface: None,
inner,
+ instance: None,
wgpu_surface: None,
device: None,
queue: None,
@@ -1106,6 +1709,8 @@ pub fn run<A: Application>() {
shift_pressed: false,
pressed_key: None,
sender,
+ active_popup: None,
+ qh: qh.clone(),
};
event_queue.roundtrip(&mut engine_state).unwrap();
@@ -1133,7 +1738,7 @@ pub fn run<A: Application>() {
let mut event_loop = EventLoop::try_new().unwrap();
let loop_handle = event_loop.handle();
- WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
+ WaylandSource::new(conn.clone(), event_queue).insert(loop_handle.clone()).unwrap();
loop_handle.insert_source(channel, |event, _metadata, app_state: &mut EngineState<A>| {
if let calloop::channel::Event::Msg(msg) = event {
@@ -1148,6 +1753,7 @@ pub fn run<A: Application>() {
const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
+ let mut last_title = settings.title.clone();
let mut last_tick = std::time::Instant::now();
loop {
event_loop.dispatch(std::time::Duration::from_millis(16), &mut engine_state).unwrap();
@@ -1195,6 +1801,84 @@ pub fn run<A: Application>() {
}
}
}
+ let current_title = engine_state.inner.settings().title;
+ if current_title != last_title {
+ if let Some(ref window) = engine_state.window {
+ window.set_title(¤t_title);
+ window.commit();
+ }
+ last_title = current_title;
+ }
+
+ let active_popovers = crate::widget::popovers::get_active();
+ if !active_popovers.is_empty() {
+ let popover_widget = unsafe { &*active_popovers[0] };
+ if let Some((px, py, pw, ph)) = popover_widget.popover_rect() {
+ if engine_state.active_popup.is_none() {
+ let wl_surface = engine_state.compositor_state.create_surface(&engine_state.qh);
+ wl_surface.set_buffer_scale(engine_state.scale_factor as i32);
+
+ let positioner = XdgPositioner::new(&engine_state.xdg_shell_state).unwrap();
+ positioner.set_size(pw as i32, ph as i32);
+
+ let (rx, ry, rw, rh) = popover_widget.rect();
+ positioner.set_anchor_rect(rx as i32, ry as i32, rw as i32, rh as i32);
+ positioner.set_anchor(Anchor::BottomLeft);
+ positioner.set_gravity(Gravity::BottomRight);
+ positioner.set_constraint_adjustment(
+ ConstraintAdjustment::SlideX | ConstraintAdjustment::SlideY
+ );
+
+ let parent_xdg_surface = XdgSurface::xdg_surface(engine_state.window.as_ref().unwrap());
+ let sctk_popup = Popup::new(
+ parent_xdg_surface,
+ &positioner,
+ &engine_state.qh,
+ &engine_state.compositor_state,
+ &engine_state.xdg_shell_state,
+ ).unwrap();
+
+ let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
+ let surface_ptr = sctk_popup.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
+ let wayland_handle = Box::leak(Box::new(WaylandSurfaceHandle {
+ display_ptr,
+ surface_ptr,
+ }));
+ let instance = engine_state.instance.as_ref().unwrap();
+ let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create popup wgpu surface");
+
+ let device = engine_state.device.as_ref().unwrap();
+ let main_config = engine_state.config.as_ref().unwrap();
+
+ let scale_f32 = engine_state.scale_factor as f32;
+ let mut popup_config = main_config.clone();
+ popup_config.width = (pw * scale_f32) as u32;
+ popup_config.height = (ph * scale_f32) as u32;
+ wgpu_surface.configure(device, &popup_config);
+
+ let cache = Cache::new(device);
+ let viewport = Viewport::new(device, &cache);
+
+ engine_state.active_popup = Some(ActivePopup {
+ sctk_popup,
+ wgpu_surface,
+ config: popup_config,
+ logical_width: pw,
+ logical_height: ph,
+ configured: false,
+ viewport,
+ x: px,
+ y: py,
+ vertex_buffer: None,
+ });
+ }
+ }
+ } else {
+ if engine_state.active_popup.is_some() {
+ engine_state.active_popup = None;
+ engine_state.redraw = true;
+ }
+ }
if engine_state.redraw {
engine_state.redraw = false;
diff --git a/src/layout.rs b/src/layout.rs
index 494d697..c513710 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -6,11 +6,17 @@ pub trait RenderTarget {
fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _font: &str) {
self.text(content, x, y, size, color);
}
+ fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _bounds: Option<[f32; 4]>) {
+ self.text(content, x, y, size, color);
+ }
+ fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, _bounds: Option<[f32; 4]>) {
+ self.text_with_font(content, x, y, size, color, font);
+ }
}
pub struct PopoverCollector {
pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
- pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>)>,
+ pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
}
impl PopoverCollector {
@@ -25,11 +31,19 @@ impl RenderTarget for PopoverCollector {
}
fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
- self.texts.push((content.to_string(), size, x, y, color, None));
+ self.texts.push((content.to_string(), size, x, y, color, None, None));
}
fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
- self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string())));
+ self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
+ }
+
+ fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
+ self.texts.push((content.to_string(), size, x, y, color, None, bounds));
+ }
+
+ fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
+ self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
}
}
@@ -39,7 +53,7 @@ pub fn render_widget<T: Widget + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
pc.rect(qc, qx, qy, qw, qh);
}
let font_opt = w.widget_font();
- for label in w.text_labels() {
+ for (label, bounds) in w.text_labels_with_bounds() {
let color_f32 = [
label.color[0] as f32 / 255.0,
label.color[1] as f32 / 255.0,
@@ -47,9 +61,9 @@ pub fn render_widget<T: Widget + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
1.0,
];
if let Some(ref font) = font_opt {
- pc.text_with_font(&label.text, label.x, label.y, label.font_size, color_f32, font);
+ pc.text_with_font_and_bounds(&label.text, label.x, label.y, label.font_size, color_f32, font, bounds);
} else {
- pc.text(&label.text, label.x, label.y, label.font_size, color_f32);
+ pc.text_with_bounds(&label.text, label.x, label.y, label.font_size, color_f32, bounds);
}
}
if w.popover_rect().is_some() {
@@ -80,7 +94,7 @@ impl UiFrame {
if let Some((qx, qy, qw, qh, qc)) = crate::widget::hover_animation::get_quad() {
pc.rect(qc, qx, qy, qw, qh);
}
- render_popovers(pc);
+ // render_popovers(pc);
}
}
@@ -140,12 +154,13 @@ impl Column {
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();
- self.y += top_room;
+ let total_h = wh + top_room;
let x = self.ax(x_off);
let y = self.ay();
w.set_row_rect(self.ox + self.cx + 8.0, self.cw - 16.0);
- render_widget(pc, w, x, y, ww, wh);
- self.y += wh;
+ let clamped_w = ww.min((self.cw - x_off).max(0.0));
+ render_widget(pc, w, x, y, clamped_w, total_h);
+ self.y += total_h;
}
pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
@@ -196,6 +211,7 @@ pub struct Section {
top: f32,
pub content_y: f32,
pub cw: f32,
+ label_width: f32,
}
impl Section {
@@ -203,10 +219,26 @@ impl Section {
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: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
- pc.text(label, left + Self::DEFAULT_MARGIN_X, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
- pc.rect([0.18, 0.18, 0.27, 1.0], left + Self::ROW_PADDING_X, top + 22.0, cw - 2.0 * Self::ROW_PADDING_X, 1.0);
- Self { left, top, content_y: top + 34.0, cw }
+ 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 { left, top, content_y: top + 19.0, cw, label_width }
}
pub fn ax(&self, x_off: f32) -> f32 {
@@ -224,10 +256,19 @@ impl Section {
pub fn widget<T: Widget + '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();
- self.content_y += top_room;
- render_widget(pc, w, self.ax(x_off), self.ay(), ww, wh);
- self.content_y += wh;
+ 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) {
+ 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);
}
pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
@@ -281,10 +322,27 @@ impl Section {
[0.25, 0.25, 0.35, 1.0] // Default gray
};
let x = self.left + Self::ROW_PADDING_X;
- let y = self.top + 22.0;
+ let y = self.top + 7.0;
let w = self.cw - 2.0 * Self::ROW_PADDING_X;
let h = self.content_y - y;
- pc.rect(border, x, y, w, 1.0);
+
+ 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 {
+ pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+ }
+ if right_edge > gap_end {
+ pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+ }
+ } else {
+ pc.rect(border, left_edge, y, w, 1.0);
+ }
+
pc.rect(border, x, y + h + 12.0, w, 1.0);
pc.rect(border, x, y, 1.0, h + 12.0);
pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
@@ -321,6 +379,180 @@ impl<'a> VStack<'a> {
}
}
+pub struct Subsection {
+ left: f32,
+ top: f32,
+ pub content_y: f32,
+ pub cw: f32,
+ label_width: f32,
+}
+
+impl Subsection {
+ 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: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> 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 { left, top, content_y: top + 17.0, cw, label_width }
+ }
+
+ 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, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+ 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) {
+ 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 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) {
+ 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);
+ }
+
+ pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
+ let x = self.ax(Self::ROW_PADDING_X);
+ let y = self.ay();
+ 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, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
+ 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(&mut self, pc: &mut dyn RenderTarget) -> f32 {
+ self.finish_focused(pc, false)
+ }
+
+ pub fn finish_focused(&mut self, pc: &mut dyn RenderTarget, focused: bool) -> f32 {
+ let border: [f32; 4] = if 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 {
+ pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+ }
+ if right_edge > gap_end {
+ pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
+ }
+ } else {
+ pc.rect(border, left_edge, y, w, 1.0);
+ }
+
+ pc.rect(border, x, y + h + 12.0, w, 1.0);
+ pc.rect(border, x, y, 1.0, h + 12.0);
+ pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
+ self.content_y + 20.0
+ }
+
+ pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SubVStack<'a> {
+ SubVStack {
+ subsection: self,
+ pc,
+ spacing,
+ }
+ }
+}
+
+pub struct SubVStack<'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) {
+ self.subsection.widget(self.pc, w, Subsection::DEFAULT_MARGIN_X, ww, wh);
+ self.subsection.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.subsection.row(count, gap, h, f);
+ self.subsection.spacing(self.spacing);
+ }
+}
+
+
pub struct SplitterLayout {
pub splitter1_x: f32,
pub splitter2_x: f32,
@@ -343,14 +575,14 @@ impl SplitterLayout {
pub fn clamp(&mut self, total_width: f32, detached_circular_network: bool) {
if detached_circular_network {
let min_s2 = self.min_column_width;
- let max_s2 = total_width - self.min_column_width;
+ let max_s2 = (total_width - self.min_column_width).max(min_s2);
self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
} else {
let min_s1 = self.min_column_width;
- let max_s1 = self.splitter2_x - self.splitter_width - self.min_column_width;
+ let max_s1 = (self.splitter2_x - self.splitter_width - self.min_column_width).max(min_s1);
self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
let min_s2 = self.splitter1_x + self.splitter_width + self.min_column_width;
- let max_s2 = total_width - self.min_column_width;
+ let max_s2 = (total_width - self.min_column_width).max(min_s2);
self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
}
}
@@ -375,6 +607,68 @@ impl SplitterLayout {
}
}
+pub struct Grid {
+ pub left: f32,
+ pub top: f32,
+ pub width: f32,
+ pub col_width: f32,
+ pub gap: f32,
+ pub col_heights: Vec<f32>,
+ pub col_lefts: Vec<f32>,
+}
+
+impl Grid {
+ pub fn new(left: f32, top: f32, width: f32, min_col_width: f32, gap: f32, count: usize) -> Self {
+ let total_gap = gap * (count - 1) as f32;
+ let total_grid_width = count as f32 * min_col_width + total_gap;
+ let left_offset = if total_grid_width < width {
+ (width - total_grid_width) / 2.0
+ } else {
+ 0.0
+ };
+
+ let col_width = min_col_width;
+
+ let mut col_lefts = Vec::with_capacity(count);
+ let col_heights = vec![top; count];
+ for i in 0..count {
+ col_lefts.push(left + left_offset + i as f32 * (col_width + gap));
+ }
+
+ Self {
+ left,
+ top,
+ width,
+ col_width,
+ gap,
+ col_heights,
+ col_lefts,
+ }
+ }
+
+ pub fn next_column(&self) -> usize {
+ let mut min_idx = 0;
+ let mut min_h = self.col_heights[0];
+ for i in 1..self.col_heights.len() {
+ if self.col_heights[i] < min_h {
+ min_h = self.col_heights[i];
+ min_idx = i;
+ }
+ }
+ min_idx
+ }
+
+ pub fn max_height(&self) -> f32 {
+ let mut max_h = self.col_heights[0];
+ for i in 1..self.col_heights.len() {
+ if self.col_heights[i] > max_h {
+ max_h = self.col_heights[i];
+ }
+ }
+ max_h
+ }
+}
+
pub struct CircularPaneLayout {
pub x: f32,
pub y: f32,
@@ -415,6 +709,259 @@ impl CircularPaneLayout {
}
}
+pub struct Radial {
+ pub center_x: f32,
+ pub center_y: f32,
+ pub aspect_ratio: f32,
+ pub base_spacing: f32,
+}
+
+impl Radial {
+ pub fn new(center_x: f32, center_y: f32, aspect_ratio: f32, base_spacing: f32) -> Self {
+ Self { center_x, center_y, aspect_ratio, base_spacing }
+ }
+
+ pub fn widget_rect(&self, idx: usize, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ if idx == 0 {
+ (self.center_x - ww / 2.0, self.center_y - wh / 2.0, ww, wh)
+ } else {
+ let mut ring = 1;
+ let mut ring_start = 1;
+ loop {
+ let ring_capacity = ring * 6;
+ if idx < ring_start + ring_capacity {
+ let pos_in_ring = idx - ring_start;
+ let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
+ let radius = (ring as f32) * self.base_spacing;
+
+ let x_offset = radius * angle.cos() * self.aspect_ratio;
+ let y_offset = radius * angle.sin();
+
+ return (
+ self.center_x + x_offset - ww / 2.0,
+ self.center_y + y_offset - wh / 2.0,
+ ww,
+ wh,
+ );
+ }
+ ring_start += ring_capacity;
+ ring += 1;
+ }
+ }
+ }
+
+ pub fn layout_widgets<T: Widget + 'static>(&self, widgets: &mut [&mut T]) {
+ let mut active_idx = 0;
+ for w in widgets.iter_mut() {
+ if !w.layout_ignore() {
+ let (_, _, ww, wh) = w.rect();
+ let use_w = if ww > 0.0 { ww } else { 100.0 };
+ let use_h = if wh > 0.0 { wh } else { 50.0 };
+ let (x, y, rw, rh) = self.widget_rect(active_idx, use_w, use_h);
+ w.set_rect(x, y, rw, rh);
+ active_idx += 1;
+ }
+ }
+ }
+
+ pub fn layout_widget_ptors(&self, widgets: &[*mut (dyn Widget + 'static)]) {
+ let mut active_idx = 0;
+ for &w_ptr in widgets {
+ let w = unsafe { &mut *w_ptr };
+ if !w.layout_ignore() {
+ let (_, _, ww, wh) = w.rect();
+ let use_w = if ww > 0.0 { ww } else { 100.0 };
+ let use_h = if wh > 0.0 { wh } else { 50.0 };
+ let (x, y, rw, rh) = self.widget_rect(active_idx, use_w, use_h);
+ w.set_rect(x, y, rw, rh);
+ active_idx += 1;
+ }
+ }
+ }
+}
+
+pub trait LayoutStrategy {
+ fn init(&mut self, left: f32, top: f32, width: f32, height: f32);
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32);
+ fn set_section_count(&mut self, _count: usize) {}
+}
+
+pub struct ColumnLayout {
+ left: f32,
+ top: f32,
+ width: f32,
+ current_y: f32,
+ gap: f32,
+}
+
+impl ColumnLayout {
+ pub fn new(gap: f32) -> Self {
+ Self {
+ left: 0.0,
+ top: 0.0,
+ width: 0.0,
+ current_y: 0.0,
+ gap,
+ }
+ }
+}
+
+impl LayoutStrategy for ColumnLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+ self.left = left;
+ self.top = top;
+ self.width = width;
+ self.current_y = top;
+ }
+
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ let x = self.left;
+ let y = self.current_y;
+ self.current_y += wh + self.gap;
+ (x, y, ww, wh)
+ }
+}
+
+pub struct GridLayout {
+ grid: Option<Grid>,
+ min_col_width: f32,
+ gap: f32,
+ num_sections: Option<usize>,
+}
+
+impl GridLayout {
+ pub fn new(min_col_width: f32, gap: f32) -> Self {
+ Self {
+ grid: None,
+ min_col_width,
+ gap,
+ num_sections: None,
+ }
+ }
+}
+
+impl LayoutStrategy for GridLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
+ let max_cols = ((width + self.gap) / (self.min_col_width + self.gap)).floor().max(1.0) as usize;
+ let count = if let Some(n) = self.num_sections {
+ n.min(max_cols).max(1)
+ } else {
+ max_cols
+ };
+ self.grid = Some(Grid::new(left, top, width, self.min_col_width, self.gap, count));
+ }
+
+ fn allocate(&mut self, _ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ if let Some(ref mut grid) = self.grid {
+ let col = grid.next_column();
+ let x = grid.col_lefts[col];
+ let y = grid.col_heights[col];
+ grid.col_heights[col] += wh + grid.gap;
+ (x, y, grid.col_width, wh)
+ } else {
+ (0.0, 0.0, 0.0, wh)
+ }
+ }
+
+ fn set_section_count(&mut self, count: usize) {
+ self.num_sections = Some(count);
+ }
+}
+
+pub struct RadialLayout {
+ radial: Option<Radial>,
+ aspect_ratio: f32,
+ base_spacing: f32,
+ idx: usize,
+}
+
+impl RadialLayout {
+ pub fn new(aspect_ratio: f32, base_spacing: f32) -> Self {
+ Self {
+ radial: None,
+ aspect_ratio,
+ base_spacing,
+ idx: 0,
+ }
+ }
+}
+
+impl LayoutStrategy for RadialLayout {
+ fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
+ let cx = left + width / 2.0;
+ let cy = top + height / 2.0;
+ let aspect = if self.aspect_ratio > 0.0 {
+ self.aspect_ratio
+ } else {
+ let screen_aspect = (width / height.max(1.0)).max(0.1);
+ 1.0 + (screen_aspect - 1.0) * 0.4
+ };
+ self.radial = Some(Radial::new(cx, cy, aspect, self.base_spacing));
+ self.idx = 0;
+ }
+
+ fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
+ if let Some(ref radial) = self.radial {
+ let rect = radial.widget_rect(self.idx, ww, wh);
+ self.idx += 1;
+ rect
+ } else {
+ (0.0, 0.0, ww, wh)
+ }
+ }
+}
+
+pub struct PageLayoutBuilder<'a, P> {
+ pub strategy: &'a mut dyn LayoutStrategy,
+ pub cx: f32,
+ pub cy: f32,
+ pub cw: f32,
+ pub ch: f32,
+ pub section_width: f32,
+ pub idx: usize,
+ _phantom: std::marker::PhantomData<P>,
+}
+
+impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
+ pub fn new(
+ strategy: &'a mut dyn LayoutStrategy,
+ cx: f32,
+ cy: f32,
+ cw: f32,
+ ch: f32,
+ section_width: f32,
+ ) -> Self {
+ strategy.init(cx, cy, cw, ch);
+ Self {
+ strategy,
+ cx,
+ cy,
+ cw,
+ ch,
+ section_width,
+ idx: 0,
+ _phantom: std::marker::PhantomData,
+ }
+ }
+
+ pub fn with_section_count(mut self, count: usize) -> Self {
+ self.strategy.set_section_count(count);
+ self.strategy.init(self.cx, self.cy, self.cw, self.ch);
+ self
+ }
+
+ pub fn add_section<F>(&mut self, final_pc: &mut P, mut render_fn: F)
+ where
+ F: FnMut(&mut P, f32, f32) -> f32,
+ {
+ let mut dummy = P::default();
+ let wh = render_fn(&mut dummy, 0.0, 0.0);
+ let (rx, ry, _, _) = self.strategy.allocate(self.section_width, wh);
+ render_fn(final_pc, rx, ry);
+ self.idx += 1;
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -462,13 +1009,13 @@ mod tests {
impl Widget for MockWidgetWithLabel {
fn rect(&self) -> (f32, f32, f32, f32) {
- (self.x, self.y, self.w, self.h)
+ (self.x, self.y - self.top_room, self.w, self.h + self.top_room)
}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.x = x;
- self.y = y;
+ self.y = y + self.top_room;
self.w = w;
- self.h = h;
+ self.h = (h - self.top_room).max(0.0);
}
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
@@ -505,4 +1052,74 @@ mod tests {
// Third widget has top_room = 15.0, so its y should be shifted by 15.0
assert_eq!(w3.y, start_y + 30.0 + 10.0 + 40.0 + 10.0 + 15.0);
}
+
+ #[test]
+ fn test_grid_layout() {
+ // Test single column layout (width = 200, min_col_width = 300)
+ let grid1 = Grid::new(10.0, 20.0, 200.0, 300.0, 10.0, 1);
+ assert_eq!(grid1.col_heights.len(), 1);
+ assert_eq!(grid1.col_lefts[0], 10.0);
+ assert_eq!(grid1.col_width, 300.0);
+
+ // Test multi column layout (width = 700, min_col_width = 300, gap = 20)
+ // count = floor((700 + 20) / (300 + 20)) = floor(720 / 320) = 2.
+ // total_gap = 20 * 1 = 20.
+ // col_width = (700 - 20) / 2 = 340.
+ let mut grid2 = Grid::new(5.0, 15.0, 700.0, 300.0, 20.0, 2);
+ assert_eq!(grid2.col_heights.len(), 2);
+ assert_eq!(grid2.col_lefts[0], 45.0);
+ assert_eq!(grid2.col_lefts[1], 365.0);
+ assert_eq!(grid2.col_width, 300.0);
+
+ assert_eq!(grid2.next_column(), 0);
+ grid2.col_heights[0] += 50.0; // Column 0 height becomes 65.0
+ assert_eq!(grid2.next_column(), 1);
+ grid2.col_heights[1] += 30.0; // Column 1 height becomes 45.0
+ assert_eq!(grid2.next_column(), 1);
+ grid2.col_heights[1] += 30.0; // Column 1 height becomes 75.0
+ assert_eq!(grid2.next_column(), 0);
+
+ assert_eq!(grid2.max_height(), 75.0);
+ }
+
+ #[test]
+ fn test_subsection() {
+ let mut pc = PopoverCollector::new();
+ let mut subsec = Subsection::new(&mut pc, 10.0, 20.0, 300.0, "Test Subsec");
+ assert_eq!(subsec.left, 10.0);
+ assert_eq!(subsec.top, 20.0);
+ assert_eq!(subsec.cw, 300.0);
+
+ let mut w = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ subsec.widget(&mut pc, &mut w, 12.0, 100.0, 40.0);
+
+ let bottom = subsec.finish(&mut pc);
+ assert!(bottom > 20.0);
+ }
+
+ #[test]
+ fn test_radial_layout() {
+ let radial = Radial::new(100.0, 100.0, 1.5, 50.0);
+
+ // Check first widget is centered at (100.0, 100.0)
+ let rect0 = radial.widget_rect(0, 40.0, 30.0);
+ assert_eq!(rect0, (100.0 - 20.0, 100.0 - 15.0, 40.0, 30.0));
+
+ // Check Ring 1 (idx = 1) vs Ring 2 (idx = 7)
+ let rect1 = radial.widget_rect(1, 40.0, 30.0);
+ let rect7 = radial.widget_rect(7, 40.0, 30.0);
+
+ let c1_x = rect1.0 + rect1.2 / 2.0;
+ let c1_y = rect1.1 + rect1.3 / 2.0;
+ let c7_x = rect7.0 + rect7.2 / 2.0;
+ let c7_y = rect7.1 + rect7.3 / 2.0;
+
+ let d1 = ((c1_x - 100.0).powi(2) + (c1_y - 100.0).powi(2)).sqrt();
+ let d7 = ((c7_x - 100.0).powi(2) + (c7_y - 100.0).powi(2)).sqrt();
+
+ // Ring 2 should be further out than Ring 1
+ assert!(d7 > d1);
+ assert!(d1 > 0.0);
+ }
}
+
diff --git a/src/lib.rs b/src/lib.rs
index 8b2c731..b5fec9f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,6 +4,7 @@ pub mod layout;
pub mod wayland;
pub mod protocol;
pub mod engine;
+pub mod scale;
pub mod colors {
pub use crate::color::*;
diff --git a/src/main.rs b/src/main.rs
index 8f48d96..303724a 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,
+ StatusBar, TextLabel, Toggle, Widget, JsonLayoutWidget, JsonLayoutConfig,
};
use glyphon::{
@@ -81,9 +81,125 @@ fn quad_vertices(
]
}
+fn rounded_rect_vertices_corners(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ corners: (bool, bool, bool, bool),
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ let r = r.min(ww * 0.5).min(h * 0.5);
+
+ let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
+ let x0 = qx;
+ let y0 = qy;
+ let x1 = qx + qw;
+ let y1 = qy + qh;
+
+ let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+ let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+ let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+ let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+
+ let clip_circle = [0.0, 0.0, 0.0];
+ verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
+ };
+
+ if r <= 0.1 || (!corners.0 && !corners.1 && !corners.2 && !corners.3) {
+ push_quad(&mut verts, x, y, ww, h);
+ return verts;
+ }
+
+ push_quad(&mut verts, x + r, y, ww - 2.0 * r, h);
+ push_quad(&mut verts, x, y + r, r, h - 2.0 * r);
+ push_quad(&mut verts, x + ww - r, y + r, r, h - 2.0 * r);
+
+ let corner_configs = [
+ (corners.0, x, y, x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI),
+ (corners.1, x + ww - r, y, x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI),
+ (corners.2, x + ww - r, y + h - r, x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI),
+ (corners.3, x, y + h - r, x + r, y + h - r, 0.5 * std::f32::consts::PI, std::f32::consts::PI),
+ ];
+
+ let segments = 16;
+ for &(is_rounded, sqx, sqy, cx, cy, start, end) in &corner_configs {
+ if is_rounded {
+ for i in 0..segments {
+ let theta1 = start + (i as f32) * (end - start) / (segments as f32);
+ let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
+
+ let x0 = cx;
+ let y0 = cy;
+ let x1 = cx + r * theta1.cos();
+ let y1 = cy + r * theta1.sin();
+ let x2 = cx + r * theta2.cos();
+ let y2 = cy + r * theta2.sin();
+
+ let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
+ let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
+ let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
+ let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
+ let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
+ let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
+
+ let clip_circle = [0.0, 0.0, 0.0];
+ verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ }
+ } else {
+ push_quad(&mut verts, sqx, sqy, r, r);
+ }
+ }
+
+ verts
+}
+
+fn rounded_rect_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+) -> Vec<Vertex> {
+ 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> {
let (x, y, ww, h) = w.rect();
- quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
+ let corners = w.rounded_corners();
+ if corners != (false, false, false, false) {
+ rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), corners)
+ } else {
+ quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
+ }
+}
+
+fn extra_quad_vertices(
+ w: &dyn Widget,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+ sw: f32, sh: f32,
+ qc: [f32; 4],
+) -> Vec<Vertex> {
+ let corners = w.rounded_corners();
+ if corners == (false, false, false, false) {
+ return quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
+ }
+
+ let (wx, wy, ww, wh) = w.rect();
+ let extra_corners = (
+ corners.0 && qx <= wx + 0.1 && qy <= wy + 0.1,
+ corners.1 && qx + qw >= wx + ww - 0.1 && qy <= wy + 0.1,
+ corners.2 && qx + qw >= wx + ww - 0.1 && qy + qh >= wy + wh - 0.1,
+ corners.3 && qx <= wx + 0.1 && qy + qh >= wy + wh - 0.1,
+ );
+
+ rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, extra_corners)
}
fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
@@ -94,6 +210,24 @@ fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buff
buffer
}
+fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
+ let metrics = Metrics::new(size, size * 1.4);
+ let mut buffer = Buffer::new(font_system, metrics);
+ let mut attrs = Attrs::new();
+ if let Some(font_name) = font {
+ let family = match font_name {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ _ => glyphon::Family::Name(font_name),
+ };
+ attrs = attrs.family(family);
+ }
+ buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
+ buffer.shape_until_scroll(font_system, true);
+ buffer
+}
+
struct State {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
@@ -127,10 +261,19 @@ struct State {
physical_width: u32,
physical_height: u32,
scale: f64,
+ json_layout: Option<JsonLayoutWidget>,
+ layout_mode: bool,
}
impl State {
- async fn new(wayland_handle: &'static clear_ui::wayland::WaylandSurfaceHandle, pw: u32, ph: u32, scale: f64) -> Self {
+ async fn new(
+ wayland_handle: &'static clear_ui::wayland::WaylandSurfaceHandle,
+ pw: u32,
+ ph: u32,
+ scale: f64,
+ json_layout_config: Option<JsonLayoutConfig>,
+ ) -> Self {
+ clear_ui::scale::set_scale_factor(scale as f32);
let lw = pw as f32 / scale as f32;
let lh = ph as f32 / scale as f32;
let sw = lw;
@@ -236,26 +379,36 @@ impl State {
let label_buffer = make_text_buffer(&mut font_system, "Hello, Clear UI!", 16.0);
let status_buffer = make_text_buffer(&mut font_system, "Click a button to interact", 12.0);
- let widgets: Vec<Box<dyn Widget>> = vec![
- Box::new(Header::new()),
- Box::new(Sidebar::new(60.0)),
- Box::new(ContentBg::new()),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button A")),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button B")),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button C")),
- Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Click Me")),
- Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0).with_label("Reset")),
- Box::new(Checkbox::new()),
- Box::new(Toggle::new()),
- Box::new(ProgressBar::new(0.65)),
- Box::new(Slider::new()),
- Box::new(Spinbox::new(0, -10, 10, 1)),
- Box::new(RangeSlider::new()),
- Box::new(StatusBar::new()),
- ];
-
- let positions = demo_positions(sw, sh);
+ let layout_mode = json_layout_config.is_some();
+ let mut widgets: Vec<Box<dyn Widget>> = Vec::new();
+ let mut positions = Vec::new();
+ let json_layout = if let Some(ref config) = json_layout_config {
+ let mut jl = JsonLayoutWidget::new(config);
+ jl.set_rect(0.0, 0.0, sw, sh);
+ positions.push((0.0, 0.0, sw, sh));
+ Some(jl)
+ } else {
+ widgets = vec![
+ Box::new(Header::new()),
+ Box::new(Sidebar::new(60.0)),
+ Box::new(ContentBg::new()),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button A")),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button B")),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button C")),
+ Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Click Me")),
+ Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0).with_label("Reset")),
+ Box::new(Checkbox::new()),
+ Box::new(Toggle::new()),
+ Box::new(ProgressBar::new(0.65)),
+ Box::new(Slider::new()),
+ Box::new(Spinbox::new(0, -10, 10, 1)),
+ Box::new(RangeSlider::new()),
+ Box::new(StatusBar::new()),
+ ];
+ positions = demo_positions(sw, sh);
+ None
+ };
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Vertex Buffer"),
@@ -291,6 +444,8 @@ impl State {
physical_width: pw,
physical_height: ph,
scale,
+ json_layout,
+ layout_mode,
};
state.apply_layout();
@@ -299,13 +454,20 @@ impl State {
}
fn apply_layout(&mut self) {
- for (i, pos) in self.positions.iter().enumerate() {
- if let Some(widget) = self.widgets.get_mut(i) {
- if widget.is_dragging() {
- continue;
+ if self.layout_mode {
+ if let Some(jl) = &mut self.json_layout {
+ clear_ui::scale::set_scale_factor(self.scale as f32);
+ jl.set_rect(0.0, 0.0, self.width, self.height);
+ }
+ } else {
+ for (i, pos) in self.positions.iter().enumerate() {
+ if let Some(widget) = self.widgets.get_mut(i) {
+ if widget.is_dragging() {
+ continue;
+ }
+ let (x, y, w, h) = *pos;
+ widget.set_rect(x, y, w, h);
}
- let (x, y, w, h) = *pos;
- widget.set_rect(x, y, w, h);
}
}
}
@@ -314,32 +476,42 @@ impl State {
let sw = self.width;
let sh = self.height;
let mut verts = Vec::new();
- let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
- draw_order.sort_by_key(|&i| self.widgets[i].z_index());
- for &i in &draw_order {
- let w = &self.widgets[i];
- verts.extend(widget_vertices(w.as_ref(), sw, sh));
- for (qx, qy, qw, qh, qc) in w.extra_quads() {
- verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+ if self.layout_mode {
+ verts.extend(quad_vertices(0.0, 0.0, sw, sh, sw, sh, [0.05, 0.05, 0.08, 1.0]));
+ if let Some(jl) = &self.json_layout {
+ verts.extend(widget_vertices(jl, sw, sh));
+ for (qx, qy, qw, qh, qc) in jl.extra_quads() {
+ verts.extend(extra_quad_vertices(jl, qx, qy, qw, qh, sw, sh, qc));
+ }
}
- }
-
- // Draw popover quads on top
- let mut popover_pc = clear_ui::layout::PopoverCollector::new();
- for &i in &draw_order {
- let w = &self.widgets[i];
- if w.popover_rect().is_some() {
- w.render_popover(&mut popover_pc);
+ } else {
+ let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
+ draw_order.sort_by_key(|&i| self.widgets[i].z_index());
+ for &i in &draw_order {
+ let w = &self.widgets[i];
+ verts.extend(widget_vertices(w.as_ref(), sw, sh));
+ for (qx, qy, qw, qh, qc) in w.extra_quads() {
+ verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc));
+ }
}
- }
- for (qc, qx, qy, qw, qh) in popover_pc.rects {
- verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
- }
- if clear_ui::widget::context_menu::is_visible() {
- for (qx, qy, qw, qh, qc) in clear_ui::widget::context_menu::extra_quads() {
+ // Draw popover quads on top
+ let mut popover_pc = clear_ui::layout::PopoverCollector::new();
+ for &i in &draw_order {
+ let w = &self.widgets[i];
+ if w.popover_rect().is_some() {
+ w.render_popover(&mut popover_pc);
+ }
+ }
+ for (qc, qx, qy, qw, qh) in popover_pc.rects {
verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
}
+
+ if clear_ui::widget::context_menu::is_visible() {
+ for (qx, qy, qw, qh, qc) in clear_ui::widget::context_menu::extra_quads() {
+ verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+ }
+ }
}
verts
}
@@ -381,6 +553,8 @@ impl State {
physical_width,
physical_height,
scale,
+ ref json_layout,
+ layout_mode,
..
} = self;
@@ -389,8 +563,9 @@ impl State {
let scale_f32 = *scale as f32;
- let mut areas: Vec<TextArea> = vec![
- TextArea {
+ let mut areas: Vec<TextArea> = Vec::new();
+ if !*layout_mode {
+ areas.push(TextArea {
buffer: label_buffer,
left: 80.0 * scale_f32,
top: 12.0 * scale_f32,
@@ -403,8 +578,8 @@ impl State {
},
default_color: glyphon::Color::rgb(0xcc, 0xcc, 0xd4),
custom_glyphs: &[],
- },
- TextArea {
+ });
+ areas.push(TextArea {
buffer: status_buffer,
left: 12.0 * scale_f32,
top: *physical_height as f32 - 24.0 * scale_f32,
@@ -417,78 +592,109 @@ impl State {
},
default_color: glyphon::Color::rgb(0x55, 0x55, 0x66),
custom_glyphs: &[],
- },
- ];
+ });
+ }
let mut widget_buffers: Vec<Buffer> = Vec::new();
- let mut widget_labels: Vec<TextLabel> = Vec::new();
- for (i, w) in self.widgets.iter().enumerate() {
- for label in w.text_labels() {
- let mut covered = false;
- for (pi, pw) in self.widgets.iter().enumerate() {
- if pi != i {
- if let Some((px, py, pw_val, ph)) = pw.popover_rect() {
- if label.is_covered_by(px, py, pw_val, ph) {
- covered = true;
- break;
+ let mut widget_labels: Vec<(TextLabel, Option<[f32; 4]>)> = Vec::new();
+ if *layout_mode {
+ if let Some(jl) = json_layout {
+ for (label, font, bounds) in jl.text_labels_with_font_and_bounds() {
+ widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
+ widget_labels.push((label, bounds));
+ }
+ }
+ } else {
+ for (i, w) in self.widgets.iter().enumerate() {
+ for (label, font, bounds) in w.text_labels_with_font_and_bounds() {
+ let mut covered = false;
+ for (pi, pw) in self.widgets.iter().enumerate() {
+ if pi != i {
+ if let Some((px, py, pw_val, ph)) = pw.popover_rect() {
+ if label.is_covered_by(px, py, pw_val, ph) {
+ covered = true;
+ break;
+ }
}
}
}
- }
- if !covered {
- widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
- widget_labels.push(label);
+ if !covered {
+ widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
+ widget_labels.push((label, bounds));
+ }
}
}
- }
- if clear_ui::widget::context_menu::is_visible() {
- for label in clear_ui::widget::context_menu::text_labels() {
- widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
- widget_labels.push(label);
+ if clear_ui::widget::context_menu::is_visible() {
+ for label in clear_ui::widget::context_menu::text_labels() {
+ widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+ widget_labels.push((label, None));
+ }
}
}
- for (buf, label) in widget_buffers.iter().zip(widget_labels.iter()) {
+ for (buf, (label, bounds)) in widget_buffers.iter().zip(widget_labels.iter()) {
+ let item_bounds = if let Some([l, t, r, b]) = bounds {
+ TextBounds {
+ left: (l * scale_f32).round() as i32,
+ top: (t * scale_f32).round() as i32,
+ right: (r * scale_f32).round() as i32,
+ bottom: (b * scale_f32).round() as i32,
+ }
+ } else {
+ TextBounds {
+ left: 0,
+ top: 0,
+ right: *physical_width as i32,
+ bottom: *physical_height as i32,
+ }
+ };
areas.push(TextArea {
buffer: buf,
left: label.x * scale_f32,
top: label.y * scale_f32,
scale: scale_f32,
- bounds: TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- },
+ bounds: item_bounds,
default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
custom_glyphs: &[],
});
}
- // Draw popover texts on top
let mut popover_pc = clear_ui::layout::PopoverCollector::new();
- for w in &self.widgets {
- if w.popover_rect().is_some() {
- w.render_popover(&mut popover_pc);
- }
- }
let mut popover_buffers = Vec::new();
- for (t, size, _x, _y, _tc, _font_opt) in &popover_pc.texts {
- popover_buffers.push(make_text_buffer(font_system, t, *size));
- }
- for (buf, (_, size, x, y, tc, _font_opt)) in popover_buffers.iter().zip(popover_pc.texts.iter()) {
- areas.push(TextArea {
- buffer: buf,
- left: *x * scale_f32,
- top: *y * scale_f32,
- scale: scale_f32,
- bounds: TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- },
+
+ if !*layout_mode {
+ // Draw popover texts on top
+ for w in &self.widgets {
+ if w.popover_rect().is_some() {
+ w.render_popover(&mut popover_pc);
+ }
+ }
+ for (t, size, _x, _y, _tc, _font_opt, _bounds) in &popover_pc.texts {
+ popover_buffers.push(make_text_buffer(font_system, t, *size));
+ }
+ for (buf, (_, size, x, y, tc, _font_opt, bounds)) in popover_buffers.iter().zip(popover_pc.texts.iter()) {
+ let item_bounds = if let Some([l, t, r, b]) = bounds {
+ TextBounds {
+ left: (l * scale_f32).round() as i32,
+ top: (t * scale_f32).round() as i32,
+ right: (r * scale_f32).round() as i32,
+ bottom: (b * scale_f32).round() as i32,
+ }
+ } else {
+ TextBounds {
+ left: 0,
+ top: 0,
+ right: *physical_width as i32,
+ bottom: *physical_height as i32,
+ }
+ };
+ areas.push(TextArea {
+ buffer: buf,
+ left: *x * scale_f32,
+ top: *y * scale_f32,
+ scale: scale_f32,
+ bounds: item_bounds,
default_color: glyphon::Color::rgb(
(tc[0] * 255.0) as u8,
(tc[1] * 255.0) as u8,
@@ -497,6 +703,7 @@ impl State {
custom_glyphs: &[],
});
}
+ }
text_renderer
.prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache)
@@ -512,7 +719,11 @@ impl State {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
- self.positions = demo_positions(self.width, self.height);
+ if self.layout_mode {
+ self.positions = vec![(0.0, 0.0, self.width, self.height)];
+ } else {
+ self.positions = demo_positions(self.width, self.height);
+ }
self.apply_layout();
self.upload_vertices();
}
@@ -642,6 +853,8 @@ struct AppState {
ctrl_pressed: bool,
shift_pressed: bool,
pressed_key: Option<PressedKey>,
+ key_repeat_delay: std::time::Duration,
+ key_repeat_interval: std::time::Duration,
inspector: Option<clear_ui::protocol::zclear_inspector_v1::ZclearInspectorV1>,
}
@@ -801,6 +1014,12 @@ impl PointerHandler for AppState {
if clear_ui::widget::context_menu::cursor_moved(state.cursor_x, state.cursor_y) {
changed = true;
}
+ } else if state.layout_mode {
+ if let Some(jl) = &mut state.json_layout {
+ if jl.cursor_moved(state.cursor_x, state.cursor_y) {
+ changed = true;
+ }
+ }
} else {
if let Some(idx) = state.drag_widget {
if state.widgets[idx].drag_update(state.cursor_x, state.cursor_y) {
@@ -834,6 +1053,12 @@ impl PointerHandler for AppState {
if clear_ui::widget::context_menu::mouse_input(btn, clear_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y) {
changed = true;
}
+ } else if st.layout_mode {
+ if let Some(jl) = &mut st.json_layout {
+ if jl.mouse_input(btn, clear_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y) {
+ changed = true;
+ }
+ }
} else {
let mut clicked_idx = None;
for i in (0..st.widgets.len()).rev() {
@@ -884,10 +1109,60 @@ impl PointerHandler for AppState {
};
if let Some(st) = &mut self.state {
let mut changed = false;
+ let mut should_close = false;
if clear_ui::widget::context_menu::is_visible() {
if clear_ui::widget::context_menu::mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
changed = true;
}
+ } else if st.layout_mode {
+ let mut clicked_btn_id = None;
+ if let Some(jl) = &mut st.json_layout {
+ if jl.mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
+ changed = true;
+ }
+ if btn == clear_ui::widget::MouseButton::Left {
+ for w in &mut jl.widgets {
+ let active_page = jl.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+ if w.page_idx != active_page {
+ continue;
+ }
+ if let Some(btn_w) = &mut w.button {
+ if btn_w.take_click() {
+ clicked_btn_id = Some(w.id.clone());
+ break;
+ }
+ }
+ }
+ }
+ }
+ if let Some(btn_id) = clicked_btn_id {
+ let mut checkboxes = std::collections::HashMap::new();
+ let mut spinboxes = std::collections::HashMap::new();
+ let mut colors = std::collections::HashMap::new();
+ let mut sliders = std::collections::HashMap::new();
+ if let Some(jl) = &st.json_layout {
+ for w in &jl.widgets {
+ if let Some(cb) = &w.checkbox {
+ checkboxes.insert(w.id.clone(), cb.checked());
+ } else if let Some(sb) = &w.spinbox {
+ spinboxes.insert(w.id.clone(), sb.value);
+ } else if let Some(cs) = &w.color_selector {
+ colors.insert(w.id.clone(), cs.color);
+ } else if let Some(sl) = &w.slider {
+ sliders.insert(w.id.clone(), sl.get_scaled_value());
+ }
+ }
+ }
+ let out_val = serde_json::json!({
+ "button": btn_id,
+ "checkboxes": checkboxes,
+ "spinboxes": spinboxes,
+ "colors": colors,
+ "sliders": sliders
+ });
+ println!("{}", out_val.to_string());
+ should_close = true;
+ }
} else {
if btn == clear_ui::widget::MouseButton::Left {
if let Some(idx) = st.drag_widget {
@@ -918,6 +1193,9 @@ impl PointerHandler for AppState {
st.upload_vertices();
self.redraw = true;
}
+ if should_close {
+ self.exit = true;
+ }
}
}
PointerEventKind::Axis { horizontal, vertical, .. } => {
@@ -927,9 +1205,17 @@ impl PointerHandler for AppState {
let delta = clear_ui::widget::MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
let mut changed = false;
- for w in &mut state.widgets {
- if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
- changed = true;
+ if !state.layout_mode {
+ for w in &mut state.widgets {
+ if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
+ changed = true;
+ }
+ }
+ } else {
+ if let Some(jl) = &mut state.json_layout {
+ if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
+ changed = true;
+ }
}
}
if changed {
@@ -1003,6 +1289,25 @@ impl KeyboardHandler for AppState {
self.ctrl_pressed = modifiers.ctrl;
self.shift_pressed = modifiers.shift;
}
+
+ fn update_repeat_info(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ info: smithay_client_toolkit::seat::keyboard::RepeatInfo,
+ ) {
+ match info {
+ smithay_client_toolkit::seat::keyboard::RepeatInfo::Repeat { rate, delay } => {
+ self.key_repeat_delay = std::time::Duration::from_millis(delay as u64);
+ let interval_ms = 1000 / rate.get() as u64;
+ self.key_repeat_interval = std::time::Duration::from_millis(interval_ms);
+ }
+ smithay_client_toolkit::seat::keyboard::RepeatInfo::Disable => {
+ self.key_repeat_delay = std::time::Duration::from_secs(999999);
+ }
+ }
+ }
}
impl AppState {
@@ -1059,7 +1364,15 @@ impl AppState {
}
if let Some(st) = &mut self.state {
- if let Some(idx) = st.focused_widget {
+ if st.layout_mode {
+ if let Some(jl) = &mut st.json_layout {
+ let mut changed = jl.keyboard_input(&custom_event);
+ if changed {
+ st.upload_vertices();
+ self.redraw = true;
+ }
+ }
+ } else if let Some(idx) = st.focused_widget {
let val = st.widgets[idx].value();
let mut changed = st.widgets[idx].keyboard_input(&custom_event);
if st.widgets[idx].value() != val {
@@ -1146,6 +1459,44 @@ delegate_registry!(AppState);
delegate_output!(AppState);
fn main() {
+ let mut layout_mode = false;
+ let mut json_layout_config: Option<JsonLayoutConfig> = None;
+
+ let args = std::env::args().skip(1).collect::<Vec<String>>();
+ let mut idx = 0;
+ while idx < args.len() {
+ let arg = &args[idx];
+ if arg == "--layout" || arg == "--json" {
+ layout_mode = true;
+ idx += 1;
+ } else {
+ idx += 1;
+ }
+ }
+
+ if layout_mode {
+ use std::io::Read;
+ let mut json_str = String::new();
+ let mut stdin = std::io::stdin();
+ match stdin.read_to_string(&mut json_str) {
+ Ok(_) => {
+ match serde_json::from_str::<JsonLayoutConfig>(&json_str) {
+ Ok(cfg) => {
+ json_layout_config = Some(cfg);
+ }
+ Err(e) => {
+ eprintln!("Failed to parse JSON layout: {}", e);
+ std::process::exit(1);
+ }
+ }
+ }
+ Err(e) => {
+ eprintln!("Failed to read JSON layout from stdin: {}", e);
+ std::process::exit(1);
+ }
+ }
+ }
+
let conn = Connection::connect_to_env().unwrap();
let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
let qh = event_queue.handle();
@@ -1175,6 +1526,8 @@ fn main() {
ctrl_pressed: false,
shift_pressed: false,
pressed_key: None,
+ key_repeat_delay: std::time::Duration::from_millis(500),
+ key_repeat_interval: std::time::Duration::from_millis(50),
inspector,
};
@@ -1186,13 +1539,23 @@ fn main() {
let surface = app.compositor_state.create_surface(&qh);
surface.set_buffer_scale(scale as i32);
- let pw = (1024.0 * scale) as u32;
- let ph = (768.0 * scale) as u32;
+ let mut win_w = 1024.0;
+ let mut win_h = 768.0;
+ if let Some(ref cfg) = json_layout_config {
+ if let Some(w) = cfg.width {
+ win_w = w as f64;
+ }
+ if let Some(h) = cfg.height {
+ win_h = h as f64;
+ }
+ }
+ let pw = (win_w * scale) as u32;
+ let ph = (win_h * scale) as u32;
let window = app.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
window.set_title("Clear UI - Test Window");
window.set_app_id("clear-ui");
- window.set_min_size(Some((pw, ph)));
+ window.set_min_size(Some((win_w as u32, win_h as u32)));
window.commit();
if let Some(ref inspector) = app.inspector {
@@ -1204,7 +1567,7 @@ fn main() {
surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
}));
- let state = pollster::block_on(State::new(wayland_handle, pw, ph, scale));
+ let state = pollster::block_on(State::new(wayland_handle, pw, ph, scale, json_layout_config));
app.window = Some(window);
app.surface = Some(surface);
@@ -1214,8 +1577,9 @@ fn main() {
let loop_handle = event_loop.handle();
WaylandSource::new(conn, event_queue).insert(loop_handle).unwrap();
- const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
- const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
+
+
+ let mut last_tick = std::time::Instant::now();
loop {
event_loop
@@ -1225,10 +1589,35 @@ fn main() {
break;
}
+ let now = std::time::Instant::now();
+ let dt = now.duration_since(last_tick).as_secs_f32();
+ last_tick = now;
+
+ if let Some(ref mut st) = app.state {
+ let mut tick_changed = false;
+ if st.layout_mode {
+ if let Some(ref mut jl) = &mut st.json_layout {
+ if jl.tick(dt) {
+ tick_changed = true;
+ }
+ }
+ } else {
+ for w in &mut st.widgets {
+ if w.tick(dt) {
+ tick_changed = true;
+ }
+ }
+ }
+ if tick_changed {
+ st.upload_vertices();
+ app.redraw = true;
+ }
+ }
+
if let Some(ref mut pk) = app.pressed_key {
let now = std::time::Instant::now();
- if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
- if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
+ if now.duration_since(pk.first_pressed) >= app.key_repeat_delay {
+ if now.duration_since(pk.last_repeated) >= app.key_repeat_interval {
pk.last_repeated = now;
let custom_event = clear_ui::widget::KeyEvent {
state: clear_ui::widget::ElementState::Pressed,
@@ -1239,7 +1628,15 @@ fn main() {
shift: app.shift_pressed,
};
if let Some(st) = &mut app.state {
- if let Some(idx) = st.focused_widget {
+ if st.layout_mode {
+ if let Some(jl) = &mut st.json_layout {
+ let mut changed = jl.keyboard_input(&custom_event);
+ if changed {
+ st.upload_vertices();
+ app.redraw = true;
+ }
+ }
+ } else if let Some(idx) = st.focused_widget {
let val = st.widgets[idx].value();
let mut changed = st.widgets[idx].keyboard_input(&custom_event);
if st.widgets[idx].value() != val {
diff --git a/src/scale.rs b/src/scale.rs
new file mode 100644
index 0000000..5c4b475
--- /dev/null
+++ b/src/scale.rs
@@ -0,0 +1,13 @@
+use std::sync::RwLock;
+
+static SCALE_FACTOR: RwLock<f32> = RwLock::new(1.0);
+
+pub fn scale_factor() -> f32 {
+ *SCALE_FACTOR.read().unwrap()
+}
+
+pub fn set_scale_factor(scale: f32) {
+ if let Ok(mut lock) = SCALE_FACTOR.write() {
+ *lock = scale;
+ }
+}
diff --git a/src/shader.wgsl b/src/shader.wgsl
index d9d4dd0..bc75fcc 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -48,20 +48,5 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let fade = smoothstep(0.0, 1.0, min_dist / feather);
final_color.a = final_color.a * fade;
}
- if (abs(in.color.a - 0.699) < 0.001) {
- // Frosted glass effect with a smooth blur simulation (no high-frequency noise/grain)
- // We use low-frequency wave combinations to create a soft, smooth organic glow/sheen
- let wave1 = sin(in.clip_position.x * 0.02) * 0.02;
- let wave2 = cos(in.clip_position.y * 0.02) * 0.02;
- let sheen = sin((in.clip_position.x + in.clip_position.y) * 0.008) * 0.03;
- let glow = wave1 + wave2 + sheen;
-
- final_color = vec4f(
- clamp(in.color.r + glow, 0.0, 1.0),
- clamp(in.color.g + glow, 0.0, 1.0),
- clamp(in.color.b + glow, 0.0, 1.0),
- 0.699
- );
- }
return final_color;
}
diff --git a/src/widget.rs b/src/widget.rs
index b1eaaf1..a99d97f 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -1074,6 +1074,34 @@ pub trait Widget {
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)]
@@ -2588,6 +2616,16 @@ impl Widget for MenuBar {
}
}
+ 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() {
@@ -2861,6 +2899,14 @@ impl Widget for MenuBar {
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 {
@@ -5509,6 +5555,10 @@ 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] {
@@ -5722,6 +5772,7 @@ impl Drop for ColorSelector {
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,
@@ -6234,6 +6285,7 @@ pub struct ScrollBox {
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)>,
}
@@ -6247,6 +6299,7 @@ impl ScrollBox {
viewport_y: 0.0,
viewport_h: 0.0,
hovered: false,
+ show_border: true,
parent: None,
children: Vec::new(),
}
@@ -7070,6 +7123,7 @@ pub struct TextItem {
pub x: f32,
pub y: f32,
pub color: glyphon::Color,
+ pub bounds: Option<[f32; 4]>,
}
// Styled label builder with optional strikethrough
@@ -7127,6 +7181,7 @@ impl StyledLabel {
x,
y,
color: self.g_color,
+ bounds: None,
});
w
}
@@ -7251,6 +7306,14 @@ impl Widget for ScrollingList {
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 {}
@@ -8546,239 +8609,863 @@ pub fn get_font_db() -> &'static resvg::usvg::fontdb::Database {
})
}
-pub struct Paginator {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- sidebar_w: f32,
- pages: Vec<String>,
- selected_page: usize,
- hovered_tab: Option<usize>,
- pressed_tab: Option<usize>,
- page_changed: bool,
- current_y: Option<f32>,
- target_y: f32,
- pub tabs_at_top: bool,
- pub tab_y_offset: f32,
- pub tabs_rotated: bool,
- current_x: Option<f32>,
- target_x: f32,
- parent: Option<*mut (dyn Widget + 'static)>,
- children: Vec<*mut (dyn Widget + 'static)>,
- pub page_widgets: Vec<Vec<*mut (dyn Widget + 'static)>>,
- pub tab_text_quads: Vec<Vec<(f32, f32, f32, f32, [f32; 4])>>,
- pub sidebar_scroll_y: f32,
+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 Paginator {
- pub fn new(sidebar_w: f32, pages: Vec<String>) -> Self {
- let num_pages = pages.len();
+impl Plate {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
- x: 0.0, y: 0.0, w: 0.0, h: 0.0,
- hovered: false,
- sidebar_w,
- pages,
- selected_page: 0,
- hovered_tab: None,
- pressed_tab: None,
- page_changed: false,
- current_y: Some(10.0),
- target_y: 10.0,
- tabs_at_top: false,
- tab_y_offset: 10.0,
- tabs_rotated: true,
- current_x: Some(0.0),
- target_x: 0.0,
- parent: None,
+ 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(),
- page_widgets: vec![Vec::new(); num_pages],
- tab_text_quads: Vec::new(),
- sidebar_scroll_y: 0.0,
+ parent: None,
+ visible: true,
+ column_layout: false,
}
}
- pub fn add_widget_to_page(&mut self, page_idx: usize, widget: *mut (dyn Widget + 'static)) {
- if page_idx < self.page_widgets.len() {
- self.page_widgets[page_idx].push(widget);
- self.add_child(widget);
- }
+ pub fn with_color(mut self, color: [f32; 4]) -> Self {
+ self.color = Some(color);
+ self
}
- pub fn clear_page_widgets(&mut self, page_idx: usize) {
- if page_idx < self.page_widgets.len() {
- for widget_ptr in &self.page_widgets[page_idx] {
- self.children.retain(|&c| !std::ptr::addr_eq(c, *widget_ptr));
- unsafe {
- (&mut **widget_ptr).set_parent(None);
- }
- }
- self.page_widgets[page_idx].clear();
- }
+ pub fn with_label(mut self, label: &str) -> Self {
+ self.base.label = Some(label.to_string());
+ self
}
- pub fn with_tabs_at_top(mut self, _top: bool) -> Self {
- self.tabs_at_top = false;
- self.update_target_pos();
+ pub fn with_blur(mut self, blur: bool) -> Self {
+ self.blur = blur;
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);
+ 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);
+ }
}
- self.update_target_pos();
- self
}
- pub fn with_tabs_rotated(mut self, _rotated: bool) -> Self {
- self.tabs_rotated = true;
- self.update_target_pos();
- self
+ fn visible(&self) -> bool {
+ self.visible
}
- pub fn vertical_tab_size(&self) -> (f32, f32) {
- if self.tabs_rotated {
- ((self.sidebar_w - 16.0).clamp(24.0, 120.0), 120.0)
+ fn color(&self) -> [f32; 4] {
+ let mut c = if let Some(c) = self.color {
+ c
+ } else if self.dragging {
+ colors::PANEL_DRAG
} else {
- (self.sidebar_w - 10.0, 40.0)
+ colors::PANEL_IDLE
+ };
+ c[3] *= self.network_opacity;
+ if self.blur {
+ c[3] = -c[3].abs();
}
+ c
}
- 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 set_network_opacity(&mut self, opacity: f32) {
+ self.network_opacity = opacity;
}
- 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;
+ fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
+ self.bounds = Some((bx, by, bw, bh));
+ }
- 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]);
+ fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
+ self.curved_circle = circle;
+ }
- 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
- };
+ 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
+ }
- let w_px = tab_w as u32;
- let h_px = if has_icon { 80 } else { 120 };
+ 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 w_px == 0 || h_px == 0 {
- self.tab_text_quads.push(Vec::new());
- continue;
- }
+ if !self.visible {
+ return;
+ }
- // Generate SVG string for the rotated text
- 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 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);
- // Render SVG using resvg and tiny_skia
- 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],
- ));
- }
- }
- }
- }
- }
- }
- if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/paginator_debug.txt") {
- use std::io::Write;
- let _ = writeln!(file, "Page {}: label='{}', w_px={}, h_px={}, quads_len={}", i, label_text, w_px, h_px, page_quads.len());
+ 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);
}
- self.tab_text_quads.push(page_quads);
}
- }
- 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);
+ 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 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);
+ 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;
+ }
+ }
+ }
}
}
- self.generate_tab_quads();
}
- pub fn selected_page(&self) -> usize {
- self.selected_page
+ 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.pages.len() {
+ 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 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;
@@ -8786,57 +9473,38 @@ impl Widget for Paginator {
self.h = h;
self.update_target_pos();
- // Perform relative wrapping layout for all page widgets
- let padding_x = 20.0;
- let gap_y = 16.0;
- let gap_x = 20.0;
+ 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 (left_x, available_w, start_y) = if self.tabs_at_top {
- (self.x + padding_x, self.w - 2.0 * padding_x, self.y + 40.0 + 20.0)
+ 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 {
- (self.x + self.sidebar_w + padding_x, self.w - self.sidebar_w - 2.0 * padding_x, self.y + 20.0)
- };
+ 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);
+ }
+ }
+ }
- for page_idx in 0..self.page_widgets.len() {
- let mut cur_y = start_y;
- let widgets = &self.page_widgets[page_idx];
- let mut i = 0;
- while i < widgets.len() {
- let w1_ptr = widgets[i];
- let w1 = unsafe { &mut *w1_ptr };
- let (_, _, w1_w, w1_h) = w1.rect();
-
- if i + 1 < widgets.len() {
- let w2_ptr = widgets[i + 1];
- let w2 = unsafe { &mut *w2_ptr };
- let (_, _, w2_w, w2_h) = w2.rect();
-
- if w1_w > 0.0 && w2_w > 0.0 && w1_w + w2_w + gap_x <= available_w {
- let top_room = w1.top_room().max(w2.top_room());
- let widget_y = cur_y + top_room;
-
- w1.set_rect(left_x, widget_y, w1_w, w1_h);
- w2.set_rect(left_x + w1_w + gap_x, widget_y, w2_w, w2_h);
-
- cur_y = widget_y + w1_h.max(w2_h) + gap_y;
- i += 2;
- continue;
- }
- }
+ fn color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
- let top_room = w1.top_room();
- let widget_y = cur_y + top_room;
- let use_w = if w1_w > 0.0 { w1_w.min(available_w) } else { available_w };
+ fn set_hovered(&mut self, v: bool) {
+ self.hovered = v;
+ }
- w1.set_rect(left_x, widget_y, use_w, w1_h);
- cur_y = widget_y + w1_h + gap_y;
- i += 1;
- }
- }
+ fn hovered(&self) -> bool {
+ self.hovered
}
- 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 {
@@ -8871,7 +9539,6 @@ impl Widget for Paginator {
let qy = by + scroll_offset;
let qh = bh;
- // Clip to paginator Y bounds
let min_y = self.y;
let max_y = self.y + self.h;
let ry1 = qy.max(min_y);
@@ -8890,145 +9557,9 @@ impl Widget for Paginator {
fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
let mut quads = Vec::new();
- if self.tabs_at_top {
- // Draw top tab bar background
- quads.push((self.x, self.y, self.w, 40.0, colors::sidebar_bg_color()));
- // Draw main page background
- quads.push((self.x, self.y + 40.0, self.w, self.h - 40.0, colors::page_low_color()));
- // Divider line below tab bar
- quads.push((self.x, self.y + 40.0 - 1.0, self.w, 1.0, [0.20, 0.20, 0.25, 0.6]));
-
- // Sliding active tab indicator
- if let Some(cx) = self.current_x {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- let bx = self.x + cx + 4.0;
- let by = self.y + 4.0;
- let bw = tab_w - 8.0;
- let bh = 40.0 - 8.0;
-
- // Sliding capsule glow
- let steps = 12;
- let start_alpha = 0.01;
- let c = 0.0006;
- 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]));
- }
- }
- }
-
- // Tab button press/hover highlight overlays
- for i in 0..self.pages.len() {
- 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 + 4.0;
- let by = self.y + 4.0;
- let bw = tab_w - 8.0;
- let bh = 40.0 - 8.0;
-
- 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]
- } else {
- [0.0, 0.0, 0.0, 0.0]
- };
-
- if c[3] > 0.0 {
- quads.push((bx, by, bw, bh, c));
- }
- }
- } else {
- // Vertical layout (old)
- quads.push((self.x, self.y, self.sidebar_w, self.h, colors::sidebar_bg_color()));
- quads.push((self.x + self.sidebar_w, self.y, self.w - self.sidebar_w, self.h, colors::page_low_color()));
-
- let min_y = self.y;
- let max_y = self.y + self.h;
- let mut push_sidebar_quad = |qx: f32, qy: f32, qw: f32, qh: f32, qc: [f32; 4], q: &mut Vec<(f32, f32, f32, f32, [f32; 4])>| {
- let ry1 = qy.max(min_y);
- let ry2 = (qy + qh).min(max_y);
- let rh = ry2 - ry1;
- if rh > 0.0 {
- q.push((qx, ry1, qw, rh, qc));
- }
- };
-
- if let Some(cy) = self.current_y {
- let (bx, by, bw, bh) = if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + cy - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (
- self.x + 5.0,
- self.y + cy - self.sidebar_scroll_y,
- bw,
- 40.0,
- )
- };
-
- 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;
- push_sidebar_quad(rx, ry, rw, rh, [0.20, 0.40, 0.65, step_alpha], &mut quads);
- }
- }
- }
-
- for i in 0..self.pages.len() {
- 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 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]
- } else {
- [0.0, 0.0, 0.0, 0.0]
- };
-
- if c[3] > 0.0 {
- push_sidebar_quad(bx, by, bw, bh, c, &mut quads);
- }
- }
- }
-
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;
@@ -9047,7 +9578,6 @@ impl Widget for Paginator {
let absolute_x = bx + qx;
let absolute_y = by + y_offset + qy;
- // Clip vertical drawing bounds to paginator height
let ry1 = absolute_y.max(min_y);
let ry2 = (absolute_y + qh).min(max_y);
let rh = ry2 - ry1;
@@ -9057,60 +9587,36 @@ impl Widget for Paginator {
}
}
}
+ } else {
+ quads.extend(self.sidebar_menu.extra_quads());
}
- // Delegate rendering to active page widgets
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &*widget_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());
- }
+ 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();
- 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 font_size = 12.0;
- let est_w = TextLabel::estimate_width(page_name, font_size);
- let color = if self.selected_page == i {
- [active_r, active_g, active_b]
- } else {
- [inactive_r, inactive_g, inactive_b]
- };
-
- 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;
- let by = self.y;
- let bw = tab_w;
- let bh = 40.0;
+ 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;
- labels.push(TextLabel {
- text: page_name.clone(),
- x: bx + (bw - est_w) / 2.0,
- y: by + (bh - font_size) / 2.0 - 1.0,
- font_size,
- color,
- });
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
+ 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;
@@ -9137,33 +9643,21 @@ impl Widget for Paginator {
}
}
}
-
- // Rotated tab text is rendered as quads in generate_tab_quads and extra_quads
- } else {
- let bx = self.x + 5.0;
- let by = self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y;
- let bw = self.sidebar_w - 10.0;
- let bh = 40.0;
-
- let text_y = by + (bh - font_size) / 2.0 - 1.0;
- if text_y >= self.y && text_y + font_size <= self.y + self.h {
- labels.push(TextLabel {
- text: page_name.clone(),
- x: bx + (bw - est_w) / 2.0,
- y: text_y,
- font_size,
- color,
- });
- }
}
+ } else {
+ labels.extend(self.sidebar_menu.text_labels());
}
- // Delegate labels to active page widgets
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &*widget_ptr };
- labels.extend(widget.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
}
@@ -9198,17 +9692,12 @@ impl Widget for Paginator {
changed = true;
}
- // Delegate to active page widgets
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- 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;
- }
+ 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;
}
}
@@ -9216,19 +9705,14 @@ impl Widget for Paginator {
}
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- // First check popover clicks for active page widgets
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &mut *widget_ptr };
- if widget.popover_rect().is_some() {
- if widget.mouse_input(button, state, px, py) {
- return true;
- }
+ 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;
}
}
}
- // Check if click is on paginator tabs
let mut clicked_tab = false;
if button == MouseButton::Left {
match state {
@@ -9278,16 +9762,8 @@ impl Widget for Paginator {
};
if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
if self.selected_page != i {
- // Unfocus all widgets on the previously selected page
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &mut *widget_ptr };
- widget.unfocus();
- }
- }
- self.selected_page = i;
+ self.set_selected_page(i);
self.page_changed = true;
- self.update_target_pos();
}
clicked_tab = true;
}
@@ -9300,16 +9776,13 @@ impl Widget for Paginator {
return true;
}
- // Delegate mouse input to widgets on active page
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- 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 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;
}
}
@@ -9317,12 +9790,12 @@ impl Widget for Paginator {
}
fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &mut *widget_ptr };
- if widget.keyboard_input(event) {
- return true;
- }
+ 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
@@ -9334,10 +9807,6 @@ impl Widget for Paginator {
let by = self.y;
let bw = self.sidebar_w;
let bh = self.h;
- if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/clear-scroll-debug.txt") {
- use std::io::Write;
- let _ = writeln!(file, "mouse_wheel check: px={}, py={}, bx={}, by={}, bw={}, bh={}, hover={}", px, py, bx, by, bw, bh, px >= bx && px <= bx + bw && py >= by && py <= by + bh);
- }
if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
let scroll_speed = 24.0;
let dy = match delta {
@@ -9346,10 +9815,6 @@ impl Widget for Paginator {
};
let old_scroll = self.sidebar_scroll_y;
let max_scroll = (self.total_sidebar_height() - self.h).max(0.0);
- if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/clear-scroll-debug.txt") {
- use std::io::Write;
- let _ = writeln!(file, "mouse_wheel action: dy={}, old_scroll={}, total_h={}, h={}, max_scroll={}", dy, old_scroll, self.total_sidebar_height(), self.h, max_scroll);
- }
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();
@@ -9358,35 +9823,34 @@ impl Widget for Paginator {
}
}
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &mut *widget_ptr };
- if widget.mouse_wheel(delta, px, py) {
- 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 self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &*widget_ptr };
- if let Some(r) = widget.popover_rect() {
- return Some(r);
- }
+ 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) {
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &*widget_ptr };
- widget.render_popover(pc);
- }
+ self.sidebar_menu.render_popover(pc);
+ if self.selected_page < self.plates.len() {
+ self.plates[self.selected_page].render_popover(pc);
}
}
@@ -9399,7 +9863,9 @@ impl Widget for Paginator {
}
}
- fn value(&self) -> i32 { self.selected_page as i32 }
+ fn value(&self) -> i32 {
+ self.selected_page as i32
+ }
fn tick(&mut self, dt: f32) -> bool {
let mut changed = false;
@@ -9426,24 +9892,91 @@ impl Widget for Paginator {
}
}
- // Delegate tick to active page widgets
- if self.selected_page < self.page_widgets.len() {
- for &widget_ptr in &self.page_widgets[self.selected_page] {
- let widget = unsafe { &mut *widget_ptr };
- if widget.tick(dt) {
- changed = true;
- }
+ 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)> { 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 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 {}
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index afbafcc..dfc790a 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -59,6 +59,8 @@ pub struct JsonLayoutWidget {
pub widgets: Vec<JsonWidget>,
pub paginator: Option<Paginator>,
pub dragging_slider_idx: Option<usize>,
+ pub page_scroll_y: Vec<f32>,
+ pub page_total_heights: Vec<f32>,
}
impl JsonLayoutWidget {
@@ -230,6 +232,8 @@ impl JsonLayoutWidget {
widgets,
paginator,
dragging_slider_idx: None,
+ page_scroll_y: vec![0.0; 16],
+ page_total_heights: vec![0.0; 16],
}
}
@@ -269,7 +273,8 @@ impl JsonLayoutWidget {
top_room = sl.top_room();
}
- w_state.y = by + *current_y + top_room;
+ let scroll_offset = self.page_scroll_y.get(p_idx).cloned().unwrap_or(0.0);
+ w_state.y = by + *current_y + top_room - scroll_offset;
w_state.w = usable_w;
if let Some(cb) = &mut w_state.checkbox {
@@ -301,6 +306,13 @@ impl JsonLayoutWidget {
*current_y += top_room + w_state.h + spacing;
}
+
+ // Store total height of each page (adding a little padding at the end)
+ for (i, &height) in page_current_y.iter().enumerate() {
+ if i < self.page_total_heights.len() {
+ self.page_total_heights[i] = height + 4.0;
+ }
+ }
}
}
@@ -354,35 +366,66 @@ impl Widget for JsonLayoutWidget {
}
let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+ let (bx, by, bw, bh) = self.rect();
+ let has_paginator = self.paginator.is_some();
+ let pad_x = if has_paginator { 76.0 } else { 16.0 };
+ let min_x = bx + pad_x - 4.0;
+ let max_x = bx + bw;
+ let min_y = by;
+ let max_y = by + bh;
+
+ 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));
+ }
+ };
for w in &self.widgets {
if w.page_idx != active_page {
continue;
}
if let Some(cb) = &w.checkbox {
- quads.push((cb.rect().0, cb.rect().1, cb.rect().2, cb.rect().3, cb.color()));
- quads.extend(cb.extra_quads());
+ push_clipped(cb.rect().0, cb.rect().1, cb.rect().2, cb.rect().3, cb.color(), &mut quads);
+ for q in cb.extra_quads() {
+ push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+ }
if let Some(hq) = cb.highlight_quad() {
- quads.push(hq);
+ push_clipped(hq.0, hq.1, hq.2, hq.3, hq.4, &mut quads);
}
} else if let Some(btn) = &w.button {
- quads.push((btn.rect().0, btn.rect().1, btn.rect().2, btn.rect().3, btn.color()));
- quads.extend(btn.extra_quads());
+ push_clipped(btn.rect().0, btn.rect().1, btn.rect().2, btn.rect().3, btn.color(), &mut quads);
+ for q in btn.extra_quads() {
+ push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+ }
if let Some(hq) = btn.highlight_quad() {
- quads.push(hq);
+ push_clipped(hq.0, hq.1, hq.2, hq.3, hq.4, &mut quads);
}
} else if let Some(lbl) = &w.label {
- quads.push((lbl.rect().0, lbl.rect().1, lbl.rect().2, lbl.rect().3, lbl.color()));
- quads.extend(lbl.extra_quads());
+ push_clipped(lbl.rect().0, lbl.rect().1, lbl.rect().2, lbl.rect().3, lbl.color(), &mut quads);
+ for q in lbl.extra_quads() {
+ push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+ }
} else if let Some(sb) = &w.spinbox {
- quads.push((sb.rect().0, sb.rect().1, sb.rect().2, sb.rect().3, sb.color()));
- quads.extend(sb.extra_quads());
+ push_clipped(sb.rect().0, sb.rect().1, sb.rect().2, sb.rect().3, sb.color(), &mut quads);
+ for q in sb.extra_quads() {
+ push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+ }
} else if let Some(cs) = &w.color_selector {
- quads.push((cs.rect().0, cs.rect().1, cs.rect().2, cs.rect().3, cs.color()));
- quads.extend(cs.extra_quads());
+ push_clipped(cs.rect().0, cs.rect().1, cs.rect().2, cs.rect().3, cs.color(), &mut quads);
+ for q in cs.extra_quads() {
+ push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+ }
} else if let Some(sl) = &w.slider {
- quads.push((sl.rect().0, sl.rect().1, sl.rect().2, sl.rect().3, sl.color()));
- quads.extend(sl.extra_quads());
+ push_clipped(sl.rect().0, sl.rect().1, sl.rect().2, sl.rect().3, sl.color(), &mut quads);
+ for q in sl.extra_quads() {
+ push_clipped(q.0, q.1, q.2, q.3, q.4, &mut quads);
+ }
}
}
quads
@@ -419,6 +462,50 @@ impl Widget for JsonLayoutWidget {
labels
}
+ fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
+ let mut labels = Vec::new();
+ let (bx, by, bw, bh) = self.rect();
+ if let Some(paginator) = &self.paginator {
+ for l in paginator.text_labels() {
+ labels.push((l, None));
+ }
+ }
+
+ let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+ let has_paginator = self.paginator.is_some();
+ let pad_x = if has_paginator { 76.0 } else { 16.0 };
+ let content_bounds = Some([bx + pad_x - 4.0, by, bx + bw, by + bh]);
+
+ for w in &self.widgets {
+ if w.page_idx != active_page {
+ continue;
+ }
+ let w_labels = if let Some(_cb) = &w.checkbox {
+ if let Some(tl) = &w.label_text {
+ vec![tl.clone()]
+ } else {
+ Vec::new()
+ }
+ } else if let Some(btn) = &w.button {
+ btn.text_labels()
+ } else if let Some(lbl) = &w.label {
+ lbl.text_labels()
+ } else if let Some(sb) = &w.spinbox {
+ sb.text_labels()
+ } else if let Some(cs) = &w.color_selector {
+ cs.text_labels()
+ } else if let Some(sl) = &w.slider {
+ sl.text_labels()
+ } else {
+ Vec::new()
+ };
+ for l in w_labels {
+ labels.push((l, content_bounds));
+ }
+ }
+ labels
+ }
+
fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
let mut changed = false;
if let Some(paginator) = &mut self.paginator {
@@ -572,4 +659,29 @@ impl Widget for JsonLayoutWidget {
}
false
}
+
+ fn mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, px: f32, py: f32) -> bool {
+ let (bx, by, bw, bh) = self.rect();
+ if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
+ let active_page = self.paginator.as_ref().map(|p| p.selected_page()).unwrap_or(0);
+ if active_page < self.page_total_heights.len() {
+ let total_height = self.page_total_heights[active_page];
+ let visible_h = bh;
+ let max_scroll_y = (total_height - visible_h).max(0.0);
+ if max_scroll_y > 0.0 {
+ let scroll_amount = match delta {
+ crate::widget::MouseScrollDelta::LineDelta(_x, y) => *y * 24.0,
+ crate::widget::MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
+ };
+ let old_scroll = self.page_scroll_y[active_page];
+ self.page_scroll_y[active_page] = (old_scroll + scroll_amount).clamp(0.0, max_scroll_y);
+ if (self.page_scroll_y[active_page] - old_scroll).abs() > 0.01 {
+ self.layout_children();
+ return true;
+ }
+ }
+ }
+ }
+ false
+ }
}