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 self.spacing,
283 |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
284 |limits| {
285 if let Some(label) = self.label.as_deref() {
286 let state = tree
287 .state
288 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
289
290 widget::text::layout(
291 state,
292 renderer,
293 limits,
294 self.width,
295 Length::Shrink,
296 label,
297 self.text_line_height,
298 self.text_size,
299 self.font,
300 self.text_alignment,
301 alignment::Vertical::Top,
302 self.text_shaping,
303 self.text_wrapping,
304 )
305 } else {
306 layout::Node::new(Size::ZERO)
307 }
308 },
309 )
310 }
311
312 fn update(
313 &mut self,
314 _state: &mut Tree,
315 event: &Event,
316 layout: Layout<'_>,
317 cursor: mouse::Cursor,
318 _renderer: &Renderer,
319 _clipboard: &mut dyn Clipboard,
320 shell: &mut Shell<'_, Message>,
321 _viewport: &Rectangle,
322 ) {
323 let Some(on_toggle) = &self.on_toggle else {
324 return;
325 };
326
327 match event {
328 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
329 | Event::Touch(touch::Event::FingerPressed { .. }) => {
330 let mouse_over = cursor.is_over(layout.bounds());
331
332 if mouse_over {
333 shell.publish(on_toggle(!self.is_toggled));
334 shell.capture_event();
335 }
336 }
337 _ => {}
338 }
339
340 let current_status = if self.on_toggle.is_none() {
341 Status::Disabled
342 } else if cursor.is_over(layout.bounds()) {
343 Status::Hovered {
344 is_toggled: self.is_toggled,
345 }
346 } else {
347 Status::Active {
348 is_toggled: self.is_toggled,
349 }
350 };
351
352 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
353 self.last_status = Some(current_status);
354 } else if self
355 .last_status
356 .is_some_and(|status| status != current_status)
357 {
358 shell.request_redraw();
359 }
360 }
361
362 fn mouse_interaction(
363 &self,
364 _state: &Tree,
365 layout: Layout<'_>,
366 cursor: mouse::Cursor,
367 _viewport: &Rectangle,
368 _renderer: &Renderer,
369 ) -> mouse::Interaction {
370 if cursor.is_over(layout.bounds()) {
371 if self.on_toggle.is_some() {
372 mouse::Interaction::Pointer
373 } else {
374 mouse::Interaction::NotAllowed
375 }
376 } else {
377 mouse::Interaction::default()
378 }
379 }
380
381 fn draw(
382 &self,
383 tree: &Tree,
384 renderer: &mut Renderer,
385 theme: &Theme,
386 style: &renderer::Style,
387 layout: Layout<'_>,
388 _cursor: mouse::Cursor,
389 viewport: &Rectangle,
390 ) {
391 const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
393
394 const SPACE_RATIO: f32 = 0.05;
397
398 let mut children = layout.children();
399 let toggler_layout = children.next().unwrap();
400
401 if self.label.is_some() {
402 let label_layout = children.next().unwrap();
403 let state: &widget::text::State<Renderer::Paragraph> =
404 tree.state.downcast_ref();
405
406 crate::text::draw(
407 renderer,
408 style,
409 label_layout,
410 state.0.raw(),
411 crate::text::Style::default(),
412 viewport,
413 );
414 }
415
416 let bounds = toggler_layout.bounds();
417 let style = theme
418 .style(&self.class, self.last_status.unwrap_or(Status::Disabled));
419
420 let border_radius = bounds.height / BORDER_RADIUS_RATIO;
421 let space = SPACE_RATIO * bounds.height;
422
423 let toggler_background_bounds = Rectangle {
424 x: bounds.x + space,
425 y: bounds.y + space,
426 width: bounds.width - (2.0 * space),
427 height: bounds.height - (2.0 * space),
428 };
429
430 renderer.fill_quad(
431 renderer::Quad {
432 bounds: toggler_background_bounds,
433 border: Border {
434 radius: border_radius.into(),
435 width: style.background_border_width,
436 color: style.background_border_color,
437 },
438 ..renderer::Quad::default()
439 },
440 style.background,
441 );
442
443 let toggler_foreground_bounds = Rectangle {
444 x: bounds.x
445 + if self.is_toggled {
446 bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
447 } else {
448 2.0 * space
449 },
450 y: bounds.y + (2.0 * space),
451 width: bounds.height - (4.0 * space),
452 height: bounds.height - (4.0 * space),
453 };
454
455 renderer.fill_quad(
456 renderer::Quad {
457 bounds: toggler_foreground_bounds,
458 border: Border {
459 radius: border_radius.into(),
460 width: style.foreground_border_width,
461 color: style.foreground_border_color,
462 },
463 ..renderer::Quad::default()
464 },
465 style.foreground,
466 );
467 }
468}
469
470impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
471 for Element<'a, Message, Theme, Renderer>
472where
473 Message: 'a,
474 Theme: Catalog + 'a,
475 Renderer: text::Renderer + 'a,
476{
477 fn from(
478 toggler: Toggler<'a, Message, Theme, Renderer>,
479 ) -> Element<'a, Message, Theme, Renderer> {
480 Element::new(toggler)
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub enum Status {
487 Active {
489 is_toggled: bool,
491 },
492 Hovered {
494 is_toggled: bool,
496 },
497 Disabled,
499}
500
501#[derive(Debug, Clone, Copy, PartialEq)]
503pub struct Style {
504 pub background: Color,
506 pub background_border_width: f32,
508 pub background_border_color: Color,
510 pub foreground: Color,
512 pub foreground_border_width: f32,
514 pub foreground_border_color: Color,
516}
517
518pub trait Catalog: Sized {
520 type Class<'a>;
522
523 fn default<'a>() -> Self::Class<'a>;
525
526 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
528}
529
530pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
534
535impl Catalog for Theme {
536 type Class<'a> = StyleFn<'a, Self>;
537
538 fn default<'a>() -> Self::Class<'a> {
539 Box::new(default)
540 }
541
542 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
543 class(self, status)
544 }
545}
546
547pub fn default(theme: &Theme, status: Status) -> Style {
549 let palette = theme.extended_palette();
550
551 let background = match status {
552 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
553 if is_toggled {
554 palette.primary.strong.color
555 } else {
556 palette.background.strong.color
557 }
558 }
559 Status::Disabled => palette.background.weak.color,
560 };
561
562 let foreground = match status {
563 Status::Active { is_toggled } => {
564 if is_toggled {
565 palette.primary.strong.text
566 } else {
567 palette.background.base.color
568 }
569 }
570 Status::Hovered { is_toggled } => {
571 if is_toggled {
572 Color {
573 a: 0.5,
574 ..palette.primary.strong.text
575 }
576 } else {
577 palette.background.weak.color
578 }
579 }
580 Status::Disabled => palette.background.base.color,
581 };
582
583 Style {
584 background,
585 foreground,
586 foreground_border_width: 0.0,
587 foreground_border_color: Color::TRANSPARENT,
588 background_border_width: 0.0,
589 background_border_color: Color::TRANSPARENT,
590 }
591}