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