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 |_| {
272 let size = if renderer::CRISP {
273 let scale_factor = renderer.scale_factor().unwrap_or(1.0);
274
275 (self.size * scale_factor).round() / scale_factor
276 } else {
277 self.size
278 };
279
280 layout::Node::new(Size::new(2.0 * size, size))
281 },
282 |limits| {
283 if let Some(label) = self.label.as_deref() {
284 let state = tree
285 .state
286 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
287
288 widget::text::layout(
289 state,
290 renderer,
291 limits,
292 label,
293 widget::text::Format {
294 width: self.width,
295 height: Length::Shrink,
296 line_height: self.text_line_height,
297 size: self.text_size,
298 font: self.font,
299 align_x: self.text_alignment,
300 align_y: alignment::Vertical::Top,
301 shaping: self.text_shaping,
302 wrapping: self.text_wrapping,
303 },
304 )
305 } else {
306 layout::Node::new(Size::ZERO)
307 }
308 },
309 )
310 }
311
312 fn update(
313 &mut self,
314 _tree: &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 is_toggled: self.is_toggled,
343 }
344 } else if cursor.is_over(layout.bounds()) {
345 Status::Hovered {
346 is_toggled: self.is_toggled,
347 }
348 } else {
349 Status::Active {
350 is_toggled: self.is_toggled,
351 }
352 };
353
354 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
355 self.last_status = Some(current_status);
356 } else if self
357 .last_status
358 .is_some_and(|status| status != current_status)
359 {
360 shell.request_redraw();
361 }
362 }
363
364 fn mouse_interaction(
365 &self,
366 _tree: &Tree,
367 layout: Layout<'_>,
368 cursor: mouse::Cursor,
369 _viewport: &Rectangle,
370 _renderer: &Renderer,
371 ) -> mouse::Interaction {
372 if cursor.is_over(layout.bounds()) {
373 if self.on_toggle.is_some() {
374 mouse::Interaction::Pointer
375 } else {
376 mouse::Interaction::NotAllowed
377 }
378 } else {
379 mouse::Interaction::default()
380 }
381 }
382
383 fn draw(
384 &self,
385 tree: &Tree,
386 renderer: &mut Renderer,
387 theme: &Theme,
388 defaults: &renderer::Style,
389 layout: Layout<'_>,
390 _cursor: mouse::Cursor,
391 viewport: &Rectangle,
392 ) {
393 let mut children = layout.children();
394 let toggler_layout = children.next().unwrap();
395
396 let style = theme.style(
397 &self.class,
398 self.last_status.unwrap_or(Status::Disabled {
399 is_toggled: self.is_toggled,
400 }),
401 );
402
403 if self.label.is_some() {
404 let label_layout = children.next().unwrap();
405 let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
406
407 crate::text::draw(
408 renderer,
409 defaults,
410 label_layout.bounds(),
411 state.raw(),
412 crate::text::Style {
413 color: style.text_color,
414 },
415 viewport,
416 );
417 }
418
419 let scale_factor = renderer.scale_factor().unwrap_or(1.0);
420 let bounds = toggler_layout.bounds();
421
422 let border_radius = style
423 .border_radius
424 .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
425
426 renderer.fill_quad(
427 renderer::Quad {
428 bounds,
429 border: Border {
430 radius: border_radius,
431 width: style.background_border_width,
432 color: style.background_border_color,
433 },
434 ..renderer::Quad::default()
435 },
436 style.background,
437 );
438
439 let toggle_bounds = {
440 let bounds = if renderer::CRISP {
442 (bounds * scale_factor).round()
443 } else {
444 bounds
445 };
446
447 let padding = (style.padding_ratio * bounds.height).round();
448
449 Rectangle {
450 x: bounds.x
451 + if self.is_toggled {
452 bounds.width - bounds.height + padding
453 } else {
454 padding
455 },
456 y: bounds.y + padding,
457 width: bounds.height - (2.0 * padding),
458 height: bounds.height - (2.0 * padding),
459 } * (1.0 / scale_factor)
460 };
461
462 renderer.fill_quad(
463 renderer::Quad {
464 bounds: toggle_bounds,
465 border: Border {
466 radius: border_radius,
467 width: style.foreground_border_width,
468 color: style.foreground_border_color,
469 },
470 ..renderer::Quad::default()
471 },
472 style.foreground,
473 );
474 }
475}
476
477impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
478 for Element<'a, Message, Theme, Renderer>
479where
480 Message: 'a,
481 Theme: Catalog + 'a,
482 Renderer: text::Renderer + 'a,
483{
484 fn from(
485 toggler: Toggler<'a, Message, Theme, Renderer>,
486 ) -> Element<'a, Message, Theme, Renderer> {
487 Element::new(toggler)
488 }
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
493pub enum Status {
494 Active {
496 is_toggled: bool,
498 },
499 Hovered {
501 is_toggled: bool,
503 },
504 Disabled {
506 is_toggled: bool,
508 },
509}
510
511#[derive(Debug, Clone, Copy, PartialEq)]
513pub struct Style {
514 pub background: Background,
516 pub background_border_width: f32,
518 pub background_border_color: Color,
520 pub foreground: Background,
522 pub foreground_border_width: f32,
524 pub foreground_border_color: Color,
526 pub text_color: Option<Color>,
528 pub border_radius: Option<border::Radius>,
532 pub padding_ratio: f32,
534}
535
536pub trait Catalog: Sized {
538 type Class<'a>;
540
541 fn default<'a>() -> Self::Class<'a>;
543
544 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
546}
547
548pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
552
553impl Catalog for Theme {
554 type Class<'a> = StyleFn<'a, Self>;
555
556 fn default<'a>() -> Self::Class<'a> {
557 Box::new(default)
558 }
559
560 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
561 class(self, status)
562 }
563}
564
565pub fn default(theme: &Theme, status: Status) -> Style {
567 let palette = theme.extended_palette();
568
569 let background = match status {
570 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
571 if is_toggled {
572 palette.primary.base.color
573 } else {
574 palette.background.strong.color
575 }
576 }
577 Status::Disabled { is_toggled } => {
578 if is_toggled {
579 palette.background.strong.color
580 } else {
581 palette.background.weak.color
582 }
583 }
584 };
585
586 let foreground = match status {
587 Status::Active { is_toggled } => {
588 if is_toggled {
589 palette.primary.base.text
590 } else {
591 palette.background.base.color
592 }
593 }
594 Status::Hovered { is_toggled } => {
595 if is_toggled {
596 Color {
597 a: 0.5,
598 ..palette.primary.base.text
599 }
600 } else {
601 palette.background.weak.color
602 }
603 }
604 Status::Disabled { .. } => palette.background.weakest.color,
605 };
606
607 Style {
608 background: background.into(),
609 foreground: foreground.into(),
610 foreground_border_width: 0.0,
611 foreground_border_color: Color::TRANSPARENT,
612 background_border_width: 0.0,
613 background_border_color: Color::TRANSPARENT,
614 text_color: None,
615 border_radius: None,
616 padding_ratio: 0.1,
617 }
618}