// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Geometry and primitive value types used across the public API. //! //! These are the cheap, copy-friendly types that flow through every //! widget builder, layout method and runtime hook: //! //! - [`Color`] — RGBA in `[0.0, 1.0]` floats; `Color::WHITE`, //! `Color::BLACK`, `Color::TRANSPARENT` constants and a `Color::hex(r, g, b)` //! constructor for byte literals. //! - [`Rect`] — axis-aligned `(x, y, width, height)`; the universal //! layout / hit-test currency. //! - [`Point`] — a 2D point used by hit testing and gesture progress. //! - [`Size`] — a `(width, height)` pair without an origin. //! - [`Corners`] — per-corner radius for the //! [`Container`](crate::container()) widget and any other rounded //! surface; coerces from `f32` for the uniform case. //! - [`WidgetId`] — a stable `&'static str` identifier for focus //! management, paired with [`crate::App::take_focus_request`]. //! //! Every type is `Copy` (or `Clone`) so passing them by value is the //! default. The crate root re-exports them all (`ltk::Color`, //! `ltk::Rect`, …) so application code rarely needs the `ltk::types::` //! prefix. /// An RGBA color with floating-point channels in the range `[0.0, 1.0]`. #[ derive( Debug, Clone, Copy, PartialEq ) ] pub struct Color { /// Red channel `[0.0, 1.0]`. pub r: f32, /// Green channel `[0.0, 1.0]`. pub g: f32, /// Blue channel `[0.0, 1.0]`. pub b: f32, /// Alpha channel — `0.0` is fully transparent, `1.0` is fully opaque. pub a: f32, } impl Color { /// Fully opaque white. pub const WHITE: Self = Self { r: 1., g: 1., b: 1., a: 1. }; /// Fully opaque black. pub const BLACK: Self = Self { r: 0., g: 0., b: 0., a: 1. }; /// Fully transparent black. pub const TRANSPARENT: Self = Self { r: 0., g: 0., b: 0., a: 0. }; /// Create an opaque color from 8-bit `r`, `g`, `b` components. pub const fn hex( r: u8, g: u8, b: u8 ) -> Self { Self { r: r as f32 / 255.0, g: g as f32 / 255.0, b: b as f32 / 255.0, a: 1.0 } } /// Create an opaque color from float `r`, `g`, `b` components in `[0.0, 1.0]`. pub fn rgb( r: f32, g: f32, b: f32 ) -> Self { Self { r, g, b, a: 1. } } /// Create a color from float `r`, `g`, `b`, `a` components in `[0.0, 1.0]`. pub fn rgba( r: f32, g: f32, b: f32, a: f32 ) -> Self { Self { r, g, b, a } } /// Convert to a [`tiny_skia::Color`] for rendering. pub fn to_tiny_skia( self ) -> tiny_skia::Color { tiny_skia::Color::from_rgba( self.r, self.g, self.b, self.a ) .unwrap_or( tiny_skia::Color::BLACK ) } } /// A 2-D point in screen coordinates (pixels, top-left origin). #[ derive( Debug, Clone, Copy, PartialEq, Default ) ] pub struct Point { /// Horizontal position in pixels. pub x: f32, /// Vertical position in pixels. pub y: f32, } /// A width/height pair in pixels. #[ derive( Debug, Clone, Copy, PartialEq, Default ) ] pub struct Size { /// Width in pixels. pub width: f32, /// Height in pixels. pub height: f32, } /// An axis-aligned rectangle in screen coordinates. #[ derive( Debug, Clone, Copy, PartialEq, Default ) ] pub struct Rect { /// Left edge in pixels. pub x: f32, /// Top edge in pixels. pub y: f32, /// Width in pixels. pub width: f32, /// Height in pixels. pub height: f32, } impl Rect { /// Returns `true` if `p` lies inside or on the boundary of this rect. pub fn contains( &self, p: Point ) -> bool { p.x >= self.x && p.x <= self.x + self.width && p.y >= self.y && p.y <= self.y + self.height } /// Returns a new rect grown by `amount` pixels on every side. pub fn expand( &self, amount: f32 ) -> Self { Self { x: self.x - amount, y: self.y - amount, width: self.width + amount * 2.0, height: self.height + amount * 2.0, } } /// Convert to [`tiny_skia::Rect`], returning `None` if dimensions are non-positive. pub fn to_tiny_skia( &self ) -> Option { tiny_skia::Rect::from_xywh( self.x, self.y, self.width, self.height ) } } /// Per-corner radii for a rounded rect, ordered top-left → top-right → /// bottom-right → bottom-left (clockwise from top-left, matching CSS /// `border-radius`'s long form). All four values are independent /// pixel radii — set any subset to `0.0` for a square corner, or use /// the [`top`](Self::top), [`bottom`](Self::bottom), /// [`left`](Self::left), [`right`](Self::right) shortcuts for the /// common asymmetric cases. /// /// The renderer caps each corner against the inscribed-circle limit /// `min(width, height) / 2`, mirroring tiny-skia / browser behaviour: /// passing absurdly large values is a "make this side a pill" idiom /// rather than an error. /// /// `f32` and `(f32, f32, f32, f32)` both convert via [`From`] so any /// API taking `impl Into` accepts a uniform radius literal /// (`.radius( 16.0 )`), an explicit set (`.radius( ( 16.0, 16.0, /// 0.0, 0.0 ) )`), or a constructed value (`.radius( Corners::top( /// 16.0 ) )`) interchangeably. #[ derive( Debug, Clone, Copy, PartialEq, Default ) ] pub struct Corners { /// Top-left corner radius in pixels. pub tl: f32, /// Top-right corner radius in pixels. pub tr: f32, /// Bottom-right corner radius in pixels. pub br: f32, /// Bottom-left corner radius in pixels. pub bl: f32, } impl Corners { /// All four corners square (radius `0`). pub const ZERO: Self = Self { tl: 0.0, tr: 0.0, br: 0.0, bl: 0.0 }; /// Uniform radius on every corner — equivalent to `r.into()` and /// the most common construction. pub const fn all( r: f32 ) -> Self { Self { tl: r, tr: r, br: r, bl: r } } /// Rounded top corners, square bottom corners. Matches the CSS /// shorthand `border-radius: r r 0 0` and the typical "card sits /// flush against the bottom of the screen" pattern (docks, /// bottom-anchored modals). pub const fn top( r: f32 ) -> Self { Self { tl: r, tr: r, br: 0.0, bl: 0.0 } } /// Rounded bottom corners, square top corners. Mirror of /// [`top`](Self::top) for top-anchored chrome. pub const fn bottom( r: f32 ) -> Self { Self { tl: 0.0, tr: 0.0, br: r, bl: r } } /// Rounded left corners, square right corners. pub const fn left( r: f32 ) -> Self { Self { tl: r, tr: 0.0, br: 0.0, bl: r } } /// Rounded right corners, square left corners. pub const fn right( r: f32 ) -> Self { Self { tl: 0.0, tr: r, br: r, bl: 0.0 } } /// `true` when every corner is `<= 0` — the renderer can take /// the fast straight-rect path. pub fn is_zero( &self ) -> bool { self.tl <= 0.0 && self.tr <= 0.0 && self.br <= 0.0 && self.bl <= 0.0 } /// `true` when every corner has the same radius. Used by the /// software path to fall back to the single-radius cubic builder /// when the asymmetric path would produce an identical curve. pub fn is_uniform( &self ) -> bool { self.tl == self.tr && self.tr == self.br && self.br == self.bl } /// The largest of the four radii. Useful for sizing the shader /// quad's anti-alias pad — the worst-case AA band has to cover /// the steepest curve. pub fn max( &self ) -> f32 { self.tl.max( self.tr ).max( self.br ).max( self.bl ) } /// Cap every corner to `min(width, height) / 2`, the inscribed- /// circle limit a rounded box can't exceed without degenerating. /// Mirrors the clamp the GLES shader applies internally; software /// path callers use it before building the path so the cubic /// control points stay inside the rect. pub fn clamp_to_size( &self, width: f32, height: f32 ) -> Self { let cap = ( width.min( height ) * 0.5 ).max( 0.0 ); Self { tl: self.tl.min( cap ).max( 0.0 ), tr: self.tr.min( cap ).max( 0.0 ), br: self.br.min( cap ).max( 0.0 ), bl: self.bl.min( cap ).max( 0.0 ), } } /// Pack as `[ tl, tr, br, bl ]` for `glUniform4fv`. Order /// matches the `vec4 u_radii` convention every fragment shader /// in `gles_render::shaders` reads. pub fn to_uniform( &self ) -> [ f32; 4 ] { [ self.tl, self.tr, self.br, self.bl ] } } impl From for Corners { fn from( r: f32 ) -> Self { Self::all( r ) } } impl From<( f32, f32, f32, f32 )> for Corners { /// Tuple form, ordered `( tl, tr, br, bl )` — matches CSS shorthand. fn from( t: ( f32, f32, f32, f32 ) ) -> Self { Self { tl: t.0, tr: t.1, br: t.2, bl: t.3 } } } /// A stable widget identifier used for focus management. /// /// Assign an id to a widget with `.id( WidgetId("my_widget") )`, then request /// focus via [`App::take_focus_request`](crate::app::App::take_focus_request). #[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub struct WidgetId( pub &'static str ); /// Pointer cursor shape, sent to the compositor via /// `wp_cursor_shape_v1` when the pointer enters a widget that /// declares one. Mirrors `cursor_icon::CursorIcon` 1:1 so the /// runtime can convert losslessly. Compositors that do not advertise /// `wp_cursor_shape_v1` ignore these — the user sees their default /// system cursor. #[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ] pub enum CursorShape { Default, ContextMenu, Help, /// "Hand" — clickable buttons, links. Pointer, /// "Spinning wheel" — work in progress, you can still interact. Progress, /// "Hourglass" — UI is busy and unresponsive. Wait, Cell, Crosshair, /// I-beam — text input fields. Text, VerticalText, Alias, Copy, Move, NoDrop, NotAllowed, /// Open hand — draggable but not yet dragging. Grab, /// Closed hand — currently dragging. Grabbing, EResize, NResize, NeResize, NwResize, SResize, SeResize, SwResize, WResize, EwResize, NsResize, NeswResize, NwseResize, ColResize, RowResize, AllScroll, ZoomIn, ZoomOut, } impl Default for CursorShape { fn default() -> Self { CursorShape::Default } } #[ cfg( test ) ] mod tests { use super::*; // ── Color ───────────────────────────────────────────────────────────────── #[ test ] fn color_hex_sets_rgb_and_full_alpha() { let c = Color::hex( 0xFF, 0x00, 0x80 ); assert!( ( c.r - 1.0 ).abs() < 1e-3 ); assert!( ( c.g - 0.0 ).abs() < 1e-6 ); assert!( ( c.b - 0x80 as f32 / 255.0 ).abs() < 1e-3 ); assert_eq!( c.a, 1.0 ); } #[ test ] fn color_rgba_stores_all_channels() { let c = Color::rgba( 0.1, 0.2, 0.3, 0.4 ); assert!( ( c.r - 0.1 ).abs() < 1e-6 ); assert!( ( c.g - 0.2 ).abs() < 1e-6 ); assert!( ( c.b - 0.3 ).abs() < 1e-6 ); assert!( ( c.a - 0.4 ).abs() < 1e-6 ); } #[ test ] fn color_white_constant_is_all_ones() { let c = Color::WHITE; assert_eq!( c.r, 1. ); assert_eq!( c.g, 1. ); assert_eq!( c.b, 1. ); assert_eq!( c.a, 1. ); } #[ test ] fn color_transparent_has_zero_alpha() { assert_eq!( Color::TRANSPARENT.a, 0. ); } #[ test ] fn color_rgb_sets_full_alpha() { let c = Color::rgb( 0.5, 0.5, 0.5 ); assert_eq!( c.a, 1.0 ); } // ── Rect ────────────────────────────────────────────────────────────────── #[ test ] fn rect_contains_interior_point() { let r = Rect { x: 10., y: 20., width: 100., height: 50. }; assert!( r.contains( Point { x: 60., y: 45. } ) ); } #[ test ] fn rect_contains_boundary_points() { let r = Rect { x: 0., y: 0., width: 100., height: 100. }; assert!( r.contains( Point { x: 0., y: 0. } ) ); assert!( r.contains( Point { x: 100., y: 100. } ) ); } #[ test ] fn rect_does_not_contain_exterior_points() { let r = Rect { x: 10., y: 20., width: 100., height: 50. }; assert!( !r.contains( Point { x: 5., y: 45. } ) ); assert!( !r.contains( Point { x: 60., y: 5. } ) ); assert!( !r.contains( Point { x: 200., y: 45. } ) ); assert!( !r.contains( Point { x: 60., y: 80. } ) ); } #[ test ] fn rect_expand_grows_in_all_directions() { let r = Rect { x: 10., y: 10., width: 80., height: 40. }; let e = r.expand( 5. ); assert_eq!( e.x, 5. ); assert_eq!( e.y, 5. ); assert_eq!( e.width, 90. ); assert_eq!( e.height, 50. ); } #[ test ] fn rect_expand_zero_is_identity() { let r = Rect { x: 1., y: 2., width: 3., height: 4. }; let e = r.expand( 0. ); assert_eq!( r, e ); } }