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 defaults: &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 let style = theme
404 .style(&self.class, self.last_status.unwrap_or(Status::Disabled));
405
406 if self.label.is_some() {
407 let label_layout = children.next().unwrap();
408 let state: &widget::text::State<Renderer::Paragraph> =
409 tree.state.downcast_ref();
410
411 crate::text::draw(
412 renderer,
413 defaults,
414 label_layout.bounds(),
415 state.raw(),
416 crate::text::Style {
417 color: style.text_color,
418 },
419 viewport,
420 );
421 }
422
423 let bounds = toggler_layout.bounds();
424
425 let border_radius = bounds.height / 2.0;
426 let space = (SPACE_RATIO * bounds.height).round();
427
428 let toggler_background_bounds = Rectangle {
429 x: bounds.x + space,
430 y: bounds.y + space,
431 width: bounds.width - (2.0 * space),
432 height: bounds.height - (2.0 * space),
433 };
434
435 renderer.fill_quad(
436 renderer::Quad {
437 bounds: toggler_background_bounds,
438 border: Border {
439 radius: border_radius.into(),
440 width: style.background_border_width,
441 color: style.background_border_color,
442 },
443 ..renderer::Quad::default()
444 },
445 style.background,
446 );
447
448 let toggler_foreground_bounds = Rectangle {
449 x: bounds.x
450 + if self.is_toggled {
451 bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
452 } else {
453 2.0 * space
454 },
455 y: bounds.y + (2.0 * space),
456 width: bounds.height - (4.0 * space),
457 height: bounds.height - (4.0 * space),
458 };
459
460 renderer.fill_quad(
461 renderer::Quad {
462 bounds: toggler_foreground_bounds,
463 border: Border {
464 radius: border_radius.into(),
465 width: style.foreground_border_width,
466 color: style.foreground_border_color,
467 },
468 ..renderer::Quad::default()
469 },
470 style.foreground,
471 );
472 }
473}
474
475impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
476 for Element<'a, Message, Theme, Renderer>
477where
478 Message: 'a,
479 Theme: Catalog + 'a,
480 Renderer: text::Renderer + 'a,
481{
482 fn from(
483 toggler: Toggler<'a, Message, Theme, Renderer>,
484 ) -> Element<'a, Message, Theme, Renderer> {
485 Element::new(toggler)
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq)]
491pub enum Status {
492 Active {
494 is_toggled: bool,
496 },
497 Hovered {
499 is_toggled: bool,
501 },
502 Disabled,
504}
505
506#[derive(Debug, Clone, Copy, PartialEq)]
508pub struct Style {
509 pub background: Color,
511 pub background_border_width: f32,
513 pub background_border_color: Color,
515 pub foreground: Color,
517 pub foreground_border_width: f32,
519 pub foreground_border_color: Color,
521 pub text_color: Option<Color>,
523}
524
525pub trait Catalog: Sized {
527 type Class<'a>;
529
530 fn default<'a>() -> Self::Class<'a>;
532
533 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
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
554pub fn default(theme: &Theme, status: Status) -> Style {
556 let palette = theme.extended_palette();
557
558 let background = match status {
559 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
560 if is_toggled {
561 palette.primary.base.color
562 } else {
563 palette.background.strong.color
564 }
565 }
566 Status::Disabled => palette.background.weak.color,
567 };
568
569 let foreground = match status {
570 Status::Active { is_toggled } => {
571 if is_toggled {
572 palette.primary.base.text
573 } else {
574 palette.background.base.color
575 }
576 }
577 Status::Hovered { is_toggled } => {
578 if is_toggled {
579 Color {
580 a: 0.5,
581 ..palette.primary.base.text
582 }
583 } else {
584 palette.background.weak.color
585 }
586 }
587 Status::Disabled => palette.background.weakest.color,
588 };
589
590 Style {
591 background,
592 foreground,
593 foreground_border_width: 0.0,
594 foreground_border_color: Color::TRANSPARENT,
595 background_border_width: 0.0,
596 background_border_color: Color::TRANSPARENT,
597 text_color: None,
598 }
599}