1use crate::core::alignment;
33use crate::core::border;
34use crate::core::layout;
35use crate::core::mouse;
36use crate::core::renderer;
37use crate::core::text;
38use crate::core::touch;
39use crate::core::widget;
40use crate::core::widget::tree::{self, Tree};
41use crate::core::window;
42use crate::core::{
43 Background, Border, Color, Event, Font, Layout, Length, Pixels, Rectangle, Shell, Size, Theme,
44 Widget,
45};
46
47pub struct Toggler<'a, Message, Theme = crate::Theme>
79where
80 Theme: Catalog,
81{
82 is_toggled: bool,
83 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
84 label: Option<text::Fragment<'a>>,
85 width: Length,
86 size: f32,
87 text_size: Option<Pixels>,
88 line_height: Option<text::LineHeight>,
89 alignment: text::Alignment,
90 text_shaping: text::Shaping,
91 wrapping: text::Wrapping,
92 spacing: f32,
93 font: Option<Font>,
94 class: Theme::Class<'a>,
95 last_status: Option<Status>,
96}
97
98impl<'a, Message, Theme> Toggler<'a, Message, Theme>
99where
100 Theme: Catalog,
101{
102 pub const DEFAULT_SIZE: f32 = 16.0;
104
105 pub fn new(is_toggled: bool) -> Self {
114 Toggler {
115 is_toggled,
116 on_toggle: None,
117 label: None,
118 width: Length::Fit,
119 size: Self::DEFAULT_SIZE,
120 text_size: None,
121 line_height: None,
122 alignment: text::Alignment::Default,
123 text_shaping: text::Shaping::default(),
124 wrapping: text::Wrapping::default(),
125 spacing: Self::DEFAULT_SIZE / 2.0,
126 font: None,
127 class: Theme::default(),
128 last_status: None,
129 }
130 }
131
132 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
134 self.label = Some(label.into_fragment());
135 self
136 }
137
138 pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
143 self.on_toggle = Some(Box::new(on_toggle));
144 self
145 }
146
147 pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
152 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
153 self
154 }
155
156 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
158 self.size = size.into().0;
159 self
160 }
161
162 pub fn width(mut self, width: impl Into<Length>) -> Self {
164 self.width = width.into();
165 self
166 }
167
168 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
170 self.text_size = Some(text_size.into());
171 self
172 }
173
174 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
176 self.line_height = Some(line_height.into());
177 self
178 }
179
180 pub fn alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
182 self.alignment = alignment.into();
183 self
184 }
185
186 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
188 self.text_shaping = shaping;
189 self
190 }
191
192 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
194 self.wrapping = wrapping;
195 self
196 }
197
198 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
200 self.spacing = spacing.into().0;
201 self
202 }
203
204 pub fn font(mut self, font: impl Into<Font>) -> Self {
208 self.font = Some(font.into());
209 self
210 }
211
212 #[must_use]
214 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
215 where
216 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
217 {
218 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
219 self
220 }
221
222 #[cfg(feature = "advanced")]
224 #[must_use]
225 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
226 self.class = class.into();
227 self
228 }
229}
230
231impl<Message, Theme> widget::Meta for Toggler<'_, Message, Theme> where Theme: Catalog {}
232
233impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Toggler<'_, Message, Theme>
234where
235 Theme: Catalog,
236 Renderer: text::Renderer,
237{
238 fn tag(&self) -> tree::Tag {
239 tree::Tag::of::<text::paragraph::Plain<Renderer::Paragraph>>()
240 }
241
242 fn state(&self) -> tree::State {
243 tree::State::new(text::paragraph::Plain::<Renderer::Paragraph>::default())
244 }
245
246 fn size(&self) -> Size<Length> {
247 Size {
248 width: self.width,
249 height: Length::Fit,
250 }
251 }
252
253 fn diff(&mut self, tree: &mut Tree) {
254 tree.children.resize_with(2, Tree::empty);
257 }
258
259 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
260 let limits = limits.width(self.width);
261
262 let size = if renderer::CRISP {
263 let scale_factor = renderer.hint_factor().unwrap_or(1.0);
264
265 (self.size * scale_factor).round() / scale_factor
266 } else {
267 self.size
268 };
269
270 let track = Size::new(2.0 * size, size);
271
272 let label = if let Some(label) = self.label.as_deref() {
273 let state = tree
274 .state
275 .downcast_mut::<text::paragraph::Plain<Renderer::Paragraph>>();
276
277 widget::text::layout(
278 state,
279 renderer,
280 &limits.shrink(Size::new(track.width + self.spacing, 0.0)),
281 label,
282 widget::text::Format {
283 width: self.width,
284 height: Length::Fit,
285 line_height: self.line_height,
286 size: self.text_size,
287 font: self.font,
288 align_x: self.alignment,
289 align_y: alignment::Vertical::Top,
290 shaping: self.text_shaping,
291 wrapping: self.wrapping,
292 ellipsis: text::Ellipsis::None,
293 },
294 )
295 } else {
296 Size::ZERO
297 };
298
299 let spacing = if self.label.is_some() {
300 self.spacing
301 } else {
302 0.0
303 };
304
305 layout::next_to_each_other(tree, track, label, spacing);
306 }
307
308 fn update(
309 &mut self,
310 _tree: &mut Tree,
311 event: &Event,
312 layout: Layout,
313 cursor: mouse::Cursor,
314 _renderer: &Renderer,
315 shell: &mut Shell<'_, Message>,
316 _viewport: &Rectangle,
317 ) {
318 let Some(on_toggle) = &self.on_toggle else {
319 return;
320 };
321
322 match event {
323 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
324 | Event::Touch(touch::Event::FingerPressed { .. }) => {
325 let mouse_over = cursor.is_over(layout.bounds());
326
327 if mouse_over {
328 shell.publish(on_toggle(!self.is_toggled));
329 shell.capture_event();
330 }
331 }
332 _ => {}
333 }
334
335 let current_status = if self.on_toggle.is_none() {
336 Status::Disabled {
337 is_toggled: self.is_toggled,
338 }
339 } else if cursor.is_over(layout.bounds()) {
340 Status::Hovered {
341 is_toggled: self.is_toggled,
342 }
343 } else {
344 Status::Active {
345 is_toggled: self.is_toggled,
346 }
347 };
348
349 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
350 self.last_status = Some(current_status);
351 } else if self
352 .last_status
353 .is_some_and(|status| status != current_status)
354 {
355 shell.request_redraw();
356 }
357 }
358
359 fn mouse_interaction(
360 &self,
361 _tree: &Tree,
362 layout: Layout,
363 cursor: mouse::Cursor,
364 _viewport: &Rectangle,
365 _renderer: &Renderer,
366 ) -> mouse::Interaction {
367 if cursor.is_over(layout.bounds()) {
368 if self.on_toggle.is_some() {
369 mouse::Interaction::Pointer
370 } else {
371 mouse::Interaction::NotAllowed
372 }
373 } else {
374 mouse::Interaction::default()
375 }
376 }
377
378 fn draw(
379 &self,
380 tree: &Tree,
381 renderer: &mut Renderer,
382 theme: &Theme,
383 defaults: &renderer::Style,
384 layout: Layout,
385 _cursor: mouse::Cursor,
386 viewport: &Rectangle,
387 ) {
388 let style = theme.style(
389 &self.class,
390 self.last_status.unwrap_or(Status::Disabled {
391 is_toggled: self.is_toggled,
392 }),
393 );
394
395 let mut children = layout.iter(&tree.children);
396 let (track_layout, _) = children.next().unwrap();
397
398 if self.label.is_some() {
399 let (label_layout, _) = children.next().unwrap();
400
401 let state = tree
402 .state
403 .downcast_ref::<text::paragraph::Plain<Renderer::Paragraph>>();
404
405 crate::text::draw(
406 renderer,
407 defaults,
408 label_layout.bounds(),
409 state.raw(),
410 crate::text::Style {
411 color: style.text_color,
412 selection: None,
413 },
414 theme.selection(),
415 viewport,
416 );
417 }
418
419 let bounds = track_layout.bounds();
420
421 let border_radius = style
422 .border_radius
423 .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
424
425 renderer.fill_quad(
426 renderer::Quad {
427 bounds,
428 border: Border {
429 radius: border_radius,
430 width: style.background_border_width,
431 color: style.background_border_color,
432 },
433 ..renderer::Quad::default()
434 },
435 style.background,
436 );
437
438 let toggle_bounds = {
439 let bounds = if renderer::CRISP {
441 let scale_factor = renderer.hint_factor().unwrap_or(1.0);
442 bounds.hint(scale_factor)
443 } else {
444 bounds
445 };
446
447 let padding = (style.padding_ratio * bounds.height).round();
448
449 Rectangle {
450 x: bounds.x
451 + if self.is_toggled {
452 bounds.width - bounds.height + padding
453 } else {
454 padding
455 },
456 y: bounds.y + padding,
457 width: bounds.height - (2.0 * padding),
458 height: bounds.height - (2.0 * padding),
459 }
460 };
461
462 renderer.fill_quad(
463 renderer::Quad {
464 bounds: toggle_bounds,
465 border: Border {
466 radius: border_radius,
467 width: style.foreground_border_width,
468 color: style.foreground_border_color,
469 },
470 ..renderer::Quad::default()
471 },
472 style.foreground,
473 );
474 }
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub enum Status {
480 Active {
482 is_toggled: bool,
484 },
485 Hovered {
487 is_toggled: bool,
489 },
490 Disabled {
492 is_toggled: bool,
494 },
495}
496
497#[derive(Debug, Clone, Copy, PartialEq)]
499pub struct Style {
500 pub background: Background,
502 pub background_border_width: f32,
504 pub background_border_color: Color,
506 pub foreground: Background,
508 pub foreground_border_width: f32,
510 pub foreground_border_color: Color,
512 pub text_color: Option<Color>,
514 pub border_radius: Option<border::Radius>,
518 pub padding_ratio: f32,
520}
521
522pub trait Catalog: Sized {
524 type Class<'a>;
526
527 fn default<'a>() -> Self::Class<'a>;
529
530 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
532
533 fn selection(&self) -> Color;
535}
536
537pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
541
542impl Catalog for Theme {
543 type Class<'a> = StyleFn<'a, Self>;
544
545 fn default<'a>() -> Self::Class<'a> {
546 Box::new(default)
547 }
548
549 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
550 class(self, status)
551 }
552
553 fn selection(&self) -> Color {
554 self.palette().background.strongest.color
555 }
556}
557
558pub fn default(theme: &Theme, status: Status) -> Style {
560 let palette = theme.palette();
561
562 let background = match status {
563 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
564 if is_toggled {
565 palette.primary.base.color
566 } else {
567 palette.background.strong.color
568 }
569 }
570 Status::Disabled { is_toggled } => {
571 if is_toggled {
572 palette.background.strong.color
573 } else {
574 palette.background.weak.color
575 }
576 }
577 };
578
579 let foreground = match status {
580 Status::Active { is_toggled } => {
581 if is_toggled {
582 palette.primary.base.text
583 } else {
584 palette.background.base.color
585 }
586 }
587 Status::Hovered { is_toggled } => {
588 if is_toggled {
589 Color {
590 a: 0.5,
591 ..palette.primary.base.text
592 }
593 } else {
594 palette.background.weak.color
595 }
596 }
597 Status::Disabled { .. } => palette.background.weakest.color,
598 };
599
600 Style {
601 background: background.into(),
602 foreground: foreground.into(),
603 foreground_border_width: 0.0,
604 foreground_border_color: Color::TRANSPARENT,
605 background_border_width: 0.0,
606 background_border_color: Color::TRANSPARENT,
607 text_color: None,
608 border_radius: None,
609 padding_ratio: 0.1,
610 }
611}