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
47pub struct Toggler<
80 'a,
81 Message,
82 Theme = crate::Theme,
83 Renderer = crate::Renderer,
84> where
85 Theme: Catalog,
86 Renderer: text::Renderer,
87{
88 is_toggled: bool,
89 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
90 label: Option<text::Fragment<'a>>,
91 width: Length,
92 size: f32,
93 text_size: Option<Pixels>,
94 text_line_height: text::LineHeight,
95 text_alignment: text::Alignment,
96 text_shaping: text::Shaping,
97 text_wrapping: text::Wrapping,
98 spacing: f32,
99 font: Option<Renderer::Font>,
100 class: Theme::Class<'a>,
101 last_status: Option<Status>,
102}
103
104impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
105where
106 Theme: Catalog,
107 Renderer: text::Renderer,
108{
109 pub const DEFAULT_SIZE: f32 = 16.0;
111
112 pub fn new(is_toggled: bool) -> Self {
121 Toggler {
122 is_toggled,
123 on_toggle: None,
124 label: None,
125 width: Length::Shrink,
126 size: Self::DEFAULT_SIZE,
127 text_size: None,
128 text_line_height: text::LineHeight::default(),
129 text_alignment: text::Alignment::Default,
130 text_shaping: text::Shaping::default(),
131 text_wrapping: text::Wrapping::default(),
132 spacing: Self::DEFAULT_SIZE / 2.0,
133 font: None,
134 class: Theme::default(),
135 last_status: None,
136 }
137 }
138
139 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
141 self.label = Some(label.into_fragment());
142 self
143 }
144
145 pub fn on_toggle(
150 mut self,
151 on_toggle: impl Fn(bool) -> Message + 'a,
152 ) -> Self {
153 self.on_toggle = Some(Box::new(on_toggle));
154 self
155 }
156
157 pub fn on_toggle_maybe(
162 mut self,
163 on_toggle: Option<impl Fn(bool) -> Message + 'a>,
164 ) -> Self {
165 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
166 self
167 }
168
169 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
171 self.size = size.into().0;
172 self
173 }
174
175 pub fn width(mut self, width: impl Into<Length>) -> Self {
177 self.width = width.into();
178 self
179 }
180
181 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
183 self.text_size = Some(text_size.into());
184 self
185 }
186
187 pub fn text_line_height(
189 mut self,
190 line_height: impl Into<text::LineHeight>,
191 ) -> Self {
192 self.text_line_height = line_height.into();
193 self
194 }
195
196 pub fn text_alignment(
198 mut self,
199 alignment: impl Into<text::Alignment>,
200 ) -> Self {
201 self.text_alignment = alignment.into();
202 self
203 }
204
205 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
207 self.text_shaping = shaping;
208 self
209 }
210
211 pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
213 self.text_wrapping = wrapping;
214 self
215 }
216
217 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
219 self.spacing = spacing.into().0;
220 self
221 }
222
223 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
227 self.font = Some(font.into());
228 self
229 }
230
231 #[must_use]
233 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
234 where
235 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
236 {
237 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
238 self
239 }
240
241 #[cfg(feature = "advanced")]
243 #[must_use]
244 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
245 self.class = class.into();
246 self
247 }
248}
249
250impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
251 for Toggler<'_, Message, Theme, Renderer>
252where
253 Theme: Catalog,
254 Renderer: text::Renderer,
255{
256 fn tag(&self) -> tree::Tag {
257 tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
258 }
259
260 fn state(&self) -> tree::State {
261 tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
262 }
263
264 fn size(&self) -> Size<Length> {
265 Size {
266 width: self.width,
267 height: Length::Shrink,
268 }
269 }
270
271 fn layout(
272 &mut self,
273 tree: &mut Tree,
274 renderer: &Renderer,
275 limits: &layout::Limits,
276 ) -> layout::Node {
277 let limits = limits.width(self.width);
278
279 layout::next_to_each_other(
280 &limits,
281 if self.label.is_some() {
282 self.spacing
283 } else {
284 0.0
285 },
286 |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
287 |limits| {
288 if let Some(label) = self.label.as_deref() {
289 let state = tree
290 .state
291 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
292
293 widget::text::layout(
294 state,
295 renderer,
296 limits,
297 label,
298 widget::text::Format {
299 width: self.width,
300 height: Length::Shrink,
301 line_height: self.text_line_height,
302 size: self.text_size,
303 font: self.font,
304 align_x: self.text_alignment,
305 align_y: alignment::Vertical::Top,
306 shaping: self.text_shaping,
307 wrapping: self.text_wrapping,
308 },
309 )
310 } else {
311 layout::Node::new(Size::ZERO)
312 }
313 },
314 )
315 }
316
317 fn update(
318 &mut self,
319 _state: &mut Tree,
320 event: &Event,
321 layout: Layout<'_>,
322 cursor: mouse::Cursor,
323 _renderer: &Renderer,
324 _clipboard: &mut dyn Clipboard,
325 shell: &mut Shell<'_, Message>,
326 _viewport: &Rectangle,
327 ) {
328 let Some(on_toggle) = &self.on_toggle else {
329 return;
330 };
331
332 match event {
333 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
334 | Event::Touch(touch::Event::FingerPressed { .. }) => {
335 let mouse_over = cursor.is_over(layout.bounds());
336
337 if mouse_over {
338 shell.publish(on_toggle(!self.is_toggled));
339 shell.capture_event();
340 }
341 }
342 _ => {}
343 }
344
345 let current_status = if self.on_toggle.is_none() {
346 Status::Disabled
347 } else if cursor.is_over(layout.bounds()) {
348 Status::Hovered {
349 is_toggled: self.is_toggled,
350 }
351 } else {
352 Status::Active {
353 is_toggled: self.is_toggled,
354 }
355 };
356
357 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
358 self.last_status = Some(current_status);
359 } else if self
360 .last_status
361 .is_some_and(|status| status != current_status)
362 {
363 shell.request_redraw();
364 }
365 }
366
367 fn mouse_interaction(
368 &self,
369 _state: &Tree,
370 layout: Layout<'_>,
371 cursor: mouse::Cursor,
372 _viewport: &Rectangle,
373 _renderer: &Renderer,
374 ) -> mouse::Interaction {
375 if cursor.is_over(layout.bounds()) {
376 if self.on_toggle.is_some() {
377 mouse::Interaction::Pointer
378 } else {
379 mouse::Interaction::NotAllowed
380 }
381 } else {
382 mouse::Interaction::default()
383 }
384 }
385
386 fn draw(
387 &self,
388 tree: &Tree,
389 renderer: &mut Renderer,
390 theme: &Theme,
391 style: &renderer::Style,
392 layout: Layout<'_>,
393 _cursor: mouse::Cursor,
394 viewport: &Rectangle,
395 ) {
396 const SPACE_RATIO: f32 = 0.05;
399
400 let mut children = layout.children();
401 let toggler_layout = children.next().unwrap();
402
403 if self.label.is_some() {
404 let label_layout = children.next().unwrap();
405 let state: &widget::text::State<Renderer::Paragraph> =
406 tree.state.downcast_ref();
407
408 crate::text::draw(
409 renderer,
410 style,
411 label_layout.bounds(),
412 state.raw(),
413 crate::text::Style::default(),
414 viewport,
415 );
416 }
417
418 let bounds = toggler_layout.bounds();
419 let style = theme
420 .style(&self.class, self.last_status.unwrap_or(Status::Disabled));
421
422 let border_radius = bounds.height / 2.0;
423 let space = (SPACE_RATIO * bounds.height).round();
424
425 let toggler_background_bounds = Rectangle {
426 x: bounds.x + space,
427 y: bounds.y + space,
428 width: bounds.width - (2.0 * space),
429 height: bounds.height - (2.0 * space),
430 };
431
432 renderer.fill_quad(
433 renderer::Quad {
434 bounds: toggler_background_bounds,
435 border: Border {
436 radius: border_radius.into(),
437 width: style.background_border_width,
438 color: style.background_border_color,
439 },
440 ..renderer::Quad::default()
441 },
442 style.background,
443 );
444
445 let toggler_foreground_bounds = Rectangle {
446 x: bounds.x
447 + if self.is_toggled {
448 bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
449 } else {
450 2.0 * space
451 },
452 y: bounds.y + (2.0 * space),
453 width: bounds.height - (4.0 * space),
454 height: bounds.height - (4.0 * space),
455 };
456
457 renderer.fill_quad(
458 renderer::Quad {
459 bounds: toggler_foreground_bounds,
460 border: Border {
461 radius: border_radius.into(),
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}
502
503#[derive(Debug, Clone, Copy, PartialEq)]
505pub struct Style {
506 pub background: Color,
508 pub background_border_width: f32,
510 pub background_border_color: Color,
512 pub foreground: Color,
514 pub foreground_border_width: f32,
516 pub foreground_border_color: Color,
518}
519
520pub trait Catalog: Sized {
522 type Class<'a>;
524
525 fn default<'a>() -> Self::Class<'a>;
527
528 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
530}
531
532pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
536
537impl Catalog for Theme {
538 type Class<'a> = StyleFn<'a, Self>;
539
540 fn default<'a>() -> Self::Class<'a> {
541 Box::new(default)
542 }
543
544 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
545 class(self, status)
546 }
547}
548
549pub fn default(theme: &Theme, status: Status) -> Style {
551 let palette = theme.extended_palette();
552
553 let background = match status {
554 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
555 if is_toggled {
556 palette.primary.base.color
557 } else {
558 palette.background.strong.color
559 }
560 }
561 Status::Disabled => palette.background.weak.color,
562 };
563
564 let foreground = match status {
565 Status::Active { is_toggled } => {
566 if is_toggled {
567 palette.primary.base.text
568 } else {
569 palette.background.base.color
570 }
571 }
572 Status::Hovered { is_toggled } => {
573 if is_toggled {
574 Color {
575 a: 0.5,
576 ..palette.primary.base.text
577 }
578 } else {
579 palette.background.weak.color
580 }
581 }
582 Status::Disabled => palette.background.weakest.color,
583 };
584
585 Style {
586 background,
587 foreground,
588 foreground_border_width: 0.0,
589 foreground_border_color: Color::TRANSPARENT,
590 background_border_width: 0.0,
591 background_border_color: Color::TRANSPARENT,
592 }
593}