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