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