1use super::*;
4
5type StyleFn<'a> = Box<dyn Fn(&Theme, iced_checkbox::Status) -> iced_checkbox::Style + 'a>;
6
7pub struct Checkbox<'a, Message, Renderer = iced_widget::Renderer>
9where
10 Renderer: core_text::Renderer,
11{
12 is_checked: bool,
13 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
14 label: Option<text::Fragment<'a>>,
15 width: Length,
16 size: f32,
17 spacing: f32,
18 text_size: Option<Pixels>,
19 text_line_height: LineHeight,
20 text_shaping: text::Shaping,
21 text_wrapping: text::Wrapping,
22 font: Option<Renderer::Font>,
23 style: StyleFn<'a>,
24}
25
26impl<Message, Renderer> std::fmt::Debug for Checkbox<'_, Message, Renderer>
27where
28 Renderer: core_text::Renderer,
29{
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.debug_struct("Checkbox")
32 .field("is_checked", &self.is_checked)
33 .field("has_on_toggle", &self.on_toggle.is_some())
34 .field("has_label", &self.label.is_some())
35 .field("width", &self.width)
36 .field("size", &self.size)
37 .field("spacing", &self.spacing)
38 .field("text_size", &self.text_size)
39 .field("text_line_height", &self.text_line_height)
40 .finish_non_exhaustive()
41 }
42}
43
44impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer>
45where
46 Renderer: core_text::Renderer,
47{
48 pub fn new(is_checked: bool) -> Self {
49 Self {
50 is_checked,
51 on_toggle: None,
52 label: None,
53 width: Length::Shrink,
54 size: tokens::component::checkbox::CONTAINER_SIZE,
55 spacing: f32::from(tokens::component::divider::LIST_ITEM_LEADING_SPACE),
56 text_size: Some(Pixels(tokens::component::checkbox::LABEL_TEXT_SIZE)),
57 text_line_height: absolute_line_height(
58 tokens::component::checkbox::LABEL_TEXT_LINE_HEIGHT,
59 ),
60 text_shaping: text::Shaping::default(),
61 text_wrapping: text::Wrapping::default(),
62 font: None,
63 style: Box::new(checkbox_style::default),
64 }
65 }
66
67 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
68 self.label = Some(label.into_fragment());
69 self
70 }
71
72 pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
73 self.on_toggle = Some(Box::new(on_toggle));
74 self
75 }
76
77 pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
78 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
79 self
80 }
81
82 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
83 self.size = size.into().0;
84 self
85 }
86
87 pub fn width(mut self, width: impl Into<Length>) -> Self {
88 self.width = width.into();
89 self
90 }
91
92 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
93 self.spacing = spacing.into().0;
94 self
95 }
96
97 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
98 self.text_size = Some(text_size.into());
99 self
100 }
101
102 pub fn text_line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
103 self.text_line_height = line_height.into();
104 self
105 }
106
107 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
108 self.text_shaping = shaping;
109 self
110 }
111
112 pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
113 self.text_wrapping = wrapping;
114 self
115 }
116
117 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
118 self.font = Some(font.into());
119 self
120 }
121
122 pub fn style(
123 mut self,
124 style: impl Fn(&Theme, iced_checkbox::Status) -> iced_checkbox::Style + 'a,
125 ) -> Self {
126 self.style = Box::new(style);
127 self
128 }
129}
130
131impl<Message, Renderer> Checkbox<'_, Message, Renderer>
132where
133 Renderer: core_text::Renderer,
134{
135 fn current_status(&self, bounds: Rectangle, cursor: mouse::Cursor) -> iced_checkbox::Status {
136 if self.on_toggle.is_none() {
137 iced_checkbox::Status::Disabled {
138 is_checked: self.is_checked,
139 }
140 } else if cursor.is_over(bounds) {
141 iced_checkbox::Status::Hovered {
142 is_checked: self.is_checked,
143 }
144 } else {
145 iced_checkbox::Status::Active {
146 is_checked: self.is_checked,
147 }
148 }
149 }
150
151 fn state_layer_color(
152 &self,
153 state: &SelectionState<Renderer::Paragraph, iced_checkbox::Status>,
154 status: iced_checkbox::Status,
155 unchecked_style: &iced_checkbox::Style,
156 checked_style: &iced_checkbox::Style,
157 ) -> Option<Color> {
158 let is_hovered = matches!(status, iced_checkbox::Status::Hovered { .. });
159
160 if !state.is_pressed && !is_hovered {
161 return None;
162 }
163
164 let color = if self.is_checked {
165 solid_color(checked_style.background)
166 } else {
167 unchecked_style.border.color
168 };
169 let opacity = if state.is_pressed {
170 tokens::state::PRESSED_STATE_LAYER_OPACITY
171 } else {
172 tokens::state::HOVER_STATE_LAYER_OPACITY
173 };
174
175 Some(alpha_color(color, opacity))
176 }
177}
178
179impl<Message, Renderer> Widget<Message, Theme, Renderer> for Checkbox<'_, Message, Renderer>
180where
181 Renderer: core_text::Renderer + core_svg::Renderer,
182{
183 fn tag(&self) -> tree::Tag {
184 tree::Tag::of::<SelectionState<Renderer::Paragraph, iced_checkbox::Status>>()
185 }
186
187 fn state(&self) -> tree::State {
188 tree::State::new(
189 SelectionState::<Renderer::Paragraph, iced_checkbox::Status>::new(self.is_checked),
190 )
191 }
192
193 fn size(&self) -> Size<Length> {
194 Size {
195 width: self.width,
196 height: Length::Shrink,
197 }
198 }
199
200 fn layout(
201 &mut self,
202 tree: &mut Tree,
203 renderer: &Renderer,
204 limits: &layout::Limits,
205 ) -> layout::Node {
206 layout::next_to_each_other(
207 &limits.width(self.width),
208 if self.label.is_some() {
209 self.spacing
210 } else {
211 0.0
212 },
213 |_| layout::Node::new(Size::new(self.size, self.size)),
214 |limits| {
215 if let Some(label) = self.label.as_deref() {
216 let state = tree
217 .state
218 .downcast_mut::<SelectionState<Renderer::Paragraph, iced_checkbox::Status>>(
219 );
220
221 core_widget::text::layout(
222 &mut state.text,
223 renderer,
224 limits,
225 label,
226 core_widget::text::Format {
227 width: self.width,
228 height: Length::Shrink,
229 line_height: self.text_line_height,
230 size: self.text_size,
231 font: self.font,
232 align_x: text::Alignment::Default,
233 align_y: alignment::Vertical::Top,
234 shaping: self.text_shaping,
235 wrapping: self.text_wrapping,
236 },
237 )
238 } else {
239 layout::Node::new(Size::ZERO)
240 }
241 },
242 )
243 }
244
245 fn update(
246 &mut self,
247 tree: &mut Tree,
248 event: &Event,
249 layout: Layout<'_>,
250 cursor: mouse::Cursor,
251 _renderer: &Renderer,
252 _clipboard: &mut dyn Clipboard,
253 shell: &mut Shell<'_, Message>,
254 _viewport: &Rectangle,
255 ) {
256 let state = tree
257 .state
258 .downcast_mut::<SelectionState<Renderer::Paragraph, iced_checkbox::Status>>();
259 let hit_bounds =
260 selection_control_hit_bounds(layout, tokens::component::checkbox::STATE_LAYER_SIZE);
261
262 match event {
263 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
264 | Event::Touch(touch::Event::FingerPressed { .. }) => {
265 if self.on_toggle.is_some() && press_is_over(event, hit_bounds, cursor) {
266 state.is_pressed = true;
267 state.press_origin = None;
268 shell.capture_event();
269 shell.request_redraw();
270 }
271 }
272 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
273 | Event::Touch(touch::Event::FingerLifted { .. }) => {
274 if state.is_pressed {
275 let is_released_over = release_is_over(event, hit_bounds, cursor);
276
277 state.is_pressed = false;
278 state.press_origin = None;
279
280 if is_released_over && let Some(on_toggle) = &self.on_toggle {
281 shell.publish((on_toggle)(!self.is_checked));
282 }
283
284 shell.capture_event();
285 shell.request_redraw();
286 }
287 }
288 Event::Touch(touch::Event::FingerLost { .. }) => {
289 if state.is_pressed {
290 state.is_pressed = false;
291 state.press_origin = None;
292 shell.request_redraw();
293 }
294 }
295 _ => {}
296 }
297
298 let now = match event {
299 Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
300 _ => None,
301 };
302
303 if state.target != self.is_checked {
304 let now = now.unwrap_or_else(Instant::now);
305 let (duration, easing) = if self.is_checked {
306 (
307 duration_ms(tokens::component::checkbox::SELECT_TRANSITION_DURATION_MS),
308 tokens::component::checkbox::SELECT_TRANSITION_EASING,
309 )
310 } else {
311 (
312 duration_ms(tokens::component::checkbox::UNSELECT_TRANSITION_DURATION_MS),
313 tokens::component::checkbox::UNSELECT_TRANSITION_EASING,
314 )
315 };
316
317 state.target = self.is_checked;
318 state
319 .position
320 .set_target(bool_value(self.is_checked), now, duration, easing);
321 state.color.set_target(
322 bool_value(self.is_checked),
323 now,
324 duration_ms(tokens::component::checkbox::OPACITY_TRANSITION_DURATION_MS),
325 tokens::motion::EASING_LINEAR,
326 );
327 state
328 .size
329 .set_target(bool_value(self.is_checked), now, duration, easing);
330 shell.request_redraw();
331 }
332
333 let current_status = self.current_status(hit_bounds, cursor);
334
335 if let Some(now) = now {
336 if state.advance(now) {
337 shell.request_redraw();
338 }
339
340 state.last_status = Some(current_status);
341 } else if state
342 .last_status
343 .is_some_and(|status| status != current_status)
344 || state.is_animating()
345 {
346 shell.request_redraw();
347 }
348 }
349
350 fn mouse_interaction(
351 &self,
352 _tree: &Tree,
353 layout: Layout<'_>,
354 cursor: mouse::Cursor,
355 _viewport: &Rectangle,
356 _renderer: &Renderer,
357 ) -> mouse::Interaction {
358 let hit_bounds =
359 selection_control_hit_bounds(layout, tokens::component::checkbox::STATE_LAYER_SIZE);
360
361 if cursor.is_over(hit_bounds) && self.on_toggle.is_some() {
362 mouse::Interaction::Pointer
363 } else {
364 mouse::Interaction::default()
365 }
366 }
367
368 fn draw(
369 &self,
370 tree: &Tree,
371 renderer: &mut Renderer,
372 theme: &Theme,
373 defaults: &renderer::Style,
374 layout: Layout<'_>,
375 _cursor: mouse::Cursor,
376 viewport: &Rectangle,
377 ) {
378 let state = tree
379 .state
380 .downcast_ref::<SelectionState<Renderer::Paragraph, iced_checkbox::Status>>();
381
382 let mut children = layout.children();
383 let control_layout = children.next().unwrap();
384 let bounds = control_layout.bounds();
385
386 let status = state
387 .last_status
388 .unwrap_or(iced_checkbox::Status::Disabled {
389 is_checked: self.is_checked,
390 });
391 let unchecked_status = match status {
392 iced_checkbox::Status::Active { .. } => {
393 iced_checkbox::Status::Active { is_checked: false }
394 }
395 iced_checkbox::Status::Hovered { .. } => {
396 iced_checkbox::Status::Hovered { is_checked: false }
397 }
398 iced_checkbox::Status::Disabled { .. } => {
399 iced_checkbox::Status::Disabled { is_checked: false }
400 }
401 };
402 let checked_status = match status {
403 iced_checkbox::Status::Active { .. } => {
404 iced_checkbox::Status::Active { is_checked: true }
405 }
406 iced_checkbox::Status::Hovered { .. } => {
407 iced_checkbox::Status::Hovered { is_checked: true }
408 }
409 iced_checkbox::Status::Disabled { .. } => {
410 iced_checkbox::Status::Disabled { is_checked: true }
411 }
412 };
413 let current_style = (self.style)(theme, status);
414 let unchecked_style = (self.style)(theme, unchecked_status);
415 let checked_style = (self.style)(theme, checked_status);
416
417 let selection = state.position.value.clamp(0.0, 1.0);
418 let opacity = state.color.value.clamp(0.0, 1.0);
419 let scale = 0.74 + 0.26 * state.size.value.clamp(0.0, 1.0);
420
421 if let Some(layer_color) =
422 self.state_layer_color(state, status, &unchecked_style, &checked_style)
423 {
424 renderer.fill_quad(
425 renderer::Quad {
426 bounds: scaled_rect(
427 bounds,
428 tokens::component::checkbox::STATE_LAYER_SIZE,
429 tokens::component::checkbox::STATE_LAYER_SIZE,
430 ),
431 border: border::rounded(tokens::component::checkbox::STATE_LAYER_SIZE / 2.0),
432 ..renderer::Quad::default()
433 },
434 Background::Color(layer_color),
435 );
436 }
437
438 renderer.fill_quad(
439 renderer::Quad {
440 bounds,
441 border: alpha_border(unchecked_style.border, 1.0 - selection),
442 ..renderer::Quad::default()
443 },
444 unchecked_style.background.scale_alpha(1.0 - selection),
445 );
446
447 if selection > 0.0 {
448 let selected_bounds = scaled_rect(bounds, bounds.width * scale, bounds.height * scale);
449
450 renderer.fill_quad(
451 renderer::Quad {
452 bounds: selected_bounds,
453 border: alpha_border(checked_style.border, selection),
454 ..renderer::Quad::default()
455 },
456 checked_style.background.scale_alpha(selection),
457 );
458 }
459
460 if opacity > 0.0 {
461 let icon_scale = 0.58 + 0.42 * selection;
462 let icon_size = tokens::component::checkbox::ICON_SIZE * icon_scale;
463 let mark_progress = if self.is_checked { selection } else { 1.0 };
464
465 renderer.draw_svg(
466 core_svg::Svg::new(core_svg::Handle::from_memory(checkbox_checkmark_svg(
467 mark_progress,
468 )))
469 .color(checked_style.icon_color)
470 .opacity(opacity),
471 scaled_rect(bounds, icon_size, icon_size),
472 *viewport,
473 );
474 }
475
476 if self.label.is_none() {
477 return;
478 }
479
480 let label_layout = children.next().unwrap();
481
482 core_widget::text::draw(
483 renderer,
484 defaults,
485 label_layout.bounds(),
486 state.text.raw(),
487 core_widget::text::Style {
488 color: current_style.text_color,
489 },
490 viewport,
491 );
492 }
493
494 fn operate(
495 &mut self,
496 _tree: &mut Tree,
497 layout: Layout<'_>,
498 _renderer: &Renderer,
499 operation: &mut dyn core_widget::Operation,
500 ) {
501 if let Some(label) = self.label.as_deref() {
502 operation.text(None, layout.bounds(), label);
503 }
504 }
505}
506
507impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
508 for Element<'a, Message, Theme, Renderer>
509where
510 Message: 'a,
511 Renderer: core_text::Renderer + core_svg::Renderer + 'a,
512{
513 fn from(checkbox: Checkbox<'a, Message, Renderer>) -> Self {
514 Element::new(checkbox)
515 }
516}
517
518pub fn control<'a, Message, Renderer>(is_checked: bool) -> Checkbox<'a, Message, Renderer>
519where
520 Renderer: core_text::Renderer + core_svg::Renderer + 'a,
521{
522 Checkbox::new(is_checked)
523 .size(tokens::component::checkbox::CONTAINER_SIZE)
524 .spacing(f32::from(
525 tokens::component::divider::LIST_ITEM_LEADING_SPACE,
526 ))
527 .text_size(tokens::component::checkbox::LABEL_TEXT_SIZE)
528 .text_line_height(absolute_line_height(
529 tokens::component::checkbox::LABEL_TEXT_LINE_HEIGHT,
530 ))
531 .style(checkbox_style::default)
532}
533
534pub fn standard<'a, Message, Renderer>(
535 is_checked: bool,
536 label: impl text::IntoFragment<'a>,
537 on_toggle: impl Fn(bool) -> Message + 'a,
538) -> Element<'a, Message, Theme, Renderer>
539where
540 Message: 'a,
541 Renderer: iced_widget::core::Renderer + core_text::Renderer + core_svg::Renderer + 'a,
542{
543 Container::new(control(is_checked).label(label).on_toggle(on_toggle))
544 .center_y(Length::Fixed(tokens::component::checkbox::STATE_LAYER_SIZE))
545 .into()
546}