1use crate::core::alignment;
34use crate::core::border;
35use crate::core::layout;
36use crate::core::mouse;
37use crate::core::renderer;
38use crate::core::text;
39use crate::core::touch;
40use crate::core::widget;
41use crate::core::widget::tree::{self, Tree};
42use crate::core::window;
43use crate::core::{
44 Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell,
45 Size, Theme, Widget,
46};
47
48pub struct Toggler<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
81where
82 Theme: Catalog,
83 Renderer: text::Renderer,
84{
85 is_toggled: bool,
86 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
87 label: Option<text::Fragment<'a>>,
88 width: Length,
89 size: f32,
90 text_size: Option<Pixels>,
91 text_line_height: text::LineHeight,
92 text_alignment: text::Alignment,
93 text_shaping: text::Shaping,
94 text_wrapping: text::Wrapping,
95 spacing: f32,
96 font: Option<Renderer::Font>,
97 class: Theme::Class<'a>,
98 last_status: Option<Status>,
99}
100
101impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
102where
103 Theme: Catalog,
104 Renderer: text::Renderer,
105{
106 pub const DEFAULT_SIZE: f32 = 16.0;
108
109 pub fn new(is_toggled: bool) -> Self {
118 Toggler {
119 is_toggled,
120 on_toggle: None,
121 label: None,
122 width: Length::Shrink,
123 size: Self::DEFAULT_SIZE,
124 text_size: None,
125 text_line_height: text::LineHeight::default(),
126 text_alignment: text::Alignment::Default,
127 text_shaping: text::Shaping::default(),
128 text_wrapping: text::Wrapping::default(),
129 spacing: Self::DEFAULT_SIZE / 2.0,
130 font: None,
131 class: Theme::default(),
132 last_status: None,
133 }
134 }
135
136 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
138 self.label = Some(label.into_fragment());
139 self
140 }
141
142 pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
147 self.on_toggle = Some(Box::new(on_toggle));
148 self
149 }
150
151 pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
156 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
157 self
158 }
159
160 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
162 self.size = size.into().0;
163 self
164 }
165
166 pub fn width(mut self, width: impl Into<Length>) -> Self {
168 self.width = width.into();
169 self
170 }
171
172 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
174 self.text_size = Some(text_size.into());
175 self
176 }
177
178 pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
180 self.text_line_height = line_height.into();
181 self
182 }
183
184 pub fn text_alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
186 self.text_alignment = alignment.into();
187 self
188 }
189
190 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
192 self.text_shaping = shaping;
193 self
194 }
195
196 pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
198 self.text_wrapping = wrapping;
199 self
200 }
201
202 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
204 self.spacing = spacing.into().0;
205 self
206 }
207
208 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
212 self.font = Some(font.into());
213 self
214 }
215
216 #[must_use]
218 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
219 where
220 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
221 {
222 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
223 self
224 }
225
226 #[cfg(feature = "advanced")]
228 #[must_use]
229 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
230 self.class = class.into();
231 self
232 }
233}
234
235impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
236 for Toggler<'_, Message, Theme, Renderer>
237where
238 Theme: Catalog,
239 Renderer: text::Renderer,
240{
241 fn tag(&self) -> tree::Tag {
242 tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
243 }
244
245 fn state(&self) -> tree::State {
246 tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
247 }
248
249 fn size(&self) -> Size<Length> {
250 Size {
251 width: self.width,
252 height: Length::Shrink,
253 }
254 }
255
256 fn layout(
257 &mut self,
258 tree: &mut Tree,
259 renderer: &Renderer,
260 limits: &layout::Limits,
261 ) -> layout::Node {
262 let limits = limits.width(self.width);
263
264 layout::next_to_each_other(
265 &limits,
266 if self.label.is_some() {
267 self.spacing
268 } else {
269 0.0
270 },
271 |_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
272 |limits| {
273 if let Some(label) = self.label.as_deref() {
274 let state = tree
275 .state
276 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
277
278 widget::text::layout(
279 state,
280 renderer,
281 limits,
282 label,
283 widget::text::Format {
284 width: self.width,
285 height: Length::Shrink,
286 line_height: self.text_line_height,
287 size: self.text_size,
288 font: self.font,
289 align_x: self.text_alignment,
290 align_y: alignment::Vertical::Top,
291 shaping: self.text_shaping,
292 wrapping: self.text_wrapping,
293 },
294 )
295 } else {
296 layout::Node::new(Size::ZERO)
297 }
298 },
299 )
300 }
301
302 fn update(
303 &mut self,
304 _tree: &mut Tree,
305 event: &Event,
306 layout: Layout<'_>,
307 cursor: mouse::Cursor,
308 _renderer: &Renderer,
309 _clipboard: &mut dyn Clipboard,
310 shell: &mut Shell<'_, Message>,
311 _viewport: &Rectangle,
312 ) {
313 let Some(on_toggle) = &self.on_toggle else {
314 return;
315 };
316
317 match event {
318 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
319 | Event::Touch(touch::Event::FingerPressed { .. }) => {
320 let mouse_over = cursor.is_over(layout.bounds());
321
322 if mouse_over {
323 shell.publish(on_toggle(!self.is_toggled));
324 shell.capture_event();
325 }
326 }
327 _ => {}
328 }
329
330 let current_status = if self.on_toggle.is_none() {
331 Status::Disabled {
332 is_toggled: self.is_toggled,
333 }
334 } else if cursor.is_over(layout.bounds()) {
335 Status::Hovered {
336 is_toggled: self.is_toggled,
337 }
338 } else {
339 Status::Active {
340 is_toggled: self.is_toggled,
341 }
342 };
343
344 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
345 self.last_status = Some(current_status);
346 } else if self
347 .last_status
348 .is_some_and(|status| status != current_status)
349 {
350 shell.request_redraw();
351 }
352 }
353
354 fn mouse_interaction(
355 &self,
356 _tree: &Tree,
357 layout: Layout<'_>,
358 cursor: mouse::Cursor,
359 _viewport: &Rectangle,
360 _renderer: &Renderer,
361 ) -> mouse::Interaction {
362 if cursor.is_over(layout.bounds()) {
363 if self.on_toggle.is_some() {
364 mouse::Interaction::Pointer
365 } else {
366 mouse::Interaction::NotAllowed
367 }
368 } else {
369 mouse::Interaction::default()
370 }
371 }
372
373 fn draw(
374 &self,
375 tree: &Tree,
376 renderer: &mut Renderer,
377 theme: &Theme,
378 defaults: &renderer::Style,
379 layout: Layout<'_>,
380 _cursor: mouse::Cursor,
381 viewport: &Rectangle,
382 ) {
383 let mut children = layout.children();
384 let toggler_layout = children.next().unwrap();
385
386 let style = theme.style(
387 &self.class,
388 self.last_status.unwrap_or(Status::Disabled {
389 is_toggled: self.is_toggled,
390 }),
391 );
392
393 if self.label.is_some() {
394 let label_layout = children.next().unwrap();
395 let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
396
397 crate::text::draw(
398 renderer,
399 defaults,
400 label_layout.bounds(),
401 state.raw(),
402 crate::text::Style {
403 color: style.text_color,
404 },
405 viewport,
406 );
407 }
408
409 let bounds = toggler_layout.bounds();
410 let border_radius = style
411 .border_radius
412 .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
413
414 renderer.fill_quad(
415 renderer::Quad {
416 bounds,
417 border: Border {
418 radius: border_radius,
419 width: style.background_border_width,
420 color: style.background_border_color,
421 },
422 ..renderer::Quad::default()
423 },
424 style.background,
425 );
426
427 let padding = (style.padding_ratio * bounds.height).round();
428 let toggler_foreground_bounds = Rectangle {
429 x: bounds.x
430 + if self.is_toggled {
431 bounds.width - bounds.height + padding
432 } else {
433 padding
434 },
435 y: bounds.y + padding,
436 width: bounds.height - (2.0 * padding),
437 height: bounds.height - (2.0 * padding),
438 };
439
440 renderer.fill_quad(
441 renderer::Quad {
442 bounds: toggler_foreground_bounds,
443 border: Border {
444 radius: border_radius,
445 width: style.foreground_border_width,
446 color: style.foreground_border_color,
447 },
448 ..renderer::Quad::default()
449 },
450 style.foreground,
451 );
452 }
453}
454
455impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
456 for Element<'a, Message, Theme, Renderer>
457where
458 Message: 'a,
459 Theme: Catalog + 'a,
460 Renderer: text::Renderer + 'a,
461{
462 fn from(
463 toggler: Toggler<'a, Message, Theme, Renderer>,
464 ) -> Element<'a, Message, Theme, Renderer> {
465 Element::new(toggler)
466 }
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub enum Status {
472 Active {
474 is_toggled: bool,
476 },
477 Hovered {
479 is_toggled: bool,
481 },
482 Disabled {
484 is_toggled: bool,
486 },
487}
488
489#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct Style {
492 pub background: Background,
494 pub background_border_width: f32,
496 pub background_border_color: Color,
498 pub foreground: Background,
500 pub foreground_border_width: f32,
502 pub foreground_border_color: Color,
504 pub text_color: Option<Color>,
506 pub border_radius: Option<border::Radius>,
510 pub padding_ratio: f32,
512}
513
514pub trait Catalog: Sized {
516 type Class<'a>;
518
519 fn default<'a>() -> Self::Class<'a>;
521
522 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
524}
525
526pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
530
531impl Catalog for Theme {
532 type Class<'a> = StyleFn<'a, Self>;
533
534 fn default<'a>() -> Self::Class<'a> {
535 Box::new(default)
536 }
537
538 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
539 class(self, status)
540 }
541}
542
543pub fn default(theme: &Theme, status: Status) -> Style {
545 let palette = theme.extended_palette();
546
547 let background = match status {
548 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
549 if is_toggled {
550 palette.primary.base.color
551 } else {
552 palette.background.strong.color
553 }
554 }
555 Status::Disabled { is_toggled } => {
556 if is_toggled {
557 palette.background.strong.color
558 } else {
559 palette.background.weak.color
560 }
561 }
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: background.into(),
587 foreground: foreground.into(),
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 text_color: None,
593 border_radius: None,
594 padding_ratio: 0.1,
595 }
596}