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, Color, Element, Event, Layout, Length, Padding, Rectangle, Shadow, Shell, Size,
31 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 shell: &mut Shell<'_, Message>,
270 viewport: &Rectangle,
271 ) {
272 self.content.as_widget_mut().update(
273 &mut tree.children[0],
274 event,
275 layout.children().next().unwrap(),
276 cursor,
277 renderer,
278 shell,
279 viewport,
280 );
281
282 if shell.is_event_captured() {
283 return;
284 }
285
286 match event {
287 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
288 | Event::Touch(touch::Event::FingerPressed { .. }) => {
289 if self.on_press.is_some() {
290 let bounds = layout.bounds();
291
292 if cursor.is_over(bounds) {
293 let state = tree.state.downcast_mut::<State>();
294
295 state.is_pressed = true;
296
297 shell.capture_event();
298 }
299 }
300 }
301 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
302 | Event::Touch(touch::Event::FingerLifted { .. }) => {
303 if let Some(on_press) = &self.on_press {
304 let state = tree.state.downcast_mut::<State>();
305
306 if state.is_pressed {
307 state.is_pressed = false;
308
309 let bounds = layout.bounds();
310
311 if cursor.is_over(bounds) {
312 shell.publish(on_press.get());
313 }
314
315 shell.capture_event();
316 }
317 }
318 }
319 Event::Touch(touch::Event::FingerLost { .. }) => {
320 let state = tree.state.downcast_mut::<State>();
321
322 state.is_pressed = false;
323 }
324 _ => {}
325 }
326
327 let current_status = if self.on_press.is_none() {
328 Status::Disabled
329 } else if cursor.is_over(layout.bounds()) {
330 let state = tree.state.downcast_ref::<State>();
331
332 if state.is_pressed {
333 Status::Pressed
334 } else {
335 Status::Hovered
336 }
337 } else {
338 Status::Active
339 };
340
341 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
342 self.status = Some(current_status);
343 } else if self.status.is_some_and(|status| status != current_status) {
344 shell.request_redraw();
345 }
346 }
347
348 fn draw(
349 &self,
350 tree: &Tree,
351 renderer: &mut Renderer,
352 theme: &Theme,
353 _style: &renderer::Style,
354 layout: Layout<'_>,
355 cursor: mouse::Cursor,
356 viewport: &Rectangle,
357 ) {
358 let bounds = layout.bounds();
359 let content_layout = layout.children().next().unwrap();
360 let style = theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
361
362 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
363 renderer.fill_quad(
364 renderer::Quad {
365 bounds,
366 border: style.border,
367 shadow: style.shadow,
368 snap: style.snap,
369 },
370 style
371 .background
372 .unwrap_or(Background::Color(Color::TRANSPARENT)),
373 );
374 }
375
376 let viewport = if self.clip {
377 bounds.intersection(viewport).unwrap_or(*viewport)
378 } else {
379 *viewport
380 };
381
382 self.content.as_widget().draw(
383 &tree.children[0],
384 renderer,
385 theme,
386 &renderer::Style {
387 text_color: style.text_color,
388 },
389 content_layout,
390 cursor,
391 &viewport,
392 );
393 }
394
395 fn mouse_interaction(
396 &self,
397 _tree: &Tree,
398 layout: Layout<'_>,
399 cursor: mouse::Cursor,
400 _viewport: &Rectangle,
401 _renderer: &Renderer,
402 ) -> mouse::Interaction {
403 let is_mouse_over = cursor.is_over(layout.bounds());
404
405 if is_mouse_over && self.on_press.is_some() {
406 mouse::Interaction::Pointer
407 } else {
408 mouse::Interaction::default()
409 }
410 }
411
412 fn overlay<'b>(
413 &'b mut self,
414 tree: &'b mut Tree,
415 layout: Layout<'b>,
416 renderer: &Renderer,
417 viewport: &Rectangle,
418 translation: Vector,
419 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
420 self.content.as_widget_mut().overlay(
421 &mut tree.children[0],
422 layout.children().next().unwrap(),
423 renderer,
424 viewport,
425 translation,
426 )
427 }
428}
429
430impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
431 for Element<'a, Message, Theme, Renderer>
432where
433 Message: Clone + 'a,
434 Theme: Catalog + 'a,
435 Renderer: crate::core::Renderer + 'a,
436{
437 fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
438 Self::new(button)
439 }
440}
441
442pub const DEFAULT_PADDING: Padding = Padding {
444 top: 5.0,
445 bottom: 5.0,
446 right: 10.0,
447 left: 10.0,
448};
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub enum Status {
453 Active,
455 Hovered,
457 Pressed,
459 Disabled,
461}
462
463#[derive(Debug, Clone, Copy, PartialEq)]
468pub struct Style {
469 pub background: Option<Background>,
471 pub text_color: Color,
473 pub border: Border,
475 pub shadow: Shadow,
477 pub snap: bool,
479}
480
481impl Style {
482 pub fn with_background(self, background: impl Into<Background>) -> Self {
484 Self {
485 background: Some(background.into()),
486 ..self
487 }
488 }
489}
490
491impl Default for Style {
492 fn default() -> Self {
493 Self {
494 background: None,
495 text_color: Color::BLACK,
496 border: Border::default(),
497 shadow: Shadow::default(),
498 snap: renderer::CRISP,
499 }
500 }
501}
502
503pub trait Catalog {
553 type Class<'a>;
555
556 fn default<'a>() -> Self::Class<'a>;
558
559 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
561}
562
563pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
565
566impl Catalog for Theme {
567 type Class<'a> = StyleFn<'a, Self>;
568
569 fn default<'a>() -> Self::Class<'a> {
570 Box::new(primary)
571 }
572
573 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
574 class(self, status)
575 }
576}
577
578pub fn primary(theme: &Theme, status: Status) -> Style {
580 let palette = theme.extended_palette();
581 let base = styled(palette.primary.base);
582
583 match status {
584 Status::Active | Status::Pressed => base,
585 Status::Hovered => Style {
586 background: Some(Background::Color(palette.primary.strong.color)),
587 ..base
588 },
589 Status::Disabled => disabled(base),
590 }
591}
592
593pub fn secondary(theme: &Theme, status: Status) -> Style {
595 let palette = theme.extended_palette();
596 let base = styled(palette.secondary.base);
597
598 match status {
599 Status::Active | Status::Pressed => base,
600 Status::Hovered => Style {
601 background: Some(Background::Color(palette.secondary.strong.color)),
602 ..base
603 },
604 Status::Disabled => disabled(base),
605 }
606}
607
608pub fn success(theme: &Theme, status: Status) -> Style {
610 let palette = theme.extended_palette();
611 let base = styled(palette.success.base);
612
613 match status {
614 Status::Active | Status::Pressed => base,
615 Status::Hovered => Style {
616 background: Some(Background::Color(palette.success.strong.color)),
617 ..base
618 },
619 Status::Disabled => disabled(base),
620 }
621}
622
623pub fn warning(theme: &Theme, status: Status) -> Style {
625 let palette = theme.extended_palette();
626 let base = styled(palette.warning.base);
627
628 match status {
629 Status::Active | Status::Pressed => base,
630 Status::Hovered => Style {
631 background: Some(Background::Color(palette.warning.strong.color)),
632 ..base
633 },
634 Status::Disabled => disabled(base),
635 }
636}
637
638pub fn danger(theme: &Theme, status: Status) -> Style {
640 let palette = theme.extended_palette();
641 let base = styled(palette.danger.base);
642
643 match status {
644 Status::Active | Status::Pressed => base,
645 Status::Hovered => Style {
646 background: Some(Background::Color(palette.danger.strong.color)),
647 ..base
648 },
649 Status::Disabled => disabled(base),
650 }
651}
652
653pub fn text(theme: &Theme, status: Status) -> Style {
655 let palette = theme.extended_palette();
656
657 let base = Style {
658 text_color: palette.background.base.text,
659 ..Style::default()
660 };
661
662 match status {
663 Status::Active | Status::Pressed => base,
664 Status::Hovered => Style {
665 text_color: palette.background.base.text.scale_alpha(0.8),
666 ..base
667 },
668 Status::Disabled => disabled(base),
669 }
670}
671
672pub fn background(theme: &Theme, status: Status) -> Style {
674 let palette = theme.extended_palette();
675 let base = styled(palette.background.base);
676
677 match status {
678 Status::Active => base,
679 Status::Pressed => Style {
680 background: Some(Background::Color(palette.background.strong.color)),
681 ..base
682 },
683 Status::Hovered => Style {
684 background: Some(Background::Color(palette.background.weak.color)),
685 ..base
686 },
687 Status::Disabled => disabled(base),
688 }
689}
690
691pub fn subtle(theme: &Theme, status: Status) -> Style {
693 let palette = theme.extended_palette();
694 let base = styled(palette.background.weakest);
695
696 match status {
697 Status::Active => base,
698 Status::Pressed => Style {
699 background: Some(Background::Color(palette.background.strong.color)),
700 ..base
701 },
702 Status::Hovered => Style {
703 background: Some(Background::Color(palette.background.weaker.color)),
704 ..base
705 },
706 Status::Disabled => disabled(base),
707 }
708}
709
710fn styled(pair: palette::Pair) -> Style {
711 Style {
712 background: Some(Background::Color(pair.color)),
713 text_color: pair.text,
714 border: border::rounded(2),
715 ..Style::default()
716 }
717}
718
719fn disabled(style: Style) -> Style {
720 Style {
721 background: style
722 .background
723 .map(|background| background.scale_alpha(0.5)),
724 text_color: style.text_color.scale_alpha(0.5),
725 ..style
726 }
727}