GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Refactor and adapt widgets to use UiContext and scale factor
examples/test_svg_render.rs | 21 +-
src/backend/mod.rs | 8 +
src/backend/wgpu_adapter.rs | 121 +
src/backend/window_runner.rs | 1900 ++++++++++++
src/context.rs | 372 +++
src/engine.rs | 1900 +-----------
src/layout.rs | 1641 +++++++----
src/lib.rs | 3 +
src/main.rs | 111 +-
src/shader.wgsl | 13 +-
src/widget/container.rs | 5076 --------------------------------
src/widget/container/breadcrumb.rs | 115 +
src/widget/container/container.rs | 36 +
src/widget/container/content_bg.rs | 236 ++
src/widget/container/header.rs | 19 +
src/widget/container/menu.rs | 1297 ++++++++
src/widget/container/mod.rs | 25 +
src/widget/container/paginator.rs | 1002 +++++++
src/widget/container/parameters_bg.rs | 1028 +++++++
src/widget/container/plate.rs | 527 ++++
src/widget/container/scroll_box.rs | 248 ++
src/widget/container/scrolling_list.rs | 117 +
src/widget/container/spreadsheet.rs | 397 +++
src/widget/container/viewport_bg.rs | 20 +
src/widget/core.rs | 54 +-
src/widget/display.rs | 2357 ---------------
src/widget/display/float3.rs | 309 ++
src/widget/display/font_preview.rs | 91 +
src/widget/display/graph.rs | 499 ++++
src/widget/display/info_box.rs | 71 +
src/widget/display/label.rs | 172 ++
src/widget/display/layout_preview.rs | 277 ++
src/widget/display/list_item.rs | 137 +
src/widget/display/mod.rs | 40 +
src/widget/display/node.rs | 189 ++
src/widget/display/panel.rs | 89 +
src/widget/display/progress_bar.rs | 28 +
src/widget/display/separator.rs | 33 +
src/widget/display/serialize.rs | 76 +
src/widget/display/sidebar.rs | 19 +
src/widget/display/splitter.rs | 68 +
src/widget/display/status_bar.rs | 106 +
src/widget/display/status_dot.rs | 42 +
src/widget/display/svg.rs | 90 +
src/widget/display/text_label.rs | 91 +
src/widget/display/usage_bar.rs | 44 +
src/widget/editor.rs | 216 ++
src/widget/input.rs | 3076 -------------------
src/widget/input/button.rs | 179 ++
src/widget/input/canvas.rs | 19 +
src/widget/input/checkbox.rs | 224 ++
src/widget/input/color_selector.rs | 542 ++++
src/widget/input/dropdown.rs | 376 +++
src/widget/input/font_selector.rs | 143 +
src/widget/input/mod.rs | 24 +
src/widget/input/slider.rs | 638 ++++
src/widget/input/spinbox.rs | 334 +++
src/widget/input/text_box.rs | 1030 +++++++
src/widget/input/trackpad.rs | 164 ++
src/widget/json_layout.rs | 459 ++-
src/widget/mod.rs | 277 +-
61 files changed, 15498 insertions(+), 13318 deletions(-)
diff --git a/examples/test_svg_render.rs b/examples/test_svg_render.rs
index e8012b2..5ddc751 100644
--- a/examples/test_svg_render.rs
+++ b/examples/test_svg_render.rs
@@ -1,25 +1,32 @@
fn main() {
let opt = resvg::usvg::Options::default();
- let mut fontdb = resvg::usvg::fontdb::Database::new();
- fontdb.load_system_fonts();
+ let fontdb = clear_ui::widget::get_font_db();
// 1. Original 32x120
let svg_32_120 = r##"<svg width="32" height="120" xmlns="http://www.w3.org/2000/svg">
- <text x="16" y="60" font-family="sans-serif" font-size="12" fill="#E6E6F2" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 16 60)">Audio</text>
+ <text x="16" y="60" font-family="Berkeley Mono" font-size="12" fill="#E6E6F2" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 16 60)">Audio</text>
</svg>"##;
- let tree1 = resvg::usvg::Tree::from_data(svg_32_120.as_bytes(), &opt, &fontdb).unwrap();
+ let tree1 = resvg::usvg::Tree::from_data(svg_32_120.as_bytes(), &opt, fontdb).unwrap();
let mut pixmap1 = resvg::tiny_skia::Pixmap::new(32, 120).unwrap();
resvg::render(&tree1, resvg::tiny_skia::Transform::default(), &mut pixmap1.as_mut());
pixmap1.save_png("/home/lsgalante/Dropbox/Clear/scratch/test_32_120.png").unwrap();
// 2. Square 120x120
let svg_120_120 = r##"<svg width="120" height="120" xmlns="http://www.w3.org/2000/svg">
- <text x="60" y="60" font-family="sans-serif" font-size="12" fill="#E6E6F2" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 60 60)">Audio</text>
+ <text x="60" y="60" font-family="Berkeley Mono" font-size="12" fill="#E6E6F2" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 60 60)">Audio</text>
</svg>"##;
- let tree2 = resvg::usvg::Tree::from_data(svg_120_120.as_bytes(), &opt, &fontdb).unwrap();
+ let tree2 = resvg::usvg::Tree::from_data(svg_120_120.as_bytes(), &opt, fontdb).unwrap();
let mut pixmap2 = resvg::tiny_skia::Pixmap::new(120, 120).unwrap();
resvg::render(&tree2, resvg::tiny_skia::Transform::default(), &mut pixmap2.as_mut());
pixmap2.save_png("/home/lsgalante/Dropbox/Clear/scratch/test_120_120.png").unwrap();
- println!("Both images rendered successfully.");
+ // Count non-transparent pixels
+ let mut count = 0;
+ for &alpha in pixmap1.data().iter().skip(3).step_by(4) {
+ if alpha > 0 {
+ count += 1;
+ }
+ }
+ println!("test_32_120.png has {} non-transparent pixels.", count);
}
+
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
new file mode 100644
index 0000000..d15d26d
--- /dev/null
+++ b/src/backend/mod.rs
@@ -0,0 +1,8 @@
+pub mod wgpu_adapter;
+pub mod window_runner;
+
+pub use wgpu_adapter::WgpuAdapter;
+pub use window_runner::{
+ EngineState, WindowSettings, LogicalPosition, LogicalSize, Application, run,
+ Vertex, LineCap, ActivePopup, PressedKey,
+};
diff --git a/src/backend/wgpu_adapter.rs b/src/backend/wgpu_adapter.rs
new file mode 100644
index 0000000..fd16a54
--- /dev/null
+++ b/src/backend/wgpu_adapter.rs
@@ -0,0 +1,121 @@
+use crate::wayland::WaylandSurfaceHandle;
+use glyphon::{Cache, FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer, Viewport};
+
+pub struct WgpuAdapter {
+ pub instance: wgpu::Instance,
+ pub surface: wgpu::Surface<'static>,
+ pub adapter: wgpu::Adapter,
+ pub device: wgpu::Device,
+ pub queue: wgpu::Queue,
+ pub config: wgpu::SurfaceConfiguration,
+ pub font_system: FontSystem,
+ pub swash_cache: SwashCache,
+ pub text_atlas: TextAtlas,
+ pub text_renderer: TextRenderer,
+ pub text_viewport: Viewport,
+}
+
+impl WgpuAdapter {
+ pub async fn new(
+ display_ptr: *mut std::ffi::c_void,
+ surface_ptr: *mut std::ffi::c_void,
+ width: u32,
+ height: u32,
+ ) -> Self {
+ let wayland_handle = Box::leak(Box::new(WaylandSurfaceHandle {
+ display_ptr,
+ surface_ptr,
+ }));
+
+ let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
+ backends: wgpu::Backends::VULKAN,
+ ..Default::default()
+ });
+
+ let surface = instance
+ .create_surface(wayland_handle)
+ .expect("Failed to create surface");
+
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: wgpu::PowerPreference::LowPower,
+ compatible_surface: Some(&surface),
+ force_fallback_adapter: false,
+ })
+ .await
+ .expect("Failed to find adapter");
+
+ let (device, queue) = adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ label: Some("GPU Device"),
+ required_features: wgpu::Features::empty(),
+ required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
+ memory_hints: wgpu::MemoryHints::MemoryUsage,
+ },
+ None,
+ )
+ .await
+ .expect("Failed to create device");
+
+ let mut config = surface
+ .get_default_config(&adapter, width.max(1), height.max(1))
+ .expect("Failed to get surface default config");
+
+ let capabilities = 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;
+ config.usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST;
+
+ let present_mode = if capabilities.present_modes.contains(&wgpu::PresentMode::Mailbox) {
+ wgpu::PresentMode::Mailbox
+ } else if capabilities.present_modes.contains(&wgpu::PresentMode::Immediate) {
+ wgpu::PresentMode::Immediate
+ } else if capabilities.present_modes.contains(&wgpu::PresentMode::FifoRelaxed) {
+ wgpu::PresentMode::FifoRelaxed
+ } else {
+ wgpu::PresentMode::Fifo
+ };
+ config.present_mode = present_mode;
+
+ surface.configure(&device, &config);
+
+ // Initialize text rendering
+ let font_system = FontSystem::new();
+ let swash_cache = SwashCache::new();
+ let cache = Cache::new(&device);
+ let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+ let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
+ let mut text_viewport = Viewport::new(&device, &cache);
+ text_viewport.update(&queue, Resolution { width: width.max(1), height: height.max(1) });
+
+ Self {
+ instance,
+ surface,
+ adapter,
+ device,
+ queue,
+ config,
+ font_system,
+ swash_cache,
+ text_atlas,
+ text_renderer,
+ text_viewport,
+ }
+ }
+
+ pub fn resize(&mut self, width: u32, height: u32) {
+ if width > 0 && height > 0 {
+ self.config.width = width;
+ self.config.height = height;
+ self.surface.configure(&self.device, &self.config);
+ self.text_viewport.update(&self.queue, Resolution { width, height });
+ }
+ }
+}
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
new file mode 100644
index 0000000..2f9d4d5
--- /dev/null
+++ b/src/backend/window_runner.rs
@@ -0,0 +1,1900 @@
+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_xdg_popup,
+ registry::{ProvidesRegistryState, RegistryState},
+ output::{OutputHandler, OutputState},
+ seat::{
+ keyboard::KeyboardHandler,
+ pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
+ Capability, SeatHandler, SeatState,
+ },
+ shell::{
+ xdg::{
+ window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
+ XdgShell,
+ },
+ WaylandSurface,
+ },
+ shm::{Shm, ShmHandler},
+};
+use wayland_client::{
+ globals::{registry_queue_init, GlobalList},
+ 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, Buffer, Attrs, Metrics,
+};
+use crate::widget::{TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey};
+use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
+use crate::backend::WgpuAdapter;
+
+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 scale = crate::scale::scale_factor();
+ let mut font_size = size;
+ let mut family_name = None;
+
+ if let Some(font_str) = font {
+ let (parsed_family, parsed_size) = crate::layout::parse_font_string(font_str);
+ if let Some(ps) = parsed_size {
+ font_size = ps;
+ }
+ family_name = Some(parsed_family);
+ }
+
+ let physical_size = font_size * scale;
+ let metrics = Metrics::new(physical_size, physical_size * 1.4);
+ let mut buf = Buffer::new(fs, metrics);
+ let mut attrs = Attrs::new();
+ if let Some(ref font_family) = family_name {
+ let family = match font_family.as_str() {
+ "monospace" => glyphon::Family::Monospace,
+ "sans-serif" => glyphon::Family::SansSerif,
+ "serif" => glyphon::Family::Serif,
+ name => glyphon::Family::Name(name),
+ };
+ attrs = attrs.family(family);
+ }
+ buf.set_text(fs, text, attrs, glyphon::Shaping::Advanced);
+ buf.shape_until_scroll(fs, true);
+ buf
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct Vertex {
+ pub position: [f32; 2],
+ pub color: [f32; 4],
+ pub clip_circle: [f32; 3], // [cx, cy, r]
+}
+
+impl Vertex {
+ const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
+ 0 => Float32x2,
+ 1 => Float32x4,
+ 2 => Float32x3,
+ ];
+
+ pub fn desc() -> wgpu::VertexBufferLayout<'static> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
+
+pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
+ let x0 = (x / sw) * 2.0 - 1.0;
+ let y0 = 1.0 - (y / sh) * 2.0;
+ let x1 = ((x + w) / sw) * 2.0 - 1.0;
+ let y1 = 1.0 - ((y + h) / sh) * 2.0;
+ [
+ Vertex { position: [x0, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
+ Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
+ Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
+ Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
+ Vertex { position: [x1, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
+ Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
+ ]
+}
+
+pub fn quad_vertices_with_clip(
+ x: f32, y: f32, w: f32, h: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+) -> [Vertex; 6] {
+ let x0 = (x / sw) * 2.0 - 1.0;
+ let y0 = 1.0 - (y / sh) * 2.0;
+ let x1 = ((x + w) / sw) * 2.0 - 1.0;
+ let y1 = 1.0 - ((y + h) / sh) * 2.0;
+ [
+ Vertex { position: [x0, y0], color, clip_circle },
+ Vertex { position: [x1, y0], color, clip_circle },
+ Vertex { position: [x0, y1], color, clip_circle },
+ Vertex { position: [x1, y0], color, clip_circle },
+ Vertex { position: [x1, y1], color, clip_circle },
+ Vertex { position: [x0, y1], color, clip_circle },
+ ]
+}
+
+pub fn quad_vertices_clipped(
+ x: f32, y: f32, w: f32, h: f32,
+ surface_w: f32, surface_h: f32,
+ color: [f32; 4],
+ clip: (f32, f32, f32, f32),
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let (cx0, cy0, cx1, cy1) = clip;
+ let ix0 = x.max(cx0);
+ let iy0 = y.max(cy0);
+ let ix1 = (x + w).min(cx1);
+ let iy1 = (y + h).min(cy1);
+ if ix1 <= ix0 || iy1 <= iy0 {
+ return Vec::new();
+ }
+ 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();
+ push_rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, clip_circle, corners, clip_rect, &mut verts);
+ verts
+}
+
+pub fn push_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)>,
+ out: &mut Vec<Vertex>,
+) {
+ 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(out, x, y, ww, h);
+ return;
+ }
+
+ // 1. Center rectangle
+ push_quad(out, x + r, y, ww - 2.0 * r, h);
+
+ // 2. Left rectangle
+ push_quad(out, x, y + r, r, h - 2.0 * r);
+
+ // 3. Right rectangle
+ push_quad(out, 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;
+
+ out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ }
+ } else {
+ push_quad(out, sqx, sqy, r, r);
+ }
+ }
+}
+
+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 push_rounded_rect_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ push_rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, clip_circle, (true, true, true, true), None, out);
+}
+
+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();
+ push_plate_bevel_vertices(x, y, ww, h, r, t, sw, sh, clip_circle, &mut verts);
+ verts
+}
+
+pub fn push_plate_bevel_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ t: f32,
+ sw: f32, sh: f32,
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ let highlight_color = [1.0, 1.0, 1.0, 0.15];
+ let shadow_color = [0.0, 0.0, 0.0, 0.25];
+
+ out.extend_from_slice(&quad_vertices_with_clip(x + r, y, ww - 2.0 * r, t, sw, sh, highlight_color, clip_circle));
+ out.extend_from_slice(&quad_vertices_with_clip(x, y + r, t, h - 2.0 * r, sw, sh, highlight_color, clip_circle));
+ out.extend_from_slice(&quad_vertices_with_clip(x + r, y + h - t, ww - 2.0 * r, t, sw, sh, shadow_color, clip_circle));
+ out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r, t, h - 2.0 * r, sw, sh, shadow_color, clip_circle));
+
+ let segments = 16;
+
+ push_arc_background_vertices(
+ x + r, y + r, r, t,
+ std::f32::consts::PI, 1.5 * std::f32::consts::PI,
+ sw, sh, highlight_color, segments, clip_circle,
+ out,
+ );
+
+ push_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,
+ out,
+ );
+ push_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,
+ out,
+ );
+
+ push_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,
+ out,
+ );
+
+ push_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,
+ out,
+ );
+ push_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,
+ out,
+ );
+}
+
+pub fn widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ push_widget_vertices(w, sw, sh, clip_circle, &mut verts);
+ verts
+}
+
+pub fn push_widget_vertices(w: &dyn crate::widget::Element, sw: f32, sh: f32, clip_circle: [f32; 3], out: &mut Vec<Vertex>) {
+ let (x, y, ww, h) = w.rect();
+ let corners = w.rounded_corners();
+ if corners != (false, false, false, false) {
+ push_rounded_rect_vertices_corners(x, y, ww, h, 12.0, sw, sh, w.color(), clip_circle, corners, None, out);
+ } else {
+ out.extend_from_slice(&quad_vertices_with_clip(x, y, ww, h, sw, sh, w.color(), clip_circle));
+ }
+
+ if w.is_plate() {
+ push_plate_bevel_vertices(x, y, ww, h, 12.0, 1.5, sw, sh, clip_circle, out);
+ }
+}
+
+pub fn extra_quad_vertices(
+ w: &dyn crate::widget::Element,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+ sw: f32, sh: f32,
+ qc: [f32; 4],
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ push_extra_quad_vertices(w, qx, qy, qw, qh, sw, sh, qc, clip_circle, &mut verts);
+ verts
+}
+
+pub fn push_extra_quad_vertices(
+ w: &dyn crate::widget::Element,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+ sw: f32, sh: f32,
+ qc: [f32; 4],
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ let corners = w.rounded_corners();
+ if corners == (false, false, false, false) {
+ out.extend_from_slice(&quad_vertices_with_clip(qx, qy, qw, qh, sw, sh, qc, clip_circle));
+ return;
+ }
+
+ 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,
+ );
+
+ push_rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, None, out);
+}
+
+pub fn extra_quad_vertices_clipped(
+ w: &dyn crate::widget::Element,
+ 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 mut verts = Vec::new();
+ push_extra_quad_vertices_clipped(w, qx, qy, qw, qh, sw, sh, qc, clip, clip_circle, &mut verts);
+ verts
+}
+
+pub fn push_extra_quad_vertices_clipped(
+ w: &dyn crate::widget::Element,
+ qx: f32, qy: f32, qw: f32, qh: f32,
+ sw: f32, sh: f32,
+ qc: [f32; 4],
+ clip: (f32, f32, f32, f32),
+ clip_circle: [f32; 3],
+ out: &mut 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;
+ }
+ out.extend_from_slice(&quad_vertices_with_clip(ix0, iy0, ix1 - ix0, iy1 - iy0, sw, sh, qc, clip_circle));
+ return;
+ }
+
+ 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,
+ );
+
+ push_rounded_rect_vertices_corners(qx, qy, qw, qh, 12.0, sw, sh, qc, clip_circle, extra_corners, Some(clip), out);
+}
+
+pub fn circle_vertices(
+ cx: f32, cy: f32, r: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ for i in 0..segments {
+ let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+ let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (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;
+
+ 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 });
+ }
+ verts
+}
+
+pub fn circle_border_vertices(
+ cx: f32, cy: f32, r: f32,
+ thickness: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ for i in 0..segments {
+ let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+ let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
+
+ let x0 = cx + (r - thickness) * theta1.cos();
+ let y0 = cy + (r - thickness) * theta1.sin();
+ 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 x3 = cx + (r - thickness) * theta2.cos();
+ let y3 = cy + (r - thickness) * 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 ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / 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 });
+
+ verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+ }
+ verts
+}
+
+pub fn arc_background_vertices(
+ cx: f32, cy: f32, r: f32,
+ thickness: f32,
+ start_angle: f32, end_angle: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+ clip_circle: [f32; 3],
+) -> Vec<Vertex> {
+ let mut verts = Vec::new();
+ push_arc_background_vertices(cx, cy, r, thickness, start_angle, end_angle, sw, sh, color, segments, clip_circle, &mut verts);
+ verts
+}
+
+pub fn push_arc_background_vertices(
+ cx: f32, cy: f32, r: f32,
+ thickness: f32,
+ start_angle: f32, end_angle: f32,
+ sw: f32, sh: f32,
+ color: [f32; 4],
+ segments: usize,
+ clip_circle: [f32; 3],
+ out: &mut Vec<Vertex>,
+) {
+ for i in 0..segments {
+ let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
+ let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
+
+ let x0 = cx + (r - thickness) * theta1.cos();
+ let y0 = cy + (r - thickness) * theta1.sin();
+ 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 x3 = cx + (r - thickness) * theta2.cos();
+ let y3 = cy + (r - thickness) * 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 ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / sh) * 2.0;
+
+ out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ out.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
+ out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+
+ out.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
+ out.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
+ out.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct WindowSettings {
+ pub title: String,
+ pub app_id: String,
+ pub width: u32,
+ pub height: u32,
+ pub fullscreen: bool,
+ pub min_size: Option<(u32, u32)>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct LogicalPosition {
+ pub x: f32,
+ pub y: f32,
+}
+
+impl LogicalPosition {
+ pub fn new(x: f32, y: f32) -> Self {
+ Self { x, y }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct LogicalSize {
+ pub width: f32,
+ pub height: f32,
+}
+
+impl LogicalSize {
+ pub fn new(width: f32, height: f32) -> Self {
+ Self { width, height }
+ }
+}
+
+pub struct RenderContext<'a> {
+ pub font_system: &'a mut FontSystem,
+}
+
+pub trait Application: Sized + 'static {
+ type Message: Send + Clone + 'static;
+
+ fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self;
+ fn settings(&self) -> WindowSettings;
+ 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 render_popovers(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
+
+ fn text_areas(&self, scale_f32: f32, bounds: TextBounds) -> Vec<TextArea<'_>> {
+ 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).round(),
+ top: (ti.y * scale_f32).round(),
+ scale: 1.0,
+ bounds: item_bounds,
+ default_color: ti.color,
+ custom_glyphs: &[],
+ }
+ }).collect()
+ }
+
+ fn clear_color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
+ }
+
+ fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool);
+ fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message>;
+ fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool);
+ fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message>;
+}
+
+pub struct PressedKey {
+ pub logical_key: Key,
+ pub text: Option<String>,
+ pub first_pressed: Instant,
+ pub last_repeated: Instant,
+}
+
+fn is_repeatable_key(key: &Key) -> bool {
+ match key {
+ Key::Named(NamedKey::Backspace) |
+ Key::Named(NamedKey::Delete) |
+ Key::Named(NamedKey::ArrowLeft) |
+ Key::Named(NamedKey::ArrowRight) |
+ Key::Named(NamedKey::ArrowUp) |
+ Key::Named(NamedKey::ArrowDown) |
+ Key::Named(NamedKey::Home) |
+ Key::Named(NamedKey::End) |
+ Key::Character(_) => true,
+ _ => false,
+ }
+}
+
+pub struct EngineState<A: Application> {
+ pub registry_state: RegistryState,
+ pub compositor_state: CompositorState,
+ pub xdg_shell_state: XdgShell,
+ pub shm_state: Shm,
+ pub seat_state: SeatState,
+ pub output_state: OutputState,
+ pub seats: Vec<wl_seat::WlSeat>,
+ pub pointer: Option<ThemedPointer>,
+ pub keyboard: Option<wl_keyboard::WlKeyboard>,
+
+ pub window: Option<XdgWindow>,
+ pub surface: Option<wl_surface::WlSurface>,
+
+ pub inner: A,
+
+ pub wgpu_adapter: Option<WgpuAdapter>,
+ pub render_pipeline: Option<wgpu::RenderPipeline>,
+ pub vertex_buffer: Option<wgpu::Buffer>,
+ pub vertex_count: u32,
+ pub overlay_vertex_buffer: Option<wgpu::Buffer>,
+ pub overlay_vertex_count: u32,
+
+ pub scale_factor: f64,
+ pub logical_width: f32,
+ pub logical_height: f32,
+
+ pub exit: bool,
+ pub redraw: bool,
+ pub first_configure_received: bool,
+ pub ctrl_pressed: bool,
+ 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> {
+ pub async fn init_gpu(&mut self, conn: &Connection, width_logical: f32, height_logical: f32) {
+ let s = self.scale_factor as f32;
+ let pw = (width_logical * s) as u32;
+ let ph = (height_logical * s) as u32;
+
+ let surface = self.surface.as_ref().expect("surface missing");
+
+ let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
+ let surface_ptr = surface.id().as_ptr() as *mut std::ffi::c_void;
+
+ let mut adapter = WgpuAdapter::new(display_ptr, surface_ptr, pw, ph).await;
+
+ let shader = adapter.device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Shader"),
+ source: wgpu::ShaderSource::Wgsl(crate::SHADER.into()),
+ });
+
+ let pipeline_layout = adapter.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Pipeline Layout"),
+ bind_group_layouts: &[],
+ push_constant_ranges: &[],
+ });
+
+ let render_pipeline = adapter.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Render Pipeline"),
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: Some("vs_main"),
+ buffers: &[Vertex::desc()],
+ compilation_options: Default::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: adapter.config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: Default::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ strip_index_format: None,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
+ multiview: None,
+ cache: None,
+ });
+
+ let vertex_buffer = adapter.device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Vertex Buffer"),
+ size: 1,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let overlay_vertex_buffer = adapter.device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Overlay Vertex Buffer"),
+ size: 1,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ self.wgpu_adapter = Some(adapter);
+ self.render_pipeline = Some(render_pipeline);
+ self.vertex_buffer = Some(vertex_buffer);
+ self.overlay_vertex_buffer = Some(overlay_vertex_buffer);
+ self.logical_width = width_logical;
+ self.logical_height = height_logical;
+ }
+
+ pub fn resize(&mut self, w: f32, h: f32) {
+ if w > 0.0 && h > 0.0 {
+ self.logical_width = w;
+ self.logical_height = h;
+ if let Some(ref mut adapter) = self.wgpu_adapter {
+ adapter.resize((w as f64 * self.scale_factor) as u32, (h as f64 * self.scale_factor) as u32);
+ }
+ }
+ }
+
+ pub fn render(&mut self) {
+ let logical_w = self.logical_width;
+ let logical_h = self.logical_height;
+ let scale_factor = self.scale_factor;
+
+ 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 adapter = self.wgpu_adapter.as_mut().unwrap();
+ let render_pipeline = self.render_pipeline.as_ref().unwrap();
+
+ // 1. Build and upload vertex buffer
+ let mut verts = Vec::new();
+ 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);
+ let needed = data.len() as wgpu::BufferAddress;
+ let mut vbuf = self.vertex_buffer.as_ref().unwrap();
+ if needed > vbuf.size() {
+ let new_vbuf = adapter.device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Vertex Buffer"),
+ size: needed,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+ self.vertex_buffer = Some(new_vbuf);
+ vbuf = self.vertex_buffer.as_ref().unwrap();
+ }
+ adapter.queue.write_buffer(vbuf, 0, data);
+ }
+
+ // 1b. Build and upload overlay vertex buffer
+ let mut overlay_quads = Vec::new();
+ self.inner.overlay_quads(&mut overlay_quads, LogicalSize::new(logical_w, logical_h), scale_factor);
+ let mut overlay_verts = Vec::new();
+ for &(qx, qy, qw, qh, qc) in &overlay_quads {
+ overlay_verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
+ }
+ self.overlay_vertex_count = overlay_verts.len() as u32;
+ if self.overlay_vertex_count > 0 {
+ let data = bytemuck::cast_slice(&overlay_verts);
+ let needed = data.len() as wgpu::BufferAddress;
+ let mut ovbuf = self.overlay_vertex_buffer.as_ref().unwrap();
+ if needed > ovbuf.size() {
+ let new_ovbuf = adapter.device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Overlay Vertex Buffer"),
+ size: needed,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+ self.overlay_vertex_buffer = Some(new_ovbuf);
+ ovbuf = self.overlay_vertex_buffer.as_ref().unwrap();
+ }
+ adapter.queue.write_buffer(ovbuf, 0, data);
+ }
+
+ // 2. Prepare text
+ let scale_f32 = scale_factor as f32;
+ let pw = (logical_w * scale_f32) as u32;
+ let ph = (logical_h * scale_f32) as u32;
+ adapter.text_viewport.update(&adapter.queue, Resolution { width: pw, height: ph });
+
+ let bounds = TextBounds { left: 0, top: 0, right: pw as i32, bottom: ph as i32 };
+ let areas = self.inner.text_areas(scale_f32, bounds);
+
+ adapter.text_renderer.prepare(&adapter.device, &adapter.queue, &mut adapter.font_system, &mut adapter.text_atlas, &adapter.text_viewport, areas, &mut adapter.swash_cache).unwrap();
+
+ // 3. Render Pass
+ let output = match adapter.surface.get_current_texture() {
+ Ok(t) => t,
+ Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+ adapter.surface.configure(&adapter.device, &adapter.config);
+ return;
+ }
+ Err(wgpu::SurfaceError::Timeout) => return,
+ Err(e) => {
+ eprintln!("Surface error: {e:?}");
+ return;
+ }
+ };
+ let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let mut encoder = adapter.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+ label: Some("Encoder"),
+ });
+
+ {
+ let cc = self.inner.clear_color();
+ let r_clear = (cc[0] as f64).powf(2.2);
+ let g_clear = (cc[1] as f64).powf(2.2);
+ let b_clear = (cc[2] as f64).powf(2.2);
+ let a_clear = cc[3] as f64;
+
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Render Pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color {
+ r: r_clear,
+ g: g_clear,
+ b: b_clear,
+ a: a_clear,
+ }),
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ if self.vertex_count > 0 {
+ pass.set_pipeline(render_pipeline);
+ pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
+ pass.draw(0..self.vertex_count, 0..1);
+ }
+
+ adapter.text_renderer.render(&adapter.text_atlas, &adapter.text_viewport, &mut pass).unwrap();
+
+ if self.overlay_vertex_count > 0 {
+ pass.set_pipeline(render_pipeline);
+ pass.set_vertex_buffer(0, self.overlay_vertex_buffer.as_ref().unwrap().slice(..));
+ pass.draw(0..self.overlay_vertex_count, 0..1);
+ }
+ }
+
+ adapter.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();
+ self.inner.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 = adapter.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();
+ }
+ adapter.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 adapter.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).round(),
+ top: (ti.y * scale_f32).round(),
+ scale: 1.0,
+ bounds,
+ default_color: ti.color,
+ custom_glyphs: &[],
+ }).collect();
+
+ popup.viewport.update(&adapter.queue, Resolution {
+ width: (popup.logical_width * scale_f32) as u32,
+ height: (popup.logical_height * scale_f32) as u32,
+ });
+
+ adapter.text_renderer.prepare(
+ &adapter.device,
+ &adapter.queue,
+ &mut adapter.font_system,
+ &mut adapter.text_atlas,
+ &popup.viewport,
+ areas,
+ &mut adapter.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(&adapter.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 = adapter.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);
+ }
+
+ adapter.text_renderer.render(&adapter.text_atlas, &popup.viewport, &mut pass).unwrap();
+ }
+
+ adapter.queue.submit(std::iter::once(popup_encoder.finish()));
+ popup_output.present();
+ }
+ }
+
+ adapter.text_atlas.trim();
+ }
+}
+
+impl<A: Application> Drop for EngineState<A> {
+ fn drop(&mut self) {
+ self.wgpu_adapter = None;
+ self.render_pipeline = None;
+ self.vertex_buffer = None;
+ self.overlay_vertex_buffer = None;
+ }
+}
+
+impl<A: Application> CompositorHandler for EngineState<A> {
+ fn scale_factor_changed(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _surface: &wl_surface::WlSurface,
+ scale_factor: i32,
+ ) {
+ _surface.set_buffer_scale(scale_factor);
+ self.scale_factor = scale_factor as f64;
+ self.resize(self.logical_width, self.logical_height);
+ self.redraw = true;
+ }
+
+ fn transform_changed(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _surface: &wl_surface::WlSurface,
+ _new_transform: wl_output::Transform,
+ ) {}
+
+ fn frame(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _surface: &wl_surface::WlSurface,
+ _time: u32,
+ ) {}
+
+ fn surface_enter(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _surface: &wl_surface::WlSurface,
+ _output: &wl_output::WlOutput,
+ ) {}
+
+ fn surface_leave(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _surface: &wl_surface::WlSurface,
+ _output: &wl_output::WlOutput,
+ ) {}
+}
+
+impl<A: Application> OutputHandler for EngineState<A> {
+ fn output_state(&mut self) -> &mut OutputState {
+ &mut self.output_state
+ }
+
+ fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
+ fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
+ fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
+}
+
+impl<A: Application> ShmHandler for EngineState<A> {
+ fn shm_state(&mut self) -> &mut Shm {
+ &mut self.shm_state
+ }
+}
+
+impl<A: Application> ProvidesRegistryState for EngineState<A> {
+ fn registry(&mut self) -> &mut RegistryState {
+ &mut self.registry_state
+ }
+
+ fn runtime_add_global(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _name: u32,
+ _interface: &str,
+ _version: u32,
+ ) {}
+
+ fn runtime_remove_global(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _name: u32,
+ _interface: &str,
+ ) {}
+}
+
+impl<A: Application> WindowHandler for EngineState<A> {
+ fn configure(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _window: &XdgWindow,
+ configure: WindowConfigure,
+ _serial: u32,
+ ) {
+ let (w, h) = configure.new_size;
+ if let (Some(w), Some(h)) = (w, h) {
+ let width = w.get();
+ let height = h.get();
+ self.resize(width as f32, height as f32);
+ } else {
+ let settings = self.inner.settings();
+ let w = settings.width as f32;
+ let h = settings.height as f32;
+ self.resize(w, h);
+ }
+ self.redraw = true;
+ self.first_configure_received = true;
+ }
+
+ fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
+ self.exit = true;
+ }
+}
+
+impl<A: Application> SeatHandler for EngineState<A> {
+ fn seat_state(&mut self) -> &mut SeatState {
+ &mut self.seat_state
+ }
+
+ fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
+ self.seats.push(seat);
+ }
+
+ fn new_capability(
+ &mut self,
+ _conn: &Connection,
+ qh: &QueueHandle<Self>,
+ seat: wl_seat::WlSeat,
+ capability: Capability,
+ ) {
+ if capability == Capability::Pointer && self.pointer.is_none() {
+ let surface = self.compositor_state.create_surface(qh);
+ let themed_pointer = self.seat_state.get_pointer_with_theme(
+ qh,
+ &seat,
+ self.shm_state.wl_shm(),
+ surface,
+ ThemeSpec::System,
+ ).unwrap();
+ self.pointer = Some(themed_pointer);
+ }
+ if capability == Capability::Keyboard && self.keyboard.is_none() {
+ let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
+ self.keyboard = Some(keyboard);
+ }
+ }
+
+ fn remove_capability(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _seat: wl_seat::WlSeat,
+ capability: Capability,
+ ) {
+ if capability == Capability::Pointer {
+ self.pointer = None;
+ }
+ if capability == Capability::Keyboard {
+ self.keyboard = None;
+ }
+ }
+
+ fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
+ self.seats.retain(|s| s != &seat);
+ }
+}
+
+impl<A: Application> PointerHandler for EngineState<A> {
+ fn pointer_frame(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _pointer: &wl_pointer::WlPointer,
+ events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
+ ) {
+ use smithay_client_toolkit::seat::pointer::PointerEventKind;
+ for event in events {
+ let (x, y) = event.position;
+ 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 { .. } => {
+ if let Some(ref themed_pointer) = self.pointer {
+ let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
+ }
+ }
+ PointerEventKind::Leave { .. } => {}
+ PointerEventKind::Motion { .. } => {
+ let mut rebuild = false;
+ self.inner.handle_pointer_move(LogicalPosition::new(lx, ly), &mut rebuild);
+ if rebuild {
+ self.redraw = true;
+ }
+ }
+ PointerEventKind::Press { button, .. } => {
+ let btn = match *button {
+ 272 => MouseButton::Left,
+ 273 => MouseButton::Right,
+ 274 => MouseButton::Middle,
+ _ => continue,
+ };
+ let mut rebuild = false;
+ if let Some(msg) = self.inner.handle_mouse_input(btn, ElementState::Pressed, LogicalPosition::new(lx, ly), &mut rebuild) {
+ let mut update_rebuild = false;
+ self.inner.update(msg, &mut update_rebuild, &mut self.exit);
+ if update_rebuild {
+ rebuild = true;
+ }
+ }
+ if rebuild {
+ self.redraw = true;
+ }
+ }
+ PointerEventKind::Release { button, .. } => {
+ let btn = match *button {
+ 272 => MouseButton::Left,
+ 273 => MouseButton::Right,
+ 274 => MouseButton::Middle,
+ _ => continue,
+ };
+ let mut rebuild = false;
+ if let Some(msg) = self.inner.handle_mouse_input(btn, ElementState::Released, LogicalPosition::new(lx, ly), &mut rebuild) {
+ let mut update_rebuild = false;
+ self.inner.update(msg, &mut update_rebuild, &mut self.exit);
+ if update_rebuild {
+ rebuild = true;
+ }
+ }
+ if rebuild {
+ self.redraw = true;
+ }
+ }
+ PointerEventKind::Axis { horizontal, vertical, .. } => {
+ let h_scroll = horizontal.absolute as f32;
+ let v_scroll = vertical.absolute as f32;
+ let delta = MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
+ let mut rebuild = false;
+ self.inner.handle_mouse_wheel(&delta, LogicalPosition::new(lx, ly), &mut rebuild);
+ if rebuild {
+ self.redraw = true;
+ }
+ }
+ }
+ }
+ }
+}
+
+impl<A: Application> KeyboardHandler for EngineState<A> {
+ fn enter(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ _surface: &wl_surface::WlSurface,
+ _serial: u32,
+ _raw_modifiers: &[u32],
+ _keysyms: &[xkeysym::Keysym],
+ ) {}
+
+ fn leave(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ _surface: &wl_surface::WlSurface,
+ _serial: u32,
+ ) {
+ self.pressed_key = None;
+ self.ctrl_pressed = false;
+ self.shift_pressed = false;
+ }
+
+ fn press_key(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ _serial: u32,
+ event: smithay_client_toolkit::seat::keyboard::KeyEvent,
+ ) {
+ self.handle_key(event, ElementState::Pressed);
+ }
+
+ fn release_key(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ _serial: u32,
+ event: smithay_client_toolkit::seat::keyboard::KeyEvent,
+ ) {
+ self.handle_key(event, ElementState::Released);
+ }
+
+ fn update_modifiers(
+ &mut self,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ _keyboard: &wl_keyboard::WlKeyboard,
+ _serial: u32,
+ modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
+ _layout: u32,
+ ) {
+ self.ctrl_pressed = modifiers.ctrl;
+ self.shift_pressed = modifiers.shift;
+ }
+}
+
+impl<A: Application> EngineState<A> {
+ fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: ElementState) {
+ let logical_key = match event.keysym {
+ xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
+ xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
+ xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
+ xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
+ xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
+ xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
+ xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
+ xkeysym::Keysym::Tab => Key::Named(NamedKey::Tab),
+ xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
+ xkeysym::Keysym::space => Key::Named(NamedKey::Space),
+ _ => {
+ if let Some(ref text) = event.utf8 {
+ Key::Character(text.clone())
+ } else if let Some(ch) = event.keysym.key_char() {
+ Key::Character(ch.to_string())
+ } else {
+ return;
+ }
+ }
+ };
+
+ let custom_event = KeyEvent {
+ state,
+ logical_key,
+ text: event.utf8.clone(),
+ repeat: false,
+ ctrl: self.ctrl_pressed,
+ shift: self.shift_pressed,
+ };
+
+ if state == ElementState::Pressed {
+ if is_repeatable_key(&custom_event.logical_key) {
+ self.pressed_key = Some(PressedKey {
+ logical_key: custom_event.logical_key.clone(),
+ text: custom_event.text.clone(),
+ first_pressed: Instant::now(),
+ last_repeated: Instant::now(),
+ });
+ } else {
+ self.pressed_key = None;
+ }
+ } else if state == ElementState::Released {
+ if let Some(ref pk) = self.pressed_key {
+ if pk.logical_key == custom_event.logical_key {
+ self.pressed_key = None;
+ }
+ }
+ }
+
+ let mut rebuild = false;
+ if let Some(msg) = self.inner.handle_key_input(&custom_event, &mut rebuild) {
+ let mut update_rebuild = false;
+ self.inner.update(msg, &mut update_rebuild, &mut self.exit);
+ if update_rebuild {
+ rebuild = true;
+ }
+ }
+ if rebuild {
+ self.redraw = true;
+ }
+ }
+}
+
+impl<A: Application> wayland_client::Dispatch<wl_registry::WlRegistry, GlobalList, Self> for EngineState<A> {
+ fn event(
+ _state: &mut Self,
+ _proxy: &wl_registry::WlRegistry,
+ _event: wl_registry::Event,
+ _data: &GlobalList,
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ ) {}
+}
+
+impl<A: Application> wayland_client::Dispatch<crate::protocol::zclear_inspector_v1::ZclearInspectorV1, ()> for EngineState<A> {
+ fn event(
+ _state: &mut Self,
+ _proxy: &crate::protocol::zclear_inspector_v1::ZclearInspectorV1,
+ _event: crate::protocol::zclear_inspector_v1::Event,
+ _data: &(),
+ _conn: &Connection,
+ _qh: &QueueHandle<Self>,
+ ) {}
+}
+
+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::Element);
+ popover.unfocus();
+ }
+ }
+ self.active_popup = None;
+ self.redraw = true;
+ }
+}
+
+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>);
+delegate_keyboard!(@<A: Application> EngineState<A>);
+delegate_registry!(@<A: Application> EngineState<A>);
+delegate_output!(@<A: Application> EngineState<A>);
+
+pub fn run<A: Application>() {
+ let conn = Connection::connect_to_env().unwrap();
+ let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
+ let qh = event_queue.handle();
+
+ let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
+ let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
+ let shm_state = Shm::bind(&globals, &qh).unwrap();
+ let seat_state = SeatState::new(&globals, &qh);
+ let output_state = OutputState::new(&globals, &qh);
+
+ let (sender, channel) = calloop::channel::channel::<A::Message>();
+
+ let inner = A::new(&qh, sender.clone());
+ let settings = inner.settings();
+
+ let mut engine_state = EngineState {
+ registry_state: RegistryState::new(&globals),
+ compositor_state,
+ xdg_shell_state,
+ shm_state,
+ seat_state,
+ output_state,
+ seats: Vec::new(),
+ pointer: None,
+ keyboard: None,
+ window: None,
+ surface: None,
+ inner,
+ wgpu_adapter: None,
+ render_pipeline: None,
+ vertex_buffer: None,
+ vertex_count: 0,
+ overlay_vertex_buffer: None,
+ overlay_vertex_count: 0,
+ scale_factor: 1.0,
+ logical_width: settings.width as f32,
+ logical_height: settings.height as f32,
+ exit: false,
+ redraw: false,
+ first_configure_received: false,
+ ctrl_pressed: false,
+ shift_pressed: false,
+ pressed_key: None,
+ sender,
+ active_popup: None,
+ qh: qh.clone(),
+ };
+
+ event_queue.roundtrip(&mut engine_state).unwrap();
+
+ let scale = detect_scale_factor(&engine_state.output_state);
+ engine_state.scale_factor = scale;
+
+ let surface = engine_state.compositor_state.create_surface(&qh);
+ surface.set_buffer_scale(scale as i32);
+ let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
+ window.set_title(&settings.title);
+ window.set_app_id(&settings.app_id);
+ if settings.fullscreen {
+ window.set_fullscreen(None);
+ }
+ if let Some((min_w, min_h)) = settings.min_size {
+ window.set_min_size(Some((min_w, min_h)));
+ }
+ window.commit();
+
+ engine_state.window = Some(window);
+ engine_state.surface = Some(surface);
+
+ pollster::block_on(engine_state.init_gpu(&conn, settings.width as f32, settings.height as f32));
+
+ let mut event_loop = EventLoop::try_new().unwrap();
+ let loop_handle = event_loop.handle();
+ 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 {
+ let mut rebuild = false;
+ app_state.inner.update(msg, &mut rebuild, &mut app_state.exit);
+ if rebuild {
+ app_state.redraw = true;
+ }
+ }
+ }).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_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();
+ if engine_state.exit {
+ break;
+ }
+
+ let now = std::time::Instant::now();
+ let mut dt = now.duration_since(last_tick).as_secs_f32();
+ last_tick = now;
+ if dt > 0.1 {
+ dt = 0.1;
+ }
+
+ let mut rebuild = false;
+ engine_state.inner.tick(dt, &mut rebuild);
+ if rebuild {
+ engine_state.redraw = true;
+ }
+
+ if let Some(ref mut pk) = engine_state.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 {
+ pk.last_repeated = now;
+ let custom_event = KeyEvent {
+ state: ElementState::Pressed,
+ logical_key: pk.logical_key.clone(),
+ text: pk.text.clone(),
+ repeat: true,
+ ctrl: engine_state.ctrl_pressed,
+ shift: engine_state.shift_pressed,
+ };
+ let mut key_rebuild = false;
+ if let Some(msg) = engine_state.inner.handle_key_input(&custom_event, &mut key_rebuild) {
+ let mut update_rebuild = false;
+ engine_state.inner.update(msg, &mut update_rebuild, &mut engine_state.exit);
+ if update_rebuild {
+ key_rebuild = true;
+ }
+ }
+ if key_rebuild {
+ engine_state.redraw = true;
+ }
+ }
+ }
+ }
+ 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.wgpu_adapter.as_ref().unwrap().instance;
+ let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create popup wgpu surface");
+
+ let device = &engine_state.wgpu_adapter.as_ref().unwrap().device;
+ let main_config = &engine_state.wgpu_adapter.as_ref().unwrap().config;
+
+ 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;
+ if engine_state.first_configure_received {
+ engine_state.render();
+ }
+ }
+ }
+}
diff --git a/src/context.rs b/src/context.rs
new file mode 100644
index 0000000..681b90c
--- /dev/null
+++ b/src/context.rs
@@ -0,0 +1,372 @@
+use std::collections::HashMap;
+use crate::widget::{Element, WidgetId, LayoutTree, Key, KeyEvent, MouseButton, ElementState, MouseScrollDelta};
+use crate::widget::core::hover_animation::HoverState;
+use crate::widget::core::context_menu::ContextMenuState;
+use crate::widget::TextBox;
+
+pub struct UiContext {
+ pub layout_tree: LayoutTree,
+ pub widget_registry: HashMap<WidgetId, *mut (dyn Element + 'static)>,
+ pub focused_widget: Option<*mut (dyn Element + 'static)>,
+ pub active_popovers: Vec<*const (dyn Element + 'static)>,
+ pub hover_state: HoverState,
+ pub cursor_pos: (f32, f32),
+ pub context_menu: ContextMenuState,
+}
+
+impl UiContext {
+ pub fn new() -> Self {
+ Self {
+ layout_tree: LayoutTree {
+ parents: HashMap::new(),
+ children: HashMap::new(),
+ },
+ widget_registry: HashMap::new(),
+ focused_widget: None,
+ active_popovers: Vec::new(),
+ hover_state: HoverState::new(),
+ cursor_pos: (0.0, 0.0),
+ context_menu: ContextMenuState::new(),
+ }
+ }
+
+ // --- Focus management ---
+ pub fn set_focused(&mut self, w: &mut dyn Element) {
+ let new_ptr = unsafe {
+ std::mem::transmute::<*mut dyn Element, *mut (dyn Element + 'static)>(w as *mut dyn Element)
+ };
+ self.set_focused_ptr(new_ptr);
+ }
+
+ pub fn set_focused_ptr(&mut self, new_ptr: *mut (dyn Element + 'static)) {
+ if let Some(old_ptr) = self.focused_widget {
+ let old_data = old_ptr as *mut () as usize;
+ let new_data = new_ptr as *mut () as usize;
+ if old_data != new_data {
+ unsafe {
+ (*old_ptr).unfocus();
+ }
+ self.focused_widget = Some(new_ptr);
+ }
+ } else {
+ self.focused_widget = Some(new_ptr);
+ }
+ }
+
+ pub fn is_focused(&self, w: &dyn Element) -> bool {
+ let addr = w as *const dyn Element as *const () as usize;
+ self.is_focused_addr(addr)
+ }
+
+ pub fn is_focused_addr(&self, addr: usize) -> bool {
+ if let Some(ptr) = self.focused_widget {
+ let current_data = ptr as *const () as usize;
+ current_data == addr
+ } else {
+ false
+ }
+ }
+
+ pub fn clear_focus(&mut self) {
+ if let Some(ptr) = self.focused_widget.take() {
+ unsafe {
+ (*ptr).unfocus();
+ }
+ }
+ }
+
+ pub fn clear_if_matches(&mut self, w: &dyn Element) {
+ let query_data = w as *const dyn Element as *const () as usize;
+ if let Some(ptr) = self.focused_widget {
+ let current_data = ptr as *const () as usize;
+ if current_data == query_data {
+ self.focused_widget = None;
+ }
+ }
+ }
+
+ pub fn has_focus(&self) -> bool {
+ self.focused_widget.is_some()
+ }
+
+ pub fn navigate_focus(&mut self, key: &Key, ctrl: bool) -> bool {
+ let ptr = match self.focused_widget {
+ Some(p) => p,
+ None => return false,
+ };
+
+ unsafe {
+ match (key, ctrl) {
+ (Key::Character(c), true) if c == "u" || c == "U" => {
+ if let Some(parent_ptr) = (*ptr).parent(self) {
+ let parent_ref = &mut *parent_ptr;
+ self.set_focused(parent_ref);
+ parent_ref.focus();
+ return true;
+ }
+ }
+ (Key::Character(c), true) if c == "i" || c == "I" => {
+ let mut children = (*ptr).children(self);
+ if !children.is_empty() {
+ let child_ref = &mut *children[0];
+ self.set_focused(child_ref);
+ child_ref.focus();
+ return true;
+ }
+ }
+ (Key::Character(c), true) if c == "j" || c == "J" => {
+ if let Some(parent_ptr) = (*ptr).parent(self) {
+ let mut siblings = (*parent_ptr).children(self);
+ let current_idx = siblings.iter().position(|&x| {
+ let a = x as *mut () as usize;
+ let b = ptr as *mut () as usize;
+ a == b
+ });
+ if let Some(idx) = current_idx {
+ let next_idx = (idx + 1) % siblings.len();
+ let sibling_ref = &mut *siblings[next_idx];
+ self.set_focused(sibling_ref);
+ sibling_ref.focus();
+ return true;
+ }
+ }
+ }
+ (Key::Character(c), true) if c == "k" || c == "K" => {
+ if let Some(parent_ptr) = (*ptr).parent(self) {
+ let mut siblings = (*parent_ptr).children(self);
+ let current_idx = siblings.iter().position(|&x| {
+ let a = x as *mut () as usize;
+ let b = ptr as *mut () as usize;
+ a == b
+ });
+ if let Some(idx) = current_idx {
+ let prev_idx = if idx == 0 { siblings.len() - 1 } else { idx - 1 };
+ let sibling_ref = &mut *siblings[prev_idx];
+ self.set_focused(sibling_ref);
+ sibling_ref.focus();
+ return true;
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ false
+ }
+
+ // --- Registry ---
+ pub fn register_widget(&mut self, id: WidgetId, ptr: *mut (dyn Element + 'static)) {
+ self.widget_registry.insert(id, ptr);
+ }
+
+ pub fn link_ids(&mut self, parent: WidgetId, child: WidgetId) {
+ self.layout_tree.parents.insert(child, parent);
+ let children = self.layout_tree.children.entry(parent).or_default();
+ if !children.contains(&child) {
+ children.push(child);
+ }
+ }
+
+ pub fn unlink_child(&mut self, parent: WidgetId, child: WidgetId) {
+ self.layout_tree.parents.remove(&child);
+ if let Some(children) = self.layout_tree.children.get_mut(&parent) {
+ children.retain(|&x| x != child);
+ }
+ }
+
+ pub fn clear_children_ids(&mut self, parent: WidgetId) {
+ if let Some(children) = self.layout_tree.children.remove(&parent) {
+ for child in children {
+ self.layout_tree.parents.remove(&child);
+ }
+ }
+ }
+
+ pub fn clear_hierarchy(&mut self) {
+ self.layout_tree.parents.clear();
+ self.layout_tree.children.clear();
+ self.widget_registry.clear();
+ }
+
+ // --- Popovers ---
+ pub fn clear_popovers(&mut self) {
+ self.active_popovers.clear();
+ }
+
+ pub fn register_popover(&mut self, w: &(dyn Element + 'static)) {
+ let ptr = w as *const (dyn Element + 'static);
+ if !self.active_popovers.contains(&ptr) {
+ self.active_popovers.push(ptr);
+ }
+ }
+
+ pub fn register_popover_ptr(&mut self, ptr: *mut (dyn Element + 'static)) {
+ let const_ptr = ptr as *const (dyn Element + 'static);
+ if !self.active_popovers.contains(&const_ptr) {
+ self.active_popovers.push(const_ptr);
+ }
+ }
+
+ pub fn is_coordinate_covered(&self, query_address: usize, px: f32, py: f32) -> bool {
+ for popover_ptr in self.active_popovers.iter() {
+ let current_data = *popover_ptr as *const () as usize;
+ if query_address == current_data {
+ continue;
+ }
+ unsafe {
+ if let Some(popover) = popover_ptr.as_ref() {
+ if let Some((x, y, width, height)) = popover.popover_rect() {
+ if px >= x && px <= x + width && py >= y && py <= y + height {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ false
+ }
+
+ // --- Hover State ---
+ pub fn set_cursor_pos(&mut self, x: f32, y: f32) {
+ self.cursor_pos = (x, y);
+ }
+
+ pub fn reset_frame_registration(&mut self) {
+ self.hover_state.registered_this_frame = false;
+ }
+
+ pub fn set_scroll_offset(&mut self, offset: f32) {
+ self.hover_state.scroll_offset = offset;
+ }
+
+ pub fn get_scroll_offset(&self) -> f32 {
+ self.hover_state.scroll_offset
+ }
+
+ pub fn register_hovered(&mut self, x: f32, y: f32, w: f32, h: f32, color: [f32; 4]) {
+ self.hover_state.target_x = Some(x);
+ self.hover_state.target_y = Some(y);
+ self.hover_state.target_w = Some(w);
+ self.hover_state.target_h = Some(h);
+ self.hover_state.target_alpha = color[3];
+ self.hover_state.registered_this_frame = true;
+ }
+
+ pub fn post_render_check(&mut self) {
+ if !self.hover_state.registered_this_frame {
+ self.hover_state.target_alpha = 0.0;
+ let (cx, cy) = self.cursor_pos;
+ self.hover_state.target_x = Some(cx);
+ self.hover_state.target_y = Some(cy + self.hover_state.scroll_offset);
+ self.hover_state.target_w = Some(0.0);
+ self.hover_state.target_h = Some(0.0);
+ }
+ }
+
+ pub fn tick_hover(&mut self, dt: f32) -> bool {
+ let s = &mut self.hover_state;
+ let decay = 15.0;
+ let mut changed = false;
+
+ if s.current_alpha <= 0.001 && s.target_alpha > 0.0 {
+ if let (Some(tx), Some(ty), Some(tw), Some(th)) = (s.target_x, s.target_y, s.target_w, s.target_h) {
+ s.current_x = tx;
+ s.current_y = ty;
+ s.current_w = tw;
+ s.current_h = th;
+ }
+ }
+
+ if (s.current_alpha - s.target_alpha).abs() > 0.001 {
+ s.current_alpha += (s.target_alpha - s.current_alpha) * (1.0 - (-decay * dt).exp());
+ changed = true;
+ } else if s.current_alpha != s.target_alpha {
+ s.current_alpha = s.target_alpha;
+ changed = true;
+ }
+
+ if let (Some(tx), Some(ty), Some(tw), Some(th)) = (s.target_x, s.target_y, s.target_w, s.target_h) {
+ if (s.current_x - tx).abs() > 0.1 {
+ s.current_x += (tx - s.current_x) * (1.0 - (-decay * dt).exp());
+ changed = true;
+ } else if s.current_x != tx {
+ s.current_x = tx;
+ changed = true;
+ }
+
+ if (s.current_y - ty).abs() > 0.1 {
+ s.current_y += (ty - s.current_y) * (1.0 - (-decay * dt).exp());
+ changed = true;
+ } else if s.current_y != ty {
+ s.current_y = ty;
+ changed = true;
+ }
+
+ if (s.current_w - tw).abs() > 0.1 {
+ s.current_w += (tw - s.current_w) * (1.0 - (-decay * dt).exp());
+ changed = true;
+ } else if s.current_w != tw {
+ s.current_w = tw;
+ changed = true;
+ }
+
+ if (s.current_h - th).abs() > 0.1 {
+ s.current_h += (th - s.current_h) * (1.0 - (-decay * dt).exp());
+ changed = true;
+ } else if s.current_h != th {
+ s.current_h = th;
+ changed = true;
+ }
+ }
+
+ changed
+ }
+
+ pub fn get_hover_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
+ let s = &self.hover_state;
+ if s.current_alpha > 0.001 {
+ Some((
+ s.current_x,
+ s.current_y,
+ s.current_w,
+ s.current_h,
+ [1.0, 1.0, 1.0, s.current_alpha],
+ ))
+ } else {
+ None
+ }
+ }
+
+ // --- Context Menu ---
+ pub fn is_context_menu_visible(&self) -> bool {
+ self.context_menu.visible
+ }
+
+ pub fn show_context_menu(&mut self, x: f32, y: f32, options: Vec<String>, target: *mut TextBox) {
+ self.context_menu.show(x, y, options, target);
+ }
+
+ pub fn hide_context_menu(&mut self) {
+ self.context_menu.hide();
+ }
+
+ pub fn hit_test_context_menu(&self, px: f32, py: f32) -> bool {
+ self.context_menu.hit_test(px, py)
+ }
+
+ pub fn cursor_moved_context_menu(&mut self, px: f32, py: f32) -> bool {
+ self.context_menu.cursor_moved(px, py)
+ }
+
+ pub fn mouse_input_context_menu(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ self.context_menu.mouse_input(button, state, px, py)
+ }
+
+ pub fn context_menu_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ self.context_menu.extra_quads()
+ }
+
+ pub fn context_menu_labels(&self) -> Vec<crate::widget::display::TextLabel> {
+ self.context_menu.text_labels()
+ }
+}
diff --git a/src/engine.rs b/src/engine.rs
index f51d8e0..c8bfaf2 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -1,1891 +1,11 @@
-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_xdg_popup,
- registry::{ProvidesRegistryState, RegistryState},
- output::{OutputHandler, OutputState},
- seat::{
- keyboard::KeyboardHandler,
- pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
- Capability, SeatHandler, SeatState,
- },
- shell::{
- xdg::{
- window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
- XdgShell,
- },
- WaylandSurface,
- },
- shm::{Shm, ShmHandler},
+pub use crate::backend::window_runner::{
+ ActivePopup, Vertex, LineCap, WindowSettings, LogicalPosition, LogicalSize,
+ RenderContext, Application, PressedKey, EngineState, run,
+ quad_vertices, quad_vertices_with_clip, quad_vertices_clipped, line_vertices,
+ vector_vertices, rounded_rect_vertices_corners, push_rounded_rect_vertices_corners,
+ rounded_rect_vertices, push_rounded_rect_vertices, plate_bevel_vertices,
+ push_plate_bevel_vertices, widget_vertices, push_widget_vertices,
+ extra_quad_vertices, push_extra_quad_vertices, extra_quad_vertices_clipped,
+ push_extra_quad_vertices_clipped, circle_vertices, circle_border_vertices,
+ arc_background_vertices, push_arc_background_vertices,
};
-use wayland_client::{
- globals::{registry_queue_init, GlobalList},
- 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, 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)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct Vertex {
- pub position: [f32; 2],
- pub color: [f32; 4],
- pub clip_circle: [f32; 3], // [cx, cy, r]
-}
-
-impl Vertex {
- const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
- 0 => Float32x2,
- 1 => Float32x4,
- 2 => Float32x3,
- ];
-
- pub fn desc() -> wgpu::VertexBufferLayout<'static> {
- wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &Self::ATTRIBS,
- }
- }
-}
-
-pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
- let x0 = (x / sw) * 2.0 - 1.0;
- let y0 = 1.0 - (y / sh) * 2.0;
- let x1 = ((x + w) / sw) * 2.0 - 1.0;
- let y1 = 1.0 - ((y + h) / sh) * 2.0;
- [
- Vertex { position: [x0, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x1, y0], color: c, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x1, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x0, y1], color: c, clip_circle: [0.0, 0.0, 0.0] },
- ]
-}
-
-pub fn quad_vertices_with_clip(
- x: f32, y: f32, w: f32, h: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- clip_circle: [f32; 3],
-) -> [Vertex; 6] {
- let x0 = (x / sw) * 2.0 - 1.0;
- let y0 = 1.0 - (y / sh) * 2.0;
- let x1 = ((x + w) / sw) * 2.0 - 1.0;
- let y1 = 1.0 - ((y + h) / sh) * 2.0;
- [
- Vertex { position: [x0, y0], color, clip_circle },
- Vertex { position: [x1, y0], color, clip_circle },
- Vertex { position: [x0, y1], color, clip_circle },
- Vertex { position: [x1, y0], color, clip_circle },
- Vertex { position: [x1, y1], color, clip_circle },
- Vertex { position: [x0, y1], color, clip_circle },
- ]
-}
-
-pub fn quad_vertices_clipped(
- x: f32, y: f32, w: f32, h: f32,
- surface_w: f32, surface_h: f32,
- color: [f32; 4],
- clip: (f32, f32, f32, f32),
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let (cx0, cy0, cx1, cy1) = clip;
- let ix0 = x.max(cx0);
- let iy0 = y.max(cy0);
- let ix1 = (x + w).min(cx1);
- let iy1 = (y + h).min(cy1);
- if ix1 <= ix0 || iy1 <= iy0 {
- return Vec::new();
- }
- 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::Element, sw: f32, sh: f32, clip_circle: [f32; 3]) -> Vec<Vertex> {
- let (x, y, ww, h) = w.rect();
- let corners = w.rounded_corners();
- let mut verts = if corners != (false, false, false, false) {
- 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::Element,
- 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::Element,
- 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(
- cx: f32, cy: f32, r: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- for i in 0..segments {
- let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (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;
-
- 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 });
- }
- verts
-}
-
-pub fn circle_border_vertices(
- cx: f32, cy: f32, r: f32,
- thickness: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- for i in 0..segments {
- let theta1 = (i as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
- let theta2 = ((i + 1) as f32) * 2.0 * std::f32::consts::PI / (segments as f32);
-
- let x0 = cx + (r - thickness) * theta1.cos();
- let y0 = cy + (r - thickness) * theta1.sin();
- 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 x3 = cx + (r - thickness) * theta2.cos();
- let y3 = cy + (r - thickness) * 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 ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / 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 });
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
- }
- verts
-}
-
-pub fn arc_background_vertices(
- cx: f32, cy: f32, r: f32,
- thickness: f32,
- start_angle: f32, end_angle: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
- clip_circle: [f32; 3],
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- for i in 0..segments {
- let theta1 = start_angle + (i as f32) * (end_angle - start_angle) / (segments as f32);
- let theta2 = start_angle + ((i + 1) as f32) * (end_angle - start_angle) / (segments as f32);
-
- let x0 = cx + (r - thickness) * theta1.cos();
- let y0 = cy + (r - thickness) * theta1.sin();
- 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 x3 = cx + (r - thickness) * theta2.cos();
- let y3 = cy + (r - thickness) * 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 ndc_x3 = (x3 / sw) * 2.0 - 1.0; let ndc_y3 = 1.0 - (y3 / 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 });
-
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- verts.push(Vertex { position: [ndc_x3, ndc_y3], color, clip_circle });
- }
- verts
-}
-
-
-#[derive(Debug, Clone)]
-pub struct WindowSettings {
- pub title: String,
- pub app_id: String,
- pub width: u32,
- pub height: u32,
- pub fullscreen: bool,
- pub min_size: Option<(u32, u32)>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct LogicalPosition {
- pub x: f32,
- pub y: f32,
-}
-
-impl LogicalPosition {
- pub fn new(x: f32, y: f32) -> Self {
- Self { x, y }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct LogicalSize {
- pub width: f32,
- pub height: f32,
-}
-
-impl LogicalSize {
- pub fn new(width: f32, height: f32) -> Self {
- Self { width, height }
- }
-}
-
-pub struct RenderContext<'a> {
- pub font_system: &'a mut FontSystem,
-}
-
-pub trait Application: Sized + 'static {
- type Message: Send + Clone + 'static;
-
- fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self;
- fn settings(&self) -> WindowSettings;
- 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| {
- 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()
- }
-
- fn clear_color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
- }
-
- fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool);
- fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message>;
- fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool);
- fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message>;
-}
-
-pub struct PressedKey {
- pub logical_key: Key,
- pub text: Option<String>,
- pub first_pressed: Instant,
- pub last_repeated: Instant,
-}
-
-fn is_repeatable_key(key: &Key) -> bool {
- match key {
- Key::Named(NamedKey::Backspace) |
- Key::Named(NamedKey::Delete) |
- Key::Named(NamedKey::ArrowLeft) |
- Key::Named(NamedKey::ArrowRight) |
- Key::Named(NamedKey::ArrowUp) |
- Key::Named(NamedKey::ArrowDown) |
- Key::Named(NamedKey::Home) |
- Key::Named(NamedKey::End) |
- Key::Character(_) => true,
- _ => false,
- }
-}
-
-pub struct EngineState<A: Application> {
- pub registry_state: RegistryState,
- pub compositor_state: CompositorState,
- pub xdg_shell_state: XdgShell,
- pub shm_state: Shm,
- pub seat_state: SeatState,
- pub output_state: OutputState,
- pub seats: Vec<wl_seat::WlSeat>,
- pub pointer: Option<ThemedPointer>,
- pub keyboard: Option<wl_keyboard::WlKeyboard>,
-
- pub window: Option<XdgWindow>,
- pub surface: Option<wl_surface::WlSurface>,
-
- 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>,
- pub config: Option<wgpu::SurfaceConfiguration>,
- pub render_pipeline: Option<wgpu::RenderPipeline>,
- pub vertex_buffer: Option<wgpu::Buffer>,
- pub vertex_count: u32,
- pub overlay_vertex_buffer: Option<wgpu::Buffer>,
- pub overlay_vertex_count: u32,
-
- pub font_system: FontSystem,
- pub swash_cache: SwashCache,
- pub text_atlas: Option<TextAtlas>,
- pub text_renderer: Option<TextRenderer>,
- pub text_viewport: Option<Viewport>,
-
- pub scale_factor: f64,
- pub logical_width: f32,
- pub logical_height: f32,
-
- pub exit: bool,
- pub redraw: bool,
- pub first_configure_received: bool,
- pub ctrl_pressed: bool,
- 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> {
- pub async fn init_gpu(&mut self, conn: &Connection, width_logical: f32, height_logical: f32) {
- let s = self.scale_factor as f32;
- let pw = (width_logical * s) as u32;
- let ph = (height_logical * s) as u32;
-
- let surface = self.surface.as_ref().expect("surface missing");
-
- let wayland_handle = Box::leak(Box::new(WaylandSurfaceHandle {
- display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
- surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
- }));
-
- let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
- 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 {
- power_preference: wgpu::PowerPreference::LowPower,
- compatible_surface: Some(&wgpu_surface),
- force_fallback_adapter: false,
- }).await.expect("failed to request adapter");
-
- let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
- label: Some("GPU Device"),
- required_features: wgpu::Features::empty(),
- required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
- memory_hints: wgpu::MemoryHints::MemoryUsage,
- }, None).await.expect("failed to request device");
-
- 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"),
- source: wgpu::ShaderSource::Wgsl(crate::SHADER.into()),
- });
-
- let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("Pipeline Layout"),
- bind_group_layouts: &[],
- push_constant_ranges: &[],
- });
-
- let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("Render Pipeline"),
- layout: Some(&pipeline_layout),
- vertex: wgpu::VertexState {
- module: &shader,
- entry_point: Some("vs_main"),
- buffers: &[Vertex::desc()],
- compilation_options: Default::default(),
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader,
- entry_point: Some("fs_main"),
- targets: &[Some(wgpu::ColorTargetState {
- format: config.format,
- blend: Some(wgpu::BlendState::ALPHA_BLENDING),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- compilation_options: Default::default(),
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- front_face: wgpu::FrontFace::Ccw,
- cull_mode: None,
- polygon_mode: wgpu::PolygonMode::Fill,
- unclipped_depth: false,
- conservative: false,
- strip_index_format: None,
- },
- depth_stencil: None,
- multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
- multiview: None,
- cache: None,
- });
-
- let cache = Cache::new(&device);
- let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
- let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
- let mut text_viewport = Viewport::new(&device, &cache);
- text_viewport.update(&queue, Resolution { width: pw, height: ph });
-
- let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let overlay_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Overlay Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- self.wgpu_surface = Some(wgpu_surface);
- self.device = Some(device);
- self.queue = Some(queue);
- self.config = Some(config);
- self.render_pipeline = Some(render_pipeline);
- self.text_atlas = Some(text_atlas);
- self.text_renderer = Some(text_renderer);
- self.text_viewport = Some(text_viewport);
- self.vertex_buffer = Some(vertex_buffer);
- self.overlay_vertex_buffer = Some(overlay_vertex_buffer);
- self.logical_width = width_logical;
- self.logical_height = height_logical;
- }
-
- pub fn resize(&mut self, w: f32, h: f32) {
- if w > 0.0 && h > 0.0 {
- self.logical_width = w;
- self.logical_height = h;
- if let (Some(device), Some(surface), Some(config)) = (&self.device, &self.wgpu_surface, &mut self.config) {
- config.width = (w as f64 * self.scale_factor) as u32;
- config.height = (h as f64 * self.scale_factor) as u32;
- surface.configure(device, config);
- }
- }
- }
-
- pub fn render(&mut self) {
- let logical_w = self.logical_width;
- let logical_h = self.logical_height;
- let scale_factor = self.scale_factor;
-
- 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();
- let render_pipeline = self.render_pipeline.as_ref().unwrap();
- let text_renderer = self.text_renderer.as_mut().unwrap();
- let text_atlas = self.text_atlas.as_mut().unwrap();
- let text_viewport = self.text_viewport.as_mut().unwrap();
- let config = self.config.as_ref().unwrap();
-
- // 1. Build and upload vertex buffer
- let mut verts = Vec::new();
- 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);
- let needed = data.len() as wgpu::BufferAddress;
- let mut vbuf = self.vertex_buffer.as_ref().unwrap();
- if needed > vbuf.size() {
- let new_vbuf = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- self.vertex_buffer = Some(new_vbuf);
- vbuf = self.vertex_buffer.as_ref().unwrap();
- }
- queue.write_buffer(vbuf, 0, data);
- }
-
- // 1b. Build and upload overlay vertex buffer
- let mut overlay_quads = Vec::new();
- self.inner.overlay_quads(&mut overlay_quads, LogicalSize::new(logical_w, logical_h), scale_factor);
- let mut overlay_verts = Vec::new();
- for &(qx, qy, qw, qh, qc) in &overlay_quads {
- overlay_verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
- }
- self.overlay_vertex_count = overlay_verts.len() as u32;
- if self.overlay_vertex_count > 0 {
- let data = bytemuck::cast_slice(&overlay_verts);
- let needed = data.len() as wgpu::BufferAddress;
- let mut ovbuf = self.overlay_vertex_buffer.as_ref().unwrap();
- if needed > ovbuf.size() {
- let new_ovbuf = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Overlay Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- self.overlay_vertex_buffer = Some(new_ovbuf);
- ovbuf = self.overlay_vertex_buffer.as_ref().unwrap();
- }
- queue.write_buffer(ovbuf, 0, data);
- }
-
- // 2. Prepare text
- let scale_f32 = scale_factor as f32;
- let pw = (logical_w * scale_f32) as u32;
- let ph = (logical_h * scale_f32) as u32;
- text_viewport.update(queue, Resolution { width: pw, height: ph });
-
- let bounds = TextBounds { left: 0, top: 0, right: pw as i32, bottom: ph as i32 };
- let areas = self.inner.text_areas(scale_f32, bounds);
-
- text_renderer.prepare(device, queue, &mut self.font_system, text_atlas, text_viewport, areas, &mut self.swash_cache).unwrap();
-
- // 3. Render Pass
- let output = match wgpu_surface.get_current_texture() {
- Ok(t) => t,
- Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
- wgpu_surface.configure(device, config);
- return;
- }
- Err(wgpu::SurfaceError::Timeout) => return,
- Err(e) => {
- eprintln!("Surface error: {e:?}");
- return;
- }
- };
- let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
- let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
- label: Some("Encoder"),
- });
-
- {
- let cc = self.inner.clear_color();
- let r_clear = (cc[0] as f64).powf(2.2);
- let g_clear = (cc[1] as f64).powf(2.2);
- let b_clear = (cc[2] as f64).powf(2.2);
- let a_clear = cc[3] as f64;
-
- let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("Render Pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: &view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color {
- r: r_clear,
- g: g_clear,
- b: b_clear,
- a: a_clear,
- }),
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
- if self.vertex_count > 0 {
- pass.set_pipeline(render_pipeline);
- pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
- pass.draw(0..self.vertex_count, 0..1);
- }
-
- text_renderer.render(text_atlas, text_viewport, &mut pass).unwrap();
-
- if self.overlay_vertex_count > 0 {
- pass.set_pipeline(render_pipeline);
- pass.set_vertex_buffer(0, self.overlay_vertex_buffer.as_ref().unwrap().slice(..));
- pass.draw(0..self.overlay_vertex_count, 0..1);
- }
- }
-
- 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;
- self.render_pipeline = None;
- self.vertex_buffer = None;
- self.overlay_vertex_buffer = None;
- self.text_atlas = None;
- self.text_renderer = None;
- self.text_viewport = None;
- }
-}
-
-impl<A: Application> CompositorHandler for EngineState<A> {
- fn scale_factor_changed(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- scale_factor: i32,
- ) {
- _surface.set_buffer_scale(scale_factor);
- self.scale_factor = scale_factor as f64;
- self.resize(self.logical_width, self.logical_height);
- self.redraw = true;
- }
-
- fn transform_changed(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _new_transform: wl_output::Transform,
- ) {}
-
- fn frame(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _time: u32,
- ) {}
-
- fn surface_enter(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _output: &wl_output::WlOutput,
- ) {}
-
- fn surface_leave(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _output: &wl_output::WlOutput,
- ) {}
-}
-
-impl<A: Application> OutputHandler for EngineState<A> {
- fn output_state(&mut self) -> &mut OutputState {
- &mut self.output_state
- }
-
- fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
- fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
- fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-}
-
-impl<A: Application> ShmHandler for EngineState<A> {
- fn shm_state(&mut self) -> &mut Shm {
- &mut self.shm_state
- }
-}
-
-impl<A: Application> ProvidesRegistryState for EngineState<A> {
- fn registry(&mut self) -> &mut RegistryState {
- &mut self.registry_state
- }
-
- fn runtime_add_global(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _name: u32,
- _interface: &str,
- _version: u32,
- ) {}
-
- fn runtime_remove_global(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _name: u32,
- _interface: &str,
- ) {}
-}
-
-impl<A: Application> WindowHandler for EngineState<A> {
- fn configure(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _window: &XdgWindow,
- configure: WindowConfigure,
- _serial: u32,
- ) {
- let (w, h) = configure.new_size;
- if let (Some(w), Some(h)) = (w, h) {
- let width = w.get();
- let height = h.get();
- self.resize(width as f32, height as f32);
- }
- self.redraw = true;
- self.first_configure_received = true;
- }
-
- fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
- self.exit = true;
- }
-}
-
-impl<A: Application> SeatHandler for EngineState<A> {
- fn seat_state(&mut self) -> &mut SeatState {
- &mut self.seat_state
- }
-
- fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
- self.seats.push(seat);
- }
-
- fn new_capability(
- &mut self,
- _conn: &Connection,
- qh: &QueueHandle<Self>,
- seat: wl_seat::WlSeat,
- capability: Capability,
- ) {
- if capability == Capability::Pointer && self.pointer.is_none() {
- let surface = self.compositor_state.create_surface(qh);
- let themed_pointer = self.seat_state.get_pointer_with_theme(
- qh,
- &seat,
- self.shm_state.wl_shm(),
- surface,
- ThemeSpec::System,
- ).unwrap();
- self.pointer = Some(themed_pointer);
- }
- if capability == Capability::Keyboard && self.keyboard.is_none() {
- let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
- self.keyboard = Some(keyboard);
- }
- }
-
- fn remove_capability(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _seat: wl_seat::WlSeat,
- capability: Capability,
- ) {
- if capability == Capability::Pointer {
- self.pointer = None;
- }
- if capability == Capability::Keyboard {
- self.keyboard = None;
- }
- }
-
- fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
- self.seats.retain(|s| s != &seat);
- }
-}
-
-impl<A: Application> PointerHandler for EngineState<A> {
- fn pointer_frame(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _pointer: &wl_pointer::WlPointer,
- events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
- ) {
- use smithay_client_toolkit::seat::pointer::PointerEventKind;
- for event in events {
- let (x, y) = event.position;
- 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 { .. } => {
- if let Some(ref themed_pointer) = self.pointer {
- let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
- }
- }
- PointerEventKind::Leave { .. } => {}
- PointerEventKind::Motion { .. } => {
- let mut rebuild = false;
- self.inner.handle_pointer_move(LogicalPosition::new(lx, ly), &mut rebuild);
- if rebuild {
- self.redraw = true;
- }
- }
- PointerEventKind::Press { button, .. } => {
- let btn = match *button {
- 272 => MouseButton::Left,
- 273 => MouseButton::Right,
- 274 => MouseButton::Middle,
- _ => continue,
- };
- let mut rebuild = false;
- if let Some(msg) = self.inner.handle_mouse_input(btn, ElementState::Pressed, LogicalPosition::new(lx, ly), &mut rebuild) {
- let mut update_rebuild = false;
- self.inner.update(msg, &mut update_rebuild, &mut self.exit);
- if update_rebuild {
- rebuild = true;
- }
- }
- if rebuild {
- self.redraw = true;
- }
- }
- PointerEventKind::Release { button, .. } => {
- let btn = match *button {
- 272 => MouseButton::Left,
- 273 => MouseButton::Right,
- 274 => MouseButton::Middle,
- _ => continue,
- };
- let mut rebuild = false;
- if let Some(msg) = self.inner.handle_mouse_input(btn, ElementState::Released, LogicalPosition::new(lx, ly), &mut rebuild) {
- let mut update_rebuild = false;
- self.inner.update(msg, &mut update_rebuild, &mut self.exit);
- if update_rebuild {
- rebuild = true;
- }
- }
- if rebuild {
- self.redraw = true;
- }
- }
- PointerEventKind::Axis { horizontal, vertical, .. } => {
- let h_scroll = horizontal.absolute as f32;
- let v_scroll = vertical.absolute as f32;
- let delta = MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
- let mut rebuild = false;
- self.inner.handle_mouse_wheel(&delta, LogicalPosition::new(lx, ly), &mut rebuild);
- if rebuild {
- self.redraw = true;
- }
- }
- }
- }
- }
-}
-
-impl<A: Application> KeyboardHandler for EngineState<A> {
- fn enter(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _surface: &wl_surface::WlSurface,
- _serial: u32,
- _raw_modifiers: &[u32],
- _keysyms: &[xkeysym::Keysym],
- ) {}
-
- fn leave(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _surface: &wl_surface::WlSurface,
- _serial: u32,
- ) {
- self.pressed_key = None;
- self.ctrl_pressed = false;
- self.shift_pressed = false;
- }
-
- fn press_key(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- event: smithay_client_toolkit::seat::keyboard::KeyEvent,
- ) {
- self.handle_key(event, ElementState::Pressed);
- }
-
- fn release_key(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- event: smithay_client_toolkit::seat::keyboard::KeyEvent,
- ) {
- self.handle_key(event, ElementState::Released);
- }
-
- fn update_modifiers(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
- _layout: u32,
- ) {
- self.ctrl_pressed = modifiers.ctrl;
- self.shift_pressed = modifiers.shift;
- }
-}
-
-impl<A: Application> EngineState<A> {
- fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: ElementState) {
- let logical_key = match event.keysym {
- xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
- xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
- xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
- xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
- xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
- xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
- xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
- xkeysym::Keysym::Tab => Key::Named(NamedKey::Tab),
- xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
- xkeysym::Keysym::space => Key::Named(NamedKey::Space),
- _ => {
- if let Some(ref text) = event.utf8 {
- Key::Character(text.clone())
- } else if let Some(ch) = event.keysym.key_char() {
- Key::Character(ch.to_string())
- } else {
- return;
- }
- }
- };
-
- let custom_event = KeyEvent {
- state,
- logical_key,
- text: event.utf8.clone(),
- repeat: false,
- ctrl: self.ctrl_pressed,
- shift: self.shift_pressed,
- };
-
- if state == ElementState::Pressed {
- if is_repeatable_key(&custom_event.logical_key) {
- self.pressed_key = Some(PressedKey {
- logical_key: custom_event.logical_key.clone(),
- text: custom_event.text.clone(),
- first_pressed: Instant::now(),
- last_repeated: Instant::now(),
- });
- } else {
- self.pressed_key = None;
- }
- } else if state == ElementState::Released {
- if let Some(ref pk) = self.pressed_key {
- if pk.logical_key == custom_event.logical_key {
- self.pressed_key = None;
- }
- }
- }
-
- let mut rebuild = false;
- if let Some(msg) = self.inner.handle_key_input(&custom_event, &mut rebuild) {
- let mut update_rebuild = false;
- self.inner.update(msg, &mut update_rebuild, &mut self.exit);
- if update_rebuild {
- rebuild = true;
- }
- }
- if rebuild {
- self.redraw = true;
- }
- }
-}
-
-impl<A: Application> wayland_client::Dispatch<wl_registry::WlRegistry, GlobalList, Self> for EngineState<A> {
- fn event(
- _state: &mut Self,
- _proxy: &wl_registry::WlRegistry,
- _event: wl_registry::Event,
- _data: &GlobalList,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- ) {}
-}
-
-impl<A: Application> wayland_client::Dispatch<crate::protocol::zclear_inspector_v1::ZclearInspectorV1, ()> for EngineState<A> {
- fn event(
- _state: &mut Self,
- _proxy: &crate::protocol::zclear_inspector_v1::ZclearInspectorV1,
- _event: crate::protocol::zclear_inspector_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- ) {}
-}
-
-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::Element);
- 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>);
-delegate_keyboard!(@<A: Application> EngineState<A>);
-delegate_registry!(@<A: Application> EngineState<A>);
-delegate_output!(@<A: Application> EngineState<A>);
-
-pub fn run<A: Application>() {
- let conn = Connection::connect_to_env().unwrap();
- let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
- let qh = event_queue.handle();
-
- let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
- let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
- let shm_state = Shm::bind(&globals, &qh).unwrap();
- let seat_state = SeatState::new(&globals, &qh);
- let output_state = OutputState::new(&globals, &qh);
-
- let (sender, channel) = calloop::channel::channel::<A::Message>();
-
- let inner = A::new(&qh, sender.clone());
- let settings = inner.settings();
-
- let font_system = FontSystem::new();
- let swash_cache = SwashCache::new();
-
- let mut engine_state = EngineState {
- registry_state: RegistryState::new(&globals),
- compositor_state,
- xdg_shell_state,
- shm_state,
- seat_state,
- output_state,
- seats: Vec::new(),
- pointer: None,
- keyboard: None,
- window: None,
- surface: None,
- inner,
- instance: None,
- wgpu_surface: None,
- device: None,
- queue: None,
- config: None,
- render_pipeline: None,
- vertex_buffer: None,
- vertex_count: 0,
- overlay_vertex_buffer: None,
- overlay_vertex_count: 0,
- font_system,
- swash_cache,
- text_atlas: None,
- text_renderer: None,
- text_viewport: None,
- scale_factor: 1.0,
- logical_width: settings.width as f32,
- logical_height: settings.height as f32,
- exit: false,
- redraw: false,
- first_configure_received: false,
- ctrl_pressed: false,
- shift_pressed: false,
- pressed_key: None,
- sender,
- active_popup: None,
- qh: qh.clone(),
- };
-
- event_queue.roundtrip(&mut engine_state).unwrap();
-
- let scale = detect_scale_factor(&engine_state.output_state);
- engine_state.scale_factor = scale;
-
- let surface = engine_state.compositor_state.create_surface(&qh);
- surface.set_buffer_scale(scale as i32);
- let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
- window.set_title(&settings.title);
- window.set_app_id(&settings.app_id);
- if settings.fullscreen {
- window.set_fullscreen(None);
- }
- if let Some((min_w, min_h)) = settings.min_size {
- window.set_min_size(Some((min_w, min_h)));
- }
- window.commit();
-
- engine_state.window = Some(window);
- engine_state.surface = Some(surface);
-
- pollster::block_on(engine_state.init_gpu(&conn, settings.width as f32, settings.height as f32));
-
- let mut event_loop = EventLoop::try_new().unwrap();
- let loop_handle = event_loop.handle();
- 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 {
- let mut rebuild = false;
- app_state.inner.update(msg, &mut rebuild, &mut app_state.exit);
- if rebuild {
- app_state.redraw = true;
- }
- }
- }).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_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();
- if engine_state.exit {
- break;
- }
-
- let now = std::time::Instant::now();
- let mut dt = now.duration_since(last_tick).as_secs_f32();
- last_tick = now;
- if dt > 0.1 {
- dt = 0.1;
- }
-
- let mut rebuild = false;
- engine_state.inner.tick(dt, &mut rebuild);
- if rebuild {
- engine_state.redraw = true;
- }
-
- if let Some(ref mut pk) = engine_state.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 {
- pk.last_repeated = now;
- let custom_event = KeyEvent {
- state: ElementState::Pressed,
- logical_key: pk.logical_key.clone(),
- text: pk.text.clone(),
- repeat: true,
- ctrl: engine_state.ctrl_pressed,
- shift: engine_state.shift_pressed,
- };
- let mut key_rebuild = false;
- if let Some(msg) = engine_state.inner.handle_key_input(&custom_event, &mut key_rebuild) {
- let mut update_rebuild = false;
- engine_state.inner.update(msg, &mut update_rebuild, &mut engine_state.exit);
- if update_rebuild {
- key_rebuild = true;
- }
- }
- if key_rebuild {
- engine_state.redraw = true;
- }
- }
- }
- }
- 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;
- if engine_state.first_configure_received {
- engine_state.render();
- }
- }
- }
- drop(engine_state);
-}
diff --git a/src/layout.rs b/src/layout.rs
index 32f9612..f47ed8b 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1,4 +1,816 @@
use crate::widget::Element;
+use crate::context::UiContext;
+use std::sync::RwLock;
+
+fn read_config() -> Option<String> {
+ let paths = [
+ "/home/lsgalante/.config/cce/config.toml",
+ "/home/lsgalante/.config/ccec/config.toml",
+ ];
+ for path in &paths {
+ if let Ok(content) = std::fs::read_to_string(path) {
+ return Some(content);
+ }
+ }
+ None
+}
+
+pub fn parse_font_string(s: &str) -> (String, Option<f32>) {
+ let s = s.trim();
+ if let Some(last_space_idx) = s.rfind(' ') {
+ let (family, size_str) = s.split_at(last_space_idx);
+ let size_str = size_str.trim();
+ if let Ok(size) = size_str.parse::<f32>() {
+ return (family.trim().to_string(), Some(size));
+ }
+ }
+ (s.to_string(), None)
+}
+
+static SECTION_PADDING: RwLock<f32> = RwLock::new(8.0);
+static SPINBOX_HEIGHT: RwLock<f32> = RwLock::new(26.0);
+static COLOR_SELECTOR_HEIGHT: RwLock<f32> = RwLock::new(22.0);
+static TEXTBOX_HEIGHT: RwLock<f32> = RwLock::new(44.0);
+static FONT_SELECTOR_HEIGHT: RwLock<f32> = RwLock::new(44.0);
+static SLIDER_HEIGHT: RwLock<f32> = RwLock::new(28.0);
+static TOGGLE_HEIGHT: RwLock<f32> = RwLock::new(44.0);
+static COLOR_SELECTOR_FONT: RwLock<String> = RwLock::new(String::new());
+static COLOR_SELECTOR_PREVIEW_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
+static COLOR_SELECTOR_PREVIEW_MARGIN: RwLock<f32> = RwLock::new(0.0);
+static MENUBAR_FONT: RwLock<String> = RwLock::new(String::new());
+static MENUBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
+static SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
+static NESTED_SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
+
+static PAGINATOR_TAB_MARGIN_X: RwLock<f32> = RwLock::new(5.0);
+static PAGINATOR_TAB_MARGIN_Y: RwLock<f32> = RwLock::new(10.0);
+static PAGINATOR_TAB_PADDING_X: RwLock<f32> = RwLock::new(10.0);
+static PAGINATOR_TAB_PADDING_Y: RwLock<f32> = RwLock::new(14.0);
+
+static PLATE_PADDING: RwLock<f32> = RwLock::new(20.0);
+static DROPDOWN_HEIGHT: RwLock<f32> = RwLock::new(44.0);
+static NESTED_SECTION_LABEL_ALIGNMENT: RwLock<u8> = RwLock::new(0);
+
+static LABEL_MARGIN: RwLock<f32> = RwLock::new(6.0);
+
+pub fn label_margin() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("label_margin") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = LABEL_MARGIN.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *LABEL_MARGIN.read().unwrap()
+}
+
+pub fn set_label_margin(margin: f32) {
+ if let Ok(mut lock) = LABEL_MARGIN.write() {
+ *lock = margin;
+ }
+}
+
+pub fn nested_section_label_alignment() -> u8 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("nested_section_label_alignment") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<u8>() {
+ if let Ok(mut lock) = NESTED_SECTION_LABEL_ALIGNMENT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *NESTED_SECTION_LABEL_ALIGNMENT.read().unwrap()
+}
+
+pub fn set_nested_section_label_alignment(align: u8) {
+ if let Ok(mut lock) = NESTED_SECTION_LABEL_ALIGNMENT.write() {
+ *lock = align;
+ }
+}
+
+static NESTED_SECTION_LABEL_OFFSET: RwLock<f32> = RwLock::new(0.0);
+
+pub fn nested_section_label_offset() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("nested_section_label_offset") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = NESTED_SECTION_LABEL_OFFSET.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *NESTED_SECTION_LABEL_OFFSET.read().unwrap()
+}
+
+pub fn set_nested_section_label_offset(offset: f32) {
+ if let Ok(mut lock) = NESTED_SECTION_LABEL_OFFSET.write() {
+ *lock = offset;
+ }
+}
+
+
+pub fn plate_padding() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("plate_padding") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = PLATE_PADDING.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *PLATE_PADDING.read().unwrap()
+}
+
+pub fn set_plate_padding(padding: f32) {
+ if let Ok(mut lock) = PLATE_PADDING.write() {
+ *lock = padding;
+ }
+}
+
+static PAGE_MARGIN: RwLock<f32> = RwLock::new(20.0);
+
+pub fn page_margin() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("page_margin") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = PAGE_MARGIN.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *PAGE_MARGIN.read().unwrap()
+}
+
+pub fn set_page_margin(margin: f32) {
+ if let Ok(mut lock) = PAGE_MARGIN.write() {
+ *lock = margin;
+ }
+}
+
+static GRID_MIN_COL_WIDTH: RwLock<f32> = RwLock::new(260.0);
+
+pub fn grid_min_col_width() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("grid_min_col_width") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = GRID_MIN_COL_WIDTH.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *GRID_MIN_COL_WIDTH.read().unwrap()
+}
+
+pub fn set_grid_min_col_width(width: f32) {
+ if let Ok(mut lock) = GRID_MIN_COL_WIDTH.write() {
+ *lock = width;
+ }
+}
+
+pub fn section_padding() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("section_padding") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = SECTION_PADDING.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *SECTION_PADDING.read().unwrap()
+}
+
+pub fn set_section_padding(padding: f32) {
+ if let Ok(mut lock) = SECTION_PADDING.write() {
+ *lock = padding;
+ }
+}
+
+pub fn spinbox_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("spinbox_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = SPINBOX_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *SPINBOX_HEIGHT.read().unwrap()
+}
+
+pub fn set_spinbox_height(height: f32) {
+ if let Ok(mut lock) = SPINBOX_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
+pub fn toggle_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("toggle_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = TOGGLE_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *TOGGLE_HEIGHT.read().unwrap()
+}
+
+pub fn set_toggle_height(height: f32) {
+ if let Ok(mut lock) = TOGGLE_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
+pub fn color_selector_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("color_selector_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = COLOR_SELECTOR_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *COLOR_SELECTOR_HEIGHT.read().unwrap()
+}
+
+pub fn set_color_selector_height(height: f32) {
+ if let Ok(mut lock) = COLOR_SELECTOR_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
+pub fn font_selector_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("font_selector_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = FONT_SELECTOR_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *FONT_SELECTOR_HEIGHT.read().unwrap()
+}
+
+pub fn set_font_selector_height(height: f32) {
+ if let Ok(mut lock) = FONT_SELECTOR_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
+pub fn color_selector_font() -> String {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ let mut font = "monospace".to_string();
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("color_selector_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = rest.trim();
+ let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+ &rest[1..rest.len() - 1]
+ } else {
+ rest
+ };
+ font = val_str.trim().to_string();
+ }
+ }
+ }
+ if let Ok(mut lock) = COLOR_SELECTOR_FONT.write() {
+ *lock = font;
+ }
+ });
+ let lock = COLOR_SELECTOR_FONT.read().unwrap();
+ if lock.is_empty() {
+ "monospace".to_string()
+ } else {
+ lock.clone()
+ }
+}
+
+pub fn set_color_selector_font(font: &str) {
+ if let Ok(mut lock) = COLOR_SELECTOR_FONT.write() {
+ *lock = font.to_string();
+ }
+}
+
+pub fn menubar_font() -> String {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ let mut font = "Outfit".to_string();
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("menubar_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = rest.trim();
+ let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+ &rest[1..rest.len() - 1]
+ } else {
+ rest
+ };
+ font = val_str.trim().to_string();
+ }
+ }
+ }
+ if let Ok(mut lock) = MENUBAR_FONT.write() {
+ *lock = font;
+ }
+ });
+ let lock = MENUBAR_FONT.read().unwrap();
+ if lock.is_empty() {
+ "Outfit".to_string()
+ } else {
+ lock.clone()
+ }
+}
+
+pub fn menubar_font_parsed() -> (String, f32) {
+ if let Ok(lock) = MENUBAR_FONT_CACHED.read() {
+ if let Some(ref val) = *lock {
+ return val.clone();
+ }
+ }
+ let font_str = menubar_font();
+ let parsed = parse_font_string(&font_str);
+ let size = parsed.1.unwrap_or(12.0);
+ let val = (parsed.0, size);
+ if let Ok(mut lock) = MENUBAR_FONT_CACHED.write() {
+ *lock = Some(val.clone());
+ }
+ val
+}
+
+pub fn set_menubar_font(font: &str) {
+ if let Ok(mut lock) = MENUBAR_FONT.write() {
+ *lock = font.to_string();
+ }
+ if let Ok(mut lock) = MENUBAR_FONT_CACHED.write() {
+ *lock = None;
+ }
+}
+
+pub fn section_label_font() -> String {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ let mut font = "Outfit".to_string();
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("section_label_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = rest.trim();
+ let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+ &rest[1..rest.len() - 1]
+ } else {
+ rest
+ };
+ font = val_str.trim().to_string();
+ }
+ }
+ }
+ if let Ok(mut lock) = SECTION_LABEL_FONT.write() {
+ *lock = font;
+ }
+ });
+ let lock = SECTION_LABEL_FONT.read().unwrap();
+ if lock.is_empty() {
+ "Outfit".to_string()
+ } else {
+ lock.clone()
+ }
+}
+
+pub fn set_section_label_font(font: &str) {
+ if let Ok(mut lock) = SECTION_LABEL_FONT.write() {
+ *lock = font.to_string();
+ }
+}
+
+pub fn nested_section_label_font() -> String {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ let mut font = "Outfit".to_string();
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("nested_section_label_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = rest.trim();
+ let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
+ &rest[1..rest.len() - 1]
+ } else {
+ rest
+ };
+ font = val_str.trim().to_string();
+ }
+ }
+ }
+ if let Ok(mut lock) = NESTED_SECTION_LABEL_FONT.write() {
+ *lock = font;
+ }
+ });
+ let lock = NESTED_SECTION_LABEL_FONT.read().unwrap();
+ if lock.is_empty() {
+ "Outfit".to_string()
+ } else {
+ lock.clone()
+ }
+}
+
+pub fn set_nested_section_label_font(font: &str) {
+ if let Ok(mut lock) = NESTED_SECTION_LABEL_FONT.write() {
+ *lock = font.to_string();
+ }
+}
+
+pub fn color_selector_preview_corner_radius() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("color_selector_preview_corner_radius") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_CORNER_RADIUS.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *COLOR_SELECTOR_PREVIEW_CORNER_RADIUS.read().unwrap()
+}
+
+pub fn set_color_selector_preview_corner_radius(radius: f32) {
+ if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_CORNER_RADIUS.write() {
+ *lock = radius;
+ }
+}
+
+pub fn color_selector_preview_margin() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("color_selector_preview_margin") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_MARGIN.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *COLOR_SELECTOR_PREVIEW_MARGIN.read().unwrap()
+}
+
+pub fn set_color_selector_preview_margin(margin: f32) {
+ if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_MARGIN.write() {
+ *lock = margin;
+ }
+}
+
+pub fn paginator_tab_margin_x() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ let mut general_margin = None;
+ let mut x_margin = None;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("paginator_tab_margin_x") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ x_margin = Some(val);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("paginator_tab_margin") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ general_margin = Some(val);
+ }
+ }
+ }
+ let val = x_margin.or(general_margin).unwrap_or(5.0);
+ if let Ok(mut lock) = PAGINATOR_TAB_MARGIN_X.write() {
+ *lock = val;
+ }
+ }
+ });
+ *PAGINATOR_TAB_MARGIN_X.read().unwrap()
+}
+
+pub fn set_paginator_tab_margin_x(margin: f32) {
+ if let Ok(mut lock) = PAGINATOR_TAB_MARGIN_X.write() {
+ *lock = margin;
+ }
+}
+
+pub fn paginator_tab_margin_y() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ let mut general_margin = None;
+ let mut y_margin = None;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("paginator_tab_margin_y") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ y_margin = Some(val);
+ }
+ } else if let Some(rest) = trimmed.strip_prefix("paginator_tab_margin") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ general_margin = Some(val);
+ }
+ }
+ }
+ let val = y_margin.or(general_margin).unwrap_or(10.0);
+ if let Ok(mut lock) = PAGINATOR_TAB_MARGIN_Y.write() {
+ *lock = val;
+ }
+ }
+ });
+ *PAGINATOR_TAB_MARGIN_Y.read().unwrap()
+}
+
+pub fn set_paginator_tab_margin_y(margin: f32) {
+ if let Ok(mut lock) = PAGINATOR_TAB_MARGIN_Y.write() {
+ *lock = margin;
+ }
+}
+
+pub fn paginator_tab_padding_x() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("paginator_tab_padding_x") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = PAGINATOR_TAB_PADDING_X.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *PAGINATOR_TAB_PADDING_X.read().unwrap()
+}
+
+pub fn set_paginator_tab_padding_x(padding: f32) {
+ if let Ok(mut lock) = PAGINATOR_TAB_PADDING_X.write() {
+ *lock = padding;
+ }
+}
+
+pub fn paginator_tab_padding_y() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("paginator_tab_padding_y") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = PAGINATOR_TAB_PADDING_Y.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *PAGINATOR_TAB_PADDING_Y.read().unwrap()
+}
+
+pub fn set_paginator_tab_padding_y(padding: f32) {
+ if let Ok(mut lock) = PAGINATOR_TAB_PADDING_Y.write() {
+ *lock = padding;
+ }
+}
+
+pub fn textbox_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("textbox_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = TEXTBOX_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *TEXTBOX_HEIGHT.read().unwrap()
+}
+
+pub fn set_textbox_height(height: f32) {
+ if let Ok(mut lock) = TEXTBOX_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
+pub fn dropdown_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("dropdown_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = DROPDOWN_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *DROPDOWN_HEIGHT.read().unwrap()
+}
+
+pub fn set_dropdown_height(height: f32) {
+ if let Ok(mut lock) = DROPDOWN_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
+pub fn slider_height() -> f32 {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("slider_height") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+ let val_str = rest.trim_end_matches('"').trim();
+ if let Ok(val) = val_str.parse::<f32>() {
+ if let Ok(mut lock) = SLIDER_HEIGHT.write() {
+ *lock = val;
+ }
+ }
+ }
+ }
+ }
+ });
+ *SLIDER_HEIGHT.read().unwrap()
+}
+
+pub fn set_slider_height(height: f32) {
+ if let Ok(mut lock) = SLIDER_HEIGHT.write() {
+ *lock = height;
+ }
+}
+
pub trait RenderTarget {
fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
@@ -47,13 +859,13 @@ impl RenderTarget for PopoverCollector {
}
}
-pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
- w.set_rect(x, y, ww, wh);
- for (qx, qy, qw, qh, qc) in w.all_quads() {
+pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
+ w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
+ for (qx, qy, qw, qh, qc) in w.all_quads(ctx) {
pc.rect(qc, qx, qy, qw, qh);
}
let font_opt = w.widget_font();
- for (label, bounds) in w.text_labels_with_bounds() {
+ for (label, bounds) in w.text_labels_with_bounds(ctx) {
let color_f32 = [
label.color[0] as f32 / 255.0,
label.color[1] as f32 / 255.0,
@@ -67,14 +879,14 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
}
}
if w.popover_rect().is_some() {
- crate::widget::popovers::register(w);
+ ctx.register_popover(w);
}
}
-pub fn render_popovers(pc: &mut dyn RenderTarget) {
- for popover_ptr in crate::widget::popovers::get_active() {
+pub fn render_popovers(pc: &mut dyn RenderTarget, ctx: &UiContext) {
+ for popover_ptr in &ctx.active_popovers {
unsafe {
- (*popover_ptr).render_popover(pc);
+ (**popover_ptr).render_popover(pc);
}
}
}
@@ -146,253 +958,86 @@ impl Column {
self.y += 22.0;
}
- pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
- let x = self.ax(x_off);
- let y = self.ay() + y_off;
- pc.text(text, x, y, font_size, color);
- }
-
- pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
- let top_room = crate::widget::label_offset(w);
- let total_h = wh + top_room;
- let x = self.ax(x_off);
- let y = self.ay();
- w.set_row_rect(self.ox + self.cx + 8.0, self.cw - 16.0);
- 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) {
- let row_y = self.ay();
- let mut row = Row {
- pc: &mut *pc,
- base_x: self.ox + self.cx,
- y: row_y,
- cursor_x: 0.0,
- spacing: 8.0,
- };
- f(&mut row);
- self.y = self.y + h;
- }
-}
-
-pub struct Row<'a> {
- pc: &'a mut dyn RenderTarget,
- base_x: f32,
- y: f32,
- pub cursor_x: f32,
- pub spacing: f32,
-}
-
-impl<'a> Row<'a> {
- pub fn set_spacing(&mut self, spacing: f32) {
- self.spacing = spacing;
- }
-
- pub fn gap(&mut self, width: f32) {
- self.cursor_x += width;
- }
-
- pub fn text(&mut self, text: &str, y_off: f32, font_size: f32, color: [f32; 4], width: f32) {
- self.pc
- .text(text, self.base_x + self.cursor_x, self.y + y_off, font_size, color);
- self.cursor_x += width + self.spacing;
- }
-
- pub fn widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
- render_widget(self.pc, w, self.base_x + self.cursor_x, self.y, ww, wh);
- self.cursor_x += ww + self.spacing;
- }
-}
-
-pub struct Section {
- left: f32,
- top: f32,
- pub content_y: f32,
- pub cw: f32,
- label_width: f32,
-}
-
-impl Section {
- pub const ROW_PADDING_X: f32 = 8.0;
- pub const DEFAULT_MARGIN_X: f32 = 12.0;
- pub const DEFAULT_ROW_GAP: f32 = 8.0;
-
- fn estimate_label_width(label: &str) -> f32 {
- let mut width = 0.0;
- for c in label.chars() {
- let factor = match c {
- 'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
- 'r' | 's' | 'J' | 'c' | 'z' => 0.42,
- 'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
- 'A'..='Z' => 0.68,
- _ => 0.55,
- };
- width += factor * 14.0;
- }
- width
- }
-
- pub fn new(pc: &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, 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 {
- 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: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
- w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
- let x = self.ax(x_off);
- let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
- let clamped_w = ww.min((right_edge - x).max(0.0));
- let top_room = crate::widget::label_offset(w);
- let total_h = wh + top_room;
- render_widget(pc, w, x, self.ay(), clamped_w, total_h);
- self.content_y += total_h;
- }
-
- pub fn widget_full<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
- let x_off = 12.0;
- let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off); // cw - 40.0
- self.widget(pc, w, x_off, ww, wh);
- }
-
- pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
- let x = self.ax(Self::ROW_PADDING_X);
- let y = self.ay();
- pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
- self.content_y += 8.0;
- }
-
- pub fn rect(&mut self, 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; // 20.0 px (12.0 px inner padding)
- let usable_w = self.cw - 2.0 * margin_x; // padding on left and right inside outer bounds
- 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.30, 0.50, 0.32, 1.0] // Focused green
- } else {
- [0.25, 0.25, 0.35, 1.0] // Default gray
- };
- let x = self.left + Self::ROW_PADDING_X;
- let y = self.top + 7.0;
- let w = self.cw - 2.0 * Self::ROW_PADDING_X;
- let h = self.content_y - y;
-
- let left_edge = x;
- let right_edge = x + w;
- if self.label_width > 0.0 {
- let label_x = self.left + (self.cw - self.label_width) / 2.0;
- let gap_margin = 6.0;
- let gap_start = label_x - gap_margin;
- let gap_end = label_x + self.label_width + gap_margin;
- if gap_start > left_edge {
- 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 text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+ let x = self.ax(x_off);
+ let y = self.ay() + y_off;
+ pc.text(text, x, y, font_size, color);
}
- pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SectionVStack<'a> {
- SectionVStack {
- section: self,
- pc,
- spacing,
+ pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
+ if let Some(pref) = w.preferred_height() {
+ wh = pref;
}
+ let top_room = crate::widget::label_offset(w);
+ 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);
+ let clamped_w = ww.min((self.cw - x_off).max(0.0));
+ render_widget(pc, w, x, y, clamped_w, total_h, ctx);
+ self.y += total_h;
+ }
+
+ pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
+ let row_y = self.ay();
+ let mut row = Row {
+ pc: &mut *pc,
+ base_x: self.ox + self.cx,
+ y: row_y,
+ cursor_x: 0.0,
+ spacing: 8.0,
+ };
+ f(&mut row);
+ self.y = self.y + h;
}
}
-pub struct SectionVStack<'a> {
- section: &'a mut Section,
+pub struct Row<'a> {
pc: &'a mut dyn RenderTarget,
- spacing: f32,
+ base_x: f32,
+ y: f32,
+ pub cursor_x: f32,
+ pub spacing: f32,
}
-impl<'a> SectionVStack<'a> {
- pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
- self.section.widget(self.pc, w, Section::DEFAULT_MARGIN_X, ww, wh);
- self.section.spacing(self.spacing);
+impl<'a> Row<'a> {
+ pub fn set_spacing(&mut self, spacing: f32) {
+ self.spacing = spacing;
}
- pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, f: F)
- where
- F: FnMut(usize, f32, f32),
- {
- self.section.row(count, gap, h, f);
- self.section.spacing(self.spacing);
+ pub fn gap(&mut self, width: f32) {
+ self.cursor_x += width;
+ }
+
+ pub fn text(&mut self, text: &str, y_off: f32, font_size: f32, color: [f32; 4], width: f32) {
+ self.pc
+ .text(text, self.base_x + self.cursor_x, self.y + y_off, font_size, color);
+ self.cursor_x += width + self.spacing;
+ }
+
+ pub fn widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, mut wh: f32, ctx: &mut UiContext) {
+ if let Some(pref) = w.preferred_height() {
+ wh = pref;
+ }
+ render_widget(self.pc, w, self.base_x + self.cursor_x, self.y, ww, wh, ctx);
+ self.cursor_x += ww + self.spacing;
}
}
-pub struct Subsection {
- left: f32,
- top: f32,
+pub struct Section {
+ pub left: f32,
+ pub top: f32,
pub content_y: f32,
pub cw: f32,
- label_width: f32,
+ pub label_width: f32,
+ pub is_child: bool,
}
-impl Subsection {
- pub const ROW_PADDING_X: f32 = 8.0;
+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 {
+ fn estimate_label_width(label: &str, font_size: f32) -> f32 {
let mut width = 0.0;
for c in label.chars() {
let factor = match c {
@@ -402,20 +1047,50 @@ impl Subsection {
'A'..='Z' => 0.68,
_ => 0.55,
};
- width += factor * 12.0;
+ width += factor * font_size;
}
width
}
+ pub fn padding(&self) -> f32 {
+ if self.is_child {
+ section_padding().max(8.0)
+ } else {
+ section_padding()
+ }
+ }
+
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 }
+ Self::new_opt(pc, left, top, cw, label, false)
+ }
+
+ pub fn new_opt(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str, is_child: bool) -> Self {
+ let font_setting = if is_child {
+ nested_section_label_font()
+ } else {
+ section_label_font()
+ };
+ let (font_fam, font_size_opt) = parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
+ let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
+ let label_width = Self::estimate_label_width(label, font_size);
+ let label_x = if is_child {
+ let base_x = match nested_section_label_alignment() {
+ 0 => left + 12.0,
+ 1 => left + (cw - label_width) / 2.0,
+ 2 => left + cw - 12.0 - label_width,
+ _ => left + 12.0,
+ };
+ base_x + nested_section_label_offset()
+ } else {
+ left + (cw - label_width) / 2.0
+ };
+ pc.text_with_font(label, label_x, top, font_size, font_color, &font_fam);
+ Self { left, top, content_y: top + font_size + 5.0, cw, label_width, is_child }
}
pub fn ax(&self, x_off: f32) -> f32 {
- let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
+ let shift = if x_off >= 12.0 { self.padding() } else { 0.0 };
self.left + x_off + shift
}
@@ -427,27 +1102,32 @@ impl Subsection {
pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
}
- pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, wh: f32) {
- w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+ pub fn widget<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
+ if let Some(pref) = w.preferred_height() {
+ wh = pref;
+ }
+ let pad = self.padding();
+ w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
let x = self.ax(x_off);
- let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
+ let right_edge = self.left + self.cw - pad;
let clamped_w = ww.min((right_edge - x).max(0.0));
let top_room = crate::widget::label_offset(w);
let total_h = wh + top_room;
- render_widget(pc, w, x, self.ay(), clamped_w, total_h);
+ render_widget(pc, w, x, self.ay(), clamped_w, total_h, ctx);
self.content_y += total_h;
}
- pub fn widget_full<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32) {
+ pub fn widget_full<T: Element + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32, ctx: &mut UiContext) {
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);
+ let ww = self.cw - 2.0 * (self.padding() + x_off);
+ self.widget(pc, w, x_off, ww, wh, ctx);
}
pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
- let x = self.ax(Self::ROW_PADDING_X);
+ let pad = self.padding();
+ let x = self.ax(pad);
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);
+ pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
self.content_y += 8.0;
}
@@ -457,7 +1137,7 @@ impl Subsection {
}
pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
- let margin_x = Self::ROW_PADDING_X + 12.0;
+ let margin_x = self.padding() + 12.0;
let usable_w = self.cw - 2.0 * margin_x;
if count == 0 {
return Vec::new();
@@ -489,20 +1169,39 @@ impl Subsection {
}
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]
+ let border: [f32; 4] = if self.is_child {
+ if focused {
+ [0.22, 0.38, 0.24, 1.0]
+ } else {
+ [0.18, 0.18, 0.25, 1.0]
+ }
} else {
- [0.18, 0.18, 0.25, 1.0]
+ if focused {
+ [0.30, 0.50, 0.32, 1.0]
+ } else {
+ [0.25, 0.25, 0.35, 1.0]
+ }
};
- let x = self.left + Self::ROW_PADDING_X;
+ let pad = self.padding();
+ let x = self.left + pad;
let y = self.top + 7.0;
- let w = self.cw - 2.0 * Self::ROW_PADDING_X;
+ let w = self.cw - 2.0 * pad;
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 label_x = if self.is_child {
+ let base_x = match nested_section_label_alignment() {
+ 0 => self.left + 12.0,
+ 1 => self.left + (self.cw - self.label_width) / 2.0,
+ 2 => self.left + self.cw - 12.0 - self.label_width,
+ _ => self.left + 12.0,
+ };
+ base_x + nested_section_label_offset()
+ } else {
+ 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;
@@ -522,33 +1221,33 @@ impl Subsection {
self.content_y + 20.0
}
- pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SubsectionVStack<'a> {
- SubsectionVStack {
- subsection: self,
+ pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SectionVStack<'a> {
+ SectionVStack {
+ section: self,
pc,
spacing,
}
}
}
-pub struct SubsectionVStack<'a> {
- subsection: &'a mut Subsection,
+pub struct SectionVStack<'a> {
+ section: &'a mut Section,
pc: &'a mut dyn RenderTarget,
spacing: f32,
}
-impl<'a> SubsectionVStack<'a> {
- pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
- self.subsection.widget(self.pc, w, Subsection::DEFAULT_MARGIN_X, ww, wh);
- self.subsection.spacing(self.spacing);
+impl<'a> SectionVStack<'a> {
+ pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32, ctx: &mut UiContext) {
+ self.section.widget(self.pc, w, Section::DEFAULT_MARGIN_X, ww, wh, ctx);
+ self.section.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);
+ self.section.row(count, gap, h, f);
+ self.section.spacing(self.spacing);
}
}
@@ -620,14 +1319,12 @@ pub struct Grid {
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
+ let col_width = if count > 0 {
+ (width - total_gap).max(0.0) / count as f32
} else {
- 0.0
+ min_col_width
};
-
- let col_width = min_col_width;
+ let left_offset = 0.0;
let mut col_lefts = Vec::with_capacity(count);
let col_heights = vec![top; count];
@@ -750,7 +1447,7 @@ impl Radial {
}
}
- pub fn layout_widgets<T: Element + 'static>(&self, widgets: &mut [&mut T]) {
+ pub fn layout_widgets<T: Element + 'static>(&self, widgets: &mut [&mut T], ctx: &mut UiContext) {
let mut active_idx = 0;
for w in widgets.iter_mut() {
if !w.layout_ignore() {
@@ -758,13 +1455,13 @@ impl Radial {
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);
+ w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(rw, rw, rh, rh), ctx);
active_idx += 1;
}
}
}
- pub fn layout_widget_ptors(&self, widgets: &[*mut (dyn Element + 'static)]) {
+ pub fn layout_widget_ptors(&self, widgets: &[*mut (dyn Element + 'static)], ctx: &mut UiContext) {
let mut active_idx = 0;
for &w_ptr in widgets {
let w = unsafe { &mut *w_ptr };
@@ -773,7 +1470,7 @@ impl Radial {
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);
+ w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(rw, rw, rh, rh), ctx);
active_idx += 1;
}
}
@@ -784,6 +1481,7 @@ 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) {}
+ fn get_column_width(&self) -> Option<f32> { None }
}
pub struct ColumnLayout {
@@ -820,6 +1518,10 @@ impl LayoutStrategy for ColumnLayout {
self.current_y += wh + self.gap;
(x, y, ww, wh)
}
+
+ fn get_column_width(&self) -> Option<f32> {
+ Some(self.width)
+ }
}
pub struct GridLayout {
@@ -842,13 +1544,14 @@ impl GridLayout {
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 min_col_width = crate::layout::grid_min_col_width();
+ let max_cols = ((width + self.gap) / (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));
+ self.grid = Some(Grid::new(left, top, width, min_col_width, self.gap, count));
}
fn allocate(&mut self, _ww: f32, wh: f32) -> (f32, f32, f32, f32) {
@@ -866,6 +1569,10 @@ impl LayoutStrategy for GridLayout {
fn set_section_count(&mut self, count: usize) {
self.num_sections = Some(count);
}
+
+ fn get_column_width(&self) -> Option<f32> {
+ self.grid.as_ref().map(|g| g.col_width)
+ }
}
pub struct RadialLayout {
@@ -954,12 +1661,13 @@ impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
where
F: FnMut(&mut SectionContext<'_, P>),
{
+ let width = self.strategy.get_column_width().unwrap_or(self.section_width);
let mut dummy = P::default();
- let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, self.section_width, label, focused);
+ let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused, false);
render_fn(&mut dummy_ctx);
let wh = dummy_ctx.finish();
- let (rx, ry, _, _) = self.strategy.allocate(self.section_width, wh);
- let mut real_ctx = SectionContext::new(final_pc, rx, ry, self.section_width, label, focused);
+ let (rx, ry, rw, _) = self.strategy.allocate(width, wh);
+ let mut real_ctx = SectionContext::new(final_pc, rx, ry, rw, label, focused, false);
render_fn(&mut real_ctx);
real_ctx.finish();
self.idx += 1;
@@ -970,11 +1678,11 @@ impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
F: FnMut(&mut SectionContext<'_, P>),
{
let mut dummy = P::default();
- let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused);
+ let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused, false);
render_fn(&mut dummy_ctx);
let wh = dummy_ctx.finish();
- let (rx, ry, _, _) = self.strategy.allocate(width, wh);
- let mut real_ctx = SectionContext::new(final_pc, rx, ry, width, label, focused);
+ let (rx, ry, rw, _) = self.strategy.allocate(width, wh);
+ let mut real_ctx = SectionContext::new(final_pc, rx, ry, rw, label, focused, false);
render_fn(&mut real_ctx);
real_ctx.finish();
self.idx += 1;
@@ -989,14 +1697,14 @@ pub struct SectionContext<'a, P> {
pub cw: f32,
pub label_width: f32,
pub focused: bool,
+ pub is_child: bool,
}
impl<'a, P: RenderTarget> SectionContext<'a, P> {
- pub const ROW_PADDING_X: f32 = 8.0;
pub const DEFAULT_MARGIN_X: f32 = 12.0;
pub const DEFAULT_ROW_GAP: f32 = 8.0;
- fn estimate_label_width(label: &str) -> f32 {
+ fn estimate_label_width(label: &str, font_size: f32) -> f32 {
let mut width = 0.0;
for c in label.chars() {
let factor = match c {
@@ -1006,28 +1714,55 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
'A'..='Z' => 0.68,
_ => 0.55,
};
- width += factor * 14.0;
+ width += factor * font_size;
}
width
}
- pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool) -> Self {
- let label_width = Self::estimate_label_width(label);
- let label_x = left + (cw - label_width) / 2.0;
- pc.text(label, label_x, top, 14.0, [0.83, 0.83, 0.83, 1.0]);
+ pub fn padding(&self) -> f32 {
+ if self.is_child {
+ section_padding().max(8.0)
+ } else {
+ section_padding()
+ }
+ }
+
+ pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool, is_child: bool) -> Self {
+ let font_setting = if is_child {
+ nested_section_label_font()
+ } else {
+ section_label_font()
+ };
+ let (font_fam, font_size_opt) = parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
+ let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
+ let label_width = Self::estimate_label_width(label, font_size);
+ let label_x = if is_child {
+ let base_x = match nested_section_label_alignment() {
+ 0 => left + 12.0,
+ 1 => left + (cw - label_width) / 2.0,
+ 2 => left + cw - 12.0 - label_width,
+ _ => left + 12.0,
+ };
+ base_x + nested_section_label_offset()
+ } else {
+ left + (cw - label_width) / 2.0
+ };
+ pc.text_with_font(label, label_x, top, font_size, font_color, &font_fam);
Self {
pc,
left,
top,
- content_y: top + 19.0,
+ content_y: top + font_size + 5.0,
cw,
label_width,
focused,
+ is_child,
}
}
pub fn ax(&self, x_off: f32) -> f32 {
- let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
+ let shift = if x_off >= 12.0 { self.padding() } else { 0.0 };
self.left + x_off + shift
}
@@ -1043,28 +1778,33 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
self.pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
}
- pub fn widget<T: Element + 'static>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
- w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
+ pub fn widget<T: Element + 'static>(&mut self, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
+ if let Some(pref) = w.preferred_height() {
+ wh = pref;
+ }
+ let pad = self.padding();
+ w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
let x = self.ax(x_off);
let y = self.ay();
- let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
+ let right_edge = self.left + self.cw - pad;
let clamped_w = ww.min((right_edge - x).max(0.0));
let top_room = crate::widget::label_offset(w);
let total_h = wh + top_room;
- render_widget(self.pc, w, x, y, clamped_w, total_h);
+ render_widget(self.pc, w, x, y, clamped_w, total_h, ctx);
self.content_y += total_h;
}
- pub fn widget_full<T: Element + 'static>(&mut self, w: &mut T, wh: f32) {
+ pub fn widget_full<T: Element + 'static>(&mut self, w: &mut T, wh: f32, ctx: &mut UiContext) {
let x_off = 12.0;
- let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off); // cw - 40.0
- self.widget(w, x_off, ww, wh);
+ let ww = self.cw - 2.0 * (self.padding() + x_off); // cw - 40.0
+ self.widget(w, x_off, ww, wh, ctx);
}
pub fn separator(&mut self) {
- let x = self.ax(Self::ROW_PADDING_X);
+ let pad = self.padding();
+ let x = self.ax(pad);
let y = self.ay();
- self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
+ self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
self.content_y += 8.0;
}
@@ -1074,7 +1814,7 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
}
pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
- let margin_x = Self::ROW_PADDING_X + 12.0;
+ let margin_x = self.padding() + 12.0;
let usable_w = self.cw - 2.0 * margin_x;
if count == 0 {
return Vec::new();
@@ -1108,189 +1848,54 @@ impl<'a, P: RenderTarget> SectionContext<'a, P> {
}
}
- pub fn add_subsection<F>(&mut self, label: &str, focused: bool, mut render_fn: F)
+ pub fn add_section<F>(&mut self, label: &str, focused: bool, mut render_fn: F)
where
- F: FnMut(&mut SubsectionContext<'_, P>),
+ F: FnMut(&mut SectionContext<'_, P>),
{
- let left = self.ax(0.0) + Self::ROW_PADDING_X;
+ let pad = self.padding();
+ let left = self.ax(0.0) + pad;
let top = self.content_y;
- let cw = self.cw - 2.0 * Self::ROW_PADDING_X;
+ let cw = self.cw - 2.0 * pad;
- let mut sub_ctx = SubsectionContext::new(self.pc, left, top, cw, label, focused);
+ let mut sub_ctx = SectionContext::new(self.pc, left, top, cw, label, focused, true);
render_fn(&mut sub_ctx);
self.content_y = sub_ctx.finish();
}
pub fn finish(self) -> f32 {
- let border: [f32; 4] = if self.focused {
- [0.30, 0.50, 0.32, 1.0] // Focused green
- } else {
- [0.25, 0.25, 0.35, 1.0] // Default gray
- };
- let x = self.left + Self::ROW_PADDING_X;
- let y = self.top + 7.0;
- let w = self.cw - 2.0 * Self::ROW_PADDING_X;
- let h = self.content_y - y;
-
- let left_edge = x;
- let right_edge = x + w;
- if self.label_width > 0.0 {
- let label_x = self.left + (self.cw - self.label_width) / 2.0;
- let gap_margin = 6.0;
- let gap_start = label_x - gap_margin;
- let gap_end = label_x + self.label_width + gap_margin;
- if gap_start > left_edge {
- self.pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
+ let border: [f32; 4] = if self.is_child {
+ if self.focused {
+ [0.22, 0.38, 0.24, 1.0]
+ } else {
+ [0.18, 0.18, 0.25, 1.0]
}
- if right_edge > gap_end {
- self.pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
- }
- } else {
- self.pc.rect(border, left_edge, y, w, 1.0);
- }
-
- self.pc.rect(border, x, y + h + 12.0, w, 1.0);
- self.pc.rect(border, x, y, 1.0, h + 12.0);
- self.pc.rect(border, x + w - 1.0, y, 1.0, h + 12.0);
- self.content_y + 20.0
- }
-}
-
-pub struct SubsectionContext<'a, P> {
- pub pc: &'a mut P,
- pub left: f32,
- pub top: f32,
- pub content_y: f32,
- pub cw: f32,
- pub label_width: f32,
- pub focused: bool,
-}
-
-impl<'a, P: RenderTarget> SubsectionContext<'a, P> {
- pub const ROW_PADDING_X: f32 = 8.0;
- pub const DEFAULT_MARGIN_X: f32 = 12.0;
- pub const DEFAULT_ROW_GAP: f32 = 8.0;
-
- fn estimate_label_width(label: &str) -> f32 {
- let mut width = 0.0;
- for c in label.chars() {
- let factor = match c {
- 'i' | 'l' | 't' | 'j' | 'f' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.28,
- 'r' | 's' | 'J' | 'c' | 'z' => 0.42,
- 'm' | 'w' | 'M' | 'W' | '&' | '@' => 0.80,
- 'A'..='Z' => 0.68,
- _ => 0.55,
- };
- width += factor * 12.0;
- }
- width
- }
-
- pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool) -> Self {
- let label_width = Self::estimate_label_width(label);
- let label_x = left + (cw - label_width) / 2.0;
- pc.text(label, label_x, top, 12.0, [0.53, 0.53, 0.60, 1.0]);
- Self {
- pc,
- left,
- top,
- content_y: top + 17.0,
- cw,
- label_width,
- focused,
- }
- }
-
- pub fn ax(&self, x_off: f32) -> f32 {
- let shift = if x_off >= 12.0 { 8.0 } else { 0.0 };
- self.left + x_off + shift
- }
-
- pub fn ay(&self) -> f32 {
- self.content_y
- }
-
- pub fn spacing(&mut self, dy: f32) {
- self.content_y += dy;
- }
-
- pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
- self.pc.text(text, self.ax(x_off), self.ay() + y_off, font_size, color);
- }
-
- pub fn widget<T: Element + 'static>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
- w.set_row_rect(self.left + Self::ROW_PADDING_X, self.cw - 2.0 * Self::ROW_PADDING_X);
- let x = self.ax(x_off);
- let y = self.ay();
- let right_edge = self.left + self.cw - Self::ROW_PADDING_X;
- let clamped_w = ww.min((right_edge - x).max(0.0));
- let top_room = crate::widget::label_offset(w);
- let total_h = wh + top_room;
- render_widget(self.pc, w, x, y, clamped_w, total_h);
- self.content_y += total_h;
- }
-
- pub fn widget_full<T: Element + 'static>(&mut self, w: &mut T, wh: f32) {
- let x_off = 12.0;
- let ww = self.cw - 2.0 * (Self::ROW_PADDING_X + x_off);
- self.widget(w, x_off, ww, wh);
- }
-
- pub fn separator(&mut self) {
- let x = self.ax(Self::ROW_PADDING_X);
- let y = self.ay();
- self.pc.rect([0.15, 0.15, 0.22, 1.0], x, y, self.cw - 2.0 * Self::ROW_PADDING_X, 1.0);
- self.content_y += 8.0;
- }
-
- pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
- self.pc.rect(color, self.ax(x_off), self.ay(), w, h);
- self.content_y += h;
- }
-
- pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
- let margin_x = Self::ROW_PADDING_X + 12.0;
- let usable_w = self.cw - 2.0 * margin_x;
- if count == 0 {
- return Vec::new();
- }
- let total_gap = gap * (count - 1) as f32;
- let col_w = (usable_w - total_gap).max(0.0) / count as f32;
-
- let mut cols = Vec::with_capacity(count);
- for i in 0..count {
- let x = self.left + margin_x + i as f32 * (col_w + gap);
- cols.push((x, col_w));
- }
- cols
- }
-
- pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
- where
- F: FnMut(usize, f32, f32),
- {
- let cols = self.row_layout(count, gap);
- for (i, &(x, w)) in cols.iter().enumerate() {
- f(i, x, w);
- }
- self.content_y += h;
- }
-
- pub fn finish(self) -> f32 {
- let border: [f32; 4] = if self.focused {
- [0.22, 0.38, 0.24, 1.0]
} else {
- [0.18, 0.18, 0.25, 1.0]
+ if self.focused {
+ [0.30, 0.50, 0.32, 1.0] // Focused green
+ } else {
+ [0.25, 0.25, 0.35, 1.0] // Default gray
+ }
};
- let x = self.left + Self::ROW_PADDING_X;
+ let pad = self.padding();
+ let x = self.left + pad;
let y = self.top + 7.0;
- let w = self.cw - 2.0 * Self::ROW_PADDING_X;
+ let w = self.cw - 2.0 * pad;
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 label_x = if self.is_child {
+ let base_x = match nested_section_label_alignment() {
+ 0 => self.left + 12.0,
+ 1 => self.left + (self.cw - self.label_width) / 2.0,
+ 2 => self.left + self.cw - 12.0 - self.label_width,
+ _ => self.left + 12.0,
+ };
+ base_x + nested_section_label_offset()
+ } else {
+ 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;
@@ -1311,14 +1916,15 @@ impl<'a, P: RenderTarget> SubsectionContext<'a, P> {
}
}
+
pub struct VStack<'b, 'a, P> {
context: &'b mut SectionContext<'a, P>,
spacing: f32,
}
impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
- pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32) {
- self.context.widget(w, SectionContext::<P>::DEFAULT_MARGIN_X, ww, wh);
+ pub fn add_widget<T: Element + 'static>(&mut self, w: &mut T, ww: f32, wh: f32, ctx: &mut UiContext) {
+ self.context.widget(w, SectionContext::<P>::DEFAULT_MARGIN_X, ww, wh, ctx);
self.context.spacing(self.spacing);
}
@@ -1369,57 +1975,66 @@ mod tests {
}
struct MockWidgetWithLabel {
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- top_room: f32,
+ base: crate::widget::Widget,
}
impl Element for MockWidgetWithLabel {
+ fn base(&self) -> Option<&crate::widget::Widget> {
+ Some(&self.base)
+ }
+ fn base_mut(&mut self) -> Option<&mut crate::widget::Widget> {
+ Some(&mut self.base)
+ }
fn rect(&self) -> (f32, f32, f32, f32) {
- (self.x, self.y - self.top_room, self.w, self.h + self.top_room)
+ let offset = crate::widget::label_offset(self);
+ (self.base.x, self.base.y - offset, self.base.w, self.base.h + offset)
}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y + self.top_room;
- self.w = w;
- self.h = (h - self.top_room).max(0.0);
+ let offset = crate::widget::label_offset(self);
+ self.base.x = x;
+ self.base.y = y + offset;
+ self.base.w = w;
+ self.base.h = (h - offset).max(0.0);
}
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
- fn top_room(&self) -> f32 {
- self.top_room
- }
}
#[test]
fn test_vstack_flow() {
+ let orig_margin = label_margin();
+ set_label_margin(6.0);
+ let _ = section_padding();
+ set_section_padding(8.0);
let mut mock_pc = MockRenderTarget { rects: Vec::new() };
let mut sec = Section::new(&mut mock_pc, 10.0, 20.0, 200.0, "Test Section");
let start_y = sec.ay();
let mut stack = sec.vstack(&mut mock_pc, 10.0);
+ let mut dummy = crate::context::UiContext::new();
let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
- stack.add_widget(&mut w1, 50.0, 30.0);
+ stack.add_widget(&mut w1, 50.0, 30.0, &mut dummy);
// Standard margin should be applied
assert_eq!(w1.x, 30.0);
assert_eq!(w1.y, start_y);
let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
- stack.add_widget(&mut w2, 60.0, 40.0);
+ stack.add_widget(&mut w2, 60.0, 40.0, &mut dummy);
// Second widget should start after first widget height + vstack spacing
assert_eq!(w2.y, start_y + 30.0 + 10.0);
- let mut w3 = MockWidgetWithLabel { x: 0.0, y: 0.0, w: 0.0, h: 0.0, top_room: 15.0 };
- stack.add_widget(&mut w3, 70.0, 50.0);
+ let mut base = crate::widget::Widget::new();
+ base.label = Some("Test Label".to_string());
+ let mut w3 = MockWidgetWithLabel { base };
+ stack.add_widget(&mut w3, 70.0, 50.0, &mut dummy);
- // 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);
+ // Third widget has label, so its y should be shifted by 18.0
+ assert_eq!(w3.base.y, start_y + 30.0 + 10.0 + 40.0 + 10.0 + 18.0);
+ set_label_margin(orig_margin);
}
#[test]
@@ -1428,7 +2043,7 @@ mod tests {
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);
+ assert_eq!(grid1.col_width, 200.0);
// Test multi column layout (width = 700, min_col_width = 300, gap = 20)
// count = floor((700 + 20) / (300 + 20)) = floor(720 / 320) = 2.
@@ -1436,9 +2051,9 @@ mod tests {
// 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[0], 5.0);
assert_eq!(grid2.col_lefts[1], 365.0);
- assert_eq!(grid2.col_width, 300.0);
+ assert_eq!(grid2.col_width, 340.0);
assert_eq!(grid2.next_column(), 0);
grid2.col_heights[0] += 50.0; // Column 0 height becomes 65.0
@@ -1454,13 +2069,15 @@ mod tests {
#[test]
fn test_subsection() {
let mut pc = PopoverCollector::new();
- let mut subsec = Subsection::new(&mut pc, 10.0, 20.0, 300.0, "Test Subsec");
+ let mut subsec = Section::new_opt(&mut pc, 10.0, 20.0, 300.0, "Test Subsec", true);
assert_eq!(subsec.left, 10.0);
assert_eq!(subsec.top, 20.0);
assert_eq!(subsec.cw, 300.0);
+ assert!(subsec.is_child);
+ let mut dummy = crate::context::UiContext::new();
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);
+ subsec.widget(&mut pc, &mut w, 12.0, 100.0, 40.0, &mut dummy);
let bottom = subsec.finish(&mut pc);
assert!(bottom > 20.0);
@@ -1490,5 +2107,39 @@ mod tests {
assert!(d7 > d1);
assert!(d1 > 0.0);
}
+
+ #[test]
+ fn test_nested_section_label_alignment() {
+ let _ = nested_section_label_alignment();
+ set_nested_section_label_alignment(1);
+ assert_eq!(nested_section_label_alignment(), 1);
+ set_nested_section_label_alignment(2);
+ assert_eq!(nested_section_label_alignment(), 2);
+ set_nested_section_label_alignment(0);
+ assert_eq!(nested_section_label_alignment(), 0);
+
+ let _ = nested_section_label_offset();
+ set_nested_section_label_offset(15.0);
+ assert_eq!(nested_section_label_offset(), 15.0);
+ set_nested_section_label_offset(0.0);
+ assert_eq!(nested_section_label_offset(), 0.0);
+ }
+
+ #[test]
+ fn test_dropdown_height() {
+ let _ = dropdown_height();
+ set_dropdown_height(48.0);
+ assert_eq!(dropdown_height(), 48.0);
+ set_dropdown_height(44.0);
+ assert_eq!(dropdown_height(), 44.0);
+ }
+
+ #[test]
+ fn test_print_fonts() {
+ let db = crate::widget::get_font_db();
+ for face in db.faces() {
+ println!("FAMILY: {:?}", face.families);
+ }
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index b5fec9f..6642d0e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,9 +5,12 @@ pub mod wayland;
pub mod protocol;
pub mod engine;
pub mod scale;
+pub mod backend;
+pub mod context;
pub mod colors {
pub use crate::color::*;
}
pub const SHADER: &str = include_str!("shader.wgsl");
+
diff --git a/src/main.rs b/src/main.rs
index f293378..6ab74c3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -263,6 +263,7 @@ struct State {
scale: f64,
json_layout: Option<JsonLayoutWidget>,
layout_mode: bool,
+ ui_context: clear_ui::context::UiContext,
}
impl State {
@@ -284,53 +285,58 @@ impl State {
..Default::default()
});
- let surface = instance
- .create_surface(wayland_handle)
- .expect("Failed to create surface");
+ let surface = unsafe { instance.create_surface(wayland_handle).unwrap() };
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
- power_preference: wgpu::PowerPreference::LowPower,
+ power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
- .expect("Failed to find adapter");
+ .unwrap();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
- label: Some("GPU Device"),
+ label: None,
required_features: wgpu::Features::empty(),
- required_limits: wgpu::Limits::downlevel_webgl2_defaults()
- .using_resolution(adapter.limits()),
+ required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::MemoryUsage,
},
None,
)
.await
- .expect("Failed to create device");
+ .unwrap();
- let mut config = surface
- .get_default_config(&adapter, pw, ph)
- .expect("Failed to get surface config");
- config.present_mode = wgpu::PresentMode::Fifo;
+ let surface_caps = surface.get_capabilities(&adapter);
+ let surface_format = surface_caps
+ .formats
+ .iter()
+ .copied()
+ .find(|f| f.is_srgb())
+ .unwrap_or(surface_caps.formats[0]);
+ let mut config = wgpu::SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format: surface_format,
+ width: pw,
+ height: ph,
+ present_mode: wgpu::PresentMode::Fifo,
+ alpha_mode: surface_caps.alpha_modes[0],
+ view_formats: vec![],
+ desired_maximum_frame_latency: 2,
+ };
surface.configure(&device, &config);
- let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Shader"),
- source: wgpu::ShaderSource::Wgsl(clear_ui::SHADER.into()),
- });
-
- let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("Pipeline Layout"),
+ let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
+ let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Render Pipeline Layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
-
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
- layout: Some(&pipeline_layout),
+ layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
@@ -352,8 +358,8 @@ impl State {
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
- polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
+ polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
@@ -366,18 +372,18 @@ impl State {
cache: None,
});
- // Initialize text rendering
let mut font_system = FontSystem::new();
let swash_cache = SwashCache::new();
let cache = Cache::new(&device);
- let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
- let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
+ let viewport = Viewport::new(&device, &cache);
+ let mut text_atlas = TextAtlas::new(&device, &queue, &cache, surface_format);
+ let text_renderer =
+ TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
- let mut text_viewport = Viewport::new(&device, &cache);
- text_viewport.update(&queue, Resolution { width: pw, height: ph });
+ let label_buffer = make_text_buffer(&mut font_system, "Design System Playground", 16.0);
+ let status_buffer = make_text_buffer(&mut font_system, "Ready", 12.0);
- 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 text_viewport = viewport;
let layout_mode = json_layout_config.is_some();
let mut widgets: Vec<Box<dyn Element>> = Vec::new();
@@ -446,6 +452,7 @@ impl State {
scale,
json_layout,
layout_mode,
+ ui_context: clear_ui::context::UiContext::new(),
};
state.apply_layout();
@@ -555,6 +562,7 @@ impl State {
scale,
ref json_layout,
layout_mode,
+ ref ui_context,
..
} = self;
@@ -599,14 +607,14 @@ impl State {
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() {
+ for (label, font, bounds) in jl.text_labels_with_font_and_bounds(ui_context) {
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() {
+ for (label, font, bounds) in w.text_labels_with_font_and_bounds(ui_context) {
let mut covered = false;
for (pi, pw) in self.widgets.iter().enumerate() {
if pi != i {
@@ -1016,7 +1024,7 @@ impl PointerHandler for AppState {
}
} else if state.layout_mode {
if let Some(jl) = &mut state.json_layout {
- if jl.cursor_moved(state.cursor_x, state.cursor_y) {
+ if jl.cursor_moved(state.cursor_x, state.cursor_y, &mut state.ui_context) {
changed = true;
}
}
@@ -1028,7 +1036,7 @@ impl PointerHandler for AppState {
}
if state.drag_widget.is_none() {
for w in &mut state.widgets {
- if w.cursor_moved(state.cursor_x, state.cursor_y) {
+ if w.cursor_moved(state.cursor_x, state.cursor_y, &mut state.ui_context) {
changed = true;
}
}
@@ -1055,14 +1063,14 @@ impl PointerHandler for AppState {
}
} 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) {
+ if jl.mouse_input(btn, clear_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y, &mut st.ui_context) {
changed = true;
}
}
} else {
let mut clicked_idx = None;
for i in (0..st.widgets.len()).rev() {
- if st.widgets[i].hit_test(st.cursor_x, st.cursor_y) {
+ if st.widgets[i].hit_test(st.cursor_x, st.cursor_y, &st.ui_context) {
clicked_idx = Some(i);
break;
}
@@ -1081,6 +1089,7 @@ impl PointerHandler for AppState {
clear_ui::widget::ElementState::Pressed,
st.cursor_x,
st.cursor_y,
+ &mut st.ui_context,
) {
changed = true;
}
@@ -1117,7 +1126,7 @@ impl PointerHandler for AppState {
} 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) {
+ if jl.mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y, &mut st.ui_context) {
changed = true;
}
if btn == clear_ui::widget::MouseButton::Left {
@@ -1126,7 +1135,7 @@ impl PointerHandler for AppState {
if w.page_idx != active_page {
continue;
}
- if let Some(btn_w) = &mut w.button {
+ if let Some(btn_w) = w.widget.as_any_mut().downcast_mut::<clear_ui::widget::Button>() {
if btn_w.take_click() {
clicked_btn_id = Some(w.id.clone());
break;
@@ -1142,13 +1151,13 @@ impl PointerHandler for AppState {
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 {
+ if let Some(cb) = w.widget.as_any().downcast_ref::<clear_ui::widget::Checkbox>() {
checkboxes.insert(w.id.clone(), cb.checked());
- } else if let Some(sb) = &w.spinbox {
+ } else if let Some(sb) = w.widget.as_any().downcast_ref::<clear_ui::widget::Spinbox>() {
spinboxes.insert(w.id.clone(), sb.value);
- } else if let Some(cs) = &w.color_selector {
+ } else if let Some(cs) = w.widget.as_any().downcast_ref::<clear_ui::widget::ColorSelector>() {
colors.insert(w.id.clone(), cs.color);
- } else if let Some(sl) = &w.slider {
+ } else if let Some(sl) = w.widget.as_any().downcast_ref::<clear_ui::widget::Slider>() {
sliders.insert(w.id.clone(), sl.get_scaled_value());
}
}
@@ -1172,7 +1181,7 @@ impl PointerHandler for AppState {
}
}
for w in &mut st.widgets {
- if w.mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
+ if w.mouse_input(btn, clear_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y, &mut st.ui_context) {
changed = true;
}
}
@@ -1207,13 +1216,13 @@ impl PointerHandler for AppState {
let mut changed = false;
if !state.layout_mode {
for w in &mut state.widgets {
- if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
+ if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
changed = true;
}
}
} else {
if let Some(jl) = &mut state.json_layout {
- if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y) {
+ if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
changed = true;
}
}
@@ -1366,7 +1375,7 @@ impl AppState {
if let Some(st) = &mut self.state {
if st.layout_mode {
if let Some(jl) = &mut st.json_layout {
- let mut changed = jl.keyboard_input(&custom_event);
+ let changed = jl.keyboard_input(&custom_event, &mut st.ui_context);
if changed {
st.upload_vertices();
self.redraw = true;
@@ -1374,7 +1383,7 @@ impl AppState {
}
} else if let Some(idx) = st.focused_widget {
let val = st.widgets[idx].value();
- let mut changed = st.widgets[idx].keyboard_input(&custom_event);
+ let mut changed = st.widgets[idx].keyboard_input(&custom_event, &mut st.ui_context);
if st.widgets[idx].value() != val {
changed = true;
}
@@ -1597,13 +1606,13 @@ fn main() {
let mut tick_changed = false;
if st.layout_mode {
if let Some(ref mut jl) = &mut st.json_layout {
- if jl.tick(dt) {
+ if jl.tick(dt, &mut st.ui_context) {
tick_changed = true;
}
}
} else {
for w in &mut st.widgets {
- if w.tick(dt) {
+ if w.tick(dt, &mut st.ui_context) {
tick_changed = true;
}
}
@@ -1630,7 +1639,7 @@ fn main() {
if let Some(st) = &mut app.state {
if st.layout_mode {
if let Some(jl) = &mut st.json_layout {
- let mut changed = jl.keyboard_input(&custom_event);
+ let changed = jl.keyboard_input(&custom_event, &mut st.ui_context);
if changed {
st.upload_vertices();
app.redraw = true;
@@ -1638,7 +1647,7 @@ fn main() {
}
} else if let Some(idx) = st.focused_widget {
let val = st.widgets[idx].value();
- let mut changed = st.widgets[idx].keyboard_input(&custom_event);
+ let mut changed = st.widgets[idx].keyboard_input(&custom_event, &mut st.ui_context);
if st.widgets[idx].value() != val {
changed = true;
}
diff --git a/src/shader.wgsl b/src/shader.wgsl
index bc75fcc..2d36cda 100644
--- a/src/shader.wgsl
+++ b/src/shader.wgsl
@@ -37,16 +37,5 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
discard;
}
}
- var final_color = in.color;
- if (in.is_background > 0.999) {
- // Feather the background quad edges
- let dist_x = 1.0 - abs(in.ndc_position.x);
- let dist_y = 1.0 - abs(in.ndc_position.y);
- let min_dist = min(dist_x, dist_y);
-
- let feather = 0.15; // Size of the feathering zone (15% of the half-width/height)
- let fade = smoothstep(0.0, 1.0, min_dist / feather);
- final_color.a = final_color.a * fade;
- }
- return final_color;
+ return in.color;
}
diff --git a/src/widget/container.rs b/src/widget/container.rs
deleted file mode 100644
index 0daecb7..0000000
--- a/src/widget/container.rs
+++ /dev/null
@@ -1,5076 +0,0 @@
-use crate::colors;
-use crate::widget::*;
-use crate::widget::display::make_widget_text_buffer;
-use crate::widget::input::{BREADCRUMB_PADDING, SEGMENT_GAP};
-
-#[derive(Clone)]
-pub struct Container {
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
-}
-
-impl Container {
- pub fn new() -> Self {
- Self { parent: None, children: Vec::new() }
- }
-}
-
-impl Element for Container {
- fn rect(&self) -> (f32, f32, f32, f32) { (0.0, 0.0, 0.0, 0.0) }
- fn set_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
-
- fn focus(&mut self) {
- focus::set_focused(self);
- }
- fn unfocus(&mut self) {}
-
- fn parent(&self) -> Option<*mut (dyn Element + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Element + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Element + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for Container {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-
-pub struct Header {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl Header {
- pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
-}
-
-impl Element for Header {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::HEADER_BG }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-}
-
-pub struct ContentBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- show_network_grid: bool,
- grid_size_x: f32,
- grid_size_y: f32,
- grid_origin_x: f32,
- grid_origin_y: f32,
- skipped_row_h: f32,
- skipped_col_w: f32,
-}
-
-impl ContentBg {
- pub fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 }
- }
-}
-
-impl Element for ContentBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] {
- if self.show_network_grid {
- [0.0, 0.0, 0.0, 0.0]
- } else {
- colors::CONTENT_BG
- }
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32) -> bool { false }
-
- fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
- fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
- fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
- fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
- return vec![];
- }
- let mut quads = Vec::new();
- let grid_color = [0.0, 0.0, 0.0, 0.0];
- let max_alpha = colors::CONTENT_BG[3]; // Peak opacity in the middle of gradient cells matches non-gradient cells
- let steps = 20; // Silky-smooth gradient transition
-
- let step_y = self.grid_size_y + self.skipped_row_h;
- let step_x = self.grid_size_x + self.skipped_col_w;
-
- if step_y >= 4.0 && step_x >= 4.0 {
- let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let ry_start = ry_start.max(-100_000);
- let ry_end = ry_end.min(100_000);
-
- let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let cx_start = cx_start.max(-100_000);
- let cx_end = cx_end.min(100_000);
-
- // Draw individual cell backgrounds to avoid stacking with gradients
- for ry in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (ry as f32) * step_y;
- let draw_start_y = y1.max(self.y);
- let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
- if draw_start_y < draw_end_y {
- for cx in cx_start..=cx_end {
- let x1 = self.grid_origin_x + (cx as f32) * step_x;
- let draw_start_x = x1.max(self.x);
- let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
- if draw_start_x < draw_end_x {
- quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
- }
- }
- }
- }
- }
-
- // Draw interstitial row gradients (horizontal bands fading to 0 alpha at left and right sides)
- if self.skipped_row_h > 0.0 {
- let step_y = self.grid_size_y + self.skipped_row_h;
- let step_x = self.grid_size_x + self.skipped_col_w;
- if step_y >= 4.0 && step_x >= 4.0 {
- let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
-
- let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let cx_start = cx_start.max(-100_000);
- let cx_end = cx_end.min(100_000);
-
- for k in k_start..=k_end {
- let y1 = self.grid_origin_y + (k as f32) * step_y;
- let y2 = y1 + self.grid_size_y;
- if y1 >= self.y + self.h {
- continue;
- }
- let draw_start_y = y2.max(self.y);
- let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
- if draw_start_y >= draw_end_y {
- continue;
- }
-
- for cx in cx_start..=cx_end {
- let x1 = self.grid_origin_x + (cx as f32) * step_x;
- let x_mid = x1 + self.grid_size_x / 2.0;
- let w_total = self.grid_size_x;
- let sub_w = w_total / steps as f32;
-
- for i in 0..steps {
- let sx_start = x1 + i as f32 * sub_w;
- let sx_end = sx_start + sub_w;
- let draw_start_x = sx_start.max(self.x);
- let draw_end_x = sx_end.min(self.x + self.w);
- if draw_start_x < draw_end_x {
- let sx_mid = (sx_start + sx_end) / 2.0;
- let dist = (sx_mid - x_mid).abs();
- let d = (dist / (w_total / 2.0)).min(1.0);
-
- // Fade the cell background color from max_alpha in the middle to transparent at the edges
- let alpha = max_alpha * (1.0 - d);
- if alpha > 0.001 {
- quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
- }
- }
- }
- }
- }
- }
- }
-
- // Draw interstitial column gradients (vertical bands fading to 0 alpha at top and bottom)
- if self.skipped_col_w > 0.0 {
- let step_y = self.grid_size_y + self.skipped_row_h;
- let step_x = self.grid_size_x + self.skipped_col_w;
- if step_y >= 4.0 && step_x >= 4.0 {
- let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
-
- let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let ry_start = ry_start.max(-100_000);
- let ry_end = ry_end.min(100_000);
-
- for k in k_start..=k_end {
- let x1 = self.grid_origin_x + (k as f32) * step_x;
- let x2 = x1 + self.grid_size_x;
- if x1 >= self.x + self.w {
- continue;
- }
- let draw_start_x = x2.max(self.x);
- let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
- if draw_start_x >= draw_end_x {
- continue;
- }
-
- for ry in ry_start..=ry_end {
- let y1 = self.grid_origin_y + (ry as f32) * step_y;
- let y_mid = y1 + self.grid_size_y / 2.0;
- let h_total = self.grid_size_y;
- let sub_h = h_total / steps as f32;
-
- for i in 0..steps {
- let sy_start = y1 + i as f32 * sub_h;
- let sy_end = sy_start + sub_h;
- let draw_start_y = sy_start.max(self.y);
- let draw_end_y = sy_end.min(self.y + self.h);
- if draw_start_y < draw_end_y {
- let sy_mid = (sy_start + sy_end) / 2.0;
- let dist = (sy_mid - y_mid).abs();
- let d = (dist / (h_total / 2.0)).min(1.0);
-
- // Fade the cell background color from max_alpha in the middle to transparent at the edges
- let alpha = max_alpha * (1.0 - d);
- if alpha > 0.001 {
- quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
- }
- }
- }
- }
- }
- }
- }
-
- // Draw the grid borders
- let step_y = self.grid_size_y + self.skipped_row_h;
- if step_y >= 4.0 {
- let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
- let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
- for k in k_start..=k_end {
- let y1 = self.grid_origin_y + (k as f32) * step_y;
- let y2 = y1 + self.grid_size_y;
- if y1 >= self.y + self.h {
- continue;
- }
- if y1 >= self.y {
- quads.push((self.x, y1, self.w, 1.0, grid_color));
- }
- if y2 >= self.y && y2 < self.y + self.h {
- quads.push((self.x, y2, self.w, 1.0, grid_color));
- }
- }
- }
-
- let step_x = self.grid_size_x + self.skipped_col_w;
- if step_x >= 4.0 {
- let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
- let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
- let k_start = k_start.max(-100_000);
- let k_end = k_end.min(100_000);
- for k in k_start..=k_end {
- let x1 = self.grid_origin_x + (k as f32) * step_x;
- let x2 = x1 + self.grid_size_x;
- if x1 >= self.x + self.w {
- continue;
- }
- if x1 >= self.x {
- quads.push((x1, self.y, 1.0, self.h, grid_color));
- }
- if x2 >= self.x && x2 < self.x + self.w {
- quads.push((x2, self.y, 1.0, self.h, grid_color));
- }
- }
- }
- quads
- }
-}
-
-pub struct ViewportBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
-}
-
-impl ViewportBg {
- pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
-}
-
-impl Element for ViewportBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::VIEWPORT_BG }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn hit_test(&self, _px: f32, _py: f32) -> bool { false }
-}
-
-pub struct ParametersBg {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- display_params: Vec<(String, String, String)>,
- dragging_param: Option<usize>,
- pub focused_param: Option<usize>,
- mouse_pos: Option<(f32, f32)>,
- sliders: Vec<Option<Slider>>,
- float3s: Vec<Option<Float3>>,
- spinboxes: Vec<Option<Spinbox>>,
- visible: bool,
-}
-
-impl ParametersBg {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
- display_params: Vec::new(),
- dragging_param: None,
- focused_param: None,
- mouse_pos: None,
- sliders: Vec::new(),
- float3s: Vec::new(),
- spinboxes: Vec::new(),
- visible: true,
- }
- }
-
- pub fn get_param_rects(&self) -> Vec<(f32, f32, f32, f32)> {
- let mut rects = Vec::new();
- let mut cur_y = self.y + 30.0;
- for p in &self.display_params {
- let h = if p.2 == "code" {
- 200.0
- } else if p.2 == "section" {
- 24.0
- } else if p.2.starts_with("float3") {
- 108.0
- } else if p.2 == "text" {
- 24.0
- } else {
- 20.0
- };
- rects.push((self.x + 8.0, cur_y, self.w - 16.0, h));
- cur_y += h + 8.0;
- }
- rects
- }
-
- fn update_slider_rects(&mut self) {
- let rects = self.get_param_rects();
- for (i, s_opt) in self.sliders.iter_mut().enumerate() {
- if let Some(s) = s_opt {
- let r = rects[i];
- let track_x = self.x + 100.0;
- let track_w = (self.w - 100.0 - 20.0).max(10.0);
- let track_y = r.1 + 4.0;
- let track_h = 12.0;
- s.set_rect(track_x, track_y, track_w, track_h);
- }
- }
- for (i, f_opt) in self.float3s.iter_mut().enumerate() {
- if let Some(f) = f_opt {
- let r = rects[i];
- f.set_rect(r.0, r.1, r.2, r.3);
- }
- }
- for (i, sb_opt) in self.spinboxes.iter_mut().enumerate() {
- if let Some(sb) = sb_opt {
- let r = rects[i];
- let box_x = self.x + 100.0;
- let box_w = (self.w - 100.0 - 16.0).max(10.0);
- sb.set_rect(box_x, r.1, box_w, r.3);
- }
- }
- }
-}
-
-fn parse_slider_range(ptype: &str) -> (f32, f32) {
- if ptype.starts_with("slider:") || ptype.starts_with("float3:") {
- let parts: Vec<&str> = ptype.split(':').collect();
- if parts.len() >= 3 {
- if let (Ok(min), Ok(max)) = (parts[1].parse::<f32>(), parts[2].parse::<f32>()) {
- return (min, max);
- }
- }
- }
- (0.0, 2.0)
-}
-
-fn parse_spinbox_range(ptype: &str) -> (i32, i32, i32) {
- if ptype.starts_with("spinbox:") {
- let parts: Vec<&str> = ptype.split(':').collect();
- if parts.len() >= 4 {
- if let (Ok(min), Ok(max), Ok(step)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>(), parts[3].parse::<i32>()) {
- return (min, max, step);
- }
- } else if parts.len() == 3 {
- if let (Ok(min), Ok(max)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>()) {
- return (min, max, 1);
- }
- }
- }
- (0, 10000, 1)
-}
-
-fn parse_float3_value(val_str: &str, min: f32, max: f32) -> [f32; 3] {
- let mut out = [0.5, 0.5, 0.5];
- let parts: Vec<&str> = val_str
- .split(|c| c == ':' || c == ',' || c == ' ')
- .filter(|s| !s.is_empty())
- .collect();
- for i in 0..3 {
- if i < parts.len() {
- if let Ok(v) = parts[i].parse::<f32>() {
- let range = max - min;
- if range != 0.0 {
- out[i] = ((v - min) / range).clamp(0.0, 1.0);
- } else {
- out[i] = 0.0;
- }
- }
- }
- }
- out
-}
-
-impl Element for ParametersBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x; self.y = y; self.w = w; self.h = h;
- self.update_slider_rects();
- }
- fn color(&self) -> [f32; 4] {
- if !self.visible {
- return [0.0, 0.0, 0.0, 0.0];
- }
- colors::PARAM_BG
- }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- }
- fn visible(&self) -> bool {
- self.visible
- }
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
- }
-
- fn set_display_params(&mut self, params: &[(String, String, String)]) {
- let mut layout_changed = self.display_params.len() != params.len();
- if !layout_changed {
- for (p_old, p_new) in self.display_params.iter().zip(params.iter()) {
- if p_old.0 != p_new.0 || p_old.2 != p_new.2 {
- layout_changed = true;
- break;
- }
- }
- }
-
- if layout_changed {
- self.display_params = params.to_vec();
- self.focused_param = None;
- self.sliders = self.display_params.iter().map(|p| {
- if p.2.starts_with("slider") {
- let val = p.1.parse::<f32>().unwrap_or(0.0);
- let (min, max) = parse_slider_range(&p.2);
- let t = if max - min != 0.0 {
- ((val - min) / (max - min)).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- Some(Slider::new().with_value(t).with_range(min, max).with_readout(true))
- } else {
- None
- }
- }).collect();
- self.float3s = self.display_params.iter().map(|p| {
- if p.2.starts_with("float3") {
- let (min, max) = parse_slider_range(&p.2);
- let vals = parse_float3_value(&p.1, min, max);
- Some(Float3::new().with_values(vals).with_range(min, max).with_label(&p.0))
- } else {
- None
- }
- }).collect();
- self.spinboxes = self.display_params.iter().map(|p| {
- if p.2.starts_with("spinbox") {
- let (min, max, step) = parse_spinbox_range(&p.2);
- let val = p.1.parse::<i32>().unwrap_or(min);
- Some(Spinbox::new(val, min, max, step))
- } else {
- None
- }
- }).collect();
- } else {
- for (i, p_new) in params.iter().enumerate() {
- if Some(i) != self.focused_param && Some(i) != self.dragging_param {
- self.display_params[i].1 = p_new.1.clone();
- if let Some(ref mut s) = self.sliders[i] {
- let val = p_new.1.parse::<f32>().unwrap_or(0.0);
- let (min, max) = parse_slider_range(&p_new.2);
- let t = if max - min != 0.0 {
- ((val - min) / (max - min)).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- s.set_value(t);
- } else if let Some(ref mut f) = self.float3s[i] {
- let (min, max) = parse_slider_range(&p_new.2);
- let vals = parse_float3_value(&p_new.1, min, max);
- f.set_values(vals);
- } else if let Some(ref mut sb) = self.spinboxes[i] {
- if !sb.editing {
- let (min, _max, _step) = parse_spinbox_range(&p_new.2);
- let val = p_new.1.parse::<i32>().unwrap_or(min);
- sb.value = val;
- }
- }
- }
- }
- }
- self.update_slider_rects();
- }
-
- fn unfocus(&mut self) {
- if let Some(idx) = self.focused_param {
- if idx < self.display_params.len() {
- let p = &mut self.display_params[idx];
- if p.2.starts_with("spinbox") {
- if let Some(sb) = &mut self.spinboxes[idx] {
- sb.unfocus();
- p.1 = sb.value.to_string();
- }
- } else if p.2.starts_with("slider") {
- if let Some(s) = &mut self.sliders[idx] {
- s.unfocus();
- let (min, max) = parse_slider_range(&p.2);
- let new_val = min + s.value * (max - min);
- p.1 = format!("{:.2}", new_val);
- }
- } else if p.2.starts_with("float3") {
- if let Some(f) = &mut self.float3s[idx] {
- f.unfocus();
- let (min, max) = parse_slider_range(&p.2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- }
- }
- }
- }
- self.focused_param = None;
- }
-
- fn node_params(&self) -> Vec<(String, String, String)> {
- self.display_params.clone()
- }
-
- fn draggable(&self) -> bool {
- self.dragging_param.is_some()
- || self.display_params.iter().any(|p| p.2.starts_with("slider") || p.2.starts_with("float3"))
- }
-
- fn is_dragging(&self) -> bool {
- self.dragging_param.is_some()
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- let rects = self.get_param_rects();
- for (i, p) in self.display_params.iter().enumerate() {
- if p.2.starts_with("slider") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y - 2.0 && py <= row_y + 18.0 {
- if let Some(s) = &mut self.sliders[i] {
- s.drag_begin(px, py);
- self.dragging_param = Some(i);
- break;
- }
- }
- } else if p.2.starts_with("float3") {
- let r = rects[i];
- if py >= r.1 && py <= r.1 + r.3 {
- if let Some(f) = &mut self.float3s[i] {
- if f.mouse_input(MouseButton::Left, ElementState::Pressed, px, py) {
- self.dragging_param = Some(i);
- break;
- }
- }
- }
- }
- }
- }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- if let Some(i) = self.dragging_param {
- if let Some(s) = &mut self.sliders[i] {
- if s.drag_update(px, py) {
- let (min, max) = parse_slider_range(&self.display_params[i].2);
- let new_val = min + s.value * (max - min);
- let old_val = &self.display_params[i].1;
- let new_val_str = format!("{:.2}", new_val);
- if *old_val != new_val_str {
- self.display_params[i].1 = new_val_str;
- return true;
- }
- }
- } else if let Some(f) = &mut self.float3s[i] {
- if f.drag_update(px, py) {
- let (min, max) = parse_slider_range(&self.display_params[i].2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- let old_val = &self.display_params[i].1;
- if *old_val != new_val_str {
- self.display_params[i].1 = new_val_str;
- return true;
- }
- }
- }
- }
- false
- }
-
- fn drag_end(&mut self) {
- if let Some(i) = self.dragging_param.take() {
- if let Some(s) = &mut self.sliders[i] {
- s.drag_end();
- } else if let Some(f) = &mut self.float3s[i] {
- f.drag_end();
- }
- }
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.mouse_pos = Some((px, py));
- true
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button == MouseButton::Left && state == ElementState::Pressed {
- let rects = self.get_param_rects();
- let mut clicked_any_focusable = false;
- for (i, p) in self.display_params.iter_mut().enumerate() {
- if p.2 == "code" {
- let r = rects[i];
- if px >= r.0 && px <= r.0 + r.2 && py >= r.1 + 18.0 && py <= r.1 + r.3 {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- break;
- }
- } else if p.2 == "text" {
- let box_x = self.x + 100.0;
- let box_w = (self.w - 100.0 - 16.0).max(10.0);
- let r = rects[i];
- if px >= box_x && px <= box_x + box_w && py >= r.1 && py <= r.1 + r.3 {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- break;
- }
- } else if p.2.starts_with("spinbox") {
- if let Some(sb) = &mut self.spinboxes[i] {
- if sb.mouse_input(button, state, px, py) {
- p.1 = sb.value.to_string();
- if sb.editing {
- self.focused_param = Some(i);
- } else {
- self.unfocus();
- }
- return true;
- }
- }
- } else if p.2.starts_with("slider") {
- let r = rects[i];
- if py >= r.1 && py <= r.1 + r.3 {
- if let Some(s) = &mut self.sliders[i] {
- if s.mouse_input(button, state, px, py) {
- if s.editing {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- }
- break;
- }
- }
- }
- } else if p.2.starts_with("float3") {
- let r = rects[i];
- if py >= r.1 && py <= r.1 + r.3 {
- if let Some(f) = &mut self.float3s[i] {
- if f.mouse_input(button, state, px, py) {
- if f.editing_idx.is_some() {
- self.focused_param = Some(i);
- clicked_any_focusable = true;
- }
- break;
- }
- }
- }
- }
- }
- if !clicked_any_focusable {
- self.unfocus();
- }
- return true;
- }
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if let Some(idx) = self.focused_param {
- if event.state == ElementState::Pressed {
- let p = &mut self.display_params[idx];
- if p.2 == "code" {
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if !p.1.is_empty() {
- p.1.pop();
- return true;
- }
- }
- Key::Named(NamedKey::Enter) => {
- p.1.push('\n');
- return true;
- }
- Key::Named(NamedKey::Escape) => {
- self.focused_param = None;
- return true;
- }
- Key::Character(s) => {
- p.1.push_str(s);
- return true;
- }
- _ => {}
- }
- } else if p.2 == "text" {
- match &event.logical_key {
- Key::Named(NamedKey::Backspace) => {
- if !p.1.is_empty() {
- p.1.pop();
- return true;
- }
- }
- Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Escape) => {
- self.focused_param = None;
- return true;
- }
- Key::Character(s) => {
- p.1.push_str(s);
- return true;
- }
- _ => {}
- }
- } else if p.2.starts_with("spinbox") {
- if let Some(sb) = &mut self.spinboxes[idx] {
- if sb.keyboard_input(event) {
- if !sb.editing {
- p.1 = sb.value.to_string();
- self.focused_param = None;
- } else {
- p.1 = sb.edit_buffer.clone();
- }
- return true;
- }
- }
- } else if p.2.starts_with("slider") {
- if let Some(s) = &mut self.sliders[idx] {
- if s.keyboard_input(event) {
- let (min, max) = parse_slider_range(&p.2);
- let new_val = min + s.value * (max - min);
- p.1 = format!("{:.2}", new_val);
- if !s.editing {
- self.focused_param = None;
- }
- return true;
- }
- }
- } else if p.2.starts_with("float3") {
- if let Some(f) = &mut self.float3s[idx] {
- if f.keyboard_input(event) {
- let (min, max) = parse_slider_range(&p.2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- p.1 = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- if f.editing_idx.is_none() {
- self.focused_param = None;
- }
- return true;
- }
- }
- }
- }
- }
- false
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- let mut changed = false;
- let rects = self.get_param_rects();
- for (i, p) in self.display_params.iter_mut().enumerate() {
- if p.2.starts_with("slider") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y - 2.0 && py <= row_y + 18.0 && px >= self.x && px <= self.x + self.w {
- if let Some(s) = &mut self.sliders[i] {
- let was_scroll = s.scroll_enabled;
- s.set_scroll(true);
- if s.mouse_wheel(delta, px, py) {
- let (min, max) = parse_slider_range(&p.2);
- let new_val = min + s.value * (max - min);
- let old_val = &p.1;
- let new_val_str = format!("{:.2}", new_val);
- if *old_val != new_val_str {
- p.1 = new_val_str;
- changed = true;
- }
- }
- s.set_scroll(was_scroll);
- }
- }
- } else if p.2.starts_with("float3") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
- if let Some(f) = &mut self.float3s[i] {
- let rects_inner = f.get_row_rects();
- for j in 0..3 {
- let r_inner = rects_inner[j];
- if py >= r_inner.1 && py <= r_inner.1 + r_inner.3 {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y,
- MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
- };
- let step = 0.02;
- let new_val = (f.values[j] - scroll_amount * step).clamp(0.0, 1.0);
- if (new_val - f.values[j]).abs() > 0.0001 {
- f.values[j] = new_val;
- if f.editing_idx == Some(j) {
- let scaled_val = f.mins[j] + f.values[j] * (f.maxs[j] - f.mins[j]);
- f.edit_buffer = format!("{:.2}", scaled_val);
- }
- let (min, max) = parse_slider_range(&p.2);
- let val0 = min + f.values[0] * (max - min);
- let val1 = min + f.values[1] * (max - min);
- let val2 = min + f.values[2] * (max - min);
- let new_val_str = format!("{:.2}:{:.2}:{:.2}", val0, val1, val2);
- if p.1 != new_val_str {
- p.1 = new_val_str;
- changed = true;
- }
- }
- }
- }
- }
- }
- } else if p.2.starts_with("spinbox") {
- let r = rects[i];
- let row_y = r.1;
- if py >= row_y && py <= row_y + r.3 && px >= self.x && px <= self.x + self.w {
- if let Some(sb) = &mut self.spinboxes[i] {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y as i32,
- MouseScrollDelta::PixelDelta(pos) => {
- let dy = pos.y;
- if dy > 0.0 { 1 } else if dy < 0.0 { -1 } else { 0 }
- }
- };
- let new_val = (sb.value + scroll_amount * sb.step).clamp(sb.min, sb.max);
- if sb.value != new_val {
- sb.value = new_val;
- p.1 = new_val.to_string();
- changed = true;
- }
- }
- }
- }
- }
- changed
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
- let rects = self.get_param_rects();
-
- // Find sections and their ranges
- let mut sections = Vec::new();
- let mut current_section: Option<(usize, usize)> = None;
- let mut in_section = false;
- for (i, p) in self.display_params.iter().enumerate() {
- if p.2 == "section" {
- if let Some((start, end)) = current_section {
- sections.push((start, end));
- }
- current_section = None;
- in_section = true;
- } else {
- if in_section {
- if let Some((_, ref mut end)) = current_section {
- *end = i;
- } else {
- current_section = Some((i, i));
- }
- }
- }
- }
- if let Some((start, end)) = current_section {
- sections.push((start, end));
- }
-
- // Draw section border boxes
- for (start, end) in sections {
- if start <= end && start < rects.len() && end < rects.len() {
- let r_start = rects[start];
- let r_end = rects[end];
- let bx = self.x + 4.0;
- let bw = self.w - 8.0;
- let by = r_start.1 - 4.0;
- let bh = (r_end.1 + r_end.3 + 4.0) - by;
-
- let border_color = [0.18, 0.18, 0.27, 1.0];
- let border_t = 1.0;
-
- // Top border
- quads.push((bx, by, bw, border_t, border_color));
- // Bottom border
- quads.push((bx, by + bh - border_t, bw, border_t, border_color));
- // Left border
- quads.push((bx, by, border_t, bh, border_color));
- // Right border
- quads.push((bx + bw - border_t, by, border_t, bh, border_color));
- }
- }
-
- for (i, p) in self.display_params.iter().enumerate() {
- let r = rects[i];
- if p.2.starts_with("slider") {
- if let Some(s) = &self.sliders[i] {
- let (sx, sy, sw, sh) = s.rect();
- quads.push((sx, sy, sw, sh, s.color()));
- quads.extend(s.extra_quads());
- }
- } else if p.2 == "section" {
- // Section header line is handled by the border box top border now
- } else if p.2.starts_with("float3") {
- if let Some(f) = &self.float3s[i] {
- quads.extend(f.extra_quads());
- }
- } else if p.2 == "code" {
- quads.push((r.0, r.1 + 18.0, r.2, r.3 - 18.0, [0.08, 0.08, 0.10, 1.0]));
- let border_color = if self.focused_param == Some(i) {
- [0.25, 0.45, 0.85, 1.0]
- } else {
- [0.20, 0.20, 0.25, 1.0]
- };
- let (bx, by, bw, bh) = (r.0, r.1 + 18.0, r.2, r.3 - 18.0);
- quads.push((bx, by, bw, 1.0, border_color));
- quads.push((bx, by + bh - 1.0, bw, 1.0, border_color));
- quads.push((bx, by, 1.0, bh, border_color));
- quads.push((bx + bw - 1.0, by, 1.0, bh, border_color));
- } else if p.2 == "text" {
- let box_x = self.x + 100.0;
- let box_w = (self.w - 100.0 - 16.0).max(10.0);
- quads.push((box_x, r.1, box_w, r.3, [0.08, 0.08, 0.10, 1.0]));
- let border_color = if self.focused_param == Some(i) {
- [0.25, 0.45, 0.85, 1.0]
- } else {
- [0.20, 0.20, 0.25, 1.0]
- };
- let (bx, by, bw, bh) = (box_x, r.1, box_w, r.3);
- let border_t = 1.0;
- quads.push((bx, by, bw, border_t, border_color));
- quads.push((bx, by + bh - border_t, bw, border_t, border_color));
- quads.push((bx, by, border_t, bh, border_color));
- quads.push((bx + bw - border_t, by, border_t, bh, border_color));
- } else if p.2.starts_with("spinbox") {
- if let Some(sb) = &self.spinboxes[i] {
- quads.push((sb.base.x, sb.base.y, sb.base.w, sb.base.h, sb.color()));
- quads.extend(sb.extra_quads());
- }
- }
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let rects = self.get_param_rects();
- let mut labels = Vec::new();
- for (i, (name, value, ptype)) in self.display_params.iter().enumerate() {
- let r = rects[i];
- if ptype.starts_with("slider") {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 8.0,
- y: r.1,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- if let Some(s) = &self.sliders[i] {
- labels.extend(s.text_labels());
- }
- } else if ptype.starts_with("float3") {
- if let Some(f) = &self.float3s[i] {
- labels.extend(f.text_labels());
- }
- } else if ptype == "section" {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 12.0,
- y: r.1 + 2.0,
- font_size: 13.0,
- color: [0xee, 0xee, 0xf0],
- });
- } else if ptype == "code" {
- labels.push(TextLabel {
- text: format!("{}:\n{}", name, value),
- x: self.x + 12.0,
- y: r.1,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- } else if ptype.starts_with("spinbox") {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 8.0,
- y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- if let Some(sb) = &self.spinboxes[i] {
- labels.extend(sb.text_labels());
- }
- } else if ptype == "text" {
- labels.push(TextLabel {
- text: name.clone(),
- x: self.x + 8.0,
- y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- let val_text = if self.focused_param == Some(i) {
- format!("{}|", value)
- } else {
- value.clone()
- };
- labels.push(TextLabel {
- text: val_text,
- x: self.x + 106.0,
- y: r.1 + (r.3 - 12.0) / 2.0 - 2.0,
- font_size: 12.0,
- color: [0xee, 0xee, 0xf0],
- });
- } else {
- labels.push(TextLabel {
- text: format!("{}: {}", name, value),
- x: self.x + 8.0,
- y: r.1,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- }
- }
- labels
- }
-}
-
-pub struct MenuBar {
- x: f32, y: f32, w: f32, h: f32,
- hovering: bool,
- pub title: String,
- pub menus: Vec<Box<Menu>>,
- pub menu_items: Vec<String>,
- pub vertical_items: Vec<String>,
- pub menu_dropdowns: Vec<Vec<String>>,
- pub menu_dropdown_checked: Vec<Vec<Option<bool>>>,
- pub hovered_menu: Option<usize>,
- pub open_menu: Option<usize>,
- pub hovered_dropdown: Option<usize>,
- pub clicked_dropdown: Option<(usize, usize)>,
- pub was_open: Option<usize>,
- pub vertical: bool,
- pub visible: bool,
- pub focused: bool,
- pub z_level: i32,
- pub center_items: bool,
- pub curved_circle: Option<(f32, f32, f32)>,
- pub title_pos: Option<(f32, f32)>,
- pub title_buf: Option<glyphon::Buffer>,
- pub curved_title_char_bufs: Vec<glyphon::Buffer>,
- pub network_opacity: f32,
-}
-
-impl MenuBar {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- x, y, w, h, hovering: false,
- title: String::new(),
- menus: Vec::new(),
- menu_items: Vec::new(),
- vertical_items: Vec::new(),
- menu_dropdowns: Vec::new(),
- menu_dropdown_checked: Vec::new(),
- hovered_menu: None,
- open_menu: None,
- hovered_dropdown: None,
- clicked_dropdown: None,
- was_open: None,
- vertical: false,
- visible: true,
- focused: false,
- z_level: 100,
- center_items: false,
- curved_circle: None,
- title_pos: None,
- title_buf: None,
- curved_title_char_bufs: Vec::new(),
- network_opacity: 1.0,
- }
- }
-
- pub fn with_center_items(mut self, center: bool) -> Self {
- self.center_items = center;
- self
- }
-
- pub fn with_title(mut self, title: &str) -> Self {
- self.title = title.to_string();
- self
- }
-
- pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
- self.menu_items.push(label.to_string());
- self.vertical_items.push(label.to_string());
- self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
- self.menu_dropdown_checked.push(vec![None; items.len()]);
-
- let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
- let mut menu = Menu::new(label, label, &item_strs);
- menu.vertical = self.vertical;
- self.menus.push(Box::new(menu));
- self
- }
-
- pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
- self.menu_items.push(horizontal_label.to_string());
- self.vertical_items.push(vertical_label.to_string());
- self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
- self.menu_dropdown_checked.push(vec![None; items.len()]);
-
- let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
- let mut menu = Menu::new(horizontal_label, vertical_label, &item_strs);
- menu.vertical = self.vertical;
- self.menus.push(Box::new(menu));
- self
- }
-
- pub fn with_vertical(mut self, vertical: bool) -> Self {
- self.vertical = vertical;
- for menu in &mut self.menus {
- menu.vertical = vertical;
- }
- self
- }
-
- pub fn with_z_index(mut self, z: i32) -> Self {
- self.z_level = z;
- self
- }
-
- fn item_y_vertical(&self, idx: usize) -> f32 {
- let mut y = 8.0;
- if !self.title.is_empty() {
- y += 24.0;
- }
- y + idx as f32 * 24.0
- }
-
- fn item_h_vertical(&self) -> f32 {
- 24.0
- }
-}
-
-impl Element for MenuBar {
- fn rect(&self) -> (f32, f32, f32, f32) {
- if !self.visible {
- return (0.0, 0.0, 0.0, 0.0);
- }
- if self.vertical {
- let total_h = if self.menus.is_empty() {
- self.h
- } else {
- let last_idx = self.menus.len() - 1;
- self.item_y_vertical(last_idx) + self.item_h_vertical()
- };
- (self.x, self.y, self.w, total_h)
- } else {
- (self.x, self.y, self.w, self.h)
- }
- }
-
- fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
- self.curved_circle = circle;
- if circle.is_none() {
- for menu in &mut self.menus {
- menu.curved_arc = None;
- }
- }
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
-
- let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
-
- if self.vertical {
- let mut cy = 8.0;
- if !self.title.is_empty() {
- cy += 24.0;
- }
- for menu in &mut self.menus {
- let ih = 24.0;
- menu.set_rect(x, y + cy, w, ih);
- menu.set_parent(Some(parent_ptr));
- cy += ih;
- }
- } else {
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
-
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let mut current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = self.title.len() as f32 * 7.5 + 24.0;
- let dtheta_title = title_w / r_mid;
- let theta_title = current_angle + dtheta_title / 2.0;
-
- let tx = ccx + r_mid * theta_title.cos() - title_w / 2.0 + 8.0;
- let ty = ccy + r_mid * theta_title.sin() - h / 2.0;
- self.title_pos = Some((tx, ty));
- current_angle += dtheta_title;
- } else {
- self.title_pos = None;
- }
-
- for menu in &mut self.menus {
- let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
- let dtheta_menu = iw / r_mid;
- let theta_menu = current_angle + dtheta_menu / 2.0;
-
- let mx = ccx + r_mid * theta_menu.cos() - iw / 2.0;
- let my = ccy + r_mid * theta_menu.sin() - h / 2.0;
-
- menu.set_rect(mx, my, iw, h);
- menu.set_parent(Some(parent_ptr));
- menu.curved_arc = Some((ccx, ccy, ccr, h, current_angle, current_angle + dtheta_menu));
- current_angle += dtheta_menu;
- }
- } else {
- self.title_pos = None;
- let mut cx = 8.0;
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- if self.w > total_width {
- cx = (self.w - total_width) / 2.0;
- }
- }
- if !self.title.is_empty() {
- cx += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &mut self.menus {
- let iw = menu.active_title().len() as f32 * 7.5 + 16.0;
- menu.set_rect(x + cx, y, iw, h);
- menu.set_parent(Some(parent_ptr));
- cx += iw;
- }
- }
- }
- }
-
- fn set_network_opacity(&mut self, opacity: f32) {
- self.network_opacity = opacity;
- }
-
- fn color(&self) -> [f32; 4] {
- if !self.visible {
- [0.0, 0.0, 0.0, 0.0]
- } else if self.focused {
- let mut c = colors::PANEL_MENU_FOCUSED;
- c[3] *= self.network_opacity;
- c
- } else {
- let mut c = colors::PANEL_MENU_BG;
- c[3] *= self.network_opacity;
- c
- }
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.hovering = v;
- }
-
- fn hovered(&self) -> bool {
- self.hovering
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let dx = px - ccx;
- let dy = py - ccy;
- let dist = (dx * dx + dy * dy).sqrt();
- if dist >= ccr - self.h && dist <= ccr {
- let angle = dy.atan2(dx);
- let mut norm_angle = angle;
- if norm_angle < 0.0 {
- norm_angle += 2.0 * std::f32::consts::PI;
- }
-
- let r_mid = ccr - self.h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let end_angle = 1.5 * std::f32::consts::PI + total_angular_width / 2.0;
-
- if norm_angle >= start_angle && norm_angle <= end_angle {
- return true;
- }
- }
- for menu in &self.menus {
- if menu.hit_test(px, py) {
- return true;
- }
- }
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
- return true;
- }
- for menu in &self.menus {
- if menu.hit_test(px, py) {
- return true;
- }
- }
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- self.set_rect(rx, ry, rw, rh);
-
- if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/home/lsgalante/Dropbox/Clear/debug.txt") {
- use std::io::Write;
- let _ = writeln!(f, "MenuBar::on_cursor_moved px={}, py={} curved={:?} rect={:?}", px, py, self.curved_circle, (rx, ry, rw, rh));
- }
-
- let mut changed = false;
- self.hovered_menu = None;
- for (idx, menu) in self.menus.iter_mut().enumerate() {
- if menu.cursor_moved(px, py) {
- changed = true;
- }
- if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/home/lsgalante/Dropbox/Clear/debug.txt") {
- use std::io::Write;
- let _ = writeln!(f, " Menu[{}] active_title={} curved={:?} hovered={} hit={}", idx, menu.active_title(), menu.curved_arc, menu.hovered(), menu.hit_test(px, py));
- }
- if menu.hovered() {
- self.hovered_menu = Some(idx);
- }
- }
- changed
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- self.set_rect(rx, ry, rw, rh);
-
- let mut changed = false;
- for menu in &mut self.menus {
- let res = menu.mouse_input(button, state, px, py);
- if res {
- changed = true;
- }
- }
- if !self.is_menu_open() {
- self.unfocus();
- }
- changed
- }
-
- fn focus(&mut self) {
- if self.is_menu_open() {
- self.focused = true;
- for menu in &mut self.menus {
- if menu.is_menu_open() {
- menu.focus();
- return;
- }
- }
- } else {
- self.focused = false;
- focus::clear_if_matches(self);
- return;
- }
- self.focused = true;
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- self.focused = false;
- focus::clear_if_matches(self);
- for menu in &mut self.menus {
- menu.unfocus();
- }
- }
-
- fn focused(&self) -> bool {
- self.focused || self.is_menu_open()
- }
-
- fn set_selected(&mut self, selected: bool) {
- self.focused = selected;
- if !selected {
- for menu in &mut self.menus {
- menu.set_selected(false);
- }
- }
- }
-
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- for menu in &mut self.menus {
- menu.set_modifiers(ctrl, shift, alt);
- }
- }
-
- fn menu_names(&self) -> Vec<String> {
- self.menu_items.clone()
- }
-
- fn menu_click(&mut self) -> Option<(usize, usize)> {
- for (idx, menu) in self.menus.iter_mut().enumerate() {
- if let Some((_, item_idx)) = menu.menu_click() {
- return Some((idx, item_idx));
- }
- }
- None
- }
-
- fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
- if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
- if item_idx < menu.len() {
- menu[item_idx] = Some(checked);
- }
- }
- if let Some(menu) = self.menus.get_mut(menu_idx) {
- menu.set_item_checked(0, item_idx, checked);
- }
- }
-
- fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
- if menu_idx < self.menu_dropdowns.len() {
- self.menu_dropdowns[menu_idx] = items.to_vec();
- self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
- }
- if let Some(menu) = self.menus.get_mut(menu_idx) {
- menu.items = items.to_vec();
- menu.item_checked = vec![Some(false); items.len()];
- menu.item_bufs.clear();
- }
- }
-
- fn is_menu_bar(&self) -> bool {
- self.visible
- }
-
- fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
- if !self.visible {
- return None;
- }
- for (idx, menu) in self.menus.iter().enumerate() {
- if menu.hit_test(px, py) {
- let mut formatted_items = Vec::new();
- for (i, item) in menu.items.iter().enumerate() {
- let checked = menu.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "✓ ",
- Some(false) => " ",
- None => "",
- };
- formatted_items.push(format!("{}{}", prefix, item));
- }
- return Some((idx, menu.active_title().to_string(), formatted_items, menu.base.x, menu.base.y, menu.base.w, menu.base.h));
- }
- }
- None
- }
-
- fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
- if let Some(menu) = self.menus.get_mut(menu_idx) {
- menu.clicked_item = Some(item_idx);
- }
- }
-
- fn is_menu_open(&self) -> bool {
- self.visible && self.menus.iter().any(|m| m.is_menu_open())
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
- for menu in &self.menus {
- quads.extend(menu.all_quads());
- }
- quads
- }
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut arcs = Vec::new();
- for menu in &self.menus {
- arcs.extend(menu.extra_arcs());
- }
- arcs
- }
-
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- if !self.visible {
- return;
- }
- if !self.title.is_empty() {
- if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
- if self.curved_title_char_bufs.len() != self.title.chars().count() {
- self.curved_title_char_bufs = self.title.chars()
- .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
- .collect();
- }
- self.title_buf = None;
- } else {
- if self.title_buf.is_none() {
- self.title_buf = Some(make_widget_text_buffer(fs, &self.title, 12.0, "Outfit"));
- }
- self.curved_title_char_bufs.clear();
- }
- } else {
- self.title_buf = None;
- self.curved_title_char_bufs.clear();
- }
- for menu in &mut self.menus {
- menu.prepare_text(fs);
- }
- }
-
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- if !self.visible {
- return Vec::new();
- }
- let mut items = Vec::new();
- let color = glyphon::Color::rgb(0xaa, 0xaa, 0xbb);
-
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - self.h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = self.title.len() as f32 * 7.5 + 24.0;
- let dtheta_title = title_w / r_mid;
-
- let char_widths: Vec<f32> = self.title.chars().map(|c| {
- TextLabel::estimate_width(&c.to_string(), 12.0)
- }).collect();
- let total_chars_width: f32 = char_widths.iter().sum();
- let mid_angle = (current_angle + current_angle + dtheta_title) / 2.0;
- let angular_width = total_chars_width / r_mid;
- let text_start_angle = mid_angle - angular_width / 2.0;
- let mut cur_char_angle = text_start_angle;
-
- for (char_idx, c_buf) in self.curved_title_char_bufs.iter().enumerate() {
- let cw = char_widths[char_idx];
- let dtheta = cw / r_mid;
- let char_center_angle = cur_char_angle + dtheta / 2.0;
-
- let tx = ccx + r_mid * char_center_angle.cos() - cw / 2.0;
- let ty = ccy + r_mid * char_center_angle.sin() - 12.0 / 2.0;
-
- items.push((c_buf, tx, ty, color));
- cur_char_angle += dtheta;
- }
- }
- } else {
- let mut start_x = 8.0;
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- if self.w > total_width {
- start_x = (self.w - total_width) / 2.0;
- }
- }
- if let Some(ref title_buf) = self.title_buf {
- items.push((title_buf, self.x + start_x, self.y + 7.0, color));
- }
- }
-
- for menu in &self.menus {
- items.extend(menu.get_text_items());
- }
- items
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let mut labels = Vec::new();
- if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - self.h / 2.0;
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- let total_angular_width = total_width / r_mid;
- let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
- let current_angle = start_angle;
-
- if !self.title.is_empty() {
- let title_w = self.title.len() as f32 * 7.5 + 24.0;
- let dtheta_title = title_w / r_mid;
- labels.extend(TextLabel::curved_layout(
- &self.title,
- ccx, ccy, r_mid,
- current_angle, current_angle + dtheta_title,
- 12.0,
- [0xaa, 0xaa, 0xbb],
- ));
- }
- } else {
- let mut start_x = 8.0;
- if self.center_items {
- let mut total_width = 8.0;
- if !self.title.is_empty() {
- total_width += self.title.len() as f32 * 7.5 + 24.0;
- }
- for menu in &self.menus {
- total_width += menu.active_title().len() as f32 * 7.5 + 16.0;
- }
- if self.w > total_width {
- start_x = (self.w - total_width) / 2.0;
- }
- }
- if !self.title.is_empty() {
- labels.push(TextLabel {
- text: self.title.clone(),
- x: self.x + start_x,
- y: self.y + 7.0,
- font_size: 12.0,
- color: [0xaa, 0xaa, 0xbb],
- });
- }
- }
- for menu in &self.menus {
- labels.extend(menu.text_labels());
- }
- labels
- }
-
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- for menu in &mut self.menus {
- menu.set_visible(visible);
- }
- }
-
- fn visible(&self) -> bool {
- self.visible
- }
-
- fn children(&self) -> Vec<*mut (dyn Element + 'static)> {
- self.menus.iter().map(|m| {
- let ptr: *const dyn Element = &**m as &dyn Element;
- ptr as *mut (dyn Element + 'static)
- }).collect()
- }
-
- fn z_index(&self) -> i32 {
- self.z_level
- }
-
- fn set_center_items(&mut self, center: bool) {
- self.center_items = center;
- }
-
- fn menu_items_list(&self) -> Vec<Vec<String>> {
- self.menu_dropdowns.clone()
- }
-
- fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
- self.menu_dropdown_checked.clone()
- }
-}
-
-impl Drop for MenuBar {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Menu {
- pub base: Widget,
- pub title: String,
- pub vertical_title: String,
- pub items: Vec<String>,
- pub item_checked: Vec<Option<bool>>,
- pub open: bool,
- pub vertical: bool,
- hovered_item: Option<usize>,
- clicked_item: Option<usize>,
- was_open: Option<usize>,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
- pub curved_arc: Option<(f32, f32, f32, f32, f32, f32)>,
- pub title_buf: Option<glyphon::Buffer>,
- pub item_bufs: Vec<glyphon::Buffer>,
- pub check_buf: Option<glyphon::Buffer>,
- pub curved_char_bufs: Vec<glyphon::Buffer>,
-}
-
-impl Menu {
- pub fn new(title: &str, vertical_title: &str, items: &[String]) -> Self {
- Self {
- base: Widget::new(),
- title: title.to_string(),
- vertical_title: vertical_title.to_string(),
- items: items.to_vec(),
- item_checked: vec![None; items.len()],
- open: false,
- vertical: false,
- hovered_item: None,
- clicked_item: None,
- was_open: None,
- parent: None,
- children: Vec::new(),
- curved_arc: None,
- title_buf: None,
- item_bufs: Vec::new(),
- check_buf: None,
- curved_char_bufs: Vec::new(),
- }
- }
-
- pub fn active_title(&self) -> &str {
- if self.vertical {
- &self.vertical_title
- } else {
- &self.title
- }
- }
-
- fn dropdown_rect(&self) -> (f32, f32, f32, f32) {
- let dh = self.items.len() as f32 * DROPDOWN_ITEM_H;
- let mut max_len = 0;
- for item in &self.items {
- max_len = max_len.max(item.len());
- }
- let dw = (max_len as f32 * 7.5 + 40.0).max(120.0);
- let dx = if self.vertical {
- self.base.x + self.base.w
- } else {
- self.base.x
- };
- let dy = if self.vertical {
- self.base.y
- } else {
- self.base.y + self.base.h
- };
- (dx, dy, dw, dh)
- }
-}
-impl Element for Menu {
- crate::impl_widget_base!(Menu);
-
- fn label(&self) -> Option<String> {
- Some(self.active_title().to_string())
- }
-
- fn color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let dx = px - cx;
- let dy = py - cy;
- let dist = (dx * dx + dy * dy).sqrt();
- if dist >= r - thickness && dist <= r {
- let angle = dy.atan2(dx);
- let mut norm_angle = angle;
- if norm_angle < 0.0 {
- norm_angle += 2.0 * std::f32::consts::PI;
- }
- if norm_angle >= start_angle && norm_angle <= end_angle {
- return true;
- }
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
- return true;
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.was_open = None;
- let was_hovering = self.base.hovered;
- self.base.hovered = self.hit_test(px, py);
- let old_item = self.hovered_item;
- self.hovered_item = None;
-
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.items.len() {
- self.hovered_item = Some(di);
- }
- }
- }
-
- was_hovering != self.base.hovered || old_item != self.hovered_item
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left || state != ElementState::Pressed {
- return false;
- }
- if !self.hit_test(px, py) {
- return false;
- }
-
- // Check dropdown click if open
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.items.len() {
- self.clicked_item = Some(di);
- self.open = false;
- return true;
- }
- }
- }
-
- // Since we passed hit_test and didn't click dropdown, it's a click on the header title
- if self.was_open == Some(0) || self.open {
- self.open = false;
- self.was_open = None;
- } else {
- self.open = true;
- self.was_open = None;
- focus::set_focused(self);
- }
- true
- }
-
- fn focus(&mut self) {
- self.open = true;
- self.base.focused = true;
- focus::set_focused(self);
- }
-
- fn unfocus(&mut self) {
- if self.open {
- self.was_open = Some(0);
- }
- self.open = false;
- self.base.focused = false;
- focus::clear_if_matches(self);
- self.hovered_item = None;
- }
-
- fn set_selected(&mut self, selected: bool) {
- self.base.focused = selected;
- if !selected {
- self.open = false;
- self.was_open = None;
- self.hovered_item = None;
- focus::clear_if_matches(self);
- }
- }
-
- fn menu_click(&mut self) -> Option<(usize, usize)> {
- self.clicked_item.take().map(|i| (0, i))
- }
-
- fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
- if item_idx < self.item_checked.len() {
- self.item_checked[item_idx] = Some(checked);
- self.item_bufs.clear();
- }
- }
-
- fn is_menu_open(&self) -> bool {
- self.open
- }
-
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- if self.curved_arc.is_some() {
- None
- } else {
- let hc = self.highlight_color()?;
- Some((self.base.x, self.base.y, self.base.w, self.base.h, hc))
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 {
- quads.push((dx, dy, dw, dh, colors::PANEL_MENU_BG));
- if let Some(di) = self.hovered_item {
- quads.push((dx, dy + di as f32 * DROPDOWN_ITEM_H, dw, DROPDOWN_ITEM_H, colors::PANEL_MENU_HOVER));
- }
- }
- }
- quads
- }
-
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- let mut arcs = Vec::new();
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- if self.base.hovered && !self.open {
- arcs.push((cx, cy, r, thickness, start_angle, end_angle, colors::PANEL_MENU_HOVER));
- }
- }
- arcs
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let r_mid = r - thickness / 2.0;
- labels.extend(TextLabel::curved_layout(
- &self.active_title(),
- cx, cy, r_mid,
- start_angle, end_angle,
- 12.0,
- [0xcc, 0xcc, 0xd4],
- ));
- } else {
- labels.push(TextLabel {
- text: self.active_title().to_string(),
- x: self.base.x + 8.0,
- y: self.base.y + 7.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- }
- if self.open {
- let (dx, dy, _, _) = self.dropdown_rect();
- for (i, item) in self.items.iter().enumerate() {
- let checked = self.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "\u{2713} ",
- Some(false) => " ",
- None => "",
- };
- labels.push(TextLabel {
- text: format!("{}{}", prefix, item),
- x: dx + 8.0,
- y: dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
- font_size: 12.0,
- color: [0xcc, 0xcc, 0xd4],
- });
- }
- }
- labels
- }
-
- fn set_visible(&mut self, visible: bool) {
- self.base.hovered = false;
- if !visible {
- self.open = false;
- self.was_open = None;
- self.hovered_item = None;
- focus::clear_if_matches(self);
- }
- }
-
- fn parent(&self) -> Option<*mut (dyn Element + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
- self.parent = parent;
- }
-
- fn z_index(&self) -> i32 {
- 100
- }
-
- fn menu_items(&self) -> Vec<String> {
- self.items.clone()
- }
-
- fn menu_item_checked(&self) -> Vec<Option<bool>> {
- self.item_checked.clone()
- }
-
- fn is_vertical(&self) -> bool {
- self.vertical
- }
-
- fn focused(&self) -> bool {
- self.base.focused
- }
-
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- let title_text = self.active_title();
- if let Some((_cx, _cy, _r, _thickness, _start_angle, _end_angle)) = self.curved_arc {
- if self.curved_char_bufs.len() != title_text.chars().count() {
- self.curved_char_bufs = title_text.chars()
- .map(|c| make_widget_text_buffer(fs, &c.to_string(), 12.0, "Outfit"))
- .collect();
- }
- self.title_buf = None;
- } else {
- if self.title_buf.is_none() {
- self.title_buf = Some(make_widget_text_buffer(fs, title_text, 12.0, "Outfit"));
- }
- self.curved_char_bufs.clear();
- }
-
- if self.open {
- if self.item_bufs.len() != self.items.len() {
- self.item_bufs = self.items.iter().enumerate().map(|(i, item)| {
- let checked = self.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "\u{2713} ",
- Some(false) => " ",
- None => "",
- };
- let text = format!("{}{}", prefix, item);
- make_widget_text_buffer(fs, &text, 12.0, "Outfit")
- }).collect();
- }
- } else {
- self.item_bufs.clear();
- }
- }
-
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- let mut items = Vec::new();
- let color = glyphon::Color::rgb(0xcc, 0xcc, 0xd4);
-
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let r_mid = r - thickness / 2.0;
- let active_title = self.active_title();
-
- let char_widths: Vec<f32> = active_title.chars().map(|c| {
- TextLabel::estimate_width(&c.to_string(), 12.0)
- }).collect();
- let total_chars_width: f32 = char_widths.iter().sum();
-
- let angular_width = total_chars_width / r_mid;
- let text_start_angle = (start_angle + end_angle) / 2.0 - angular_width / 2.0;
- let mut cur_char_angle = text_start_angle;
-
- for (char_idx, c_buf) in self.curved_char_bufs.iter().enumerate() {
- if char_idx < char_widths.len() {
- let cw = char_widths[char_idx];
- let dtheta = cw / r_mid;
- let char_center_angle = cur_char_angle + dtheta / 2.0;
-
- let tx = cx + r_mid * char_center_angle.cos() - cw / 2.0;
- let ty = cy + r_mid * char_center_angle.sin() - 12.0 / 2.0;
-
- items.push((c_buf, tx, ty, color));
- cur_char_angle += dtheta;
- }
- }
- } else {
- if let Some(ref title_buf) = self.title_buf {
- items.push((title_buf, self.base.x + 8.0, self.base.y + 7.0, color));
- }
- }
-
- if self.open {
- let (dx, dy, _, _) = self.dropdown_rect();
- for (i, item_buf) in self.item_bufs.iter().enumerate() {
- items.push((
- item_buf,
- dx + 8.0,
- dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
- color,
- ));
- }
- }
-
- items
- }
-}
-
-unsafe impl Send for Menu {}
-unsafe impl Sync for Menu {}
-
-impl Drop for Menu {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-
-
-#[derive(Debug, Clone)]
-pub struct Breadcrumb {
- x: f32, y: f32, w: f32, h: f32,
- hovered: bool,
- path: Vec<String>,
- hovered_seg: Option<usize>,
- clicked_seg: Option<usize>,
- pub network_opacity: f32,
-}
-
-impl Breadcrumb {
- pub fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false,
- path: Vec::new(), hovered_seg: None, clicked_seg: None, network_opacity: 1.0 }
- }
-
- fn seg_at(&self, px: f32) -> Option<usize> {
- let mut cx = self.x + BREADCRUMB_PADDING;
- for (i, seg) in self.path.iter().enumerate() {
- let w = seg.len() as f32 * 7.5;
- if px >= cx && px < cx + w {
- return Some(i);
- }
- cx += w + SEGMENT_GAP;
- }
- None
- }
-}
-
-impl Element for Breadcrumb {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn set_network_opacity(&mut self, opacity: f32) { self.network_opacity = opacity; }
- fn color(&self) -> [f32; 4] { [0.10, 0.10, 0.14, self.network_opacity] }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovered;
- self.hovered = self.hit_test(px, py);
- let old = self.hovered_seg;
- self.hovered_seg = if self.hovered { self.seg_at(px) } else { None };
- was != self.hovered || old != self.hovered_seg
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, _py: f32) -> bool {
- if button != MouseButton::Left || state != ElementState::Pressed { return false; }
- if let Some(i) = self.seg_at(px) {
- if i < self.path.len() - 1 {
- self.clicked_seg = Some(i);
- return true;
- }
- }
- false
- }
-
- fn set_path(&mut self, segments: &[String]) {
- let mut s = Vec::with_capacity(segments.len().max(1));
- if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
- s.push("/".to_string());
- } else {
- s.push("/".to_string());
- for name in segments {
- s.push(format!(" \u{203A} {}", name));
- }
- }
- self.path = s;
- }
-
- fn path_click(&mut self) -> Option<usize> { self.clicked_seg.take() }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if let Some(i) = self.hovered_seg {
- let mut cx = self.x + BREADCRUMB_PADDING;
- for j in 0..i {
- let w = self.path[j].len() as f32 * 7.5;
- cx += w + SEGMENT_GAP;
- }
- let w = self.path[i].len() as f32 * 7.5;
- quads.push((cx, self.y, w, self.h, [1.0, 1.0, 1.0, 0.06]));
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- let mut cx = self.x + BREADCRUMB_PADDING;
- for (i, seg) in self.path.iter().enumerate() {
- labels.push(TextLabel {
- text: seg.clone(),
- x: cx,
- y: self.y + 6.0,
- font_size: 12.0,
- color: if i == self.path.len() - 1 { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] },
- });
- cx += seg.len() as f32 * 7.5 + SEGMENT_GAP;
- }
- labels
- }
-}
-
-
-pub struct Spreadsheet {
- x: f32,
- y: f32,
- w: f32,
- h: f32,
- hovered: bool,
- visible: bool,
- headers: Vec<String>,
- rows: Vec<Vec<String>>,
- scroll_y: f32,
- scroll_velocity: f32,
- dragging_scrollbar: bool,
- drag_offset_y: f32,
- scrollbar_hovered: bool,
- scrollbar_thumb_hovered: bool,
-}
-
-impl Spreadsheet {
- pub fn new() -> Self {
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- hovered: false,
- visible: false,
- headers: Vec::new(),
- rows: Vec::new(),
- scroll_y: 0.0,
- scroll_velocity: 0.0,
- dragging_scrollbar: false,
- drag_offset_y: 0.0,
- scrollbar_hovered: false,
- scrollbar_thumb_hovered: false,
- }
- }
-}
-
-impl Element for Spreadsheet {
- fn rect(&self) -> (f32, f32, f32, f32) {
- if !self.visible {
- (0.0, 0.0, 0.0, 0.0)
- } else {
- (self.x, self.y, self.w, self.h)
- }
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- }
-
- fn color(&self) -> [f32; 4] {
- if !self.visible {
- [0.0, 0.0, 0.0, 0.0]
- } else {
- colors::PARAM_BG
- }
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.hovered = v;
- }
-
- fn hovered(&self) -> bool {
- self.hovered
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- px >= rx && px <= rx + rw && py >= ry && py <= ry + rh
- }
-
- fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
- }
-
- fn visible(&self) -> bool {
- self.visible
- }
-
- fn set_spreadsheet_data(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
- self.headers = headers;
- self.rows = rows;
-
- // Clamp scroll_y to new bounds
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- let max_scroll_y = (content_h - visible_h).max(0.0);
- self.scroll_y = self.scroll_y.clamp(0.0, max_scroll_y);
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was_hovered = self.hovered;
- self.hovered = self.hit_test(px, py);
-
- let was_sb_hovered = self.scrollbar_hovered;
- let was_thumb_hovered = self.scrollbar_thumb_hovered;
-
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scrollbar_w = 6.0;
- let scrollbar_padding = 2.0;
- let scrollbar_x = self.x + self.w - scrollbar_w - scrollbar_padding;
- let track_y = self.y + 24.0;
-
- self.scrollbar_hovered = px >= scrollbar_x - 2.0 && px <= self.x + self.w
- && py >= track_y && py <= self.y + self.h;
-
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let scroll_ratio = self.scroll_y / max_scroll_y;
- let track_scroll_range = visible_h - thumb_h;
- let thumb_y = track_y + scroll_ratio * track_scroll_range;
-
- self.scrollbar_thumb_hovered = px >= scrollbar_x - 2.0 && px <= self.x + self.w
- && py >= thumb_y && py <= thumb_y + thumb_h;
- } else {
- self.scrollbar_hovered = false;
- self.scrollbar_thumb_hovered = false;
- }
-
- was_hovered != self.hovered
- || was_sb_hovered != self.scrollbar_hovered
- || was_thumb_hovered != self.scrollbar_thumb_hovered
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.visible {
- return false;
- }
- if self.hit_test(px, py) {
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scroll_amount = match delta {
- MouseScrollDelta::LineDelta(_x, y) => *y * 24.0,
- MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
- };
- self.scroll_velocity += scroll_amount * 12.0;
- return true;
- }
- }
- false
- }
-
- fn draggable(&self) -> bool {
- if !self.visible {
- return false;
- }
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- visible_h > 0.0 && content_h > visible_h
- }
-
- fn is_dragging(&self) -> bool {
- self.dragging_scrollbar
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- self.scroll_velocity = 0.0;
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scrollbar_w = 6.0;
- let scrollbar_padding = 2.0;
- let scrollbar_x = self.x + self.w - scrollbar_w - scrollbar_padding;
- let track_y = self.y + 24.0;
-
- if px >= scrollbar_x - 4.0 && px <= self.x + self.w
- && py >= track_y && py <= self.y + self.h
- {
- self.dragging_scrollbar = true;
-
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let scroll_ratio = self.scroll_y / max_scroll_y;
- let track_scroll_range = visible_h - thumb_h;
- let thumb_y = track_y + scroll_ratio * track_scroll_range;
-
- if py >= thumb_y && py <= thumb_y + thumb_h {
- self.drag_offset_y = py - thumb_y;
- } else {
- self.drag_offset_y = thumb_h / 2.0;
- let new_thumb_y = py - self.drag_offset_y;
- let scroll_ratio = if track_scroll_range > 0.0 {
- ((new_thumb_y - track_y) / track_scroll_range).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- self.scroll_y = scroll_ratio * max_scroll_y;
- }
- }
- }
- }
-
- fn drag_update(&mut self, _px: f32, py: f32) -> bool {
- if self.dragging_scrollbar {
- self.scroll_velocity = 0.0;
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let track_y = self.y + 24.0;
- let track_scroll_range = visible_h - thumb_h;
-
- let new_thumb_y = py - self.drag_offset_y;
- let scroll_ratio = if track_scroll_range > 0.0 {
- ((new_thumb_y - track_y) / track_scroll_range).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- let old_scroll_y = self.scroll_y;
- self.scroll_y = scroll_ratio * max_scroll_y;
-
- return (self.scroll_y - old_scroll_y).abs() > 0.01;
- }
- }
- false
- }
-
- fn drag_end(&mut self) {
- self.dragging_scrollbar = false;
- self.scroll_velocity = 0.0;
- }
-
- fn tick(&mut self, dt: f32) -> bool {
- if self.scroll_velocity.abs() > 0.01 {
- let content_h = self.rows.len() as f32 * 24.0;
- let visible_h = (self.h - 24.0).max(0.0);
- let max_scroll_y = (content_h - visible_h).max(0.0);
- let old_scroll_y = self.scroll_y;
-
- self.scroll_y = (self.scroll_y + self.scroll_velocity * dt).clamp(0.0, max_scroll_y);
-
- // Decelerate with friction (exponential decay)
- let friction = 8.0;
- self.scroll_velocity *= (-friction * dt).exp();
-
- if self.scroll_y == 0.0 || self.scroll_y == max_scroll_y {
- self.scroll_velocity = 0.0;
- }
-
- if self.scroll_velocity.abs() < 5.0 {
- self.scroll_velocity = 0.0;
- }
-
- (self.scroll_y - old_scroll_y).abs() > 0.01
- } else {
- false
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
-
- // Header bg
- quads.push((self.x, self.y, self.w, 24.0, [0.12, 0.12, 0.16, 0.4]));
-
- // Zebra rows
- let row_h = 24.0;
- let body_top = self.y + 24.0;
- let body_bottom = self.y + self.h;
- for i in 0..self.rows.len() {
- let ry = self.y + 24.0 + i as f32 * row_h - self.scroll_y;
- if ry + row_h <= body_top || ry >= body_bottom {
- continue;
- }
- let draw_y = ry.max(body_top);
- let draw_h = (ry + row_h).min(body_bottom) - draw_y;
- if draw_h > 0.0 {
- let row_color = if i % 2 == 0 {
- [0.10, 0.10, 0.13, 0.15]
- } else {
- [0.08, 0.08, 0.11, 0.05]
- };
- quads.push((self.x, draw_y, self.w, draw_h, row_color));
-
- // Horizontal row separator
- let sep_y = ry + row_h;
- if sep_y >= body_top && sep_y < body_bottom {
- quads.push((self.x, sep_y, self.w, 1.0, [0.20, 0.20, 0.25, 0.15]));
- }
- }
- }
-
- // Header separator
- quads.push((self.x, self.y + 24.0, self.w, 1.0, [0.20, 0.20, 0.25, 0.25]));
-
- // Vertical separators
- let divider_h = self.h;
- if divider_h > 0.0 && !self.headers.is_empty() {
- let n_cols = self.headers.len();
- for i in 1..n_cols {
- let r = i as f32 / n_cols as f32;
- quads.push((self.x + self.w * r, self.y, 1.0, divider_h, [0.20, 0.20, 0.25, 0.15]));
- }
- }
-
- // Scrollbar track & thumb
- let content_h = self.rows.len() as f32 * row_h;
- let visible_h = (self.h - 24.0).max(0.0);
- if visible_h > 0.0 && content_h > visible_h {
- let scrollbar_w = 6.0;
- let scrollbar_padding = 2.0;
- let scrollbar_x = self.x + self.w - scrollbar_w - scrollbar_padding;
- let track_y = self.y + 24.0;
- let track_h = visible_h;
-
- // Track BG
- quads.push((scrollbar_x, track_y, scrollbar_w, track_h, [0.05, 0.05, 0.08, 0.15]));
-
- // Thumb
- let thumb_h = ((visible_h / content_h) * visible_h).clamp(15.0_f32.min(visible_h), visible_h);
- let max_scroll_y = content_h - visible_h;
- let scroll_ratio = self.scroll_y / max_scroll_y;
- let track_scroll_range = visible_h - thumb_h;
- let thumb_y = track_y + scroll_ratio * track_scroll_range;
-
- let thumb_color = if self.dragging_scrollbar {
- [0.40, 0.40, 0.48, 1.0]
- } else if self.scrollbar_thumb_hovered {
- [0.32, 0.32, 0.38, 1.0]
- } else if self.scrollbar_hovered {
- [0.24, 0.24, 0.30, 0.9]
- } else {
- [0.18, 0.18, 0.24, 0.7]
- };
-
- quads.push((scrollbar_x, thumb_y, scrollbar_w, thumb_h, thumb_color));
- }
-
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
- }
- let mut labels = Vec::new();
- if self.headers.is_empty() {
- return labels;
- }
-
- let n_cols = self.headers.len();
- for (i, header) in self.headers.iter().enumerate() {
- let cx = self.x + self.w * (i as f32 / n_cols as f32) + 8.0;
- labels.push(TextLabel {
- text: header.clone(),
- x: cx,
- y: self.y + 6.0,
- font_size: 12.0,
- color: [0xdd, 0xdd, 0xee],
- });
- }
-
- let row_h = 24.0;
- let body_top = self.y + 24.0;
- let body_bottom = self.y + self.h;
- for (i, row) in self.rows.iter().enumerate() {
- let ry = self.y + 24.0 + i as f32 * row_h - self.scroll_y;
- // Only show text if the row is fully inside the spreadsheet body
- if ry < body_top || ry + row_h > body_bottom {
- continue;
- }
-
- for (col_idx, val) in row.iter().enumerate().take(n_cols) {
- let cx = self.x + self.w * (col_idx as f32 / n_cols as f32) + 8.0;
- labels.push(TextLabel {
- text: val.clone(),
- x: cx,
- y: ry + 6.0,
- font_size: 12.0,
- color: [0xbb, 0xbb, 0xcc],
- });
- }
- }
- labels
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ScrollBox {
- x: f32, y: f32, w: f32, h: f32,
- pub scroll_y: f32,
- pub content_h: f32,
- pub viewport_y: f32,
- pub viewport_h: f32,
- hovered: bool,
- pub show_border: bool,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
-}
-
-impl ScrollBox {
- pub fn new() -> Self {
- Self {
- x: 0.0, y: 0.0, w: 0.0, h: 0.0,
- scroll_y: 0.0,
- content_h: 0.0,
- viewport_y: 0.0,
- viewport_h: 0.0,
- hovered: false,
- show_border: true,
- parent: None,
- children: Vec::new(),
- }
- }
-
- pub fn update_bounds(&mut self, content_h: f32, viewport_y: f32, viewport_h: f32) {
- self.content_h = content_h;
- self.viewport_y = viewport_y;
- self.viewport_h = viewport_h;
- let max_scroll = (content_h - viewport_h).max(0.0);
- self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
- }
-
- pub fn get_item_draw_y(&self, virtual_y: f32, item_h: f32) -> Option<f32> {
- let draw_y = self.viewport_y + virtual_y - self.scroll_y;
- if draw_y >= self.viewport_y - 1.0 && draw_y + item_h <= self.viewport_y + self.viewport_h + 1.0 {
- Some(draw_y)
- } else {
- None
- }
- }
-}
-
-impl Element for ScrollBox {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { [0.08, 0.08, 0.12, 0.3] }
- fn set_hovered(&mut self, v: bool) { self.hovered = v; }
- fn hovered(&self) -> bool { self.hovered }
- fn highlight_color(&self) -> Option<[f32; 4]> { None }
-
- fn focus(&mut self) {
- focus::set_focused(self);
- }
- fn unfocus(&mut self) {}
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button == MouseButton::Left && state == ElementState::Pressed {
- if self.hit_test(px, py) {
- self.focus();
- return true;
- }
- }
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovered;
- self.hovered = self.hit_test(px, py);
- was != self.hovered
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if self.hit_test(px, py) {
- let scroll_speed = 24.0;
- let dy = match delta {
- MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
- MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
- };
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y + dy).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- } else {
- false
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
-
- // Background
- quads.push((self.x, self.y, self.w, self.h, [0.08, 0.08, 0.12, 0.3]));
-
- // Border lines
- let box_border_color = if focus::is_focused(self) {
- [0.30, 0.50, 0.32, 1.0] // Focused green
- } else if self.hovered {
- [0.25, 0.25, 0.35, 1.0] // Hovered
- } else {
- [0.18, 0.18, 0.24, 1.0] // Default
- };
- quads.push((self.x, self.y, self.w, 1.0, box_border_color)); // Top
- quads.push((self.x, self.y + self.h - 1.0, self.w, 1.0, box_border_color)); // Bottom
- quads.push((self.x, self.y, 1.0, self.h, box_border_color)); // Left
- quads.push((self.x + self.w - 1.0, self.y, 1.0, self.h, box_border_color)); // Right
-
- // Scrollbar
- if self.content_h > self.viewport_h {
- let sb_x = self.x + self.w - 8.0;
- let sb_w = 4.0;
- let sb_track_h = self.viewport_h - 8.0;
- let sb_track_y = self.viewport_y + 4.0;
-
- // Track
- quads.push((sb_x, sb_track_y, sb_w, sb_track_h, [0.15, 0.15, 0.20, 0.3]));
-
- // Thumb
- let visible_ratio = self.viewport_h / self.content_h;
- let thumb_h = (sb_track_h * visible_ratio).clamp(20.0, sb_track_h);
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
- let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
-
- quads.push((sb_x, thumb_y, sb_w, thumb_h, [0.60, 0.60, 0.65, 0.4]));
- }
-
- quads
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if !focus::is_focused(self) {
- return false;
- }
- if event.state != ElementState::Pressed {
- return false;
- }
- if event.ctrl {
- match &event.logical_key {
- Key::Character(c) if c == "n" || c == "N" => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- Key::Character(c) if c == "p" || c == "P" => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- _ => false,
- }
- } else {
- match &event.logical_key {
- Key::Named(NamedKey::ArrowDown) => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- Key::Named(NamedKey::ArrowUp) => {
- let old_scroll = self.scroll_y;
- let max_scroll = (self.content_h - self.viewport_h).max(0.0);
- self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max_scroll);
- (self.scroll_y - old_scroll).abs() > 0.01
- }
- _ => false,
- }
- }
- }
-
- fn parent(&self) -> Option<*mut (dyn Element + 'static)> { self.parent }
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) { self.parent = parent; }
- fn children(&self) -> Vec<*mut (dyn Element + 'static)> { self.children.clone() }
- fn add_child(&mut self, child: *mut (dyn Element + 'static)) { self.children.push(child); }
- fn clear_children(&mut self) { self.children.clear(); }
-}
-
-impl Drop for ScrollBox {
- fn drop(&mut self) {
- focus::clear_if_matches(self);
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn menu_item_x(title: &str, menu_items: &[String], idx: usize) -> f32 {
- let mut x = 8.0;
- if !title.is_empty() {
- x += title.len() as f32 * 7.5 + 24.0;
- }
- for i in 0..idx {
- x += menu_items[i].len() as f32 * 7.5 + 16.0;
- }
- x
- }
-
- fn menu_item_w(menu_items: &[String], idx: usize) -> f32 {
- menu_items[idx].len() as f32 * 7.5 + 16.0
- }
-
- #[test]
- fn test_rangeslider_interaction() {
- let mut rs = RangeSlider::new();
- rs.set_rect(10.0, 10.0, 200.0, 20.0);
-
- // Low value: 0.2, High value: 0.8
- let (low, high) = rs.values();
- assert_eq!(low, 0.2);
- assert_eq!(high, 0.8);
-
- // Thumb size = h * 0.9 = 18.0
- // Range = w - thumb_size = 200.0 - 18.0 = 182.0
- // Thumb low center: x + 0.2 * 182.0 + 9.0 = 10.0 + 36.4 + 9.0 = 55.4
- // Thumb high center: x + 0.8 * 182.0 + 9.0 = 10.0 + 145.6 + 9.0 = 164.6
-
- // 1. Drag Low thumb from 0.2 to 0.45
- // Click at px = 55.4 (center of low thumb)
- rs.drag_begin(55.4, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
-
- // Drag to px = 100.9 (new low value = (100.9 - offset(9.0) - 10.0) / 182.0 = 81.9 / 182.0 = 0.45)
- let changed = rs.drag_update(100.9, 20.0);
- assert!(changed);
- assert!((rs.values().0 - 0.45).abs() < 0.01);
- assert_eq!(rs.values().1, 0.8); // High value unchanged
-
- rs.drag_end();
- assert_eq!(rs.active_thumb, None);
-
- // 2. Drag High thumb from 0.8 to 0.6
- // Click at px = 164.6 (center of high thumb)
- rs.drag_begin(164.6, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
-
- // Drag to px = 128.2 (new high value = (128.2 - offset(9.0) - 10.0) / 182.0 = 109.2 / 182.0 = 0.6)
- let changed = rs.drag_update(128.2, 20.0);
- assert!(changed);
- assert!((rs.values().1 - 0.6).abs() < 0.01);
-
- rs.drag_end();
- }
-
- #[test]
- fn test_rangeslider_overlap() {
- let mut rs = RangeSlider::new().with_values(0.5, 0.5);
- rs.set_rect(10.0, 10.0, 200.0, 20.0);
-
- // Both low and high are 0.5. Thumb center = 10.0 + 0.5 * 182.0 + 9.0 = 110.0
- // Click to the left of center should select Low thumb
- rs.drag_begin(109.0, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
- rs.drag_end();
-
- // Click to the right of center should select High thumb
- rs.drag_begin(111.0, 20.0);
- assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
- rs.drag_end();
-
- // Drag Low thumb past High value (0.5). It should be constrained to 0.5
- rs.drag_begin(110.0, 20.0); // selects low
- rs.drag_update(150.0, 20.0); // drag past high
- assert_eq!(rs.values().0, 0.5); // constrained
- rs.drag_end();
- }
-
-
- #[test]
- fn test_node_toggle_geometry_visibility() {
- let mut node = Node::new(100.0, 100.0, 200.0, 50.0, "Test Node");
-
- // 1. Initial state
- assert!(node.geom_visible());
- assert!(!node.take_geom_toggle());
- assert!(node.draggable());
-
- // Get the toggle rect
- let (tx, ty, tw, th) = node.toggle_rect();
-
- // 2. Hover toggle area
- // Move cursor inside toggle area
- let changed = node.cursor_moved(tx + tw / 2.0, ty + th / 2.0);
- assert!(changed);
- assert!(node.toggle_hovered);
- assert!(!node.draggable(), "Node should not be draggable when hovering over the toggle widget");
-
- // Move cursor outside toggle area but inside node
- let changed2 = node.cursor_moved(tx - 10.0, ty + th / 2.0);
- assert!(changed2);
- assert!(!node.toggle_hovered);
- assert!(node.draggable());
-
- // 3. Click toggle area
- // Move cursor back inside toggle area
- node.cursor_moved(tx + tw / 2.0, ty + th / 2.0);
- // Press Left button
- let input_changed = node.mouse_input(MouseButton::Left, ElementState::Pressed, tx + tw / 2.0, ty + th / 2.0);
- assert!(input_changed);
- assert!(!node.geom_visible(), "Geometry visibility should be toggled off");
- assert!(node.take_geom_toggle(), "take_geom_toggle should return true after toggle click");
- assert!(!node.take_geom_toggle(), "take_geom_toggle should clear state after being called once");
-
- // Click again to toggle back on
- let input_changed2 = node.mouse_input(MouseButton::Left, ElementState::Pressed, tx + tw / 2.0, ty + th / 2.0);
- assert!(input_changed2);
- assert!(node.geom_visible(), "Geometry visibility should be toggled back on");
- assert!(node.take_geom_toggle());
- }
-
- #[test]
- fn test_graph_interaction() {
- let mut graph = Graph::new();
- graph.set_rect(0.0, 0.0, 800.0, 600.0);
- graph.set_grid_sizes(100.0, 50.0);
- graph.set_skipped_sizes(10.0, 20.0);
- graph.set_grid_origin(0.0, 0.0);
-
- let nodes = vec![
- GraphNode {
- name: "Node A".to_string(),
- position: (0.0, 0.0),
- parameters: vec![],
- geom_visible: true,
- },
- GraphNode {
- name: "Node B".to_string(),
- position: (2.0, 1.0),
- parameters: vec![],
- geom_visible: true,
- },
- ];
- graph.set_nodes(&nodes);
-
- // 1. Initial State
- assert_eq!(graph.get_nodes().len(), 2);
- assert_eq!(graph.selected_node(), None);
-
- // 2. Select Node A
- // Node A screen rect: (0, 0, 100, 50)
- let clicked = graph.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 25.0);
- assert!(clicked);
- assert_eq!(graph.selected_node(), Some(0));
- assert!(graph.draggable());
-
- // 3. Drag Node A
- graph.drag_begin(50.0, 25.0);
- assert!(graph.is_dragging());
-
- // Enable snapping
- graph.set_grid_snap_enabled(true);
- graph.drag_update(170.0, 85.0); // drag offset from Node A center (50, 25): nx = 170 - 50 = 120, ny = 85 - 25 = 60
- assert_eq!(graph.drag_node_pos, Some((120.0, 60.0)));
-
- graph.drag_end();
- assert_eq!(graph.get_nodes()[0].position, (1.0, 1.0)); // Snapped grid position: (120/120, 60/60)
-
- // 4. Toggle geometry visibility of Node B
- // Node B screen rect: (2 * 120 = 240, 1 * 60 = 60, 100, 50)
- // Toggle button: tx = 240 + 100 - 30 = 310, ty = 60 + (50 - 18)/2 = 76, tw = 18, th = 18
- let clicked_toggle = graph.mouse_input(MouseButton::Left, ElementState::Pressed, 319.0, 85.0);
- assert!(clicked_toggle);
- assert_eq!(graph.take_node_geom_toggle(), Some((1, false)));
- }
-
-
- #[test]
- fn test_menubar_vertical_horizontal_labels() {
- // Create MenuBar with custom horizontal and vertical labels
- let mut menubar = MenuBar::new(0.0, 0.0, 120.0, 30.0)
- .with_title("App")
- .with_item_vh("FileH", "FileV", &["Open", "Save"])
- .with_item("Edit", &["Undo"]);
-
- // 1. Horizontal mode (default)
- assert!(!menubar.vertical);
- let labels_h = menubar.text_labels();
- // Title should be "App", item 0 should be "FileH", item 1 should be "Edit"
- assert_eq!(labels_h[0].text, "App");
- assert_eq!(labels_h[1].text, "FileH");
- assert_eq!(labels_h[2].text, "Edit");
-
- // Hover test in horizontal layout
- let ix = menu_item_x("App", &["FileH".to_string(), "Edit".to_string()], 0);
- let iw = menu_item_w(&["FileH".to_string(), "Edit".to_string()], 0);
-
- // Move cursor inside "FileH" bounds
- menubar.cursor_moved(ix + iw / 2.0, 15.0);
- assert_eq!(menubar.hovered_menu, Some(0));
-
- // 2. Vertical mode
- let mut menubar_v = menubar.with_vertical(true);
- assert!(menubar_v.vertical);
- let labels_v = menubar_v.text_labels();
- // Title should be "App", item 0 should be "FileV", item 1 should be "Edit"
- assert_eq!(labels_v[0].text, "App");
- assert_eq!(labels_v[1].text, "FileV");
- assert_eq!(labels_v[2].text, "Edit");
-
- // Hover test in vertical layout
- let iy = menubar_v.item_y_vertical(0);
- let ih = menubar_v.item_h_vertical();
-
- // Move cursor inside "FileV" bounds
- menubar_v.cursor_moved(20.0, iy + ih / 2.0);
- assert_eq!(menubar_v.hovered_menu, Some(0));
- }
-
- #[test]
- fn test_spreadsheet_dynamic() {
- let mut spreadsheet = Spreadsheet::new();
- spreadsheet.set_rect(10.0, 10.0, 100.0, 200.0);
- spreadsheet.set_visible(true);
-
- // Initially empty
- assert!(spreadsheet.headers.is_empty());
- assert!(spreadsheet.rows.is_empty());
- assert!(spreadsheet.text_labels().is_empty());
-
- // Set dynamic headers and rows
- let headers = vec!["ColA".to_string(), "ColB".to_string()];
- let rows = vec![
- vec!["Val1".to_string(), "Val2".to_string()],
- vec!["Val3".to_string(), "Val4".to_string()],
- ];
- spreadsheet.set_spreadsheet_data(headers, rows);
-
- assert_eq!(spreadsheet.headers.len(), 2);
- assert_eq!(spreadsheet.rows.len(), 2);
-
- // Verify labels generated
- let labels = spreadsheet.text_labels();
- // 2 headers + 4 cell values = 6 labels total
- assert_eq!(labels.len(), 6);
- assert_eq!(labels[0].text, "ColA");
- assert_eq!(labels[1].text, "ColB");
- assert_eq!(labels[2].text, "Val1");
- assert_eq!(labels[3].text, "Val2");
- assert_eq!(labels[4].text, "Val3");
- assert_eq!(labels[5].text, "Val4");
-
- // Verify positions are correct (col 0 starts at x = 10.0 + 8.0 = 18.0)
- assert_eq!(labels[0].x, 18.0);
- // Col 1 starts at x = 10.0 + 100.0 * 0.5 + 8.0 = 68.0
- assert_eq!(labels[1].x, 68.0);
- assert_eq!(labels[2].x, 18.0);
- assert_eq!(labels[3].x, 68.0);
- }
-
- #[test]
- fn test_spreadsheet_scrolling() {
- let mut spreadsheet = Spreadsheet::new();
- // Visible height is 100px. Header is 24px, so body is 76px.
- spreadsheet.set_rect(0.0, 0.0, 100.0, 100.0);
- spreadsheet.set_visible(true);
-
- let headers = vec!["ColA".to_string()];
- // Each row is 24px. With 10 rows, content_h = 240px.
- let mut rows = Vec::new();
- for i in 0..10 {
- rows.push(vec![format!("Row{}", i)]);
- }
- spreadsheet.set_spreadsheet_data(headers, rows);
-
- // Content height is 240px, visible height is 100px (body is 76px).
- // Since content height > visible body height, it should be draggable.
- assert!(spreadsheet.draggable());
-
- // Max scroll height = 240.0 - 76.0 = 164.0
-
- // Initial scroll position should be 0.0
- assert_eq!(spreadsheet.scroll_y, 0.0);
-
- // Scroll down via mouse wheel (positive delta scrolls content down, scroll_y increases via tick)
- let delta = MouseScrollDelta::LineDelta(0.0, 2.0);
- // Mouse over spreadsheet (50, 50)
- let changed = spreadsheet.mouse_wheel(&delta, 50.0, 50.0);
- assert!(changed);
- assert_eq!(spreadsheet.scroll_y, 0.0);
- assert!(spreadsheet.scroll_velocity > 0.0);
-
- // Tick to apply velocity
- let mut ticked_change = false;
- for _ in 0..100 {
- if spreadsheet.tick(0.016) {
- ticked_change = true;
- }
- }
- assert!(ticked_change);
- assert!(spreadsheet.scroll_y > 0.0);
- assert_eq!(spreadsheet.scroll_velocity, 0.0);
-
- // Scroll back to top
- let delta_up = MouseScrollDelta::LineDelta(0.0, -10.0);
- spreadsheet.mouse_wheel(&delta_up, 50.0, 50.0);
- assert!(spreadsheet.scroll_velocity < 0.0);
-
- // Tick back to top
- for _ in 0..100 {
- spreadsheet.tick(0.016);
- }
- assert_eq!(spreadsheet.scroll_y, 0.0);
- assert_eq!(spreadsheet.scroll_velocity, 0.0);
-
- // Drag test
- // Scrollbar width is 6px. Padding is 2px. Width is 100px.
- // Scrollbar track x is from 92px to 98px.
- // Let's drag. Click at (94, 50).
- spreadsheet.drag_begin(94.0, 50.0);
- assert!(spreadsheet.is_dragging());
-
- // Update drag to y = 80
- let changed_drag = spreadsheet.drag_update(94.0, 80.0);
- assert!(changed_drag);
- assert!(spreadsheet.scroll_y > 0.0);
-
- // End drag
- spreadsheet.drag_end();
- assert!(!spreadsheet.is_dragging());
- }
-
- #[test]
- fn test_spreadsheet_zero_height_no_panic() {
- let mut spreadsheet = Spreadsheet::new();
- // Visible height is set to 0.0
- spreadsheet.set_rect(0.0, 0.0, 100.0, 0.0);
- spreadsheet.set_visible(true);
-
- let headers = vec!["ColA".to_string()];
- let mut rows = Vec::new();
- for i in 0..10 {
- rows.push(vec![format!("Row{}", i)]);
- }
- // This should not panic
- spreadsheet.set_spreadsheet_data(headers, rows);
-
- // This should not panic
- spreadsheet.cursor_moved(50.0, 50.0);
-
- let delta = MouseScrollDelta::LineDelta(0.0, -2.0);
- // This should not panic
- spreadsheet.mouse_wheel(&delta, 50.0, 50.0);
-
- // This should not panic
- assert!(!spreadsheet.draggable());
-
- // This should not panic
- spreadsheet.drag_begin(94.0, 50.0);
- spreadsheet.drag_update(94.0, 80.0);
- spreadsheet.drag_end();
-
- // This should not panic and return empty quads for scrollbar
- let _quads = spreadsheet.extra_quads();
- // The header quad and divider (if any) are drawn, but scrollbar is not
- // Let's verify that the scrollbar was not drawn
- // (the last quad would be the scrollbar thumb with thumb_color if drawn,
- // but here scrollbar track & thumb shouldn't be added)
- assert_eq!(spreadsheet.scroll_y, 0.0);
-
- // Let's check text labels (should be empty because self.h is 0)
- let labels = spreadsheet.text_labels();
- // Headers labels are still generated since they don't depend on scroll/height,
- // but rows shouldn't be
- assert_eq!(labels.len(), 1); // Only header ColA
- }
-
- #[test]
- fn test_scroll_box_bounds_scrolling() {
- let mut sb = ScrollBox::new();
- sb.set_rect(10.0, 20.0, 100.0, 100.0);
-
- // 1. Initially scroll is 0
- assert_eq!(sb.scroll_y, 0.0);
-
- // 2. Update bounds: content_h = 150 (greater than viewport_h = 100)
- sb.update_bounds(150.0, 20.0, 100.0);
- assert_eq!(sb.scroll_y, 0.0);
- assert_eq!(sb.content_h, 150.0);
- assert_eq!(sb.viewport_h, 100.0);
-
- // 3. Scroll inside bounds
- let delta = MouseScrollDelta::LineDelta(0.0, -2.0); // scroll down by 2 lines (48px)
- let changed = sb.mouse_wheel(&delta, 50.0, 50.0);
- assert!(changed);
- assert_eq!(sb.scroll_y, 48.0);
-
- // 4. Clamps at max scroll: 150 - 100 = 50
- let delta_large = MouseScrollDelta::LineDelta(0.0, -10.0);
- sb.mouse_wheel(&delta_large, 50.0, 50.0);
- assert_eq!(sb.scroll_y, 50.0);
-
- // 5. Test item draw coordinates
- // Virtual item at virtual_y = 10, item_h = 24
- // Screen draw y = viewport_y + virtual_y - scroll_y = 20 + 10 - 50 = -20
- // -20 < viewport_y + 2.0 (22.0), so it should return None (not visible)
- assert!(sb.get_item_draw_y(10.0, 24.0).is_none());
-
- // Virtual item at virtual_y = 60, item_h = 24
- // Screen draw y = 20 + 60 - 50 = 30
- // 30 >= 22.0 and 30 + 24 <= 118.0, so it should return Some(30.0)
- assert_eq!(sb.get_item_draw_y(60.0, 24.0), Some(30.0));
- }
-
- #[test]
- fn test_dropdown_widget_interaction() {
- let options = vec!["Option A".to_string(), "Option B".to_string(), "Option C".to_string()];
- let mut dd = Dropdown::new(options, 0);
- dd.set_rect(10.0, 10.0, 100.0, 24.0);
-
- // 1. Initial State
- assert!(!dd.open);
- assert_eq!(dd.selected, 0);
-
- // 2. Click trigger area opens dropdown
- let input_changed = dd.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
- assert!(input_changed);
- assert!(dd.open);
-
- // 3. Hovering options inside popover
- // Popover starts at y = 10 + 24 = 34. Options are of height 24 each.
- // Hover option B at y = 34 + 24 + 12 = 70.0
- let move_changed = dd.cursor_moved(50.0, 70.0);
- assert!(move_changed);
- assert_eq!(dd.hovered_item, Some(1));
-
- // 4. Click option B selects it and closes dropdown
- let select_changed = dd.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 70.0);
- assert!(select_changed);
- assert!(!dd.open);
- assert_eq!(dd.selected, 1);
- assert!(dd.take_change());
- }
-
- #[test]
- fn test_textbox_selection_highlight() {
- let mut tb = TextBox::new("Initial Text".to_string());
- tb.set_rect(10.0, 10.0, 200.0, 30.0);
-
- // 1. Initial state
- assert!(!tb.editing);
- assert!(!tb.all_selected);
-
- // 2. Click focuses and triggers highlighting
- let clicked = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
- assert!(clicked);
- assert!(tb.editing);
- assert!(tb.all_selected);
- assert_eq!(tb.edit_buffer, "Initial Text");
-
- // 3. Typing a key replaces all text
- let key_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Character("A".to_string()),
- text: Some("A".to_string()),
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled = tb.keyboard_input(&key_ev);
- assert!(handled);
- assert!(!tb.all_selected);
- assert_eq!(tb.edit_buffer, "A");
-
- // 4. Pressing Enter commits change
- let enter_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::Enter),
- text: None,
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled_enter = tb.keyboard_input(&enter_ev);
- assert!(handled_enter);
- assert!(!tb.editing);
- assert_eq!(tb.text, "A");
- assert!(tb.take_change());
- }
-
- #[test]
- fn test_textbox_drag_and_modifier_selection() {
- let mut tb = TextBox::new("Hello World".to_string());
- tb.set_rect(10.0, 10.0, 200.0, 30.0);
-
- // 1. Initial click focuses and selects all
- let pressed = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 50.0, 20.0);
- assert!(pressed);
- let released = tb.mouse_input(MouseButton::Left, ElementState::Released, 50.0, 20.0);
- assert!(released);
- assert!(tb.editing);
- assert!(tb.all_selected);
- assert_eq!(tb.cursor_idx, 11);
- assert_eq!(tb.select_anchor, Some(0));
-
- // 2. Click inside placed caret at index 5 (x = 10 + 8 + 5 * 7.2 = 54)
- let pressed_inside = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 54.0, 20.0);
- assert!(pressed_inside);
- assert_eq!(tb.cursor_idx, 5);
- assert_eq!(tb.select_anchor, Some(5));
- assert!(!tb.all_selected);
-
- // 3. Drag to index 11 (x = 10 + 8 + 11 * 7.2 = 97.2)
- tb.drag_begin(54.0, 20.0);
- let updated = tb.drag_update(97.2, 20.0);
- assert!(updated);
- assert_eq!(tb.cursor_idx, 11);
- assert_eq!(tb.select_anchor, Some(5));
- tb.drag_end();
-
- // 4. Keyboard ArrowLeft with Shift shrinks selection from 11 to 10
- let left_shift_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowLeft),
- text: None,
- repeat: false,
- ctrl: false,
- shift: true,
- };
- let handled = tb.keyboard_input(&left_shift_ev);
- assert!(handled);
- assert_eq!(tb.cursor_idx, 10);
- assert_eq!(tb.select_anchor, Some(5));
-
- // 5. Keyboard ArrowLeft without Shift collapses selection to start (index 5)
- let left_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowLeft),
- text: None,
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled = tb.keyboard_input(&left_ev);
- assert!(handled);
- assert_eq!(tb.cursor_idx, 5);
- assert_eq!(tb.select_anchor, None);
-
- // 6. Keyboard Shift+Up highlights to beginning (cursor 0, anchor 5)
- let up_shift_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowUp),
- text: None,
- repeat: false,
- ctrl: false,
- shift: true,
- };
- let handled = tb.keyboard_input(&up_shift_ev);
- assert!(handled);
- assert_eq!(tb.cursor_idx, 0);
- assert_eq!(tb.select_anchor, Some(5));
-
- // 7. Typing a key replaces selected range "Hello" with "Rust"
- let rust_ev = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Character("Rust".to_string()),
- text: Some("Rust".to_string()),
- repeat: false,
- ctrl: false,
- shift: false,
- };
- let handled = tb.keyboard_input(&rust_ev);
- assert!(handled);
- assert_eq!(tb.edit_buffer, "Rust World");
- assert_eq!(tb.cursor_idx, 4);
- assert_eq!(tb.select_anchor, None);
- }
-
- #[test]
- fn test_paginator_rotated_tabs() {
- let pages = vec!["📁 Browse".to_string(), "🌐 Network".to_string()];
- let mut paginator = Paginator::new(56.0, pages)
- .with_tab_y_offset(100.0)
- .with_tabs_rotated(true);
-
- paginator.set_rect(0.0, 0.0, 1000.0, 600.0);
-
- // Verify target_y is updated correctly
- // selected_page = 0: tab_y_offset = 100.0
- assert_eq!(paginator.target_y, 100.0);
-
- paginator.set_selected_page(1);
- // selected_page = 1: tab_y_offset + 1 * (120.0 + 10.0) = 230.0
- assert_eq!(paginator.target_y, 230.0);
-
- // Verify hover coordinates
- // Tab 0 bx/by bounds:
- // tab_w = 32.0, tab_h = 120.0, spacing = 10.0, sidebar_w = 48.0
- // bx = self.x + (sidebar_w - tab_w) / 2 = 8.0
- // by = self.y + tab_y_offset + i * 130.0 = 100.0
- // bx range: [8.0, 40.0], by range: [100.0, 220.0]
-
- // Hover at (24.0, 150.0) should hit Tab 0
- let hover_tab0 = paginator.on_cursor_moved(24.0, 150.0);
- assert!(hover_tab0);
- assert_eq!(paginator.hovered_tab, Some(0));
-
- // Hover at (24.0, 280.0) should hit Tab 1 (by range: [230.0, 350.0])
- let hover_tab1 = paginator.on_cursor_moved(24.0, 280.0);
- assert!(hover_tab1);
- assert_eq!(paginator.hovered_tab, Some(1));
-
- // Clicking Tab 0 selects it
- let click_tab0 = paginator.mouse_input(MouseButton::Left, ElementState::Pressed, 24.0, 150.0);
- assert!(click_tab0);
- assert_eq!(paginator.pressed_tab, Some(0));
-
- let release_tab0 = paginator.mouse_input(MouseButton::Left, ElementState::Released, 24.0, 150.0);
- assert!(release_tab0);
- assert_eq!(paginator.selected_page, 0);
-
- // Check vertical text formatting
- let labels = paginator.text_labels();
- assert_eq!(labels.len(), 2);
- assert_eq!(labels[0].text, "📁");
- assert_eq!(labels[1].text, "🌐");
-
- // Check that rotated text quads were generated
- assert!(!paginator.tab_text_quads[0].is_empty());
- assert!(!paginator.tab_text_quads[1].is_empty());
- }
-
- #[test]
- fn test_svg_text_rendering() {
- let svg_data = r##"<svg width="32" height="120" xmlns="http://www.w3.org/2000/svg">
- <text x="16" y="60" font-family="sans-serif" font-size="12" fill="#E6E6F2" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 16 60)">Audio</text>
-</svg>"##.as_bytes();
-
- let opt = resvg::usvg::Options::default();
- let mut fontdb = resvg::usvg::fontdb::Database::new();
- fontdb.load_system_fonts();
- let tree = resvg::usvg::Tree::from_data(svg_data, &opt, &fontdb).unwrap();
-
- let mut pixmap = resvg::tiny_skia::Pixmap::new(32, 120).unwrap();
- resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
-
- pixmap.save_png("/home/lsgalante/Dropbox/Clear/scratch/test_svg.png").unwrap();
-
- // Check that some pixels were drawn (are non-transparent)
- let pixels = pixmap.data();
- let mut non_transparent = 0;
- for i in (3..pixels.len()).step_by(4) {
- if pixels[i] > 0 {
- non_transparent += 1;
- }
- }
- assert!(non_transparent > 0, "Should have rendered some text pixels");
- }
-}
-
-// Generic text item layout wrapper
-#[derive(Debug, Clone)]
-pub struct ScrollingList {
- pub scroll_box: ScrollBox,
- pub item_height: f32,
- pub item_gap: f32,
-}
-
-impl ScrollingList {
- pub fn new(item_height: f32, item_gap: f32) -> Self {
- Self {
- scroll_box: ScrollBox::new(),
- item_height,
- item_gap,
- }
- }
-
- pub fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
- let item_height_full = self.item_height + self.item_gap;
- let content_h = count as f32 * item_height_full;
- self.scroll_box.update_bounds(content_h, viewport_y, viewport_h);
- }
-
- pub fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
- let item_height_full = self.item_height + self.item_gap;
- let virtual_y = idx as f32 * item_height_full + offset;
- self.scroll_box.get_item_draw_y(virtual_y, self.item_height)
- }
-
- pub fn scroll_y(&self) -> f32 {
- self.scroll_box.scroll_y
- }
-
- pub fn set_scroll_y(&mut self, val: f32) {
- self.scroll_box.scroll_y = val;
- }
-}
-
-impl Default for ScrollingList {
- fn default() -> Self {
- Self::new(24.0, 4.0)
- }
-}
-
-impl Element for ScrollingList {
- fn rect(&self) -> (f32, f32, f32, f32) {
- self.scroll_box.rect()
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.scroll_box.set_rect(x, y, w, h);
- }
-
- fn color(&self) -> [f32; 4] {
- self.scroll_box.color()
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.scroll_box.set_hovered(v);
- }
-
- fn hovered(&self) -> bool {
- self.scroll_box.hovered()
- }
-
- fn highlight_color(&self) -> Option<[f32; 4]> {
- self.scroll_box.highlight_color()
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.scroll_box.cursor_moved(px, py)
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- self.scroll_box.mouse_wheel(delta, px, py)
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- self.scroll_box.mouse_input(button, state, px, py)
- }
-
- fn focus(&mut self) {
- self.scroll_box.focus();
- }
-
- fn unfocus(&mut self) {
- self.scroll_box.unfocus();
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- self.scroll_box.extra_quads()
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- self.scroll_box.keyboard_input(event)
- }
-
- fn parent(&self) -> Option<*mut (dyn Element + 'static)> { self.scroll_box.parent() }
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) { self.scroll_box.set_parent(parent); }
- fn children(&self) -> Vec<*mut (dyn Element + 'static)> { self.scroll_box.children() }
- fn add_child(&mut self, child: *mut (dyn Element + 'static)) { self.scroll_box.add_child(child); }
- fn clear_children(&mut self) { self.scroll_box.clear_children(); }
-
- fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
- self.update_bounds(count, viewport_y, viewport_h);
- }
-
- fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
- self.get_item_draw_y(idx, offset)
- }
-}
-
-unsafe impl Send for Container {}
-unsafe impl Sync for Container {}
-unsafe impl Send for ScrollBox {}
-unsafe impl Sync for ScrollBox {}
-unsafe impl Send for Spinbox {}
-unsafe impl Sync for Spinbox {}
-unsafe impl Send for ColorSelector {}
-unsafe impl Sync for ColorSelector {}
-unsafe impl Send for ScrollingList {}
-unsafe impl Sync for ScrollingList {}
-
-// ── Dropdown Widget ──
-
-#[derive(Debug, Clone)]
-pub struct Plate {
- pub base: Widget,
- 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 Element + 'static)>,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub visible: bool,
- pub column_layout: bool,
-}
-
-impl Plate {
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: Widget::new_rect(x, y, w, h),
- dragging: false,
- drag_ox: 0.0,
- drag_oy: 0.0,
- drag_start_x: 0.0,
- drag_start_y: 0.0,
- bounds: None,
- color: None,
- curved_circle: None,
- network_opacity: 1.0,
- blur: true,
- children: Vec::new(),
- parent: None,
- visible: true,
- column_layout: false,
- }
- }
-
- pub fn with_color(mut self, color: [f32; 4]) -> Self {
- self.color = Some(color);
- self
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn with_blur(mut self, blur: bool) -> Self {
- self.blur = blur;
- self
- }
-
- pub fn set_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-}
-
-impl Element for Plate {
- crate::impl_widget_base!(Plate);
- fn is_plate(&self) -> bool { true }
- fn rounded_corners(&self) -> (bool, bool, bool, bool) { (true, true, true, true) }
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> { None }
-
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- for &child_ptr in &self.children {
- unsafe {
- (*child_ptr).set_modifiers(ctrl, shift, alt);
- }
- }
- }
-
- fn visible(&self) -> bool {
- self.visible
- }
-
- fn color(&self) -> [f32; 4] {
- let mut c = if let Some(c) = self.color {
- c
- } else if self.dragging {
- colors::PANEL_DRAG
- } else {
- colors::PANEL_IDLE
- };
- c[3] *= self.network_opacity;
- if self.blur {
- c[3] = -c[3].abs();
- }
- c
- }
-
- fn set_network_opacity(&mut self, opacity: f32) {
- self.network_opacity = opacity;
- }
-
- fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
- self.bounds = Some((bx, by, bw, bh));
- }
-
- fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
- self.curved_circle = circle;
- }
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((cx, cy, r)) = self.curved_circle {
- let dx = px - cx;
- let dy = py - cy;
- return dx * dx + dy * dy <= r * r;
- }
-
- let (x, y, w, h) = self.rect();
- if px < x || px >= x + w || py < y || py >= y + h {
- return false;
- }
-
- let r = 12.0f32.min(w * 0.5).min(h * 0.5);
- if r <= 0.1 {
- return true;
- }
-
- // Check corners
- if px < x + r && py < y + r {
- let dx = px - (x + r);
- let dy = py - (y + r);
- return dx * dx + dy * dy <= r * r;
- }
- if px >= x + w - r && py < y + r {
- let dx = px - (x + w - r);
- let dy = py - (y + r);
- return dx * dx + dy * dy <= r * r;
- }
- if px >= x + w - r && py >= y + h - r {
- let dx = px - (x + w - r);
- let dy = py - (y + h - r);
- return dx * dx + dy * dy <= r * r;
- }
- if px < x + r && py >= y + h - r {
- let dx = px - (x + r);
- let dy = py - (y + h - r);
- return dx * dx + dy * dy <= r * r;
- }
-
- true
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- if let Some(b) = self.base_mut() {
- b.x = x;
- b.y = y;
- b.w = w;
- b.h = h;
- }
-
- if !self.visible {
- return;
- }
-
- let padding_x = 20.0;
- let padding_y = 20.0;
- let left_x = x + padding_x;
- let available_w = (w - 2.0 * padding_x).max(1.0);
- let start_y = y + padding_y;
- let available_h = (h - 2.0 * padding_y).max(1.0);
-
- let center_x = left_x + available_w / 2.0;
- let center_y = start_y + available_h / 2.0;
- let aspect_ratio = available_w / available_h;
-
- let mut active_widgets = Vec::new();
- for &w_ptr in &self.children {
- let w = unsafe { &*w_ptr };
- if !w.layout_ignore() {
- active_widgets.push(w_ptr);
- }
- }
-
- if self.column_layout {
- let mut current_y = start_y;
- let spacing = 12.0;
- for &w_ptr in &active_widgets {
- let w = unsafe { &mut *w_ptr };
- let (_, _, ww, wh) = w.rect();
- let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
- let top = crate::widget::label_offset(w);
- let use_h = if wh > 0.0 { wh } else { 24.0 + top };
-
- w.set_rect(left_x, current_y, use_w, use_h);
- current_y += use_h + spacing;
- }
- } else {
- let mut total_diagonal = 0.0;
- let mut count = 0;
- for &w_ptr in &active_widgets {
- let w = unsafe { &*w_ptr };
- let (_, _, ww, wh) = w.rect();
- let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
- let use_h = if wh > 0.0 { wh } else { 50.0 };
- total_diagonal += (use_w * use_w + use_h * use_h).sqrt();
- count += 1;
- }
- let avg_diagonal = if count > 0 { total_diagonal / count as f32 } else { 100.0 };
- let base_spacing = (avg_diagonal * 0.55).max(60.0);
-
- for (i, &w_ptr) in active_widgets.iter().enumerate() {
- let w = unsafe { &mut *w_ptr };
- let (_, _, ww, wh) = w.rect();
- let use_w = if ww > 0.0 { ww.min(available_w) } else { available_w };
- let use_h = if wh > 0.0 { wh } else { 50.0 };
-
- if i == 0 {
- w.set_rect(center_x - use_w / 2.0, center_y - use_h / 2.0, use_w, use_h);
- } else {
- let mut ring = 1;
- let mut ring_start = 1;
- let mut placed = false;
- while !placed {
- let ring_capacity = ring * 6;
- if i < ring_start + ring_capacity {
- let pos_in_ring = i - ring_start;
- let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
- let radius = (ring as f32) * base_spacing;
-
- let x_offset = radius * angle.cos() * aspect_ratio;
- let y_offset = radius * angle.sin();
-
- w.set_rect(
- center_x + x_offset - use_w / 2.0,
- center_y + y_offset - use_h / 2.0,
- use_w,
- use_h,
- );
- placed = true;
- } else {
- ring_start += ring_capacity;
- ring += 1;
- }
- }
- }
- }
- }
- }
-
- fn parent(&self) -> Option<*mut (dyn Element + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
- self.parent = parent;
- }
-
- fn children(&self) -> Vec<*mut (dyn Element + 'static)> {
- self.children.clone()
- }
-
- fn add_child(&mut self, child: *mut (dyn Element + 'static)) {
- self.children.push(child);
- }
-
- fn clear_children(&mut self) {
- self.children.clear();
- }
-
- 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 Element + '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 Element + 'static)) {
- if page_idx < self.plates.len() {
- self.plates[page_idx].add_child(widget);
- unsafe {
- (*widget).set_parent(Some(&mut self.plates[page_idx] as *mut _));
- }
- }
- }
-
- pub fn clear_page_widgets(&mut self, page_idx: usize) {
- if page_idx < self.plates.len() {
- self.plates[page_idx].clear_children();
- }
- }
-
- pub fn with_tabs_at_top(mut self, top: bool) -> Self {
- self.tabs_at_top = top;
- self.update_target_pos();
- self
- }
-
- pub fn with_tab_y_offset(mut self, offset: f32) -> Self {
- self.tab_y_offset = offset;
- if self.current_y == Some(10.0) {
- self.current_y = Some(offset);
- }
- self.update_target_pos();
- self
- }
-
- pub fn with_tabs_rotated(mut self, rotated: bool) -> Self {
- self.tabs_rotated = rotated;
- self.update_target_pos();
- self
- }
-
- pub fn selected_page(&self) -> usize {
- self.selected_page
- }
-
- pub fn set_selected_page(&mut self, page: usize) {
- if page < self.plates.len() {
- if self.selected_page != page {
- self.plates[self.selected_page].visible = false;
- self.selected_page = page;
- self.plates[self.selected_page].visible = true;
- self.update_target_pos();
-
- // Update MenuBar focus/selection
- for (i, menu) in self.sidebar_menu.menus.iter_mut().enumerate() {
- menu.set_selected(i == page);
- }
- }
- }
- }
-
- pub fn set_pages(&mut self, pages: Vec<String>) {
- self.pages = pages.clone();
- let num_pages = pages.len();
-
- let mut sidebar_menu = MenuBar::new(0.0, 0.0, self.sidebar_w, 0.0)
- .with_vertical(true);
- for page in &pages {
- sidebar_menu = sidebar_menu.with_item(page, &[]);
- }
- self.sidebar_menu = sidebar_menu;
-
- let mut plates = Vec::new();
- for _ in 0..num_pages {
- let mut plate = Plate::new(0.0, 0.0, 0.0, 0.0);
- plate.visible = false;
- plates.push(plate);
- }
- self.plates = plates;
- if self.selected_page >= num_pages {
- self.selected_page = 0;
- }
- if !self.plates.is_empty() {
- self.plates[self.selected_page].visible = true;
- self.sidebar_menu.menus[self.selected_page].set_selected(true);
- }
- self.update_target_pos();
- }
-
- pub fn set_scale_factor(&mut self, scale: f32) {
- self.scale_factor = scale;
- }
-
- pub fn vertical_tab_size(&self) -> (f32, f32) {
- if self.tabs_rotated {
- ((self.sidebar_w - 16.0).clamp(24.0, 120.0), 120.0)
- } else {
- (self.sidebar_w - 10.0, 40.0)
- }
- }
-
- fn total_sidebar_height(&self) -> f32 {
- let (_, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let step = if self.tabs_rotated { tab_h + spacing } else { 50.0 };
- self.tab_y_offset + self.pages.len() as f32 * step - spacing
- }
-
- fn update_target_pos(&mut self) {
- if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- self.target_x = self.selected_page as f32 * tab_w;
- if self.current_x.is_none() {
- self.current_x = Some(self.target_x);
- }
- } else if self.tabs_rotated {
- let (_, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let target = self.tab_y_offset + self.selected_page as f32 * (tab_h + spacing);
- self.target_y = target;
- if self.current_y.is_none() {
- self.current_y = Some(target);
- }
- } else {
- let target = self.tab_y_offset + self.selected_page as f32 * 50.0;
- self.target_y = target;
- if self.current_y.is_none() {
- self.current_y = Some(target);
- }
- }
- self.generate_tab_quads();
- }
-
- fn generate_tab_quads(&mut self) {
- self.tab_text_quads.clear();
- let (tab_w, _) = 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 Element for Paginator {
- fn rect(&self) -> (f32, f32, f32, f32) {
- (self.x, self.y, self.w, self.h)
- }
-
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- self.update_target_pos();
-
- let self_ptr = self as *mut Paginator;
- self.sidebar_menu.set_parent(Some(self_ptr));
- for plate in &mut self.plates {
- plate.set_parent(Some(self_ptr));
- }
-
- let tabs_at_top = self.tabs_at_top;
- if tabs_at_top {
- self.sidebar_menu.set_rect(self.x, self.y, self.w, 40.0);
- for plate in &mut self.plates {
- plate.set_rect(self.x, self.y + 40.0, self.w, (self.h - 40.0).max(0.0));
- }
- } else {
- let sidebar_w = self.sidebar_w;
- self.sidebar_menu.set_rect(self.x, self.y, sidebar_w, self.h);
- for plate in &mut self.plates {
- plate.set_rect(self.x + sidebar_w, self.y, (self.w - sidebar_w).max(0.0), self.h);
- }
- }
- }
-
- fn color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
- }
-
- fn set_hovered(&mut self, v: bool) {
- self.hovered = v;
- }
-
- fn hovered(&self) -> bool {
- self.hovered
- }
-
- fn highlight_quad(&self) -> Option<(f32, f32, f32, f32, [f32; 4])> {
- if let Some(i) = self.hovered_tab {
- if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- let bx = self.x + i as f32 * tab_w + 2.0;
- let by = self.y + 2.0;
- let bw = tab_w - 4.0;
- let bh = 40.0 - 4.0;
- let scroll_offset = hover_animation::get_scroll_offset();
- Some((bx, by + scroll_offset, bw, bh, colors::HIGHLIGHT_SECONDARY))
- } else {
- let (bx, by, bw, bh) = if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (
- self.x + 5.0,
- self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y,
- bw,
- 40.0,
- )
- };
- let scroll_offset = hover_animation::get_scroll_offset();
- let qy = by + scroll_offset;
- let qh = bh;
-
- let min_y = self.y;
- let max_y = self.y + self.h;
- let ry1 = qy.max(min_y);
- let ry2 = (qy + qh).min(max_y);
- let rh = ry2 - ry1;
- if rh > 0.0 {
- Some((bx, ry1, bw, rh, colors::HIGHLIGHT_SECONDARY))
- } else {
- None
- }
- }
- } else {
- None
- }
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.tabs_rotated {
- quads.push((self.x, self.y, self.sidebar_w, self.h, colors::sidebar_bg_color()));
-
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let min_y = self.y;
- let max_y = self.y + self.h;
-
- // Draw primary highlight for the selected active tab
- if let Some(cy) = self.current_y {
- let bx = self.x + (self.sidebar_w - tab_w) / 2.0;
- let by = self.y + cy - self.sidebar_scroll_y;
- let ry1 = by.max(min_y);
- let ry2 = (by + tab_h).min(max_y);
- let rh = ry2 - ry1;
- if rh > 0.0 {
- quads.push((bx, ry1, tab_w, rh, colors::highlight_primary_color()));
- }
- }
-
- for (i, page_name) in self.pages.iter().enumerate() {
- let bx = self.x + (self.sidebar_w - tab_w) / 2.0;
- let by = self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y;
-
- let trimmed = page_name.trim();
- let has_icon = trimmed.find(' ').is_some();
- let y_offset = if has_icon { 40.0 } else { 0.0 };
-
- if i < self.tab_text_quads.len() {
- for &(qx, qy, qw, qh, qc) in &self.tab_text_quads[i] {
- let absolute_x = bx + qx;
- let absolute_y = by + y_offset + qy;
-
- let ry1 = absolute_y.max(min_y);
- let ry2 = (absolute_y + qh).min(max_y);
- let rh = ry2 - ry1;
- if rh > 0.0 {
- quads.push((absolute_x, ry1, qw, rh, qc));
- }
- }
- }
- }
- } else {
- quads.extend(self.sidebar_menu.extra_quads());
- }
-
- if self.selected_page < self.plates.len() {
- quads.extend(self.plates[self.selected_page].extra_quads());
- }
- quads
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- let mut labels = Vec::new();
- if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- let active_color = colors::paginator_tab_label_color();
- let active_srgb = colors::to_srgb(active_color);
- let active_r = (active_srgb[0] * 255.0) as u8;
- let active_g = (active_srgb[1] * 255.0) as u8;
- let active_b = (active_srgb[2] * 255.0) as u8;
- let inactive_r = (active_r as f32 * 0.78) as u8;
- let inactive_g = (active_g as f32 * 0.78) as u8;
- let inactive_b = (active_b as f32 * 0.78) as u8;
-
- for (i, page_name) in self.pages.iter().enumerate() {
- let color = if self.selected_page == i {
- [active_r, active_g, active_b]
- } else {
- [inactive_r, inactive_g, inactive_b]
- };
- let bx = self.x + (self.sidebar_w - tab_w) / 2.0;
- let by = self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y;
- let bw = tab_w;
-
- let trimmed = page_name.trim();
- let has_icon = trimmed.find(' ').is_some();
- if has_icon {
- if let Some(space_idx) = trimmed.find(' ') {
- let (icon, _) = trimmed.split_at(space_idx);
- let icon = icon.trim();
- if !icon.is_empty() {
- let icon_font_size = 14.0;
- let est_icon_w = TextLabel::estimate_width(icon, icon_font_size);
- let icon_y = by + 12.0;
- if icon_y >= self.y && icon_y + icon_font_size <= self.y + self.h {
- labels.push(TextLabel {
- text: icon.to_string(),
- x: bx + (bw - est_icon_w) / 2.0,
- y: icon_y,
- font_size: icon_font_size,
- color,
- });
- }
- }
- }
- }
- }
- } else {
- labels.extend(self.sidebar_menu.text_labels());
- }
-
- if self.selected_page < self.plates.len() {
- labels.extend(self.plates[self.selected_page].text_labels());
- }
- labels
- }
-
- fn text_labels_with_bounds(&self) -> Vec<(TextLabel, Option<[f32; 4]>)> {
- let mut labels = Vec::new();
- for l in self.text_labels() {
- labels.push((l, None));
- }
- labels
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let mut changed = false;
- let was_hovered_tab = self.hovered_tab;
- self.hovered_tab = None;
- for i in 0..self.pages.len() {
- let (bx, by, bw, bh) = if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + i as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- };
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
- self.hovered_tab = Some(i);
- break;
- }
- }
- if was_hovered_tab != self.hovered_tab {
- changed = true;
- }
-
- if self.sidebar_menu.cursor_moved(px, py) {
- changed = true;
- }
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].cursor_moved(px, py) {
- changed = true;
- }
- }
-
- changed
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].popover_rect().is_some() {
- if self.plates[self.selected_page].mouse_input(button, state, px, py) {
- return true;
- }
- }
- }
-
- let mut clicked_tab = false;
- if button == MouseButton::Left {
- match state {
- ElementState::Pressed => {
- self.pressed_tab = None;
- for i in 0..self.pages.len() {
- let (bx, by, bw, bh) = if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + i as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- };
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
- self.pressed_tab = Some(i);
- clicked_tab = true;
- break;
- }
- }
- }
- ElementState::Released => {
- if let Some(i) = self.pressed_tab.take() {
- let (bx, by, bw, bh) = if self.tabs_at_top {
- let tab_w = if self.pages.is_empty() { 0.0 } else { self.w / self.pages.len() as f32 };
- (self.x + i as f32 * tab_w, self.y, tab_w, 40.0)
- } else if self.tabs_rotated {
- let (tab_w, tab_h) = self.vertical_tab_size();
- let spacing = 10.0;
- (
- self.x + (self.sidebar_w - tab_w) / 2.0,
- self.y + self.tab_y_offset + i as f32 * (tab_h + spacing) - self.sidebar_scroll_y,
- tab_w,
- tab_h,
- )
- } else {
- let bw = self.sidebar_w - 10.0;
- (self.x + 5.0, self.y + self.tab_y_offset + i as f32 * 50.0 - self.sidebar_scroll_y, bw, 40.0)
- };
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh && py >= self.y && py <= self.y + self.h {
- if self.selected_page != i {
- self.set_selected_page(i);
- self.page_changed = true;
- }
- clicked_tab = true;
- }
- }
- }
- }
- }
-
- if clicked_tab {
- return true;
- }
-
- if self.sidebar_menu.mouse_input(button, state, px, py) {
- return true;
- }
-
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].mouse_input(button, state, px, py) {
- return true;
- }
- }
-
- false
- }
-
- fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
- if self.sidebar_menu.keyboard_input(event) {
- return true;
- }
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].keyboard_input(event) {
- return true;
- }
- }
- false
- }
-
- fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.tabs_at_top {
- let bx = self.x;
- let by = self.y;
- let bw = self.sidebar_w;
- let bh = self.h;
- if px >= bx && px <= bx + bw && py >= by && py <= by + bh {
- let scroll_speed = 24.0;
- let dy = match delta {
- MouseScrollDelta::LineDelta(_, y) => -y * scroll_speed,
- MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
- };
- let old_scroll = self.sidebar_scroll_y;
- let max_scroll = (self.total_sidebar_height() - self.h).max(0.0);
- self.sidebar_scroll_y = (self.sidebar_scroll_y + dy).clamp(0.0, max_scroll);
- if (self.sidebar_scroll_y - old_scroll).abs() > 0.01 {
- self.update_target_pos();
- return true;
- }
- }
- }
-
- if self.sidebar_menu.mouse_wheel(delta, px, py) {
- return true;
- }
-
- if self.selected_page < self.plates.len() {
- if self.plates[self.selected_page].mouse_wheel(delta, px, py) {
- return true;
- }
- }
- false
- }
-
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- if let Some(r) = self.sidebar_menu.popover_rect() {
- return Some(r);
- }
- if self.selected_page < self.plates.len() {
- if let Some(r) = self.plates[self.selected_page].popover_rect() {
- return Some(r);
- }
- }
- None
- }
-
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- self.sidebar_menu.render_popover(pc);
- if self.selected_page < self.plates.len() {
- self.plates[self.selected_page].render_popover(pc);
- }
- }
-
- fn take_click(&mut self) -> bool {
- if self.page_changed {
- self.page_changed = false;
- true
- } else {
- false
- }
- }
-
- fn value(&self) -> i32 {
- self.selected_page as i32
- }
-
- fn tick(&mut self, dt: f32) -> bool {
- let mut changed = false;
- if let Some(current) = self.current_y {
- let diff = self.target_y - current;
- if diff.abs() > 0.1 {
- let decay = 15.0;
- let next = current + diff * (1.0 - (-decay * dt).exp());
- self.current_y = Some(next);
- changed = true;
- } else {
- self.current_y = Some(self.target_y);
- }
- }
- if let Some(current) = self.current_x {
- let diff = self.target_x - current;
- if diff.abs() > 0.1 {
- let decay = 15.0;
- let next = current + diff * (1.0 - (-decay * dt).exp());
- self.current_x = Some(next);
- changed = true;
- } else {
- self.current_x = Some(self.target_x);
- }
- }
-
- let self_ptr = self as *mut Paginator;
- self.sidebar_menu.set_parent(Some(self_ptr));
- for plate in &mut self.plates {
- plate.set_parent(Some(self_ptr));
- }
-
- if self.sidebar_menu.tick(dt) {
- changed = true;
- }
-
- for plate in &mut self.plates {
- if plate.tick(dt) {
- changed = true;
- }
- }
-
- changed
- }
-
- fn parent(&self) -> Option<*mut (dyn Element + 'static)> {
- self.parent
- }
-
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
- self.parent = parent;
- }
-
- fn children(&self) -> Vec<*mut (dyn Element + 'static)> {
- let mut list = Vec::new();
- list.push(&self.sidebar_menu as *const dyn Element as *mut dyn Element);
- for plate in &self.plates {
- list.push(plate as *const dyn Element as *mut dyn Element);
- }
- list
- }
-
- fn add_child(&mut self, _child: *mut (dyn Element + '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 Element + 'static)) {
- self.add_widget_to_page(page_idx, widget);
- }
- fn clear_page_widgets(&mut self, page_idx: usize) {
- self.clear_page_widgets(page_idx);
- }
- fn menu_items_list(&self) -> Vec<Vec<String>> {
- self.sidebar_menu.menu_items_list()
- }
- fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> {
- self.sidebar_menu.menu_checked_list()
- }
-}
-
-unsafe impl Send for Paginator {}
-unsafe impl Sync for Paginator {}
-
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
new file mode 100644
index 0000000..a2f381d
--- /dev/null
+++ b/src/widget/container/breadcrumb.rs
@@ -0,0 +1,115 @@
+use crate::widget::*;
+use crate::widget::input::{BREADCRUMB_PADDING, SEGMENT_GAP};
+use crate::widget::display::TextLabel;
+
+#[derive(Debug, Clone)]
+pub struct Breadcrumb {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+ path: Vec<String>,
+ hovered_seg: Option<usize>,
+ clicked_seg: Option<usize>,
+ pub network_opacity: f32,
+}
+
+impl Breadcrumb {
+ pub fn set_network_opacity(&mut self, opacity: f32) {
+ self.network_opacity = opacity;
+ }
+
+ pub fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false,
+ path: Vec::new(), hovered_seg: None, clicked_seg: None, network_opacity: 1.0 }
+ }
+
+ fn seg_at(&self, px: f32) -> Option<usize> {
+ let mut cx = self.x + BREADCRUMB_PADDING;
+ for (i, seg) in self.path.iter().enumerate() {
+ let w = seg.len() as f32 * 7.5;
+ if px >= cx && px < cx + w {
+ return Some(i);
+ }
+ cx += w + SEGMENT_GAP;
+ }
+ None
+ }
+}
+
+impl Element for Breadcrumb {
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+ fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ }
+
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+
+ fn color(&self) -> [f32; 4] { [0.10, 0.10, 0.14, self.network_opacity] }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ let was = self.hovered;
+ self.hovered = self.hit_test(px, py, ctx);
+ let old = self.hovered_seg;
+ self.hovered_seg = if self.hovered { self.seg_at(px) } else { None };
+ was != self.hovered || old != self.hovered_seg
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, _py: f32, ctx: &mut UiContext) -> bool {
+ if button != MouseButton::Left || state != ElementState::Pressed { return false; }
+ if let Some(i) = self.seg_at(px) {
+ if i < self.path.len() - 1 {
+ self.clicked_seg = Some(i);
+ return true;
+ }
+ }
+ false
+ }
+
+ fn set_path(&mut self, segments: &[String]) {
+ let mut s = Vec::with_capacity(segments.len().max(1));
+ if segments.is_empty() || (segments.len() == 1 && segments[0].is_empty()) {
+ s.push("/".to_string());
+ } else {
+ s.push("/".to_string());
+ for name in segments {
+ s.push(format!(" \u{203A} {}", name));
+ }
+ }
+ self.path = s;
+ }
+
+ fn path_click(&mut self) -> Option<usize> { self.clicked_seg.take() }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let mut quads = Vec::new();
+ if let Some(i) = self.hovered_seg {
+ let mut cx = self.x + BREADCRUMB_PADDING;
+ for j in 0..i {
+ let w = self.path[j].len() as f32 * 7.5;
+ cx += w + SEGMENT_GAP;
+ }
+ let w = self.path[i].len() as f32 * 7.5;
+ quads.push((cx, self.y, w, self.h, [1.0, 1.0, 1.0, 0.06]));
+ }
+ quads
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = Vec::new();
+ let mut cx = self.x + BREADCRUMB_PADDING;
+ for (i, seg) in self.path.iter().enumerate() {
+ labels.push(TextLabel {
+ text: seg.clone(),
+ x: cx,
+ y: self.y + 6.0,
+ font_size: 12.0,
+ color: if i == self.path.len() - 1 { [0xcc, 0xcc, 0xd4] } else { [0x88, 0x88, 0x99] },
+ });
+ cx += seg.len() as f32 * 7.5 + SEGMENT_GAP;
+ }
+ labels
+ }
+}
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
new file mode 100644
index 0000000..0e96a0d
--- /dev/null
+++ b/src/widget/container/container.rs
@@ -0,0 +1,36 @@
+use crate::widget::*;
+
+#[derive(Clone)]
+pub struct Container {
+ pub parent: Option<*mut (dyn Element + 'static)>,
+ pub children: Vec<*mut (dyn Element + 'static)>,
+}
+
+impl Container {
+ pub fn new() -> Self {
+ Self { parent: None, children: Vec::new() }
+ }
+}
+
+impl Element for Container {
+ fn rect(&self) -> (f32, f32, f32, f32) { (0.0, 0.0, 0.0, 0.0) }
+ fn set_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
+ fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+
+ fn focus(&mut self) {
+ focus::set_focused(self);
+ }
+ fn unfocus(&mut self) {}
+
+ fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.parent }
+ fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) { self.parent = parent; }
+ fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> { self.children.clone() }
+ fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) { self.children.push(child); }
+ fn clear_children(&mut self, ctx: &mut UiContext) { self.children.clear(); }
+}
+
+impl Drop for Container {
+ fn drop(&mut self) {
+ focus::clear_if_matches(self);
+ }
+}
diff --git a/src/widget/container/content_bg.rs b/src/widget/container/content_bg.rs
new file mode 100644
index 0000000..5e53ad8
--- /dev/null
+++ b/src/widget/container/content_bg.rs
@@ -0,0 +1,236 @@
+use crate::colors;
+use crate::widget::*;
+
+pub struct ContentBg {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+ show_network_grid: bool,
+ grid_size_x: f32,
+ grid_size_y: f32,
+ grid_origin_x: f32,
+ grid_origin_y: f32,
+ skipped_row_h: f32,
+ skipped_col_w: f32,
+}
+
+impl ContentBg {
+ pub fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false, show_network_grid: false, grid_size_x: 150.0, grid_size_y: 75.0, grid_origin_x: 0.0, grid_origin_y: 0.0, skipped_row_h: 37.5, skipped_col_w: 37.5 }
+ }
+}
+
+impl Element for ContentBg {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] {
+ if self.show_network_grid {
+ [0.0, 0.0, 0.0, 0.0]
+ } else {
+ colors::CONTENT_BG
+ }
+ }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+ fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
+
+ fn set_show_network_grid(&mut self, show: bool) { self.show_network_grid = show; }
+ fn set_grid_sizes(&mut self, gx: f32, gy: f32) { self.grid_size_x = gx; self.grid_size_y = gy; }
+ fn set_grid_origin(&mut self, ox: f32, oy: f32) { self.grid_origin_x = ox; self.grid_origin_y = oy; }
+ fn set_skipped_sizes(&mut self, row_h: f32, col_w: f32) { self.skipped_row_h = row_h; self.skipped_col_w = col_w; }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.show_network_grid || self.grid_size_x <= 0.0 || self.grid_size_y <= 0.0 {
+ return vec![];
+ }
+ let mut quads = Vec::new();
+ let grid_color = [0.0, 0.0, 0.0, 0.0];
+ let max_alpha = colors::CONTENT_BG[3]; // Peak opacity in the middle of gradient cells matches non-gradient cells
+ let steps = 20; // Silky-smooth gradient transition
+
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ let step_x = self.grid_size_x + self.skipped_col_w;
+
+ if step_y >= 4.0 && step_x >= 4.0 {
+ let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let ry_start = ry_start.max(-100_000);
+ let ry_end = ry_end.min(100_000);
+
+ let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let cx_start = cx_start.max(-100_000);
+ let cx_end = cx_end.min(100_000);
+
+ // Draw individual cell backgrounds to avoid stacking with gradients
+ for ry in ry_start..=ry_end {
+ let y1 = self.grid_origin_y + (ry as f32) * step_y;
+ let draw_start_y = y1.max(self.y);
+ let draw_end_y = (y1 + self.grid_size_y).min(self.y + self.h);
+ if draw_start_y < draw_end_y {
+ for cx in cx_start..=cx_end {
+ let x1 = self.grid_origin_x + (cx as f32) * step_x;
+ let draw_start_x = x1.max(self.x);
+ let draw_end_x = (x1 + self.grid_size_x).min(self.x + self.w);
+ if draw_start_x < draw_end_x {
+ quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, colors::CONTENT_BG));
+ }
+ }
+ }
+ }
+ }
+
+ // Draw interstitial row gradients (horizontal bands fading to 0 alpha at left and right sides)
+ if self.skipped_row_h > 0.0 {
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ let step_x = self.grid_size_x + self.skipped_col_w;
+ if step_y >= 4.0 && step_x >= 4.0 {
+ let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+
+ let cx_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let cx_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let cx_start = cx_start.max(-100_000);
+ let cx_end = cx_end.min(100_000);
+
+ for k in k_start..=k_end {
+ let y1 = self.grid_origin_y + (k as f32) * step_y;
+ let y2 = y1 + self.grid_size_y;
+ if y1 >= self.y + self.h {
+ continue;
+ }
+ let draw_start_y = y2.max(self.y);
+ let draw_end_y = (y2 + self.skipped_row_h).min(self.y + self.h);
+ if draw_start_y >= draw_end_y {
+ continue;
+ }
+
+ for cx in cx_start..=cx_end {
+ let x1 = self.grid_origin_x + (cx as f32) * step_x;
+ let x_mid = x1 + self.grid_size_x / 2.0;
+ let w_total = self.grid_size_x;
+ let sub_w = w_total / steps as f32;
+
+ for i in 0..steps {
+ let sx_start = x1 + i as f32 * sub_w;
+ let sx_end = sx_start + sub_w;
+ let draw_start_x = sx_start.max(self.x);
+ let draw_end_x = sx_end.min(self.x + self.w);
+ if draw_start_x < draw_end_x {
+ let sx_mid = (sx_start + sx_end) / 2.0;
+ let dist = (sx_mid - x_mid).abs();
+ let d = (dist / (w_total / 2.0)).min(1.0);
+
+ // Fade the cell background color from max_alpha in the middle to transparent at the edges
+ let alpha = max_alpha * (1.0 - d);
+ if alpha > 0.001 {
+ quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Draw interstitial column gradients (vertical bands fading to 0 alpha at top and bottom)
+ if self.skipped_col_w > 0.0 {
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ let step_x = self.grid_size_x + self.skipped_col_w;
+ if step_y >= 4.0 && step_x >= 4.0 {
+ let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+
+ let ry_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let ry_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let ry_start = ry_start.max(-100_000);
+ let ry_end = ry_end.min(100_000);
+
+ for k in k_start..=k_end {
+ let x1 = self.grid_origin_x + (k as f32) * step_x;
+ let x2 = x1 + self.grid_size_x;
+ if x1 >= self.x + self.w {
+ continue;
+ }
+ let draw_start_x = x2.max(self.x);
+ let draw_end_x = (x2 + self.skipped_col_w).min(self.x + self.w);
+ if draw_start_x >= draw_end_x {
+ continue;
+ }
+
+ for ry in ry_start..=ry_end {
+ let y1 = self.grid_origin_y + (ry as f32) * step_y;
+ let y_mid = y1 + self.grid_size_y / 2.0;
+ let h_total = self.grid_size_y;
+ let sub_h = h_total / steps as f32;
+
+ for i in 0..steps {
+ let sy_start = y1 + i as f32 * sub_h;
+ let sy_end = sy_start + sub_h;
+ let draw_start_y = sy_start.max(self.y);
+ let draw_end_y = sy_end.min(self.y + self.h);
+ if draw_start_y < draw_end_y {
+ let sy_mid = (sy_start + sy_end) / 2.0;
+ let dist = (sy_mid - y_mid).abs();
+ let d = (dist / (h_total / 2.0)).min(1.0);
+
+ // Fade the cell background color from max_alpha in the middle to transparent at the edges
+ let alpha = max_alpha * (1.0 - d);
+ if alpha > 0.001 {
+ quads.push((draw_start_x, draw_start_y, draw_end_x - draw_start_x, draw_end_y - draw_start_y, [colors::CONTENT_BG[0], colors::CONTENT_BG[1], colors::CONTENT_BG[2], alpha]));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Draw the grid borders
+ let step_y = self.grid_size_y + self.skipped_row_h;
+ if step_y >= 4.0 {
+ let k_start = ((self.y - self.grid_origin_y) / step_y).floor() as i32 - 1;
+ let k_end = ((self.y + self.h - self.grid_origin_y) / step_y).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+ for k in k_start..=k_end {
+ let y1 = self.grid_origin_y + (k as f32) * step_y;
+ let y2 = y1 + self.grid_size_y;
+ if y1 >= self.y + self.h {
+ continue;
+ }
+ if y1 >= self.y {
+ quads.push((self.x, y1, self.w, 1.0, grid_color));
+ }
+ if y2 >= self.y && y2 < self.y + self.h {
+ quads.push((self.x, y2, self.w, 1.0, grid_color));
+ }
+ }
+ }
+
+ let step_x = self.grid_size_x + self.skipped_col_w;
+ if step_x >= 4.0 {
+ let k_start = ((self.x - self.grid_origin_x) / step_x).floor() as i32 - 1;
+ let k_end = ((self.x + self.w - self.grid_origin_x) / step_x).ceil() as i32 + 1;
+ let k_start = k_start.max(-100_000);
+ let k_end = k_end.min(100_000);
+ for k in k_start..=k_end {
+ let x1 = self.grid_origin_x + (k as f32) * step_x;
+ let x2 = x1 + self.grid_size_x;
+ if x1 >= self.x + self.w {
+ continue;
+ }
+ if x1 >= self.x {
+ quads.push((x1, self.y, 1.0, self.h, grid_color));
+ }
+ if x2 >= self.x && x2 < self.x + self.w {
+ quads.push((x2, self.y, 1.0, self.h, grid_color));
+ }
+ }
+ }
+ quads
+ }
+}
diff --git a/src/widget/container/header.rs b/src/widget/container/header.rs
new file mode 100644
index 0000000..44d6754
--- /dev/null
+++ b/src/widget/container/header.rs
@@ -0,0 +1,19 @@
+use crate::colors;
+use crate::widget::*;
+
+pub struct Header {
+ x: f32, y: f32, w: f32, h: f32,
+ hovered: bool,
+}
+
+impl Header {
+ pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+}
+
+impl Element for Header {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::HEADER_BG }
+ fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+ fn hovered(&self) -> bool { self.hovered }
+}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
new file mode 100644
index 0000000..fddd71d
--- /dev/null
+++ b/src/widget/container/menu.rs
@@ -0,0 +1,1297 @@
+use crate::colors;
+use crate::widget::*;
+use crate::widget::display::{make_widget_text_buffer, TextLabel};
+
+pub struct MenuBar {
+ x: f32, y: f32, w: f32, h: f32,
+ hovering: bool,
+ pub title: String,
+ pub menus: Vec<Box<Menu>>,
+ pub menu_items: Vec<String>,
+ pub vertical_items: Vec<String>,
+ pub menu_dropdowns: Vec<Vec<String>>,
+ pub menu_dropdown_checked: Vec<Vec<Option<bool>>>,
+ pub hovered_menu: Option<usize>,
+ pub open_menu: Option<usize>,
+ pub hovered_dropdown: Option<usize>,
+ pub clicked_dropdown: Option<(usize, usize)>,
+ pub was_open: Option<usize>,
+ pub vertical: bool,
+ pub visible: bool,
+ pub focused: bool,
+ pub z_level: i32,
+ pub center_items: bool,
+ pub curved_circle: Option<(f32, f32, f32)>,
+ pub title_pos: Option<(f32, f32)>,
+ pub title_buf: Option<glyphon::Buffer>,
+ pub curved_title_char_bufs: Vec<glyphon::Buffer>,
+ pub network_opacity: f32,
+ pub font_family: String,
+ pub label: Option<String>,
+}
+
+impl MenuBar {
+ pub fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
+ self.curved_circle = circle;
+ if circle.is_none() {
+ for menu in &mut self.menus {
+ menu.curved_arc = None;
+ }
+ }
+ }
+
+ pub fn set_network_opacity(&mut self, opacity: f32) {
+ self.network_opacity = opacity;
+ }
+
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self {
+ x, y, w, h, hovering: false,
+ title: String::new(),
+ menus: Vec::new(),
+ menu_items: Vec::new(),
+ vertical_items: Vec::new(),
+ menu_dropdowns: Vec::new(),
+ menu_dropdown_checked: Vec::new(),
+ hovered_menu: None,
+ open_menu: None,
+ hovered_dropdown: None,
+ clicked_dropdown: None,
+ was_open: None,
+ vertical: false,
+ visible: true,
+ focused: false,
+ z_level: 100,
+ center_items: false,
+ curved_circle: None,
+ title_pos: None,
+ title_buf: None,
+ curved_title_char_bufs: Vec::new(),
+ network_opacity: 1.0,
+ font_family: crate::layout::menubar_font(),
+ label: None,
+ }
+ }
+
+ pub fn with_label(mut self, label: &str) -> Self {
+ self.label = Some(label.to_string());
+ self
+ }
+
+ pub fn set_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
+ }
+
+ pub fn with_center_items(mut self, center: bool) -> Self {
+ self.center_items = center;
+ self
+ }
+
+ pub fn with_title(mut self, title: &str) -> Self {
+ self.title = title.to_string();
+ self
+ }
+
+ pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
+ self.menu_items.push(label.to_string());
+ self.vertical_items.push(label.to_string());
+ self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
+ self.menu_dropdown_checked.push(vec![None; items.len()]);
+
+ let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
+ let mut menu = Menu::new(label, label, &item_strs);
+ menu.vertical = self.vertical;
+ self.menus.push(Box::new(menu));
+ self
+ }
+
+ pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
+ self.menu_items.push(horizontal_label.to_string());
+ self.vertical_items.push(vertical_label.to_string());
+ self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
+ self.menu_dropdown_checked.push(vec![None; items.len()]);
+
+ let item_strs: Vec<String> = items.iter().map(|s| s.to_string()).collect();
+ let mut menu = Menu::new(horizontal_label, vertical_label, &item_strs);
+ menu.vertical = self.vertical;
+ self.menus.push(Box::new(menu));
+ self
+ }
+
+ pub fn with_vertical(mut self, vertical: bool) -> Self {
+ self.vertical = vertical;
+ for menu in &mut self.menus {
+ menu.vertical = vertical;
+ }
+ self
+ }
+
+ pub fn with_z_index(mut self, z: i32) -> Self {
+ self.z_level = z;
+ self
+ }
+
+ fn item_h_vertical(&self, idx: usize) -> f32 {
+ let font_size = 12.0;
+ let line_height = font_size * 1.2;
+ let padding_y = 12.0;
+ if let Some(menu) = self.menus.get(idx) {
+ let label_len = menu.active_title().chars().count() as f32;
+ label_len * line_height + padding_y
+ } else {
+ 24.0
+ }
+ }
+
+ fn item_y_vertical(&self, idx: usize) -> f32 {
+ let mut y = 8.0;
+ if !self.title.is_empty() {
+ let font_size = 12.0;
+ let line_height = font_size * 1.2;
+ let title_h = self.title.chars().count() as f32 * line_height;
+ y += title_h + 8.0;
+ }
+ for i in 0..idx {
+ y += self.item_h_vertical(i);
+ }
+ y
+ }
+}
+
+impl Element for MenuBar {
+ fn as_any(&self) -> &dyn std::any::Any { self }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
+ fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+ self as *const Self as *mut Self as *mut (dyn Element + 'static)
+ }
+
+ fn label(&self) -> Option<String> {
+ self.label.clone()
+ }
+
+ fn set_text(&mut self, text: &str) {
+ self.label = Some(text.to_string());
+ }
+
+ fn rect(&self) -> (f32, f32, f32, f32) {
+ if !self.visible {
+ return (0.0, 0.0, 0.0, 0.0);
+ }
+ if self.vertical {
+ let total_h = if self.menus.is_empty() {
+ self.h
+ } else {
+ let last_idx = self.menus.len() - 1;
+ self.item_y_vertical(last_idx) + self.item_h_vertical(last_idx)
+ };
+ (self.x, self.y, self.w, total_h)
+ } else {
+ (self.x, self.y, self.w, self.h)
+ }
+ }
+
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
+ self.x = x;
+ self.y = y;
+ self.w = w;
+ self.h = h;
+
+ let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
+
+ let font_setting = crate::layout::menubar_font();
+ let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(12.0);
+ let char_w = 7.5 * (font_size / 12.0);
+ let ih = font_size + 12.0;
+
+ if self.vertical {
+ let mut cy = 8.0;
+ if !self.title.is_empty() {
+ let font_size = 12.0;
+ let line_height = font_size * 1.2;
+ let title_h = self.title.chars().count() as f32 * line_height;
+ cy += title_h + 8.0;
+ }
+ let item_heights: Vec<f32> = (0..self.menus.len())
+ .map(|idx| self.item_h_vertical(idx))
+ .collect();
+ let mut dummy = crate::context::UiContext::new();
+ for (idx, menu) in self.menus.iter_mut().enumerate() {
+ let item_h = item_heights[idx];
+ menu.set_rect(x, y + cy, w, item_h);
+ menu.set_parent(Some(parent_ptr), &mut dummy);
+ cy += item_h;
+ }
+ } else {
+ let padding_x = crate::layout::paginator_tab_padding_x();
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * char_w + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ }
+
+ let total_angular_width = total_width / r_mid;
+ let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+ let mut current_angle = start_angle;
+
+ if !self.title.is_empty() {
+ let title_w = self.title.len() as f32 * char_w + 24.0;
+ let dtheta_title = title_w / r_mid;
+ let theta_title = current_angle + dtheta_title / 2.0;
+
+ let tx = ccx + r_mid * theta_title.cos() - title_w / 2.0 + 8.0;
+ let ty = ccy + r_mid * theta_title.sin() - h / 2.0;
+ self.title_pos = Some((tx, ty));
+ current_angle += dtheta_title;
+ } else {
+ self.title_pos = None;
+ }
+
+ let mut dummy = crate::context::UiContext::new();
+ for menu in &mut self.menus {
+ let iw = menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ let dtheta_menu = iw / r_mid;
+ let theta_menu = current_angle + dtheta_menu / 2.0;
+
+ let mx = ccx + r_mid * theta_menu.cos() - iw / 2.0;
+ let my = ccy + r_mid * theta_menu.sin() - h / 2.0;
+
+ menu.set_rect(mx, my, iw, h);
+ menu.set_parent(Some(parent_ptr), &mut dummy);
+ menu.curved_arc = Some((ccx, ccy, ccr, h, current_angle, current_angle + dtheta_menu));
+ current_angle += dtheta_menu;
+ }
+ } else {
+ self.title_pos = None;
+ let mut cx = 8.0;
+ if self.center_items {
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * char_w + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ }
+ if self.w > total_width {
+ cx = (self.w - total_width) / 2.0;
+ }
+ }
+ if !self.title.is_empty() {
+ cx += self.title.len() as f32 * char_w + 24.0;
+ }
+ let mut dummy = crate::context::UiContext::new();
+ for menu in &mut self.menus {
+ let iw = menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ menu.set_rect(x + cx, y, iw, h);
+ menu.set_parent(Some(parent_ptr), &mut dummy);
+ cx += iw;
+ }
+ }
+ }
+ }
+
+ fn color(&self) -> [f32; 4] {
+ if !self.visible {
+ [0.0, 0.0, 0.0, 0.0]
+ } else if self.focused {
+ let mut c = colors::PANEL_MENU_FOCUSED;
+ c[3] *= self.network_opacity;
+ c
+ } else {
+ let mut c = colors::PANEL_MENU_BG;
+ c[3] *= self.network_opacity;
+ c
+ }
+ }
+
+ fn set_hovered(&mut self, v: bool) {
+ self.hovering = v;
+ }
+
+ fn hovered(&self) -> bool {
+ self.hovering
+ }
+
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ if crate::widget::popovers::is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
+ return false;
+ }
+ let font_setting = crate::layout::menubar_font();
+ let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(12.0);
+ let char_w = 7.5 * (font_size / 12.0);
+
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let dx = px - ccx;
+ let dy = py - ccy;
+ let dist = (dx * dx + dy * dy).sqrt();
+ if dist >= ccr - self.h && dist <= ccr {
+ let angle = dy.atan2(dx);
+ let mut norm_angle = angle;
+ if norm_angle < 0.0 {
+ norm_angle += 2.0 * std::f32::consts::PI;
+ }
+
+ let r_mid = ccr - self.h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * char_w + 24.0;
+ }
+ let padding_x = crate::layout::paginator_tab_padding_x();
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ }
+ let total_angular_width = total_width / r_mid;
+ let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+ let end_angle = 1.5 * std::f32::consts::PI + total_angular_width / 2.0;
+
+ if norm_angle >= start_angle && norm_angle <= end_angle {
+ return true;
+ }
+ }
+ for menu in &self.menus {
+ if menu.hit_test(px, py, ctx) {
+ return true;
+ }
+ }
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
+ return true;
+ }
+ for menu in &self.menus {
+ if menu.hit_test(px, py, ctx) {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ self.set_rect(rx, ry, rw, rh);
+
+ let mut changed = false;
+ self.hovered_menu = None;
+ for (idx, menu) in self.menus.iter_mut().enumerate() {
+ if menu.cursor_moved(px, py, ctx) {
+ changed = true;
+ }
+ if menu.hovered() {
+ self.hovered_menu = Some(idx);
+ }
+ }
+ changed
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ if !self.visible {
+ return false;
+ }
+ let (rx, ry, rw, rh) = self.rect();
+ self.set_rect(rx, ry, rw, rh);
+
+ let mut changed = false;
+ for menu in &mut self.menus {
+ let res = menu.mouse_input(button, state, px, py, ctx);
+ if res {
+ changed = true;
+ }
+ }
+ if !self.is_menu_open() {
+ self.unfocus();
+ }
+ changed
+ }
+
+ fn focus(&mut self) {
+ if self.is_menu_open() {
+ self.focused = true;
+ for menu in &mut self.menus {
+ if menu.is_menu_open() {
+ menu.focus();
+ return;
+ }
+ }
+ } else {
+ self.focused = false;
+ focus::clear_if_matches(self);
+ return;
+ }
+ self.focused = true;
+ focus::set_focused(self);
+ }
+
+ fn unfocus(&mut self) {
+ self.focused = false;
+ focus::clear_if_matches(self);
+ for menu in &mut self.menus {
+ menu.unfocus();
+ }
+ }
+
+ fn focused(&self, ctx: &UiContext) -> bool {
+ self.focused || self.is_menu_open()
+ }
+
+ fn set_selected(&mut self, selected: bool) {
+ self.focused = selected;
+ if !selected {
+ for menu in &mut self.menus {
+ menu.set_selected(false);
+ }
+ }
+ }
+
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ for menu in &mut self.menus {
+ menu.set_modifiers(ctrl, shift, alt);
+ }
+ }
+
+ fn menu_names(&self) -> Vec<String> {
+ self.menu_items.clone()
+ }
+
+ fn menu_click(&mut self) -> Option<(usize, usize)> {
+ for (idx, menu) in self.menus.iter_mut().enumerate() {
+ if let Some((_, item_idx)) = menu.menu_click() {
+ return Some((idx, item_idx));
+ }
+ }
+ None
+ }
+
+ fn set_item_checked(&mut self, menu_idx: usize, item_idx: usize, checked: bool) {
+ if let Some(menu) = self.menu_dropdown_checked.get_mut(menu_idx) {
+ if item_idx < menu.len() {
+ menu[item_idx] = Some(checked);
+ }
+ }
+ if let Some(menu) = self.menus.get_mut(menu_idx) {
+ menu.set_item_checked(0, item_idx, checked);
+ }
+ }
+
+ fn set_menu_items(&mut self, menu_idx: usize, items: &[String]) {
+ if menu_idx < self.menu_dropdowns.len() {
+ self.menu_dropdowns[menu_idx] = items.to_vec();
+ self.menu_dropdown_checked[menu_idx] = vec![Some(false); items.len()];
+ }
+ if let Some(menu) = self.menus.get_mut(menu_idx) {
+ menu.items = items.to_vec();
+ menu.item_checked = vec![Some(false); items.len()];
+ menu.item_bufs.clear();
+ }
+ }
+
+ fn is_menu_bar(&self) -> bool {
+ self.visible
+ }
+
+ fn get_menu_items_at(&self, px: f32, py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> {
+ if !self.visible {
+ return None;
+ }
+ let dummy = crate::context::UiContext::new();
+ for (idx, menu) in self.menus.iter().enumerate() {
+ if menu.hit_test(px, py, &dummy) {
+ let mut formatted_items = Vec::new();
+ for (i, item) in menu.items.iter().enumerate() {
+ let checked = menu.item_checked.get(i).and_then(|&v| v);
+ let prefix = match checked {
+ Some(true) => "✓ ",
+ Some(false) => " ",
+ None => "",
+ };
+ formatted_items.push(format!("{}{}", prefix, item));
+ }
+ return Some((idx, menu.active_title().to_string(), formatted_items, menu.base.x, menu.base.y, menu.base.w, menu.base.h));
+ }
+ }
+ None
+ }
+
+ fn trigger_menu_click(&mut self, menu_idx: usize, item_idx: usize) {
+ if let Some(menu) = self.menus.get_mut(menu_idx) {
+ menu.clicked_item = Some(item_idx);
+ }
+ }
+
+ fn is_menu_open(&self) -> bool {
+ self.visible && self.menus.iter().any(|m| m.is_menu_open())
+ }
+
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut quads = Vec::new();
+ for menu in &self.menus {
+ quads.extend(menu.all_quads(ctx));
+ }
+ quads
+ }
+
+ fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut arcs = Vec::new();
+ for menu in &self.menus {
+ arcs.extend(menu.extra_arcs());
+ }
+ arcs
+ }
+
+ fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
+ if !self.visible {
+ return;
+ }
+ let current_font = crate::layout::menubar_font();
+ if self.font_family != current_font {
+ self.font_family = current_font;
+ self.title_buf = None;
+ self.curved_title_char_bufs.clear();
+ for menu in &mut self.menus {
+ menu.title_buf = None;
+ menu.curved_char_bufs.clear();
+ menu.item_bufs.clear();
+ }
+ }
+ let (font_fam, font_size_opt) = crate::layout::parse_font_string(&self.font_family);
+ let font_size = font_size_opt.unwrap_or(12.0);
+
+ if !self.title.is_empty() {
+ if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
+ if self.curved_title_char_bufs.len() != self.title.chars().count() {
+ let font_fam_clone = font_fam.clone();
+ self.curved_title_char_bufs = self.title.chars()
+ .map(|c| make_widget_text_buffer(fs, &c.to_string(), font_size, &font_fam_clone))
+ .collect();
+ }
+ self.title_buf = None;
+ } else if self.vertical {
+ self.title_buf = None;
+ self.curved_title_char_bufs.clear();
+ } else {
+ if self.title_buf.is_none() {
+ self.title_buf = Some(make_widget_text_buffer(fs, &self.title, font_size, &font_fam));
+ }
+ self.curved_title_char_bufs.clear();
+ }
+ } else {
+ self.title_buf = None;
+ self.curved_title_char_bufs.clear();
+ }
+ for menu in &mut self.menus {
+ menu.prepare_text(fs);
+ }
+ }
+
+ fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let mut items = Vec::new();
+ let color = glyphon::Color::rgb(0xaa, 0xaa, 0xbb);
+
+ let font_setting = crate::layout::menubar_font();
+ let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(12.0);
+ let char_w = 7.5 * (font_size / 12.0);
+
+ let padding_x = crate::layout::paginator_tab_padding_x();
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - self.h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * char_w + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ }
+ let total_angular_width = total_width / r_mid;
+ let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+ let current_angle = start_angle;
+
+ if !self.title.is_empty() {
+ let title_w = self.title.len() as f32 * char_w + 24.0;
+ let dtheta_title = title_w / r_mid;
+
+ let char_widths: Vec<f32> = self.title.chars().map(|c| {
+ TextLabel::estimate_width(&c.to_string(), font_size)
+ }).collect();
+ let total_chars_width: f32 = char_widths.iter().sum();
+ let mid_angle = (current_angle + current_angle + dtheta_title) / 2.0;
+ let angular_width = total_chars_width / r_mid;
+ let text_start_angle = mid_angle - angular_width / 2.0;
+ let mut cur_char_angle = text_start_angle;
+
+ for (char_idx, c_buf) in self.curved_title_char_bufs.iter().enumerate() {
+ let cw = char_widths[char_idx];
+ let dtheta = cw / r_mid;
+ let char_center_angle = cur_char_angle + dtheta / 2.0;
+
+ let tx = ccx + r_mid * char_center_angle.cos() - cw / 2.0;
+ let ty = ccy + r_mid * char_center_angle.sin() - font_size / 2.0;
+
+ items.push((c_buf, tx, ty, color));
+ cur_char_angle += dtheta;
+ }
+ }
+ } else if !self.vertical {
+ let mut start_x = 8.0;
+ if self.center_items {
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * char_w + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ }
+ if self.w > total_width {
+ start_x = (self.w - total_width) / 2.0;
+ }
+ }
+ if let Some(ref title_buf) = self.title_buf {
+ let text_y = self.y + (self.h - font_size) / 2.0;
+ items.push((title_buf, self.x + start_x, text_y, color));
+ }
+ }
+
+ for menu in &self.menus {
+ items.extend(menu.get_text_items());
+ }
+ items
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ if !self.visible {
+ return Vec::new();
+ }
+ let padding_x = crate::layout::paginator_tab_padding_x();
+ let mut labels = Vec::new();
+
+ if let Some(ref label) = self.label {
+ if self.vertical {
+ let font_size = 12.0;
+ let line_height = font_size * 1.2;
+ let label_len = label.chars().count() as f32;
+ let total_h = label_len * line_height;
+ let start_y = self.y - (crate::layout::label_margin() + total_h);
+ for (i, c) in label.chars().enumerate() {
+ let char_str = c.to_string();
+ let char_w = TextLabel::estimate_width(&char_str, font_size);
+ let x_pos = self.x + (self.w - char_w) / 2.0;
+ let y_pos = start_y + i as f32 * line_height;
+ labels.push(TextLabel {
+ text: char_str,
+ x: x_pos,
+ y: y_pos,
+ font_size,
+ color: [0x83, 0x83, 0x8a],
+ });
+ }
+ } else {
+ labels.push(TextLabel {
+ text: label.clone(),
+ x: self.x,
+ y: self.y - (12.0 + crate::layout::label_margin()),
+ font_size: 12.0,
+ color: [0x83, 0x83, 0x8a],
+ });
+ }
+ }
+
+ let font_setting = crate::layout::menubar_font();
+ let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
+ let font_size = font_size_opt.unwrap_or(12.0);
+ let char_w = 7.5 * (font_size / 12.0);
+
+ if let Some((ccx, ccy, ccr)) = self.curved_circle {
+ let r_mid = ccr - self.h / 2.0;
+ let mut total_width = 8.0;
+ if !self.title.is_empty() {
+ total_width += self.title.len() as f32 * char_w + 24.0;
+ }
+ for menu in &self.menus {
+ total_width += menu.active_title().len() as f32 * char_w + 2.0 * padding_x;
+ }
+ let total_angular_width = total_width / r_mid;
+ let start_angle = 1.5 * std::f32::consts::PI - total_angular_width / 2.0;
+ let current_angle = start_angle;
+
+ if !self.title.is_empty() {
+ let title_w = self.title.len() as f32 * char_w + 24.0;
+ let dtheta_title = title_w / r_mid;
+ labels.extend(TextLabel::curved_layout(
+ &self.title,
+ ccx, ccy, r_mid,
+ current_angle, current_angle + dtheta_title,
+ font_size,
+ [0xaa, 0xaa, 0xbb],
+ ));
+ }
+ } else if self.vertical {
+ if !self.title.is_empty() {
+ let line_height = font_size * 1.2;
+ let start_y = self.y + 8.0;
+ for (i, c) in self.title.chars().enumerate() {
+ let char_str = c.to_string();
+ let char_w = TextLabel::estimate_width(&char_str, font_size);
+ let x_pos = self.x + (self.w
diff truncated