// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! 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 { pub( crate ) path: Vec, pub( crate ) on_node: Option Msg>>, pub( crate ) on_release: Option, pub( crate ) hide_stroke: bool, pub( crate ) error: bool, pub( crate ) size: Option, } impl PatternGrid { pub fn new( path: Vec ) -> 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 ) -> 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( self, f: &super::MapFn ) -> PatternGrid where U: Clone + 'static, Msg: 'static, { let on_node = self.on_node.map( |old| -> Arc 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 { ( 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( path: Vec ) -> PatternGrid { PatternGrid::new( path ) } impl From> for Element { fn from( g: PatternGrid ) -> Self { Element::PatternGrid( g ) } }