Files
ltk/src/widget/pattern_grid/mod.rs
T
admin c9ae5438f8
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
pattern_grid: explicit side through PatternGrid::size
The grid's preferred size was the theme's fluid constant (264 px at the reference surface) capped by the width the parent allocates, so a caller could only shrink it: a lock screen that wants the grid to span most of a phone's width had no way to ask for it, and a container's max_width did nothing past the default. PatternGrid gains a size field with a size( impl Into<Length> ) builder; preferred_size resolves it through Canvas::resolve_geom like the other geometry overrides and still caps it at max_width, and map_msg carries it across. Without the builder the fluid default applies as before.
docs/widgets.md describes the builder next to hide_stroke and error; CHANGELOG lists it under Unreleased.
2026-09-10 21:33:55 +02:00

181 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! A 3×3 unlock-pattern grid. The widget is stateless: the app owns the
//! visited node list and passes it back in for drawing. A press or a drag
//! that reaches a node fires `on_node( index )` with the node's index,
//! 0 to 8 row-major; the app decides whether to append it. Release fires
//! `on_release`.
use std::sync::Arc;
use crate::types::{ Color, Length, Point, Rect };
use crate::render::Canvas;
use super::Element;
mod theme;
#[ cfg( test ) ]
mod tests;
pub const NODES: u8 = 9;
pub struct PatternGrid<Msg: Clone>
{
pub( crate ) path: Vec<u8>,
pub( crate ) on_node: Option<Arc<dyn Fn( u8 ) -> Msg>>,
pub( crate ) on_release: Option<Msg>,
pub( crate ) hide_stroke: bool,
pub( crate ) error: bool,
pub( crate ) size: Option<Length>,
}
impl<Msg: Clone> PatternGrid<Msg>
{
pub fn new( path: Vec<u8> ) -> Self
{
Self { path, on_node: None, on_release: None, hide_stroke: false, error: false, size: None }
}
/// Side of the square grid; without it the theme's fluid default applies.
pub fn size( mut self, s: impl Into<Length> ) -> Self
{
self.size = Some( s.into() );
self
}
pub fn on_node( mut self, f: impl Fn( u8 ) -> Msg + 'static ) -> Self
{
self.on_node = Some( Arc::new( f ) );
self
}
pub fn on_release( mut self, msg: Msg ) -> Self
{
self.on_release = Some( msg );
self
}
/// Draw only the nodes, never the stroke: against shoulder surfing.
pub fn hide_stroke( mut self, hide: bool ) -> Self
{
self.hide_stroke = hide;
self
}
/// Paint the current path in the danger colour.
pub fn error( mut self, error: bool ) -> Self
{
self.error = error;
self
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> PatternGrid<U>
where
U: Clone + 'static,
Msg: 'static,
{
let on_node = self.on_node.map( |old| -> Arc<dyn Fn( u8 ) -> U>
{
let mapper = Arc::clone( f );
Arc::new( move |n| ( *mapper )( ( *old )( n ) ) )
} );
PatternGrid
{
path: self.path,
on_node,
on_release: self.on_release.map( |m| ( *f )( m ) ),
hide_stroke: self.hide_stroke,
error: self.error,
size: self.size,
}
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
let size = match self.size
{
Some( l ) => canvas.resolve_geom( l ),
None => canvas.geom_px( theme::SIZE ),
};
let size = size.min( max_width );
( size, size )
}
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, _focused: bool )
{
let palette = crate::theme::palette();
let active = if self.error { palette.danger } else { palette.accent };
let node_r = canvas.geom_px( theme::NODE_RADIUS );
let dot_r = canvas.geom_px( theme::DOT_RADIUS );
let stroke_w = canvas.geom_px( theme::STROKE_W );
if !self.hide_stroke
{
for pair in self.path.windows( 2 )
{
let a = node_center( rect, pair[ 0 ] );
let b = node_center( rect, pair[ 1 ] );
canvas.draw_line( a.x, a.y, b.x, b.y, active, stroke_w );
}
}
for node in 0..NODES
{
let c = node_center( rect, node );
let visited = self.path.contains( &node );
if visited
{
let ring = Color { a: active.a * theme::RING_ALPHA, ..active };
circle( canvas, c, node_r, ring );
}
circle( canvas, c, dot_r, if visited { active } else { palette.text_secondary } );
}
}
}
fn circle( canvas: &mut Canvas, center: Point, radius: f32, color: Color )
{
let rect = Rect { x: center.x - radius, y: center.y - radius, width: radius * 2.0, height: radius * 2.0 };
canvas.fill_rect( rect, color, radius );
}
/// Centre of node `index` (0 to 8, row-major) inside `rect`.
pub fn node_center( rect: Rect, index: u8 ) -> Point
{
let col = ( index % 3 ) as f32;
let row = ( index / 3 ) as f32;
let side = rect.width.min( rect.height );
let ox = rect.x + ( rect.width - side ) / 2.0;
let oy = rect.y + ( rect.height - side ) / 2.0;
let cell = side / 3.0;
Point { x: ox + cell * ( col + 0.5 ), y: oy + cell * ( row + 0.5 ) }
}
/// The node under `pos`, if within the snap radius of one.
pub fn node_at( rect: Rect, pos: Point, snap_radius: f32 ) -> Option<u8>
{
( 0..NODES ).find( |&node|
{
let c = node_center( rect, node );
( pos.x - c.x ).hypot( pos.y - c.y ) <= snap_radius
} )
}
/// Snap radius in design pixels, resolved by the input path through the canvas density.
pub fn snap_design_px() -> f32
{
theme::SNAP_RADIUS
}
pub fn pattern_grid<Msg: Clone>( path: Vec<u8> ) -> PatternGrid<Msg>
{
PatternGrid::new( path )
}
impl<Msg: Clone + 'static> From<PatternGrid<Msg>> for Element<Msg>
{
fn from( g: PatternGrid<Msg> ) -> Self
{
Element::PatternGrid( g )
}
}