Files
ltk/src/types.rs

433 lines
12 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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>
{
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<Corners>` 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<f32> 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 );
}
}