1use crate::core::alignment;
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 Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
44 Rectangle, Shell, Size, Theme, Widget,
45};
46
47#[allow(missing_debug_implementations)]
80pub 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 &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 _state: &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 } else if cursor.is_over(layout.bounds()) {
349 Status::Hovered {
350 is_toggled: self.is_toggled,
351 }
352 } else {
353 Status::Active {
354 is_toggled: self.is_toggled,
355 }
356 };
357
358 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
359 self.last_status = Some(current_status);
360 } else if self
361 .last_status
362 .is_some_and(|status| status != current_status)
363 {
364 shell.request_redraw();
365 }
366 }
367
368 fn mouse_interaction(
369 &self,
370 _state: &Tree,
371 layout: Layout<'_>,
372 cursor: mouse::Cursor,
373 _viewport: &Rectangle,
374 _renderer: &Renderer,
375 ) -> mouse::Interaction {
376 if cursor.is_over(layout.bounds()) {
377 if self.on_toggle.is_some() {
378 mouse::Interaction::Pointer
379 } else {
380 mouse::Interaction::NotAllowed
381 }
382 } else {
383 mouse::Interaction::default()
384 }
385 }
386
387 fn draw(
388 &self,
389 tree: &Tree,
390 renderer: &mut Renderer,
391 theme: &Theme,
392 style: &renderer::Style,
393 layout: Layout<'_>,
394 _cursor: mouse::Cursor,
395 viewport: &Rectangle,
396 ) {
397 const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
399
400 const SPACE_RATIO: f32 = 0.05;
403
404 let mut children = layout.children();
405 let toggler_layout = children.next().unwrap();
406
407 if self.label.is_some() {
408 let label_layout = children.next().unwrap();
409 let state: &widget::text::State<Renderer::Paragraph> =
410 tree.state.downcast_ref();
411
412 crate::text::draw(
413 renderer,
414 style,
415 label_layout.bounds(),
416 state.raw(),
417 crate::text::Style::default(),
418 viewport,
419 );
420 }
421
422 let bounds = toggler_layout.bounds();
423 let style = theme
424 .style(&self.class, self.last_status.unwrap_or(Status::Disabled));
425
426 let border_radius = bounds.height / BORDER_RADIUS_RATIO;
427 let space = (SPACE_RATIO * bounds.height).round();
428
429 let toggler_background_bounds = Rectangle {
430 x: bounds.x + space,
431 y: bounds.y + space,
432 width: bounds.width - (2.0 * space),
433 height: bounds.height - (2.0 * space),
434 };
435
436 renderer.fill_quad(
437 renderer::Quad {
438 bounds: toggler_background_bounds,
439 border: Border {
440 radius: border_radius.into(),
441 width: style.background_border_width,
442 color: style.background_border_color,
443 },
444 ..renderer::Quad::default()
445 },
446 style.background,
447 );
448
449 let toggler_foreground_bounds = Rectangle {
450 x: bounds.x
451 + if self.is_toggled {
452 bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
453 } else {
454 2.0 * space
455 },
456 y: bounds.y + (2.0 * space),
457 width: bounds.height - (4.0 * space),
458 height: bounds.height - (4.0 * space),
459 };
460
461 renderer.fill_quad(
462 renderer::Quad {
463 bounds: toggler_foreground_bounds,
464 border: Border {
465 radius: border_radius.into(),
466 width: style.foreground_border_width,
467 color: style.foreground_border_color,
468 },
469 ..renderer::Quad::default()
470 },
471 style.foreground,
472 );
473 }
474}
475
476impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
477 for Element<'a, Message, Theme, Renderer>
478where
479 Message: 'a,
480 Theme: Catalog + 'a,
481 Renderer: text::Renderer + 'a,
482{
483 fn from(
484 toggler: Toggler<'a, Message, Theme, Renderer>,
485 ) -> Element<'a, Message, Theme, Renderer> {
486 Element::new(toggler)
487 }
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub enum Status {
493 Active {
495 is_toggled: bool,
497 },
498 Hovered {
500 is_toggled: bool,
502 },
503 Disabled,
505}
506
507#[derive(Debug, Clone, Copy, PartialEq)]
509pub struct Style {
510 pub background: Color,
512 pub background_border_width: f32,
514 pub background_border_color: Color,
516 pub foreground: Color,
518 pub foreground_border_width: f32,
520 pub foreground_border_color: Color,
522}
523
524pub trait Catalog: Sized {
526 type Class<'a>;
528
529 fn default<'a>() -> Self::Class<'a>;
531
532 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
534}
535
536pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
540
541impl Catalog for Theme {
542 type Class<'a> = StyleFn<'a, Self>;
543
544 fn default<'a>() -> Self::Class<'a> {
545 Box::new(default)
546 }
547
548 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
549 class(self, status)
550 }
551}
552
553pub fn default(theme: &Theme, status: Status) -> Style {
555 let palette = theme.extended_palette();
556
557 let background = match status {
558 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
559 if is_toggled {
560 palette.primary.strong.color
561 } else {
562 palette.background.strong.color
563 }
564 }
565 Status::Disabled => palette.background.weak.color,
566 };
567
568 let foreground = match status {
569 Status::Active { is_toggled } => {
570 if is_toggled {
571 palette.primary.strong.text
572 } else {
573 palette.background.base.color
574 }
575 }
576 Status::Hovered { is_toggled } => {
577 if is_toggled {
578 Color {
579 a: 0.5,
580 ..palette.primary.strong.text
581 }
582 } else {
583 palette.background.weak.color
584 }
585 }
586 Status::Disabled => palette.background.base.color,
587 };
588
589 Style {
590 background,
591 foreground,
592 foreground_border_width: 0.0,
593 foreground_border_color: Color::TRANSPARENT,
594 background_border_width: 0.0,
595 background_border_color: Color::TRANSPARENT,
596 }
597}