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