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