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