Skip to main content

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, Color, Element, Event, Layout, Length, Padding, Rectangle, Shadow, Shell, Size,
31    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/// ```
71pub 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    /// Creates a new [`Button`] with the given content.
106    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    /// Sets the width of the [`Button`].
123    pub fn width(mut self, width: impl Into<Length>) -> Self {
124        self.width = width.into();
125        self
126    }
127
128    /// Sets the height of the [`Button`].
129    pub fn height(mut self, height: impl Into<Length>) -> Self {
130        self.height = height.into();
131        self
132    }
133
134    /// Sets the [`Padding`] of the [`Button`].
135    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
136        self.padding = padding.into();
137        self
138    }
139
140    /// Sets the message that will be produced when the [`Button`] is pressed.
141    ///
142    /// Unless `on_press` is called, the [`Button`] will be disabled.
143    pub fn on_press(mut self, on_press: Message) -> Self {
144        self.on_press = Some(OnPress::Direct(on_press));
145        self
146    }
147
148    /// Sets the message that will be produced when the [`Button`] is pressed.
149    ///
150    /// This is analogous to [`Button::on_press`], but using a closure to produce
151    /// the message.
152    ///
153    /// This closure will only be called when the [`Button`] is actually pressed and,
154    /// therefore, this method is useful to reduce overhead if creating the resulting
155    /// message is slow.
156    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    /// Sets the message that will be produced when the [`Button`] is pressed,
162    /// if `Some`.
163    ///
164    /// If `None`, the [`Button`] will be disabled.
165    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    /// Sets whether the contents of the [`Button`] should be clipped on
171    /// overflow.
172    pub fn clip(mut self, clip: bool) -> Self {
173        self.clip = clip;
174        self
175    }
176
177    /// Sets the style of the [`Button`].
178    #[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    /// Sets the style class of the [`Button`].
188    #[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
442/// The default [`Padding`] of a [`Button`].
443pub const DEFAULT_PADDING: Padding = Padding {
444    top: 5.0,
445    bottom: 5.0,
446    right: 10.0,
447    left: 10.0,
448};
449
450/// The possible status of a [`Button`].
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452pub enum Status {
453    /// The [`Button`] can be pressed.
454    Active,
455    /// The [`Button`] can be pressed and it is being hovered.
456    Hovered,
457    /// The [`Button`] is being pressed.
458    Pressed,
459    /// The [`Button`] cannot be pressed.
460    Disabled,
461}
462
463/// The style of a button.
464///
465/// If not specified with [`Button::style`]
466/// the theme will provide the style.
467#[derive(Debug, Clone, Copy, PartialEq)]
468pub struct Style {
469    /// The [`Background`] of the button.
470    pub background: Option<Background>,
471    /// The text [`Color`] of the button.
472    pub text_color: Color,
473    /// The [`Border`] of the button.
474    pub border: Border,
475    /// The [`Shadow`] of the button.
476    pub shadow: Shadow,
477    /// Whether the button should be snapped to the pixel grid.
478    pub snap: bool,
479}
480
481impl Style {
482    /// Updates the [`Style`] with the given [`Background`].
483    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
503/// The theme catalog of a [`Button`].
504///
505/// All themes that can be used with [`Button`]
506/// must implement this trait.
507///
508/// # Example
509/// ```no_run
510/// # use iced_widget::core::{Color, Background};
511/// # use iced_widget::button::{Catalog, Status, Style};
512/// # struct MyTheme;
513/// #[derive(Debug, Default)]
514/// pub enum ButtonClass {
515///     #[default]
516///     Primary,
517///     Secondary,
518///     Danger
519/// }
520///
521/// impl Catalog for MyTheme {
522///     type Class<'a> = ButtonClass;
523///     
524///     fn default<'a>() -> Self::Class<'a> {
525///         ButtonClass::default()
526///     }
527///     
528///
529///     fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
530///         let mut style = Style::default();
531///
532///         match class {
533///             ButtonClass::Primary => {
534///                 style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
535///             },
536///             ButtonClass::Secondary => {
537///                 style.background = Some(Background::Color(Color::WHITE));
538///             },
539///             ButtonClass::Danger => {
540///                 style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
541///             },
542///         }
543///
544///         style
545///     }
546/// }
547/// ```
548///
549/// Although, in order to use [`Button::style`]
550/// with `MyTheme`, [`Catalog::Class`] must implement
551/// `From<StyleFn<'_, MyTheme>>`.
552pub trait Catalog {
553    /// The item class of the [`Catalog`].
554    type Class<'a>;
555
556    /// The default class produced by the [`Catalog`].
557    fn default<'a>() -> Self::Class<'a>;
558
559    /// The [`Style`] of a class with the given status.
560    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
561}
562
563/// A styling function for a [`Button`].
564pub 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
578/// A primary button; denoting a main action.
579pub 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
593/// A secondary button; denoting a complementary action.
594pub 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
608/// A success button; denoting a good outcome.
609pub 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
623/// A warning button; denoting a risky action.
624pub 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
638/// A danger button; denoting a destructive action.
639pub 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
653/// A text button; useful for links.
654pub 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
672/// A button using background shades.
673pub 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
691/// A subtle button using weak background shades.
692pub 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}