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, Color, Element, Event, Font, Layout, Length, Pixels, Rectangle, Shell,
45 Size, Theme, Widget,
46};
47
48pub struct Toggler<'a, Message, Theme = crate::Theme>
81where
82 Theme: Catalog,
83{
84 is_toggled: bool,
85 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
86 label: Option<text::Fragment<'a>>,
87 width: Length,
88 size: f32,
89 text_size: Option<Pixels>,
90 line_height: Option<text::LineHeight>,
91 alignment: text::Alignment,
92 text_shaping: text::Shaping,
93 wrapping: text::Wrapping,
94 spacing: f32,
95 font: Option<Font>,
96 class: Theme::Class<'a>,
97 last_status: Option<Status>,
98}
99
100impl<'a, Message, Theme> Toggler<'a, Message, Theme>
101where
102 Theme: Catalog,
103{
104 pub const DEFAULT_SIZE: f32 = 16.0;
106
107 pub fn new(is_toggled: bool) -> Self {
116 Toggler {
117 is_toggled,
118 on_toggle: None,
119 label: None,
120 width: Length::Shrink,
121 size: Self::DEFAULT_SIZE,
122 text_size: None,
123 line_height: None,
124 alignment: text::Alignment::Default,
125 text_shaping: text::Shaping::default(),
126 wrapping: text::Wrapping::default(),
127 spacing: Self::DEFAULT_SIZE / 2.0,
128 font: None,
129 class: Theme::default(),
130 last_status: None,
131 }
132 }
133
134 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
136 self.label = Some(label.into_fragment());
137 self
138 }
139
140 pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
145 self.on_toggle = Some(Box::new(on_toggle));
146 self
147 }
148
149 pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
154 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
155 self
156 }
157
158 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
160 self.size = size.into().0;
161 self
162 }
163
164 pub fn width(mut self, width: impl Into<Length>) -> Self {
166 self.width = width.into();
167 self
168 }
169
170 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
172 self.text_size = Some(text_size.into());
173 self
174 }
175
176 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
178 self.line_height = Some(line_height.into());
179 self
180 }
181
182 pub fn alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
184 self.alignment = alignment.into();
185 self
186 }
187
188 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
190 self.text_shaping = shaping;
191 self
192 }
193
194 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
196 self.wrapping = wrapping;
197 self
198 }
199
200 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
202 self.spacing = spacing.into().0;
203 self
204 }
205
206 pub fn font(mut self, font: impl Into<Font>) -> Self {
210 self.font = Some(font.into());
211 self
212 }
213
214 #[must_use]
216 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
217 where
218 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
219 {
220 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
221 self
222 }
223
224 #[cfg(feature = "advanced")]
226 #[must_use]
227 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
228 self.class = class.into();
229 self
230 }
231}
232
233impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Toggler<'_, Message, Theme>
234where
235 Theme: Catalog,
236 Renderer: text::Renderer,
237{
238 fn tag(&self) -> tree::Tag {
239 tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
240 }
241
242 fn state(&self) -> tree::State {
243 tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
244 }
245
246 fn size(&self) -> Size<Length> {
247 Size {
248 width: self.width,
249 height: Length::Shrink,
250 }
251 }
252
253 fn layout(
254 &mut self,
255 tree: &mut Tree,
256 renderer: &Renderer,
257 limits: &layout::Limits,
258 ) -> layout::Node {
259 let limits = limits.width(self.width);
260
261 layout::next_to_each_other(
262 &limits,
263 if self.label.is_some() {
264 self.spacing
265 } else {
266 0.0
267 },
268 |_| {
269 let size = if renderer::CRISP {
270 let scale_factor = renderer.hint_factor().unwrap_or(1.0);
271
272 (self.size * scale_factor).round() / scale_factor
273 } else {
274 self.size
275 };
276
277 layout::Node::new(Size::new(2.0 * size, size))
278 },
279 |limits| {
280 if let Some(label) = self.label.as_deref() {
281 let state = tree
282 .state
283 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
284
285 widget::text::layout(
286 state,
287 renderer,
288 limits,
289 label,
290 widget::text::Format {
291 width: self.width,
292 height: Length::Shrink,
293 line_height: self.line_height,
294 size: self.text_size,
295 font: self.font,
296 align_x: self.alignment,
297 align_y: alignment::Vertical::Top,
298 shaping: self.text_shaping,
299 wrapping: self.wrapping,
300 ellipsis: text::Ellipsis::None,
301 },
302 )
303 } else {
304 layout::Node::new(Size::ZERO)
305 }
306 },
307 )
308 }
309
310 fn update(
311 &mut self,
312 _tree: &mut Tree,
313 event: &Event,
314 layout: Layout<'_>,
315 cursor: mouse::Cursor,
316 _renderer: &Renderer,
317 shell: &mut Shell<'_, Message>,
318 _viewport: &Rectangle,
319 ) {
320 let Some(on_toggle) = &self.on_toggle else {
321 return;
322 };
323
324 match event {
325 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
326 | Event::Touch(touch::Event::FingerPressed { .. }) => {
327 let mouse_over = cursor.is_over(layout.bounds());
328
329 if mouse_over {
330 shell.publish(on_toggle(!self.is_toggled));
331 shell.capture_event();
332 }
333 }
334 _ => {}
335 }
336
337 let current_status = if self.on_toggle.is_none() {
338 Status::Disabled {
339 is_toggled: self.is_toggled,
340 }
341 } else if cursor.is_over(layout.bounds()) {
342 Status::Hovered {
343 is_toggled: self.is_toggled,
344 }
345 } else {
346 Status::Active {
347 is_toggled: self.is_toggled,
348 }
349 };
350
351 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
352 self.last_status = Some(current_status);
353 } else if self
354 .last_status
355 .is_some_and(|status| status != current_status)
356 {
357 shell.request_redraw();
358 }
359 }
360
361 fn mouse_interaction(
362 &self,
363 _tree: &Tree,
364 layout: Layout<'_>,
365 cursor: mouse::Cursor,
366 _viewport: &Rectangle,
367 _renderer: &Renderer,
368 ) -> mouse::Interaction {
369 if cursor.is_over(layout.bounds()) {
370 if self.on_toggle.is_some() {
371 mouse::Interaction::Pointer
372 } else {
373 mouse::Interaction::NotAllowed
374 }
375 } else {
376 mouse::Interaction::default()
377 }
378 }
379
380 fn draw(
381 &self,
382 tree: &Tree,
383 renderer: &mut Renderer,
384 theme: &Theme,
385 defaults: &renderer::Style,
386 layout: Layout<'_>,
387 _cursor: mouse::Cursor,
388 viewport: &Rectangle,
389 ) {
390 let mut children = layout.children();
391 let toggler_layout = children.next().unwrap();
392
393 let style = theme.style(
394 &self.class,
395 self.last_status.unwrap_or(Status::Disabled {
396 is_toggled: self.is_toggled,
397 }),
398 );
399
400 if self.label.is_some() {
401 let label_layout = children.next().unwrap();
402 let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
403
404 crate::text::draw(
405 renderer,
406 defaults,
407 label_layout.bounds(),
408 state.raw(),
409 crate::text::Style {
410 color: style.text_color,
411 },
412 viewport,
413 );
414 }
415
416 let scale_factor = renderer.hint_factor().unwrap_or(1.0);
417 let bounds = toggler_layout.bounds();
418
419 let border_radius = style
420 .border_radius
421 .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
422
423 renderer.fill_quad(
424 renderer::Quad {
425 bounds,
426 border: Border {
427 radius: border_radius,
428 width: style.background_border_width,
429 color: style.background_border_color,
430 },
431 ..renderer::Quad::default()
432 },
433 style.background,
434 );
435
436 let toggle_bounds = {
437 let bounds = if renderer::CRISP {
439 (bounds * scale_factor).round() * (1.0 / scale_factor)
440 } else {
441 bounds
442 };
443
444 let padding = (style.padding_ratio * bounds.height).round();
445
446 Rectangle {
447 x: bounds.x
448 + if self.is_toggled {
449 bounds.width - bounds.height + padding
450 } else {
451 padding
452 },
453 y: bounds.y + padding,
454 width: bounds.height - (2.0 * padding),
455 height: bounds.height - (2.0 * padding),
456 }
457 };
458
459 renderer.fill_quad(
460 renderer::Quad {
461 bounds: toggle_bounds,
462 border: Border {
463 radius: border_radius,
464 width: style.foreground_border_width,
465 color: style.foreground_border_color,
466 },
467 ..renderer::Quad::default()
468 },
469 style.foreground,
470 );
471 }
472}
473
474impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme>>
475 for Element<'a, Message, Theme, Renderer>
476where
477 Message: 'a,
478 Theme: Catalog + 'a,
479 Renderer: text::Renderer + 'a,
480{
481 fn from(toggler: Toggler<'a, Message, Theme>) -> Element<'a, Message, Theme, Renderer> {
482 Element::new(toggler)
483 }
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum Status {
489 Active {
491 is_toggled: bool,
493 },
494 Hovered {
496 is_toggled: bool,
498 },
499 Disabled {
501 is_toggled: bool,
503 },
504}
505
506#[derive(Debug, Clone, Copy, PartialEq)]
508pub struct Style {
509 pub background: Background,
511 pub background_border_width: f32,
513 pub background_border_color: Color,
515 pub foreground: Background,
517 pub foreground_border_width: f32,
519 pub foreground_border_color: Color,
521 pub text_color: Option<Color>,
523 pub border_radius: Option<border::Radius>,
527 pub padding_ratio: f32,
529}
530
531pub trait Catalog: Sized {
533 type Class<'a>;
535
536 fn default<'a>() -> Self::Class<'a>;
538
539 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
541}
542
543pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
547
548impl Catalog for Theme {
549 type Class<'a> = StyleFn<'a, Self>;
550
551 fn default<'a>() -> Self::Class<'a> {
552 Box::new(default)
553 }
554
555 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
556 class(self, status)
557 }
558}
559
560pub fn default(theme: &Theme, status: Status) -> Style {
562 let palette = theme.palette();
563
564 let background = match status {
565 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
566 if is_toggled {
567 palette.primary.base.color
568 } else {
569 palette.background.strong.color
570 }
571 }
572 Status::Disabled { is_toggled } => {
573 if is_toggled {
574 palette.background.strong.color
575 } else {
576 palette.background.weak.color
577 }
578 }
579 };
580
581 let foreground = match status {
582 Status::Active { is_toggled } => {
583 if is_toggled {
584 palette.primary.base.text
585 } else {
586 palette.background.base.color
587 }
588 }
589 Status::Hovered { is_toggled } => {
590 if is_toggled {
591 Color {
592 a: 0.5,
593 ..palette.primary.base.text
594 }
595 } else {
596 palette.background.weak.color
597 }
598 }
599 Status::Disabled { .. } => palette.background.weakest.color,
600 };
601
602 Style {
603 background: background.into(),
604 foreground: foreground.into(),
605 foreground_border_width: 0.0,
606 foreground_border_color: Color::TRANSPARENT,
607 background_border_width: 0.0,
608 background_border_color: Color::TRANSPARENT,
609 text_color: None,
610 border_radius: None,
611 padding_ratio: 0.1,
612 }
613}