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                    snap: style.snap,
388                },
389                style
390                    .background
391                    .unwrap_or(Background::Color(Color::TRANSPARENT)),
392            );
393        }
394
395        let viewport = if self.clip {
396            bounds.intersection(viewport).unwrap_or(*viewport)
397        } else {
398            *viewport
399        };
400
401        self.content.as_widget().draw(
402            &tree.children[0],
403            renderer,
404            theme,
405            &renderer::Style {
406                text_color: style.text_color,
407            },
408            content_layout,
409            cursor,
410            &viewport,
411        );
412    }
413
414    fn mouse_interaction(
415        &self,
416        _tree: &Tree,
417        layout: Layout<'_>,
418        cursor: mouse::Cursor,
419        _viewport: &Rectangle,
420        _renderer: &Renderer,
421    ) -> mouse::Interaction {
422        let is_mouse_over = cursor.is_over(layout.bounds());
423
424        if is_mouse_over && self.on_press.is_some() {
425            mouse::Interaction::Pointer
426        } else {
427            mouse::Interaction::default()
428        }
429    }
430
431    fn overlay<'b>(
432        &'b mut self,
433        tree: &'b mut Tree,
434        layout: Layout<'b>,
435        renderer: &Renderer,
436        viewport: &Rectangle,
437        translation: Vector,
438    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
439        self.content.as_widget_mut().overlay(
440            &mut tree.children[0],
441            layout.children().next().unwrap(),
442            renderer,
443            viewport,
444            translation,
445        )
446    }
447}
448
449impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
450    for Element<'a, Message, Theme, Renderer>
451where
452    Message: Clone + 'a,
453    Theme: Catalog + 'a,
454    Renderer: crate::core::Renderer + 'a,
455{
456    fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
457        Self::new(button)
458    }
459}
460
461/// The default [`Padding`] of a [`Button`].
462pub(crate) const DEFAULT_PADDING: Padding = Padding {
463    top: 5.0,
464    bottom: 5.0,
465    right: 10.0,
466    left: 10.0,
467};
468
469/// The possible status of a [`Button`].
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub enum Status {
472    /// The [`Button`] can be pressed.
473    Active,
474    /// The [`Button`] can be pressed and it is being hovered.
475    Hovered,
476    /// The [`Button`] is being pressed.
477    Pressed,
478    /// The [`Button`] cannot be pressed.
479    Disabled,
480}
481
482/// The style of a button.
483///
484/// If not specified with [`Button::style`]
485/// the theme will provide the style.
486#[derive(Debug, Clone, Copy, PartialEq)]
487pub struct Style {
488    /// The [`Background`] of the button.
489    pub background: Option<Background>,
490    /// The text [`Color`] of the button.
491    pub text_color: Color,
492    /// The [`Border`] of the button.
493    pub border: Border,
494    /// The [`Shadow`] of the button.
495    pub shadow: Shadow,
496    /// Whether the button should be snapped to the pixel grid.
497    pub snap: bool,
498}
499
500impl Style {
501    /// Updates the [`Style`] with the given [`Background`].
502    pub fn with_background(self, background: impl Into<Background>) -> Self {
503        Self {
504            background: Some(background.into()),
505            ..self
506        }
507    }
508}
509
510impl Default for Style {
511    fn default() -> Self {
512        Self {
513            background: None,
514            text_color: Color::BLACK,
515            border: Border::default(),
516            shadow: Shadow::default(),
517            snap: cfg!(feature = "crisp"),
518        }
519    }
520}
521
522/// The theme catalog of a [`Button`].
523///
524/// All themes that can be used with [`Button`]
525/// must implement this trait.
526///
527/// # Example
528/// ```no_run
529/// # use iced_widget::core::{Color, Background};
530/// # use iced_widget::button::{Catalog, Status, Style};
531/// # struct MyTheme;
532/// #[derive(Debug, Default)]
533/// pub enum ButtonClass {
534///     #[default]
535///     Primary,
536///     Secondary,
537///     Danger
538/// }
539///
540/// impl Catalog for MyTheme {
541///     type Class<'a> = ButtonClass;
542///     
543///     fn default<'a>() -> Self::Class<'a> {
544///         ButtonClass::default()
545///     }
546///     
547///
548///     fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
549///         let mut style = Style::default();
550///
551///         match class {
552///             ButtonClass::Primary => {
553///                 style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
554///             },
555///             ButtonClass::Secondary => {
556///                 style.background = Some(Background::Color(Color::WHITE));
557///             },
558///             ButtonClass::Danger => {
559///                 style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
560///             },
561///         }
562///
563///         style
564///     }
565/// }
566/// ```
567///
568/// Although, in order to use [`Button::style`]
569/// with `MyTheme`, [`Catalog::Class`] must implement
570/// `From<StyleFn<'_, MyTheme>>`.
571pub trait Catalog {
572    /// The item class of the [`Catalog`].
573    type Class<'a>;
574
575    /// The default class produced by the [`Catalog`].
576    fn default<'a>() -> Self::Class<'a>;
577
578    /// The [`Style`] of a class with the given status.
579    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
580}
581
582/// A styling function for a [`Button`].
583pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
584
585impl Catalog for Theme {
586    type Class<'a> = StyleFn<'a, Self>;
587
588    fn default<'a>() -> Self::Class<'a> {
589        Box::new(primary)
590    }
591
592    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
593        class(self, status)
594    }
595}
596
597/// A primary button; denoting a main action.
598pub fn primary(theme: &Theme, status: Status) -> Style {
599    let palette = theme.extended_palette();
600    let base = styled(palette.primary.base);
601
602    match status {
603        Status::Active | Status::Pressed => base,
604        Status::Hovered => Style {
605            background: Some(Background::Color(palette.primary.strong.color)),
606            ..base
607        },
608        Status::Disabled => disabled(base),
609    }
610}
611
612/// A secondary button; denoting a complementary action.
613pub fn secondary(theme: &Theme, status: Status) -> Style {
614    let palette = theme.extended_palette();
615    let base = styled(palette.secondary.base);
616
617    match status {
618        Status::Active | Status::Pressed => base,
619        Status::Hovered => Style {
620            background: Some(Background::Color(palette.secondary.strong.color)),
621            ..base
622        },
623        Status::Disabled => disabled(base),
624    }
625}
626
627/// A success button; denoting a good outcome.
628pub fn success(theme: &Theme, status: Status) -> Style {
629    let palette = theme.extended_palette();
630    let base = styled(palette.success.base);
631
632    match status {
633        Status::Active | Status::Pressed => base,
634        Status::Hovered => Style {
635            background: Some(Background::Color(palette.success.strong.color)),
636            ..base
637        },
638        Status::Disabled => disabled(base),
639    }
640}
641
642/// A warning button; denoting a risky action.
643pub fn warning(theme: &Theme, status: Status) -> Style {
644    let palette = theme.extended_palette();
645    let base = styled(palette.warning.base);
646
647    match status {
648        Status::Active | Status::Pressed => base,
649        Status::Hovered => Style {
650            background: Some(Background::Color(palette.warning.strong.color)),
651            ..base
652        },
653        Status::Disabled => disabled(base),
654    }
655}
656
657/// A danger button; denoting a destructive action.
658pub fn danger(theme: &Theme, status: Status) -> Style {
659    let palette = theme.extended_palette();
660    let base = styled(palette.danger.base);
661
662    match status {
663        Status::Active | Status::Pressed => base,
664        Status::Hovered => Style {
665            background: Some(Background::Color(palette.danger.strong.color)),
666            ..base
667        },
668        Status::Disabled => disabled(base),
669    }
670}
671
672/// A text button; useful for links.
673pub fn text(theme: &Theme, status: Status) -> Style {
674    let palette = theme.extended_palette();
675
676    let base = Style {
677        text_color: palette.background.base.text,
678        ..Style::default()
679    };
680
681    match status {
682        Status::Active | Status::Pressed => base,
683        Status::Hovered => Style {
684            text_color: palette.background.base.text.scale_alpha(0.8),
685            ..base
686        },
687        Status::Disabled => disabled(base),
688    }
689}
690
691fn styled(pair: palette::Pair) -> Style {
692    Style {
693        background: Some(Background::Color(pair.color)),
694        text_color: pair.text,
695        border: border::rounded(2),
696        ..Style::default()
697    }
698}
699
700fn disabled(style: Style) -> Style {
701    Style {
702        background: style
703            .background
704            .map(|background| background.scale_alpha(0.5)),
705        text_color: style.text_color.scale_alpha(0.5),
706        ..style
707    }
708}