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 }
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 (layout, tree) = layout.iter(&tree.children).next().unwrap();
375 let style = theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
376
377 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
378 renderer.fill_quad(
379 renderer::Quad {
380 bounds,
381 border: style.border,
382 shadow: style.shadow,
383 snap: style.snap,
384 },
385 style
386 .background
387 .unwrap_or(Background::Color(Color::TRANSPARENT)),
388 );
389 }
390
391 let viewport = if self.clip {
392 bounds.intersection(viewport).unwrap_or(*viewport)
393 } else {
394 *viewport
395 };
396
397 self.content.draw(
398 tree,
399 renderer,
400 theme,
401 &renderer::Style {
402 text_color: style.text_color,
403 },
404 layout,
405 cursor,
406 &viewport,
407 );
408 }
409
410 fn mouse_interaction(
411 &self,
412 _tree: &Tree,
413 layout: Layout,
414 cursor: mouse::Cursor,
415 _viewport: &Rectangle,
416 _renderer: &Renderer,
417 ) -> mouse::Interaction {
418 let is_mouse_over = cursor.is_over(layout.bounds());
419
420 if is_mouse_over && self.on_press.is_some() {
421 mouse::Interaction::Pointer
422 } else {
423 mouse::Interaction::default()
424 }
425 }
426
427 fn overlay<'b>(
428 &'b mut self,
429 tree: &'b mut Tree,
430 layout: Layout,
431 renderer: &Renderer,
432 viewport: &Rectangle,
433 translation: Vector,
434 window: Size,
435 ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
436 let (layout, tree) = layout.iter_mut(&mut tree.children).next().unwrap();
437
438 self.content
439 .overlay(tree, layout, renderer, viewport, translation, window)
440 }
441}
442
443pub const DEFAULT_PADDING: Padding = Padding {
445 top: 5.0,
446 bottom: 5.0,
447 right: 10.0,
448 left: 10.0,
449};
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub enum Status {
454 Active,
456 Hovered,
458 Pressed,
460 Disabled,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq)]
469pub struct Style {
470 pub background: Option<Background>,
472 pub text_color: Color,
474 pub border: Border,
476 pub shadow: Shadow,
478 pub snap: bool,
480}
481
482impl Style {
483 pub fn with_background(self, background: impl Into<Background>) -> Self {
485 Self {
486 background: Some(background.into()),
487 ..self
488 }
489 }
490}
491
492impl Default for Style {
493 fn default() -> Self {
494 Self {
495 background: None,
496 text_color: Color::BLACK,
497 border: Border::default(),
498 shadow: Shadow::default(),
499 snap: renderer::CRISP,
500 }
501 }
502}
503
504pub trait Catalog {
552 type Class<'a>;
554
555 fn default<'a>() -> Self::Class<'a>;
557
558 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
560}
561
562pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
564
565impl Catalog for Theme {
566 type Class<'a> = StyleFn<'a, Self>;
567
568 fn default<'a>() -> Self::Class<'a> {
569 Box::new(primary)
570 }
571
572 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
573 class(self, status)
574 }
575}
576
577pub fn primary(theme: &Theme, status: Status) -> Style {
579 let palette = theme.palette();
580 let base = styled(palette.primary.base);
581
582 match status {
583 Status::Active | Status::Pressed => base,
584 Status::Hovered => Style {
585 background: Some(Background::Color(palette.primary.strong.color)),
586 ..base
587 },
588 Status::Disabled => disabled(base),
589 }
590}
591
592pub fn secondary(theme: &Theme, status: Status) -> Style {
594 let palette = theme.palette();
595 let base = styled(palette.secondary.base);
596
597 match status {
598 Status::Active | Status::Pressed => base,
599 Status::Hovered => Style {
600 background: Some(Background::Color(palette.secondary.strong.color)),
601 ..base
602 },
603 Status::Disabled => disabled(base),
604 }
605}
606
607pub fn success(theme: &Theme, status: Status) -> Style {
609 let palette = theme.palette();
610 let base = styled(palette.success.base);
611
612 match status {
613 Status::Active | Status::Pressed => base,
614 Status::Hovered => Style {
615 background: Some(Background::Color(palette.success.strong.color)),
616 ..base
617 },
618 Status::Disabled => disabled(base),
619 }
620}
621
622pub fn warning(theme: &Theme, status: Status) -> Style {
624 let palette = theme.palette();
625 let base = styled(palette.warning.base);
626
627 match status {
628 Status::Active | Status::Pressed => base,
629 Status::Hovered => Style {
630 background: Some(Background::Color(palette.warning.strong.color)),
631 ..base
632 },
633 Status::Disabled => disabled(base),
634 }
635}
636
637pub fn danger(theme: &Theme, status: Status) -> Style {
639 let palette = theme.palette();
640 let base = styled(palette.danger.base);
641
642 match status {
643 Status::Active | Status::Pressed => base,
644 Status::Hovered => Style {
645 background: Some(Background::Color(palette.danger.strong.color)),
646 ..base
647 },
648 Status::Disabled => disabled(base),
649 }
650}
651
652pub fn text(theme: &Theme, status: Status) -> Style {
654 let palette = theme.palette();
655
656 let base = Style {
657 text_color: palette.background.base.text,
658 ..Style::default()
659 };
660
661 match status {
662 Status::Active | Status::Pressed => base,
663 Status::Hovered => Style {
664 text_color: palette.background.base.text.scale_alpha(0.8),
665 ..base
666 },
667 Status::Disabled => disabled(base),
668 }
669}
670
671pub fn background(theme: &Theme, status: Status) -> Style {
673 let palette = theme.palette();
674 let base = styled(palette.background.base);
675
676 match status {
677 Status::Active => base,
678 Status::Pressed => Style {
679 background: Some(Background::Color(palette.background.strong.color)),
680 ..base
681 },
682 Status::Hovered => Style {
683 background: Some(Background::Color(palette.background.weak.color)),
684 ..base
685 },
686 Status::Disabled => disabled(base),
687 }
688}
689
690pub fn subtle(theme: &Theme, status: Status) -> Style {
692 let palette = theme.palette();
693 let base = styled(palette.background.weakest);
694
695 match status {
696 Status::Active => base,
697 Status::Pressed => Style {
698 background: Some(Background::Color(palette.background.strong.color)),
699 ..base
700 },
701 Status::Hovered => Style {
702 background: Some(Background::Color(palette.background.weaker.color)),
703 ..base
704 },
705 Status::Disabled => disabled(base),
706 }
707}
708
709fn styled(pair: palette::Pair) -> Style {
710 Style {
711 background: Some(Background::Color(pair.color)),
712 text_color: pair.text,
713 border: border::rounded(2),
714 ..Style::default()
715 }
716}
717
718fn disabled(style: Style) -> Style {
719 Style {
720 background: style
721 .background
722 .map(|background| background.scale_alpha(0.5)),
723 text_color: style.text_color.scale_alpha(0.5),
724 ..style
725 }
726}