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 {
267 state.is_pressed = true;
268 state.press_origin = None;
269 shell.capture_event();
270 shell.request_redraw();
271 }
272 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
273 | Event::Touch(touch::Event::FingerLifted { .. })
274 if state.is_pressed =>
275 {
276 let is_released_over = release_is_over(event, hit_bounds, cursor);
277
278 state.is_pressed = false;
279 state.press_origin = None;
280
281 if is_released_over && let Some(on_toggle) = &self.on_toggle {
282 shell.publish((on_toggle)(!self.is_checked));
283 }
284
285 shell.capture_event();
286 shell.request_redraw();
287 }
288 Event::Touch(touch::Event::FingerLost { .. }) if state.is_pressed => {
289 state.is_pressed = false;
290 state.press_origin = None;
291 shell.request_redraw();
292 }
293 _ => {}
294 }
295
296 let now = match event {
297 Event::Window(window::Event::RedrawRequested(now)) => Some(*now),
298 _ => None,
299 };
300
301 if state.target != self.is_checked {
302 let now = now.unwrap_or_else(Instant::now);
303 let (duration, easing) = if self.is_checked {
304 (
305 duration_ms(tokens::component::checkbox::SELECT_TRANSITION_DURATION_MS),
306 tokens::component::checkbox::SELECT_TRANSITION_EASING,
307 )
308 } else {
309 (
310 duration_ms(tokens::component::checkbox::UNSELECT_TRANSITION_DURATION_MS),
311 tokens::component::checkbox::UNSELECT_TRANSITION_EASING,
312 )
313 };
314
315 state.target = self.is_checked;
316 state
317 .position
318 .set_target(bool_value(self.is_checked), now, duration, easing);
319 state.color.set_target(
320 bool_value(self.is_checked),
321 now,
322 duration_ms(tokens::component::checkbox::OPACITY_TRANSITION_DURATION_MS),
323 tokens::motion::EASING_LINEAR,
324 );
325 state
326 .size
327 .set_target(bool_value(self.is_checked), now, duration, easing);
328 shell.request_redraw();
329 }
330
331 let current_status = self.current_status(hit_bounds, cursor);
332
333 if let Some(now) = now {
334 if state.advance(now) {
335 shell.request_redraw();
336 }
337
338 state.last_status = Some(current_status);
339 } else if state
340 .last_status
341 .is_some_and(|status| status != current_status)
342 || state.is_animating()
343 {
344 shell.request_redraw();
345 }
346 }
347
348 fn mouse_interaction(
349 &self,
350 _tree: &Tree,
351 layout: Layout<'_>,
352 cursor: mouse::Cursor,
353 _viewport: &Rectangle,
354 _renderer: &Renderer,
355 ) -> mouse::Interaction {
356 let hit_bounds =
357 selection_control_hit_bounds(layout, tokens::component::checkbox::STATE_LAYER_SIZE);
358
359 if cursor.is_over(hit_bounds) && self.on_toggle.is_some() {
360 mouse::Interaction::Pointer
361 } else {
362 mouse::Interaction::default()
363 }
364 }
365
366 fn draw(
367 &self,
368 tree: &Tree,
369 renderer: &mut Renderer,
370 theme: &Theme,
371 defaults: &renderer::Style,
372 layout: Layout<'_>,
373 _cursor: mouse::Cursor,
374 viewport: &Rectangle,
375 ) {
376 let state = tree
377 .state
378 .downcast_ref::<SelectionState<Renderer::Paragraph, iced_checkbox::Status>>();
379
380 let mut children = layout.children();
381 let control_layout = children.next().unwrap();
382 let bounds = control_layout.bounds();
383
384 let status = state
385 .last_status
386 .unwrap_or(iced_checkbox::Status::Disabled {
387 is_checked: self.is_checked,
388 });
389 let unchecked_status = match status {
390 iced_checkbox::Status::Active { .. } => {
391 iced_checkbox::Status::Active { is_checked: false }
392 }
393 iced_checkbox::Status::Hovered { .. } => {
394 iced_checkbox::Status::Hovered { is_checked: false }
395 }
396 iced_checkbox::Status::Disabled { .. } => {
397 iced_checkbox::Status::Disabled { is_checked: false }
398 }
399 };
400 let checked_status = match status {
401 iced_checkbox::Status::Active { .. } => {
402 iced_checkbox::Status::Active { is_checked: true }
403 }
404 iced_checkbox::Status::Hovered { .. } => {
405 iced_checkbox::Status::Hovered { is_checked: true }
406 }
407 iced_checkbox::Status::Disabled { .. } => {
408 iced_checkbox::Status::Disabled { is_checked: true }
409 }
410 };
411 let current_style = (self.style)(theme, status);
412 let unchecked_style = (self.style)(theme, unchecked_status);
413 let checked_style = (self.style)(theme, checked_status);
414
415 let selection = state.position.value.clamp(0.0, 1.0);
416 let opacity = state.color.value.clamp(0.0, 1.0);
417 let scale = 0.74 + 0.26 * state.size.value.clamp(0.0, 1.0);
418
419 if let Some(layer_color) =
420 self.state_layer_color(state, status, &unchecked_style, &checked_style)
421 {
422 renderer.fill_quad(
423 renderer::Quad {
424 bounds: scaled_rect(
425 bounds,
426 tokens::component::checkbox::STATE_LAYER_SIZE,
427 tokens::component::checkbox::STATE_LAYER_SIZE,
428 ),
429 border: border::rounded(tokens::component::checkbox::STATE_LAYER_SIZE / 2.0),
430 ..renderer::Quad::default()
431 },
432 Background::Color(layer_color),
433 );
434 }
435
436 renderer.fill_quad(
437 renderer::Quad {
438 bounds,
439 border: alpha_border(unchecked_style.border, 1.0 - selection),
440 ..renderer::Quad::default()
441 },
442 unchecked_style.background.scale_alpha(1.0 - selection),
443 );
444
445 if selection > 0.0 {
446 let selected_bounds = scaled_rect(bounds, bounds.width * scale, bounds.height * scale);
447
448 renderer.fill_quad(
449 renderer::Quad {
450 bounds: selected_bounds,
451 border: alpha_border(checked_style.border, selection),
452 ..renderer::Quad::default()
453 },
454 checked_style.background.scale_alpha(selection),
455 );
456 }
457
458 if opacity > 0.0 {
459 let icon_scale = 0.58 + 0.42 * selection;
460 let icon_size = tokens::component::checkbox::ICON_SIZE * icon_scale;
461 let mark_progress = if self.is_checked { selection } else { 1.0 };
462
463 renderer.draw_svg(
464 core_svg::Svg::new(core_svg::Handle::from_memory(checkbox_checkmark_svg(
465 mark_progress,
466 )))
467 .color(checked_style.icon_color)
468 .opacity(opacity),
469 scaled_rect(bounds, icon_size, icon_size),
470 *viewport,
471 );
472 }
473
474 if self.label.is_none() {
475 return;
476 }
477
478 let label_layout = children.next().unwrap();
479
480 core_widget::text::draw(
481 renderer,
482 defaults,
483 label_layout.bounds(),
484 state.text.raw(),
485 core_widget::text::Style {
486 color: current_style.text_color,
487 },
488 viewport,
489 );
490 }
491
492 fn operate(
493 &mut self,
494 _tree: &mut Tree,
495 layout: Layout<'_>,
496 _renderer: &Renderer,
497 operation: &mut dyn core_widget::Operation,
498 ) {
499 if let Some(label) = self.label.as_deref() {
500 operation.text(None, layout.bounds(), label);
501 }
502 }
503}
504
505impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
506 for Element<'a, Message, Theme, Renderer>
507where
508 Message: 'a,
509 Renderer: core_text::Renderer + core_svg::Renderer + 'a,
510{
511 fn from(checkbox: Checkbox<'a, Message, Renderer>) -> Self {
512 Element::new(checkbox)
513 }
514}
515
516pub fn control<'a, Message, Renderer>(is_checked: bool) -> Checkbox<'a, Message, Renderer>
517where
518 Renderer: core_text::Renderer + core_svg::Renderer + 'a,
519{
520 Checkbox::new(is_checked)
521 .size(tokens::component::checkbox::CONTAINER_SIZE)
522 .spacing(f32::from(
523 tokens::component::divider::LIST_ITEM_LEADING_SPACE,
524 ))
525 .text_size(tokens::component::checkbox::LABEL_TEXT_SIZE)
526 .text_line_height(absolute_line_height(
527 tokens::component::checkbox::LABEL_TEXT_LINE_HEIGHT,
528 ))
529 .style(checkbox_style::default)
530}
531
532pub fn standard<'a, Message, Renderer>(
533 is_checked: bool,
534 label: impl text::IntoFragment<'a>,
535 on_toggle: impl Fn(bool) -> Message + 'a,
536) -> Element<'a, Message, Theme, Renderer>
537where
538 Message: 'a,
539 Renderer: iced_widget::core::Renderer + core_text::Renderer + core_svg::Renderer + 'a,
540{
541 Container::new(control(is_checked).label(label).on_toggle(on_toggle))
542 .center_y(Length::Fixed(tokens::component::checkbox::STATE_LAYER_SIZE))
543 .into()
544}