1use crate::core::border::{self, Border};
20use crate::core::layout;
21use crate::core::mouse;
22use crate::core::overlay;
23use crate::core::renderer;
24use crate::core::theme::palette;
25use crate::core::touch;
26use crate::core::widget::tree::{self, Tree};
27use crate::core::widget::{Meta, Operation};
28use crate::core::window;
29use crate::core::{
30 Background, Color, Event, Layout, Length, Padding, Rectangle, Shadow, Shell, Size, Theme,
31 Vector, Widget,
32};
33
34pub struct Button<'a, Message, W = crate::Element<'a, Message>, Theme = crate::Theme>
72where
73 Theme: Catalog,
74{
75 content: W,
76 on_press: Option<OnPress<'a, Message>>,
77 width: Length,
78 height: Length,
79 padding: Padding,
80 clip: bool,
81 class: Theme::Class<'a>,
82 status: Option<Status>,
83}
84
85enum OnPress<'a, Message> {
86 Direct(Message),
87 Closure(Box<dyn Fn() -> Message + 'a>),
88}
89
90impl<Message: Clone> OnPress<'_, Message> {
91 fn get(&self) -> Message {
92 match self {
93 OnPress::Direct(message) => message.clone(),
94 OnPress::Closure(f) => f(),
95 }
96 }
97}
98
99impl<'a, Message, W, Theme> Button<'a, Message, W, Theme>
100where
101 Theme: Catalog,
102{
103 pub fn new(content: W) -> Self {
105 Button {
106 content,
107 on_press: None,
108 width: Length::Fit,
109 height: Length::Fit,
110 padding: DEFAULT_PADDING,
111 clip: false,
112 class: Theme::default(),
113 status: None,
114 }
115 }
116
117 pub fn width(mut self, width: impl Into<Length>) -> Self {
119 self.width = width.into();
120 self
121 }
122
123 pub fn height(mut self, height: impl Into<Length>) -> Self {
125 self.height = height.into();
126 self
127 }
128
129 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
131 self.padding = padding.into();
132 self
133 }
134
135 pub fn on_press(mut self, on_press: Message) -> Self {
139 self.on_press = Some(OnPress::Direct(on_press));
140 self
141 }
142
143 pub fn on_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
152 self.on_press = Some(OnPress::Closure(Box::new(on_press)));
153 self
154 }
155
156 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
161 self.on_press = on_press.map(OnPress::Direct);
162 self
163 }
164
165 pub fn on_press_maybe_with(mut self, on_press: Option<impl Fn() -> Message + 'a>) -> Self {
177 self.on_press = on_press.map(|on_press| OnPress::Closure(Box::new(on_press)));
178 self
179 }
180
181 pub fn clip(mut self, clip: bool) -> Self {
184 self.clip = clip;
185 self
186 }
187
188 #[must_use]
190 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
191 where
192 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
193 {
194 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
195 self
196 }
197
198 #[cfg(feature = "advanced")]
200 #[must_use]
201 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
202 self.class = class.into();
203 self
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208struct State {
209 is_pressed: bool,
210}
211
212impl<'a, Message, W, Theme> Meta for Button<'a, Message, W, Theme> where Theme: Catalog {}
213
214impl<'a, Message, W, Theme, Renderer> Widget<Message, Theme, Renderer>
215 for Button<'a, Message, W, Theme>
216where
217 Message: 'a + Clone,
218 W: Widget<Message, Theme, Renderer>,
219 Renderer: 'a + crate::core::Renderer,
220 Theme: Catalog,
221{
222 fn tag(&self) -> tree::Tag {
223 tree::Tag::of::<State>()
224 }
225
226 fn state(&self) -> tree::State {
227 tree::State::new(State::default())
228 }
229
230 fn diff(&mut self, tree: &mut Tree) {
231 tree.diff_children(std::slice::from_mut(&mut self.content));
232
233 let size = self.content.size();
234 self.width = self.width.stack(size.width);
235 self.height = self.height.stack(size.height);
236 }
237
238 fn size(&self) -> Size<Length> {
239 Size {
240 width: self.width,
241 height: self.height,
242 }
243 }
244
245 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
246 layout::padded(
247 tree,
248 limits,
249 self.width,
250 self.height,
251 self.padding,
252 |tree, limits| {
253 self.content.layout(tree, renderer, limits);
254
255 tree.size
256 },
257 );
258 }
259
260 fn operate(
261 &mut self,
262 tree: &mut Tree,
263 layout: Layout,
264 viewport: &Rectangle,
265 renderer: &Renderer,
266 operation: &mut dyn Operation,
267 ) {
268 operation.container(None, layout.bounds(), viewport);
269 operation.traverse(&mut |operation| {
270 let (layout, tree) = layout.iter_mut(&mut tree.children).next().unwrap();
271
272 self.content
273 .operate(tree, layout, viewport, renderer, operation);
274 });
275 }
276
277 fn update(
278 &mut self,
279 tree: &mut Tree,
280 event: &Event,
281 layout: Layout,
282 cursor: mouse::Cursor,
283 renderer: &Renderer,
284 shell: &mut Shell<'_, Message>,
285 viewport: &Rectangle,
286 ) {
287 let (content_layout, content_tree) = layout.iter_mut(&mut tree.children).next().unwrap();
288
289 self.content.update(
290 content_tree,
291 event,
292 content_layout,
293 cursor,
294 renderer,
295 shell,
296 viewport,
297 );
298
299 if shell.is_event_captured() {
300 return;
301 }
302
303 match event {
304 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
305 | Event::Touch(touch::Event::FingerPressed { .. })
306 if self.on_press.is_some() =>
307 {
308 let bounds = layout.bounds();
309
310 if cursor.is_over(bounds) {
311 let state = tree.state.downcast_mut::<State>();
312
313 state.is_pressed = true;
314
315 shell.capture_event();
316 }
317 }
318 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
319 | Event::Touch(touch::Event::FingerLifted { .. }) => {
320 if let Some(on_press) = &self.on_press {
321 let state = tree.state.downcast_mut::<State>();
322
323 if state.is_pressed {
324 state.is_pressed = false;
325
326 let bounds = layout.bounds();
327
328 if cursor.is_over(bounds) {
329 shell.publish(on_press.get());
330 }
331
332 shell.capture_event();
333 }
334 }
335 }
336 Event::Touch(touch::Event::FingerLost { .. }) => {
337 let state = tree.state.downcast_mut::<State>();
338
339 state.is_pressed = false;
340 }
341 _ => {}
342 }
343
344 let current_status = if self.on_press.is_none() {
345 Status::Disabled
346 } else if cursor.is_over(layout.bounds()) {
347 let state = tree.state.downcast_ref::<State>();
348
349 if state.is_pressed {
350 Status::Pressed
351 } else {
352 Status::Hovered
353 }
354 } else {
355 Status::Active
356 };
357
358 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
359 self.status = Some(current_status);
360 } else if self.status.is_some_and(|status| status != current_status) {
361 shell.request_redraw();
362 }
363 }
364
365 fn draw(
366 &self,
367 tree: &Tree,
368 renderer: &mut Renderer,
369 theme: &Theme,
370 _style: &renderer::Style,
371 layout: Layout,
372 cursor: mouse::Cursor,
373 viewport: &Rectangle,
374 ) {
375 let bounds = layout.bounds();
376 let (layout, tree) = layout.iter(&tree.children).next().unwrap();
377 let style = theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
378
379 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
380 renderer.fill_quad(
381 renderer::Quad {
382 bounds,
383 border: style.border,
384 shadow: style.shadow,
385 snap: style.snap,
386 },
387 style
388 .background
389 .unwrap_or(Background::Color(Color::TRANSPARENT)),
390 );
391 }
392
393 let viewport = if self.clip {
394 bounds.intersection(viewport).unwrap_or(*viewport)
395 } else {
396 *viewport
397 };
398
399 self.content.draw(
400 tree,
401 renderer,
402 theme,
403 &renderer::Style {
404 text_color: style.text_color,
405 },
406 layout,
407 cursor,
408 &viewport,
409 );
410 }
411
412 fn mouse_interaction(
413 &self,
414 _tree: &Tree,
415 layout: Layout,
416 cursor: mouse::Cursor,
417 _viewport: &Rectangle,
418 _renderer: &Renderer,
419 ) -> mouse::Interaction {
420 let is_mouse_over = cursor.is_over(layout.bounds());
421
422 if is_mouse_over && self.on_press.is_some() {
423 mouse::Interaction::Pointer
424 } else {
425 mouse::Interaction::default()
426 }
427 }
428
429 fn overlay<'b>(
430 &'b mut self,
431 tree: &'b mut Tree,
432 layout: Layout,
433 renderer: &Renderer,
434 viewport: &Rectangle,
435 translation: Vector,
436 window: Size,
437 ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
438 let (layout, tree) = layout.iter_mut(&mut tree.children).next().unwrap();
439
440 self.content
441 .overlay(tree, layout, renderer, viewport, translation, window)
442 }
443}
444
445pub const DEFAULT_PADDING: Padding = Padding {
447 top: 5.0,
448 bottom: 5.0,
449 right: 10.0,
450 left: 10.0,
451};
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum Status {
456 Active,
458 Hovered,
460 Pressed,
462 Disabled,
464}
465
466#[derive(Debug, Clone, Copy, PartialEq)]
471pub struct Style {
472 pub background: Option<Background>,
474 pub text_color: Color,
476 pub border: Border,
478 pub shadow: Shadow,
480 pub snap: bool,
482}
483
484impl Style {
485 pub fn with_background(self, background: impl Into<Background>) -> Self {
487 Self {
488 background: Some(background.into()),
489 ..self
490 }
491 }
492}
493
494impl Default for Style {
495 fn default() -> Self {
496 Self {
497 background: None,
498 text_color: Color::BLACK,
499 border: Border::default(),
500 shadow: Shadow::default(),
501 snap: renderer::CRISP,
502 }
503 }
504}
505
506pub trait Catalog {
554 type Class<'a>;
556
557 fn default<'a>() -> Self::Class<'a>;
559
560 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
562}
563
564pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
566
567impl Catalog for Theme {
568 type Class<'a> = StyleFn<'a, Self>;
569
570 fn default<'a>() -> Self::Class<'a> {
571 Box::new(primary)
572 }
573
574 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
575 class(self, status)
576 }
577}
578
579pub fn primary(theme: &Theme, status: Status) -> Style {
581 let palette = theme.palette();
582 let base = styled(palette.primary.base);
583
584 match status {
585 Status::Active | Status::Pressed => base,
586 Status::Hovered => Style {
587 background: Some(Background::Color(palette.primary.strong.color)),
588 ..base
589 },
590 Status::Disabled => disabled(base),
591 }
592}
593
594pub fn secondary(theme: &Theme, status: Status) -> Style {
596 let palette = theme.palette();
597 let base = styled(palette.secondary.base);
598
599 match status {
600 Status::Active | Status::Pressed => base,
601 Status::Hovered => Style {
602 background: Some(Background::Color(palette.secondary.strong.color)),
603 ..base
604 },
605 Status::Disabled => disabled(base),
606 }
607}
608
609pub fn success(theme: &Theme, status: Status) -> Style {
611 let palette = theme.palette();
612 let base = styled(palette.success.base);
613
614 match status {
615 Status::Active | Status::Pressed => base,
616 Status::Hovered => Style {
617 background: Some(Background::Color(palette.success.strong.color)),
618 ..base
619 },
620 Status::Disabled => disabled(base),
621 }
622}
623
624pub fn warning(theme: &Theme, status: Status) -> Style {
626 let palette = theme.palette();
627 let base = styled(palette.warning.base);
628
629 match status {
630 Status::Active | Status::Pressed => base,
631 Status::Hovered => Style {
632 background: Some(Background::Color(palette.warning.strong.color)),
633 ..base
634 },
635 Status::Disabled => disabled(base),
636 }
637}
638
639pub fn danger(theme: &Theme, status: Status) -> Style {
641 let palette = theme.palette();
642 let base = styled(palette.danger.base);
643
644 match status {
645 Status::Active | Status::Pressed => base,
646 Status::Hovered => Style {
647 background: Some(Background::Color(palette.danger.strong.color)),
648 ..base
649 },
650 Status::Disabled => disabled(base),
651 }
652}
653
654pub fn text(theme: &Theme, status: Status) -> Style {
656 let palette = theme.palette();
657
658 let base = Style {
659 text_color: palette.background.base.text,
660 ..Style::default()
661 };
662
663 match status {
664 Status::Active | Status::Pressed => base,
665 Status::Hovered => Style {
666 text_color: palette.background.base.text.scale_alpha(0.8),
667 ..base
668 },
669 Status::Disabled => disabled(base),
670 }
671}
672
673pub fn background(theme: &Theme, status: Status) -> Style {
675 let palette = theme.palette();
676 let base = styled(palette.background.base);
677
678 match status {
679 Status::Active => base,
680 Status::Pressed => Style {
681 background: Some(Background::Color(palette.background.strong.color)),
682 ..base
683 },
684 Status::Hovered => Style {
685 background: Some(Background::Color(palette.background.weak.color)),
686 ..base
687 },
688 Status::Disabled => disabled(base),
689 }
690}
691
692pub fn subtle(theme: &Theme, status: Status) -> Style {
694 let palette = theme.palette();
695 let base = styled(palette.background.weakest);
696
697 match status {
698 Status::Active => base,
699 Status::Pressed => Style {
700 background: Some(Background::Color(palette.background.strong.color)),
701 ..base
702 },
703 Status::Hovered => Style {
704 background: Some(Background::Color(palette.background.weaker.color)),
705 ..base
706 },
707 Status::Disabled => disabled(base),
708 }
709}
710
711fn styled(pair: palette::Pair) -> Style {
712 Style {
713 background: Some(Background::Color(pair.color)),
714 text_color: pair.text,
715 border: border::rounded(2),
716 ..Style::default()
717 }
718}
719
720fn disabled(style: Style) -> Style {
721 Style {
722 background: style
723 .background
724 .map(|background| background.scale_alpha(0.5)),
725 text_color: style.text_color.scale_alpha(0.5),
726 ..style
727 }
728}