iced_widget/
button.rs

1//! Buttons allow your users to perform actions by pressing them.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } }
6//! # pub type State = ();
7//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
8//! use iced::widget::button;
9//!
10//! #[derive(Clone)]
11//! enum Message {
12//!     ButtonPressed,
13//! }
14//!
15//! fn view(state: &State) -> Element<'_, Message> {
16//!     button("Press me!").on_press(Message::ButtonPressed).into()
17//! }
18//! ```
19use 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/// A generic widget that produces a message when pressed.
35///
36/// # Example
37/// ```no_run
38/// # mod iced { pub mod widget { pub use iced_widget::*; } }
39/// # pub type State = ();
40/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
41/// use iced::widget::button;
42///
43/// #[derive(Clone)]
44/// enum Message {
45///     ButtonPressed,
46/// }
47///
48/// fn view(state: &State) -> Element<'_, Message> {
49///     button("Press me!").on_press(Message::ButtonPressed).into()
50/// }
51/// ```
52///
53/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
54/// be disabled:
55///
56/// ```no_run
57/// # mod iced { pub mod widget { pub use iced_widget::*; } }
58/// # pub type State = ();
59/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
60/// use iced::widget::button;
61///
62/// #[derive(Clone)]
63/// enum Message {
64///     ButtonPressed,
65/// }
66///
67/// fn view(state: &State) -> Element<'_, Message> {
68///     button("I am disabled!").into()
69/// }
70/// ```
71#[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    /// Creates a new [`Button`] with the given content.
107    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    /// Sets the width of the [`Button`].
126    pub fn width(mut self, width: impl Into<Length>) -> Self {
127        self.width = width.into();
128        self
129    }
130
131    /// Sets the height of the [`Button`].
132    pub fn height(mut self, height: impl Into<Length>) -> Self {
133        self.height = height.into();
134        self
135    }
136
137    /// Sets the [`Padding`] of the [`Button`].
138    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
139        self.padding = padding.into();
140        self
141    }
142
143    /// Sets the message that will be produced when the [`Button`] is pressed.
144    ///
145    /// Unless `on_press` is called, the [`Button`] will be disabled.
146    pub fn on_press(mut self, on_press: Message) -> Self {
147        self.on_press = Some(OnPress::Direct(on_press));
148        self
149    }
150
151    /// Sets the message that will be produced when the [`Button`] is pressed.
152    ///
153    /// This is analogous to [`Button::on_press`], but using a closure to produce
154    /// the message.
155    ///
156    /// This closure will only be called when the [`Button`] is actually pressed and,
157    /// therefore, this method is useful to reduce overhead if creating the resulting
158    /// message is slow.
159    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    /// Sets the message that will be produced when the [`Button`] is pressed,
168    /// if `Some`.
169    ///
170    /// If `None`, the [`Button`] will be disabled.
171    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    /// Sets whether the contents of the [`Button`] should be clipped on
177    /// overflow.
178    pub fn clip(mut self, clip: bool) -> Self {
179        self.clip = clip;
180        self
181    }
182
183    /// Sets the style of the [`Button`].
184    #[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    /// Sets the style class of the [`Button`].
194    #[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
460/// The default [`Padding`] of a [`Button`].
461pub(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/// The possible status of a [`Button`].
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum Status {
471    /// The [`Button`] can be pressed.
472    Active,
473    /// The [`Button`] can be pressed and it is being hovered.
474    Hovered,
475    /// The [`Button`] is being pressed.
476    Pressed,
477    /// The [`Button`] cannot be pressed.
478    Disabled,
479}
480
481/// The style of a button.
482///
483/// If not specified with [`Button::style`]
484/// the theme will provide the style.
485#[derive(Debug, Clone, Copy, PartialEq)]
486pub struct Style {
487    /// The [`Background`] of the button.
488    pub background: Option<Background>,
489    /// The text [`Color`] of the button.
490    pub text_color: Color,
491    /// The [`Border`] of the button.
492    pub border: Border,
493    /// The [`Shadow`] of the button.
494    pub shadow: Shadow,
495}
496
497impl Style {
498    /// Updates the [`Style`] with the given [`Background`].
499    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
518/// The theme catalog of a [`Button`].
519///
520/// All themes that can be used with [`Button`]
521/// must implement this trait.
522///
523/// # Example
524/// ```no_run
525/// # use iced_widget::core::{Color, Background};
526/// # use iced_widget::button::{Catalog, Status, Style};
527/// # struct MyTheme;
528/// #[derive(Debug, Default)]
529/// pub enum ButtonClass {
530///     #[default]
531///     Primary,
532///     Secondary,
533///     Danger
534/// }
535///
536/// impl Catalog for MyTheme {
537///     type Class<'a> = ButtonClass;
538///     
539///     fn default<'a>() -> Self::Class<'a> {
540///         ButtonClass::default()
541///     }
542///     
543///
544///     fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
545///         let mut style = Style::default();
546///
547///         match class {
548///             ButtonClass::Primary => {
549///                 style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
550///             },
551///             ButtonClass::Secondary => {
552///                 style.background = Some(Background::Color(Color::WHITE));
553///             },
554///             ButtonClass::Danger => {
555///                 style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
556///             },
557///         }
558///
559///         style
560///     }
561/// }
562/// ```
563///
564/// Although, in order to use [`Button::style`]
565/// with `MyTheme`, [`Catalog::Class`] must implement
566/// `From<StyleFn<'_, MyTheme>>`.
567pub trait Catalog {
568    /// The item class of the [`Catalog`].
569    type Class<'a>;
570
571    /// The default class produced by the [`Catalog`].
572    fn default<'a>() -> Self::Class<'a>;
573
574    /// The [`Style`] of a class with the given status.
575    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
576}
577
578/// A styling function for a [`Button`].
579pub 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
593/// A primary button; denoting a main action.
594pub 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
608/// A secondary button; denoting a complementary action.
609pub 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
623/// A success button; denoting a good outcome.
624pub 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
638/// A warning button; denoting a risky action.
639pub 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
653/// A danger button; denoting a destructive action.
654pub 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
668/// A text button; useful for links.
669pub 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}