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
109 Button {
110 content,
111 on_press: None,
112 width: Length::Fit,
113 height: Length::Fit,
114 padding: DEFAULT_PADDING,
115 clip: false,
116 class: Theme::default(),
117 status: None,
118 }
119 }
120
121 pub fn width(mut self, width: impl Into<Length>) -> Self {
123 self.width = width.into();
124 self
125 }
126
127 pub fn height(mut self, height: impl Into<Length>) -> Self {
129 self.height = height.into();
130 self
131 }
132
133 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
135 self.padding = padding.into();
136 self
137 }
138
139 pub fn on_press(mut self, on_press: Message) -> Self {
143 self.on_press = Some(OnPress::Direct(on_press));
144 self
145 }
146
147 pub fn on_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
156 self.on_press = Some(OnPress::Closure(Box::new(on_press)));
157 self
158 }
159
160 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
165 self.on_press = on_press.map(OnPress::Direct);
166 self
167 }
168
169 pub fn clip(mut self, clip: bool) -> Self {
172 self.clip = clip;
173 self
174 }
175
176 #[must_use]
178 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
179 where
180 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
181 {
182 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
183 self
184 }
185
186 #[cfg(feature = "advanced")]
188 #[must_use]
189 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
190 self.class = class.into();
191 self
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
196struct State {
197 is_pressed: bool,
198}
199
200impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
201 for Button<'a, Message, Theme, Renderer>
202where
203 Message: 'a + Clone,
204 Renderer: 'a + crate::core::Renderer,
205 Theme: Catalog,
206{
207 fn tag(&self) -> tree::Tag {
208 tree::Tag::of::<State>()
209 }
210
211 fn state(&self) -> tree::State {
212 tree::State::new(State::default())
213 }
214
215 fn diff(&mut self, tree: &mut Tree) {
216 tree.diff_children(std::slice::from_mut(&mut self.content));
217
218 let size = self.content.as_widget().size();
219 self.width = self.width.enclose(size.width);
220 self.height = self.height.enclose(size.height);
221 }
222
223 fn size(&self) -> Size<Length> {
224 Size {
225 width: self.width,
226 height: self.height,
227 }
228 }
229
230 fn layout(
231 &mut self,
232 tree: &mut Tree,
233 renderer: &Renderer,
234 limits: &layout::Limits,
235 ) -> layout::Node {
236 layout::padded(limits, self.width, self.height, self.padding, |limits| {
237 self.content
238 .as_widget_mut()
239 .layout(&mut tree.children[0], renderer, limits)
240 })
241 }
242
243 fn operate(
244 &mut self,
245 tree: &mut Tree,
246 layout: Layout<'_>,
247 renderer: &Renderer,
248 operation: &mut dyn Operation,
249 ) {
250 operation.container(None, layout.bounds());
251 operation.traverse(&mut |operation| {
252 self.content.as_widget_mut().operate(
253 &mut tree.children[0],
254 layout.children().next().unwrap(),
255 renderer,
256 operation,
257 );
258 });
259 }
260
261 fn update(
262 &mut self,
263 tree: &mut Tree,
264 event: &Event,
265 layout: Layout<'_>,
266 cursor: mouse::Cursor,
267 renderer: &Renderer,
268 shell: &mut Shell<'_, Message>,
269 viewport: &Rectangle,
270 ) {
271 self.content.as_widget_mut().update(
272 &mut tree.children[0],
273 event,
274 layout.children().next().unwrap(),
275 cursor,
276 renderer,
277 shell,
278 viewport,
279 );
280
281 if shell.is_event_captured() {
282 return;
283 }
284
285 match event {
286 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
287 | Event::Touch(touch::Event::FingerPressed { .. })
288 if self.on_press.is_some() =>
289 {
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 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
301 | Event::Touch(touch::Event::FingerLifted { .. }) => {
302 if let Some(on_press) = &self.on_press {
303 let state = tree.state.downcast_mut::<State>();
304
305 if state.is_pressed {
306 state.is_pressed = false;
307
308 let bounds = layout.bounds();
309
310 if cursor.is_over(bounds) {
311 shell.publish(on_press.get());
312 }
313
314 shell.capture_event();
315 }
316 }
317 }
318 Event::Touch(touch::Event::FingerLost { .. }) => {
319 let state = tree.state.downcast_mut::<State>();
320
321 state.is_pressed = false;
322 }
323 _ => {}
324 }
325
326 let current_status = if self.on_press.is_none() {
327 Status::Disabled
328 } else if cursor.is_over(layout.bounds()) {
329 let state = tree.state.downcast_ref::<State>();
330
331 if state.is_pressed {
332 Status::Pressed
333 } else {
334 Status::Hovered
335 }
336 } else {
337 Status::Active
338 };
339
340 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
341 self.status = Some(current_status);
342 } else if self.status.is_some_and(|status| status != current_status) {
343 shell.request_redraw();
344 }
345 }
346
347 fn draw(
348 &self,
349 tree: &Tree,
350 renderer: &mut Renderer,
351 theme: &Theme,
352 _style: &renderer::Style,
353 layout: Layout<'_>,
354 cursor: mouse::Cursor,
355 viewport: &Rectangle,
356 ) {
357 let bounds = layout.bounds();
358 let content_layout = layout.children().next().unwrap();
359 let style = theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
360
361 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
362 renderer.fill_quad(
363 renderer::Quad {
364 bounds,
365 border: style.border,
366 shadow: style.shadow,
367 snap: style.snap,
368 },
369 style
370 .background
371 .unwrap_or(Background::Color(Color::TRANSPARENT)),
372 );
373 }
374
375 let viewport = if self.clip {
376 bounds.intersection(viewport).unwrap_or(*viewport)
377 } else {
378 *viewport
379 };
380
381 self.content.as_widget().draw(
382 &tree.children[0],
383 renderer,
384 theme,
385 &renderer::Style {
386 text_color: style.text_color,
387 },
388 content_layout,
389 cursor,
390 &viewport,
391 );
392 }
393
394 fn mouse_interaction(
395 &self,
396 _tree: &Tree,
397 layout: Layout<'_>,
398 cursor: mouse::Cursor,
399 _viewport: &Rectangle,
400 _renderer: &Renderer,
401 ) -> mouse::Interaction {
402 let is_mouse_over = cursor.is_over(layout.bounds());
403
404 if is_mouse_over && self.on_press.is_some() {
405 mouse::Interaction::Pointer
406 } else {
407 mouse::Interaction::default()
408 }
409 }
410
411 fn overlay<'b>(
412 &'b mut self,
413 tree: &'b mut Tree,
414 layout: Layout<'b>,
415 renderer: &Renderer,
416 viewport: &Rectangle,
417 translation: Vector,
418 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
419 self.content.as_widget_mut().overlay(
420 &mut tree.children[0],
421 layout.children().next().unwrap(),
422 renderer,
423 viewport,
424 translation,
425 )
426 }
427}
428
429impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
430 for Element<'a, Message, Theme, Renderer>
431where
432 Message: Clone + 'a,
433 Theme: Catalog + 'a,
434 Renderer: crate::core::Renderer + 'a,
435{
436 fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
437 Self::new(button)
438 }
439}
440
441pub const DEFAULT_PADDING: Padding = Padding {
443 top: 5.0,
444 bottom: 5.0,
445 right: 10.0,
446 left: 10.0,
447};
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451pub enum Status {
452 Active,
454 Hovered,
456 Pressed,
458 Disabled,
460}
461
462#[derive(Debug, Clone, Copy, PartialEq)]
467pub struct Style {
468 pub background: Option<Background>,
470 pub text_color: Color,
472 pub border: Border,
474 pub shadow: Shadow,
476 pub snap: bool,
478}
479
480impl Style {
481 pub fn with_background(self, background: impl Into<Background>) -> Self {
483 Self {
484 background: Some(background.into()),
485 ..self
486 }
487 }
488}
489
490impl Default for Style {
491 fn default() -> Self {
492 Self {
493 background: None,
494 text_color: Color::BLACK,
495 border: Border::default(),
496 shadow: Shadow::default(),
497 snap: renderer::CRISP,
498 }
499 }
500}
501
502pub 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}