433 lines
12 KiB
Rust
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 );
|
|
}
|
|
}
|