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, Rectangle, Shadow,
31    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/// ```
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        clipboard: &mut dyn Clipboard,
270        shell: &mut Shell<'_, Message>,
271        viewport: &Rectangle,
272    ) {
273        self.content.as_widget_mut().update(
274            &mut tree.children[0],
275            event,
276            layout.children().next().unwrap(),
277            cursor,
278            renderer,
279            clipboard,
280            shell,
281            viewport,
282        );
283
284        if shell.is_event_captured() {
285            return;
286        }
287
288        match event {
289            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
290            | Event::Touch(touch::Event::FingerPressed { .. }) => {
291                if self.on_press.is_some() {
292                    let bounds = layout.bounds();
293
294                    if cursor.is_over(bounds) {
295                        let state = tree.state.downcast_mut::<State>();
296
297                        state.is_pressed = true;
298
299                        shell.capture_event();
300                    }
301                }
302            }
303            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
304            | Event::Touch(touch::Event::FingerLifted { .. }) => {
305                if let Some(on_press) = &self.on_press {
306                    let state = tree.state.downcast_mut::<State>();
307
308                    if state.is_pressed {
309                        state.is_pressed = false;
310
311                        let bounds = layout.bounds();
312
313                        if cursor.is_over(bounds) {
314                            shell.publish(on_press.get());
315                        }
316
317                        shell.capture_event();
318                    }
319                }
320            }
321            Event::Touch(touch::Event::FingerLost { .. }) => {
322                let state = tree.state.downcast_mut::<State>();
323
324                state.is_pressed = false;
325            }
326            _ => {}
327        }
328
329        let current_status = if self.on_press.is_none() {
330            Status::Disabled
331        } else if cursor.is_over(layout.bounds()) {
332            let state = tree.state.downcast_ref::<State>();
333
334            if state.is_pressed {
335                Status::Pressed
336            } else {
337                Status::Hovered
338            }
339        } else {
340            Status::Active
341        };
342
343        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
344            self.status = Some(current_status);
345        } else if self.status.is_some_and(|status| status != current_status) {
346            shell.request_redraw();
347        }
348    }
349
350    fn draw(
351        &self,
352        tree: &Tree,
353        renderer: &mut Renderer,
354        theme: &Theme,
355        _style: &renderer::Style,
356        layout: Layout<'_>,
357        cursor: mouse::Cursor,
358        viewport: &Rectangle,
359    ) {
360        let bounds = layout.bounds();
361        let content_layout = layout.children().next().unwrap();
362        let style = theme.style(&self.class, self.status.unwrap_or(Status::Disabled));
363
364        if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
365            renderer.fill_quad(
366                renderer::Quad {
367                    bounds,
368                    border: style.border,
369                    shadow: style.shadow,
370                    snap: style.snap,
371                },
372                style
373                    .background
374                    .unwrap_or(Background::Color(Color::TRANSPARENT)),
375            );
376        }
377
378        let viewport = if self.clip {
379            bounds.intersection(viewport).unwrap_or(*viewport)
380        } else {
381            *viewport
382        };
383
384        self.content.as_widget().draw(
385            &tree.children[0],
386            renderer,
387            theme,
388            &renderer::Style {
389                text_color: style.text_color,
390            },
391            content_layout,
392            cursor,
393            &viewport,
394        );
395    }
396
397    fn mouse_interaction(
398        &self,
399        _tree: &Tree,
400        layout: Layout<'_>,
401        cursor: mouse::Cursor,
402        _viewport: &Rectangle,
403        _renderer: &Renderer,
404    ) -> mouse::Interaction {
405        let is_mouse_over = cursor.is_over(layout.bounds());
406
407        if is_mouse_over && self.on_press.is_some() {
408            mouse::Interaction::Pointer
409        } else {
410            mouse::Interaction::default()
411        }
412    }
413
414    fn overlay<'b>(
415        &'b mut self,
416        tree: &'b mut Tree,
417        layout: Layout<'b>,
418        renderer: &Renderer,
419        viewport: &Rectangle,
420        translation: Vector,
421    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
422        self.content.as_widget_mut().overlay(
423            &mut tree.children[0],
424            layout.children().next().unwrap(),
425            renderer,
426            viewport,
427            translation,
428        )
429    }
430}
431
432impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
433    for Element<'a, Message, Theme, Renderer>
434where
435    Message: Clone + 'a,
436    Theme: Catalog + 'a,
437    Renderer: crate::core::Renderer + 'a,
438{
439    fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
440        Self::new(button)
441    }
442}
443
444/// The default [`Padding`] of a [`Button`].
445pub const DEFAULT_PADDING: Padding = Padding {
446    top: 5.0,
447    bottom: 5.0,
448    right: 10.0,
449    left: 10.0,
450};
451
452/// The possible status of a [`Button`].
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454pub enum Status {
455    /// The [`Button`] can be pressed.
456    Active,
457    /// The [`Button`] can be pressed and it is being hovered.
458    Hovered,
459    /// The [`Button`] is being pressed.
460    Pressed,
461    /// The [`Button`] cannot be pressed.
462    Disabled,
463}
464
465/// The style of a button.
466///
467/// If not specified with [`Button::style`]
468/// the theme will provide the style.
469#[derive(Debug, Clone, Copy, PartialEq)]
470pub struct Style {
471    /// The [`Background`] of the button.
472    pub background: Option<Background>,
473    /// The text [`Color`] of the button.
474    pub text_color: Color,
475    /// The [`Border`] of the button.
476    pub border: Border,
477    /// The [`Shadow`] of the button.
478    pub shadow: Shadow,
479    /// Whether the button should be snapped to the pixel grid.
480    pub snap: bool,
481}
482
483impl Style {
484    /// Updates the [`Style`] with the given [`Background`].
485    pub fn with_background(self, background: impl Into<Background>) -> Self {
486        Self {
487            background: Some(background.into()),
488            ..self
489        }
490    }
491}
492
493impl Default for Style {
494    fn default() -> Self {
495        Self {
496            background: None,
497            text_color: Color::BLACK,
498            border: Border::default(),
499            shadow: Shadow::default(),
500            snap: cfg!(feature = "crisp"),
501        }
502    }
503}
504
505/// The theme catalog of a [`Button`].
506///
507/// All themes that can be used with [`Button`]
508/// must implement this trait.
509///
510/// # Example
511/// ```no_run
512/// # use iced_widget::core::{Color, Background};
513/// # use iced_widget::button::{Catalog, Status, Style};
514/// # struct MyTheme;
515/// #[derive(Debug, Default)]
516/// pub enum ButtonClass {
517///     #[default]
518///     Primary,
519///     Secondary,
520///     Danger
521/// }
522///
523/// impl Catalog for MyTheme {
524///     type Class<'a> = ButtonClass;
525///     
526///     fn default<'a>() -> Self::Class<'a> {
527///         ButtonClass::default()
528///     }
529///     
530///
531///     fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
532///         let mut style = Style::default();
533///
534///         match class {
535///             ButtonClass::Primary => {
536///                 style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921)));
537///             },
538///             ButtonClass::Secondary => {
539///                 style.background = Some(Background::Color(Color::WHITE));
540///             },
541///             ButtonClass::Danger => {
542///                 style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502)));
543///             },
544///         }
545///
546///         style
547///     }
548/// }
549/// ```
550///
551/// Although, in order to use [`Button::style`]
552/// with `MyTheme`, [`Catalog::Class`] must implement
553/// `From<StyleFn<'_, MyTheme>>`.
554pub trait Catalog {
555    /// The item class of the [`Catalog`].
556    type Class<'a>;
557
558    /// The default class produced by the [`Catalog`].
559    fn default<'a>() -> Self::Class<'a>;
560
561    /// The [`Style`] of a class with the given status.
562    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
563}
564
565/// A styling function for a [`Button`].
566pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
567
568impl Catalog for Theme {
569    type Class<'a> = StyleFn<'a, Self>;
570
571    fn default<'a>() -> Self::Class<'a> {
572        Box::new(primary)
573    }
574
575    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
576        class(self, status)
577    }
578}
579
580/// A primary button; denoting a main action.
581pub fn primary(theme: &Theme, status: Status) -> Style {
582    let palette = theme.extended_palette();
583    let base = styled(palette.primary.base);
584
585    match status {
586        Status::Active | Status::Pressed => base,
587        Status::Hovered => Style {
588            background: Some(Background::Color(palette.primary.strong.color)),
589            ..base
590        },
591        Status::Disabled => disabled(base),
592    }
593}
594
595/// A secondary button; denoting a complementary action.
596pub fn secondary(theme: &Theme, status: Status) -> Style {
597    let palette = theme.extended_palette();
598    let base = styled(palette.secondary.base);
599
600    match status {
601        Status::Active | Status::Pressed => base,
602        Status::Hovered => Style {
603            background: Some(Background::Color(palette.secondary.strong.color)),
604            ..base
605        },
606        Status::Disabled => disabled(base),
607    }
608}
609
610/// A success button; denoting a good outcome.
611pub fn success(theme: &Theme, status: Status) -> Style {
612    let palette = theme.extended_palette();
613    let base = styled(palette.success.base);
614
615    match status {
616        Status::Active | Status::Pressed => base,
617        Status::Hovered => Style {
618            background: Some(Background::Color(palette.success.strong.color)),
619            ..base
620        },
621        Status::Disabled => disabled(base),
622    }
623}
624
625/// A warning button; denoting a risky action.
626pub fn warning(theme: &Theme, status: Status) -> Style {
627    let palette = theme.extended_palette();
628    let base = styled(palette.warning.base);
629
630    match status {
631        Status::Active | Status::Pressed => base,
632        Status::Hovered => Style {
633            background: Some(Background::Color(palette.warning.strong.color)),
634            ..base
635        },
636        Status::Disabled => disabled(base),
637    }
638}
639
640/// A danger button; denoting a destructive action.
641pub fn danger(theme: &Theme, status: Status) -> Style {
642    let palette = theme.extended_palette();
643    let base = styled(palette.danger.base);
644
645    match status {
646        Status::Active | Status::Pressed => base,
647        Status::Hovered => Style {
648            background: Some(Background::Color(palette.danger.strong.color)),
649            ..base
650        },
651        Status::Disabled => disabled(base),
652    }
653}
654
655/// A text button; useful for links.
656pub fn text(theme: &Theme, status: Status) -> Style {
657    let palette = theme.extended_palette();
658
659    let base = Style {
660        text_color: palette.background.base.text,
661        ..Style::default()
662    };
663
664    match status {
665        Status::Active | Status::Pressed => base,
666        Status::Hovered => Style {
667            text_color: palette.background.base.text.scale_alpha(0.8),
668            ..base
669        },
670        Status::Disabled => disabled(base),
671    }
672}
673
674/// A button using background shades.
675pub fn background(theme: &Theme, status: Status) -> Style {
676    let palette = theme.extended_palette();
677    let base = styled(palette.background.base);
678
679    match status {
680        Status::Active => base,
681        Status::Pressed => Style {
682            background: Some(Background::Color(palette.background.strong.color)),
683            ..base
684        },
685        Status::Hovered => Style {
686            background: Some(Background::Color(palette.background.weak.color)),
687            ..base
688        },
689        Status::Disabled => disabled(base),
690    }
691}
692
693/// A subtle button using weak background shades.
694pub fn subtle(theme: &Theme, status: Status) -> Style {
695    let palette = theme.extended_palette();
696    let base = styled(palette.background.weakest);
697
698    match status {
699        Status::Active => base,
700        Status::Pressed => Style {
701            background: Some(Background::Color(palette.background.strong.color)),
702            ..base
703        },
704        Status::Hovered => Style {
705            background: Some(Background::Color(palette.background.weaker.color)),
706            ..base
707        },
708        Status::Disabled => disabled(base),
709    }
710}
711
712fn styled(pair: palette::Pair) -> Style {
713    Style {
714        background: Some(Background::Color(pair.color)),
715        text_color: pair.text,
716        border: border::rounded(2),
717        ..Style::default()
718    }
719}
720
721fn disabled(style: Style) -> Style {
722    Style {
723        background: style
724            .background
725            .map(|background| background.scale_alpha(0.5)),
726        text_color: style.text_color.scale_alpha(0.5),
727        ..style
728    }
729}