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 )
	}
}