ltk/widget/checkbox/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::types::{ Rect, WidgetId };
use crate::render::Canvas;
use super::Element;
mod theme;
#[cfg(test)]
mod tests;
/// A two-state opt-in control with a square box and a check glyph.
///
/// Use for individual binary choices inside a form (terms acceptance,
/// multi-select lists, "remember me"). The widget is stateless — the
/// application owns `checked` and rebuilds the checkbox from current state
/// on every frame.
///
/// ```rust,no_run
/// # use ltk::{ checkbox, Checkbox };
/// # #[ derive( Clone ) ] enum Msg { ToggleTerms }
/// # struct App { accept_terms: bool }
/// # impl App { fn _ex( &self ) -> Checkbox<Msg> {
/// // In view():
/// checkbox( self.accept_terms )
/// .label( "I accept the terms" )
/// .on_toggle( Msg::ToggleTerms )
/// # }}
/// ```
///
/// See also [`Toggle`](super::toggle::Toggle) for prominent on / off
/// switches (settings panels, system toggles) and
/// [`Radio`](super::radio::Radio) for mutually-exclusive selection in a
/// group.
pub struct Checkbox<Msg: Clone>
{
/// Current checked state. Drawn from this field every frame; the
/// runtime never mutates it.
pub checked: bool,
/// Message emitted on activation. `None` leaves the checkbox inert.
pub on_toggle: Option<Msg>,
/// Optional label drawn to the right of the box.
pub label: Option<String>,
/// Optional stable identifier for focus management.
pub id: Option<WidgetId>,
}
impl<Msg: Clone> Checkbox<Msg>
{
/// Create a checkbox in the given state, with no label and no
/// callback. Wire activation through [`Self::on_toggle`] before adding
/// it to a widget tree.
pub fn new( checked: bool ) -> Self
{
Self { checked, on_toggle: None, label: None, id: None }
}
/// Set the message emitted when the checkbox is activated. The
/// application's `update` is responsible for flipping `checked` in
/// response.
pub fn on_toggle( mut self, msg: Msg ) -> Self
{
self.on_toggle = Some( msg );
self
}
/// Set a text label rendered to the right of the box. The checkbox's
/// preferred width grows to fit `box_size + gap + label_width`,
/// clamped to `max_width`.
pub fn label( mut self, label: impl Into<String> ) -> Self
{
self.label = Some( label.into() );
self
}
/// Assign a stable identifier for focus management.
pub fn id( mut self, id: WidgetId ) -> Self
{
self.id = Some( id );
self
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
{
let w = if let Some( ref label ) = self.label
{
let text_w = canvas.measure_text( label, theme::FONT_SIZE );
( theme::BOX_SIZE + theme::GAP + text_w ).min( max_width )
} else {
theme::BOX_SIZE.min( max_width )
};
( w, theme::HEIGHT )
}
/// Focus ring on the box extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px` beyond
/// the box edge (which sits flush with the widget's left edge).
pub fn paint_bounds( &self, rect: Rect ) -> Rect
{
rect.expand( theme::FOCUS_W + 2.0 + theme::FOCUS_W * 0.5 + 1.0 )
}
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, focused: bool )
{
let box_y = rect.y + ( rect.height - theme::BOX_SIZE ) / 2.0;
let box_rect = Rect
{
x: rect.x,
y: box_y,
width: theme::BOX_SIZE,
height: theme::BOX_SIZE,
};
if self.checked
{
canvas.fill_rect( box_rect, theme::box_checked(), theme::RADIUS );
let cx = rect.x + theme::BOX_SIZE / 2.0;
let cy = box_y + theme::BOX_SIZE / 2.0;
let s = theme::BOX_SIZE * 0.3;
canvas.draw_line( cx - s, cy, cx - s * 0.3, cy + s * 0.7, theme::check_color(), theme::CHECK_W );
canvas.draw_line( cx - s * 0.3, cy + s * 0.7, cx + s, cy - s * 0.5, theme::check_color(), theme::CHECK_W );
} else {
canvas.stroke_rect( box_rect, theme::box_border(), theme::BORDER_W, theme::RADIUS );
}
if focused
{
let ring = box_rect.expand( theme::FOCUS_W + 2.0 );
canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, theme::RADIUS + theme::FOCUS_W + 2.0 );
}
if let Some( ref label ) = self.label
{
let text_x = rect.x + theme::BOX_SIZE + theme::GAP;
let text_y = rect.y + ( rect.height + theme::FONT_SIZE ) / 2.0 - 2.0;
canvas.draw_text( label, text_x, text_y, theme::FONT_SIZE, theme::label_color() );
}
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Checkbox<U>
where
U: Clone + 'static,
Msg: 'static,
{
Checkbox
{
checked: self.checked,
on_toggle: self.on_toggle.map( |m| ( *f )( m ) ),
label: self.label,
id: self.id,
}
}
}
/// Create a [`Checkbox`] in the given state.
///
/// Shorthand for [`Checkbox::new`]. Wire activation with
/// [`Checkbox::on_toggle`] and add a label with [`Checkbox::label`]:
///
/// ```rust,no_run
/// # use ltk::{ checkbox, Checkbox };
/// # #[ derive( Clone ) ] enum Msg { ToggleAccept }
/// # struct App { accept: bool }
/// # impl App { fn _ex( &self ) -> Checkbox<Msg> {
/// checkbox( self.accept )
/// .label( "I accept the terms" )
/// .on_toggle( Msg::ToggleAccept )
/// # }}
/// ```
pub fn checkbox<Msg: Clone>( checked: bool ) -> Checkbox<Msg>
{
Checkbox::new( checked )
}
impl<Msg: Clone + 'static> From<Checkbox<Msg>> for Element<Msg>
{
fn from( c: Checkbox<Msg> ) -> Self
{
Element::Checkbox( c )
}
}