Skip to main content

cosmic/widget/button/
widget.rs

1// Copyright 2019 H�ctor Ram�n, Iced contributors
2// Copyright 2023 System76 <info@system76.com>
3// SPDX-License-Identifier: MIT
4
5//! Allow your users to perform actions by pressing a button.
6//!
7//! A [`Button`] has some local [`State`].
8
9use iced::Alignment;
10use iced_runtime::core::widget::Id;
11use iced_runtime::{Action, Task, keyboard, task};
12
13use iced_core::event::{self, Event};
14use iced_core::renderer::{self, Quad, Renderer};
15use iced_core::widget::Operation;
16use iced_core::widget::tree::{self, Tree};
17use iced_core::{
18    Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell,
19    Vector, Widget, layout, mouse, overlay, svg, touch,
20};
21use iced_renderer::core::widget::operation;
22
23use crate::theme::THEME;
24
25pub use super::style::{Catalog, Style};
26
27/// Internally defines different button widget variants.
28enum Variant<Message> {
29    Normal,
30    Image {
31        close_icon: svg::Handle,
32        on_remove: Option<Message>,
33    },
34}
35
36/// A generic button which emits a message when pressed.
37#[allow(missing_debug_implementations)]
38#[must_use]
39pub struct Button<'a, Message> {
40    id: Id,
41    #[cfg(feature = "a11y")]
42    name: Option<std::borrow::Cow<'a, str>>,
43    #[cfg(feature = "a11y")]
44    description: Option<iced_accessibility::Description<'a>>,
45    #[cfg(feature = "a11y")]
46    label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
47    content: crate::Element<'a, Message>,
48    on_press: Option<Box<dyn Fn(Vector, Rectangle) -> Message + 'a>>,
49    on_press_down: Option<Box<dyn Fn(Vector, Rectangle) -> Message + 'a>>,
50    width: Length,
51    height: Length,
52    padding: Padding,
53    selected: bool,
54    style: crate::theme::Button,
55    variant: Variant<Message>,
56    force_enabled: bool,
57}
58
59impl<'a, Message: Clone + 'a> Button<'a, Message> {
60    /// Creates a new [`Button`] with the given content.
61    pub(super) fn new(content: impl Into<crate::Element<'a, Message>>) -> Self {
62        Self {
63            id: Id::unique(),
64            #[cfg(feature = "a11y")]
65            name: None,
66            #[cfg(feature = "a11y")]
67            description: None,
68            #[cfg(feature = "a11y")]
69            label: None,
70            content: content.into(),
71            on_press: None,
72            on_press_down: None,
73            width: Length::Shrink,
74            height: Length::Shrink,
75            padding: Padding::new(5.0),
76            selected: false,
77            style: crate::theme::Button::default(),
78            variant: Variant::Normal,
79            force_enabled: false,
80        }
81    }
82
83    pub fn new_image(
84        content: impl Into<crate::Element<'a, Message>>,
85        on_remove: Option<Message>,
86    ) -> Self {
87        Self {
88            id: Id::unique(),
89            #[cfg(feature = "a11y")]
90            name: None,
91            #[cfg(feature = "a11y")]
92            description: None,
93            force_enabled: false,
94            #[cfg(feature = "a11y")]
95            label: None,
96            content: content.into(),
97            on_press: None,
98            on_press_down: None,
99            width: Length::Shrink,
100            height: Length::Shrink,
101            padding: Padding::new(5.0),
102            selected: false,
103            style: crate::theme::Button::default(),
104            variant: Variant::Image {
105                on_remove,
106                close_icon: crate::widget::icon::from_name("window-close-symbolic")
107                    .size(8)
108                    .icon()
109                    .into_svg_handle()
110                    .unwrap_or_else(|| {
111                        let bytes: &'static [u8] = &[];
112                        iced_core::svg::Handle::from_memory(bytes)
113                    }),
114            },
115        }
116    }
117
118    /// Sets the [`Id`] of the [`Button`].
119    #[inline]
120    pub fn id(mut self, id: Id) -> Self {
121        self.id = id;
122        self
123    }
124
125    /// Sets the width of the [`Button`].
126    #[inline]
127    pub fn width(mut self, width: impl Into<Length>) -> Self {
128        self.width = width.into();
129        self
130    }
131
132    /// Sets the height of the [`Button`].
133    #[inline]
134    pub fn height(mut self, height: impl Into<Length>) -> Self {
135        self.height = height.into();
136        self
137    }
138
139    /// Sets the [`Padding`] of the [`Button`].
140    #[inline]
141    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
142        self.padding = padding.into();
143        self
144    }
145
146    /// Sets the message that will be produced when the [`Button`] is pressed and released.
147    ///
148    /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
149    #[inline]
150    pub fn on_press(mut self, on_press: Message) -> Self {
151        self.on_press = Some(Box::new(move |_, _| on_press.clone()));
152        self
153    }
154
155    /// Sets the message that will be produced when the [`Button`] is pressed and released.
156    ///
157    /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
158    #[inline]
159    pub fn on_press_with_rectangle(
160        mut self,
161        on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
162    ) -> Self {
163        self.on_press = Some(Box::new(on_press));
164        self
165    }
166
167    /// Sets the message that will be produced when the [`Button`] is pressed,
168    ///
169    /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
170    #[inline]
171    pub fn on_press_down(mut self, on_press: Message) -> Self {
172        self.on_press_down = Some(Box::new(move |_, _| on_press.clone()));
173        self
174    }
175
176    /// Sets the message that will be produced when the [`Button`] is pressed,
177    ///
178    /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
179    #[inline]
180    pub fn on_press_down_with_rectange(
181        mut self,
182        on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
183    ) -> Self {
184        self.on_press_down = Some(Box::new(on_press));
185        self
186    }
187
188    /// Sets the message that will be produced when the [`Button`] is pressed,
189    /// if `Some`.
190    ///
191    /// If `None`, the [`Button`] will be disabled.
192    #[inline]
193    pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
194        if let Some(m) = on_press {
195            self.on_press(m)
196        } else {
197            self.on_press = None;
198            self
199        }
200    }
201
202    /// Sets the message that will be produced when the [`Button`] is pressed and released.
203    ///
204    /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
205    #[inline]
206    pub fn on_press_maybe_with_rectangle(
207        mut self,
208        on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
209    ) -> Self {
210        self.on_press = Some(Box::new(on_press));
211        self
212    }
213
214    /// Sets the message that will be produced when the [`Button`] is pressed,
215    /// if `Some`.
216    ///
217    /// If `None`, the [`Button`] will be disabled.
218    #[inline]
219    pub fn on_press_down_maybe(mut self, on_press: Option<Message>) -> Self {
220        if let Some(m) = on_press {
221            self.on_press(m)
222        } else {
223            self.on_press_down = None;
224            self
225        }
226    }
227
228    /// Sets the message that will be produced when the [`Button`] is pressed and released.
229    ///
230    /// Unless `on_press` or `on_press_down` is called, the [`Button`] will be disabled.
231    #[inline]
232    pub fn on_press_down_maybe_with_rectangle(
233        mut self,
234        on_press: impl Fn(Vector, Rectangle) -> Message + 'a,
235    ) -> Self {
236        self.on_press_down = Some(Box::new(on_press));
237        self
238    }
239
240    /// Sets the the [`Button`] to enabled whether or not it has handlers for on press.
241    #[inline]
242    pub fn force_enabled(mut self, enabled: bool) -> Self {
243        self.force_enabled = enabled;
244        self
245    }
246
247    /// Sets the widget to a selected state.
248    ///
249    /// Displays a selection indicator on image buttons.
250    #[inline]
251    pub fn selected(mut self, selected: bool) -> Self {
252        self.selected = selected;
253
254        self
255    }
256
257    /// Sets the style variant of this [`Button`].
258    #[inline]
259    pub fn class(mut self, style: crate::theme::Button) -> Self {
260        self.style = style;
261        self
262    }
263
264    #[cfg(feature = "a11y")]
265    /// Sets the name of the [`Button`].
266    pub fn name(mut self, name: impl Into<std::borrow::Cow<'a, str>>) -> Self {
267        self.name = Some(name.into());
268        self
269    }
270
271    #[cfg(feature = "a11y")]
272    /// Sets the description of the [`Button`].
273    pub fn description_widget<T: iced_accessibility::Describes>(mut self, description: &T) -> Self {
274        self.description = Some(iced_accessibility::Description::Id(
275            description.description(),
276        ));
277        self
278    }
279
280    #[cfg(feature = "a11y")]
281    /// Sets the description of the [`Button`].
282    pub fn description(mut self, description: impl Into<std::borrow::Cow<'a, str>>) -> Self {
283        self.description = Some(iced_accessibility::Description::Text(description.into()));
284        self
285    }
286
287    #[cfg(feature = "a11y")]
288    /// Sets the label of the [`Button`].
289    pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
290        self.label = Some(label.label().into_iter().map(|l| l.into()).collect());
291        self
292    }
293}
294
295impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
296    for Button<'a, Message>
297{
298    fn tag(&self) -> tree::Tag {
299        tree::Tag::of::<State>()
300    }
301
302    fn state(&self) -> tree::State {
303        tree::State::new(State::new())
304    }
305
306    fn children(&self) -> Vec<Tree> {
307        vec![Tree::new(&self.content)]
308    }
309
310    fn diff(&mut self, tree: &mut Tree) {
311        tree.diff_children(std::slice::from_mut(&mut self.content));
312    }
313
314    fn size(&self) -> iced_core::Size<Length> {
315        iced_core::Size::new(self.width, self.height)
316    }
317
318    fn layout(
319        &mut self,
320        tree: &mut Tree,
321        renderer: &crate::Renderer,
322        limits: &layout::Limits,
323    ) -> layout::Node {
324        layout(
325            renderer,
326            limits,
327            self.width,
328            self.height,
329            self.padding,
330            |renderer, limits| {
331                self.content
332                    .as_widget_mut()
333                    .layout(&mut tree.children[0], renderer, limits)
334            },
335        )
336    }
337
338    fn operate(
339        &mut self,
340        tree: &mut Tree,
341        layout: Layout<'_>,
342        renderer: &crate::Renderer,
343        operation: &mut dyn Operation<()>,
344    ) {
345        operation.container(None, layout.bounds());
346        operation.traverse(&mut |operation| {
347            self.content.as_widget_mut().operate(
348                &mut tree.children[0],
349                layout
350                    .children()
351                    .next()
352                    .unwrap()
353                    .with_virtual_offset(layout.virtual_offset()),
354                renderer,
355                operation,
356            );
357        });
358        let state = tree.state.downcast_mut::<State>();
359        operation.focusable(Some(&self.id), layout.bounds(), state);
360    }
361
362    fn update(
363        &mut self,
364        tree: &mut Tree,
365        event: &Event,
366        layout: Layout<'_>,
367        cursor: mouse::Cursor,
368        renderer: &crate::Renderer,
369        clipboard: &mut dyn Clipboard,
370        shell: &mut Shell<'_, Message>,
371        viewport: &Rectangle,
372    ) {
373        if let Variant::Image {
374            on_remove: Some(on_remove),
375            ..
376        } = &self.variant
377        {
378            // Capture mouse/touch events on the removal button
379            match event {
380                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
381                | Event::Touch(touch::Event::FingerPressed { .. }) => {
382                    if let Some(position) = cursor.position() {
383                        if removal_bounds(layout.bounds(), 4.0).contains(position) {
384                            shell.publish(on_remove.clone());
385                            shell.capture_event();
386                            return;
387                        }
388                    }
389                }
390
391                _ => (),
392            }
393        }
394        self.content.as_widget_mut().update(
395            &mut tree.children[0],
396            event,
397            layout
398                .children()
399                .next()
400                .unwrap()
401                .with_virtual_offset(layout.virtual_offset()),
402            cursor,
403            renderer,
404            clipboard,
405            shell,
406            viewport,
407        );
408        if shell.is_event_captured() {
409            return;
410        }
411
412        update(
413            self.id.clone(),
414            event,
415            layout,
416            cursor,
417            shell,
418            self.on_press.as_deref(),
419            self.on_press_down.as_deref(),
420            || tree.state.downcast_mut::<State>(),
421        )
422    }
423
424    #[allow(clippy::too_many_lines)]
425    fn draw(
426        &self,
427        tree: &Tree,
428        renderer: &mut crate::Renderer,
429        theme: &crate::Theme,
430        renderer_style: &renderer::Style,
431        layout: Layout<'_>,
432        cursor: mouse::Cursor,
433        viewport: &Rectangle,
434    ) {
435        let bounds = layout.bounds();
436        if !viewport.intersects(&bounds) {
437            return;
438        }
439
440        // FIXME: Why is there no content layout
441        let Some(content_layout) = layout.children().next() else {
442            return;
443        };
444
445        let mut headerbar_alpha = None;
446
447        let is_enabled =
448            self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled;
449        let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
450
451        let state = tree.state.downcast_ref::<State>();
452
453        let mut styling = if !is_enabled {
454            theme.disabled(&self.style)
455        } else if is_mouse_over {
456            if state.is_pressed {
457                if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
458                    headerbar_alpha = Some(0.8);
459                }
460
461                theme.pressed(state.is_focused, self.selected, &self.style)
462            } else {
463                if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
464                    headerbar_alpha = Some(0.8);
465                }
466                theme.hovered(state.is_focused, self.selected, &self.style)
467            }
468        } else {
469            if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
470                headerbar_alpha = Some(0.75);
471            }
472
473            theme.active(state.is_focused, self.selected, &self.style)
474        };
475        if matches!(
476            self.style,
477            crate::theme::Button::MenuItem | crate::theme::Button::MenuFolder
478        ) {
479            match theme.list_item_position {
480                Some((Alignment::Start, _)) => {
481                    styling.border_radius =
482                        styling.border_radius.bottom(theme.cosmic().radius_0()[3]);
483                }
484                Some((Alignment::End, _)) => {
485                    styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]);
486                }
487                Some((Alignment::Center, _)) => {}
488                None => {
489                    styling.border_radius = theme.cosmic().radius_0().into();
490                }
491            }
492        }
493
494        let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
495
496        // Menu roots should share the accent color that icons get in the header.
497        let mut text_color = if matches!(self.style, crate::theme::Button::MenuRoot) {
498            icon_color
499        } else {
500            styling.text_color.unwrap_or(renderer_style.text_color)
501        };
502
503        if let Some(alpha) = headerbar_alpha {
504            icon_color.a = alpha;
505            text_color.a = alpha;
506        }
507
508        draw::<_, crate::Theme>(
509            renderer,
510            bounds,
511            *viewport,
512            &styling,
513            |renderer, _styling| {
514                self.content.as_widget().draw(
515                    &tree.children[0],
516                    renderer,
517                    theme,
518                    &renderer::Style {
519                        icon_color,
520                        text_color,
521                        scale_factor: renderer_style.scale_factor,
522                    },
523                    content_layout.with_virtual_offset(layout.virtual_offset()),
524                    cursor,
525                    &viewport.intersection(&bounds).unwrap_or_default(),
526                );
527            },
528            matches!(self.variant, Variant::Image { .. }),
529        );
530
531        if let Variant::Image {
532            close_icon,
533            on_remove,
534        } = &self.variant
535        {
536            renderer.with_layer(*viewport, |renderer| {
537                let selection_background = theme.selection_background();
538
539                let c_rad = THEME.lock().unwrap().cosmic().corner_radii;
540
541                if self.selected {
542                    renderer.fill_quad(
543                        Quad {
544                            bounds: Rectangle {
545                                width: 24.0,
546                                height: 20.0,
547                                x: bounds.x + styling.border_width,
548                                y: bounds.y + (bounds.height - 20.0 - styling.border_width),
549                            },
550                            border: Border {
551                                radius: [
552                                    c_rad.radius_0[0],
553                                    c_rad.radius_s[1],
554                                    c_rad.radius_0[2],
555                                    c_rad.radius_s[3],
556                                ]
557                                .into(),
558                                ..Default::default()
559                            },
560                            shadow: Shadow::default(),
561                            snap: true,
562                        },
563                        selection_background,
564                    );
565
566                    let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone())
567                        .color(icon_color);
568                    let bounds = Rectangle {
569                        width: 16.0,
570                        height: 16.0,
571                        x: bounds.x + 5.0 + styling.border_width,
572                        y: bounds.y + (bounds.height - 18.0 - styling.border_width),
573                    };
574                    if bounds.intersects(viewport) {
575                        iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds);
576                    }
577                }
578
579                if on_remove.is_some() {
580                    if let Some(position) = cursor.position() {
581                        if bounds.contains(position) {
582                            let bounds = removal_bounds(layout.bounds(), 4.0);
583                            renderer.fill_quad(
584                                renderer::Quad {
585                                    bounds,
586                                    shadow: Shadow::default(),
587                                    border: Border {
588                                        radius: c_rad.radius_m.into(),
589                                        ..Default::default()
590                                    },
591                                    snap: true,
592                                },
593                                selection_background,
594                            );
595                            let svg_handle = svg::Svg::new(close_icon.clone()).color(icon_color);
596                            iced_core::svg::Renderer::draw_svg(
597                                renderer,
598                                svg_handle,
599                                Rectangle {
600                                    width: 16.0,
601                                    height: 16.0,
602                                    x: bounds.x + 4.0,
603                                    y: bounds.y + 4.0,
604                                },
605                                Rectangle {
606                                    width: 16.0,
607                                    height: 16.0,
608                                    x: bounds.x + 4.0,
609                                    y: bounds.y + 4.0,
610                                },
611                            );
612                        }
613                    }
614                }
615            });
616        }
617    }
618
619    fn mouse_interaction(
620        &self,
621        _tree: &Tree,
622        layout: Layout<'_>,
623        cursor: mouse::Cursor,
624        _viewport: &Rectangle,
625        _renderer: &crate::Renderer,
626    ) -> mouse::Interaction {
627        mouse_interaction(
628            layout.with_virtual_offset(layout.virtual_offset()),
629            cursor,
630            self.on_press.is_some(),
631        )
632    }
633
634    fn overlay<'b>(
635        &'b mut self,
636        tree: &'b mut Tree,
637        layout: Layout<'b>,
638        renderer: &crate::Renderer,
639        viewport: &Rectangle,
640        mut translation: Vector,
641    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
642        let position = layout.bounds().position();
643        translation.x += position.x;
644        translation.y += position.y;
645        self.content.as_widget_mut().overlay(
646            &mut tree.children[0],
647            layout
648                .children()
649                .next()
650                .unwrap()
651                .with_virtual_offset(layout.virtual_offset()),
652            renderer,
653            viewport,
654            translation,
655        )
656    }
657
658    #[cfg(feature = "a11y")]
659    /// get the a11y nodes for the widget
660    fn a11y_nodes(
661        &self,
662        layout: Layout<'_>,
663        state: &Tree,
664        p: mouse::Cursor,
665    ) -> iced_accessibility::A11yTree {
666        use iced_accessibility::accesskit::{Action, Node, NodeId, Rect, Role};
667        use iced_accessibility::{A11yNode, A11yTree};
668        // TODO why is state None sometimes?
669        if matches!(state.state, iced_core::widget::tree::State::None) {
670            tracing::info!("Button state is missing.");
671            return A11yTree::default();
672        }
673
674        let child_layout = layout.children().next().unwrap();
675        let child_tree = state.children.first();
676
677        let Rectangle {
678            x,
679            y,
680            width,
681            height,
682        } = layout.bounds();
683        let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64);
684        let is_hovered = state.state.downcast_ref::<State>().is_hovered;
685
686        let mut node = Node::new(Role::Button);
687        node.add_action(Action::Focus);
688        node.add_action(Action::Click);
689        node.set_bounds(bounds);
690        if let Some(name) = self.name.as_ref() {
691            node.set_label(name.clone());
692        }
693        match self.description.as_ref() {
694            Some(iced_accessibility::Description::Id(id)) => {
695                node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
696            }
697            Some(iced_accessibility::Description::Text(text)) => {
698                node.set_description(text.clone());
699            }
700            None => {}
701        }
702
703        if let Some(label) = self.label.as_ref() {
704            node.set_labelled_by(label.clone());
705        }
706
707        if self.on_press.is_none() {
708            node.set_disabled();
709        }
710        // TODO hover
711        // if is_hovered {
712        //     node.set_hovered();
713        // }
714
715        if let Some(child_tree) = child_tree.map(|child_tree| {
716            self.content.as_widget().a11y_nodes(
717                child_layout.with_virtual_offset(layout.virtual_offset()),
718                child_tree,
719                p,
720            )
721        }) {
722            A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree)
723        } else {
724            A11yTree::leaf(node, self.id.clone())
725        }
726    }
727
728    fn id(&self) -> Option<Id> {
729        Some(self.id.clone())
730    }
731
732    fn set_id(&mut self, id: Id) {
733        self.id = id;
734    }
735}
736
737impl<'a, Message: Clone + 'a> From<Button<'a, Message>> for crate::Element<'a, Message> {
738    fn from(button: Button<'a, Message>) -> Self {
739        Self::new(button)
740    }
741}
742
743/// The local state of a [`Button`].
744#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
745#[allow(clippy::struct_field_names)]
746pub struct State {
747    is_hovered: bool,
748    is_pressed: bool,
749    is_focused: bool,
750}
751
752impl State {
753    /// Creates a new [`State`].
754    #[inline]
755    pub fn new() -> Self {
756        Self::default()
757    }
758
759    /// Returns whether the [`Button`] is currently focused or not.
760    #[inline]
761    pub fn is_focused(self) -> bool {
762        self.is_focused
763    }
764
765    /// Returns whether the [`Button`] is currently hovered or not.
766    #[inline]
767    pub fn is_hovered(self) -> bool {
768        self.is_hovered
769    }
770
771    /// Focuses the [`Button`].
772    #[inline]
773    pub fn focus(&mut self) {
774        self.is_focused = true;
775    }
776
777    /// Unfocuses the [`Button`].
778    #[inline]
779    pub fn unfocus(&mut self) {
780        self.is_focused = false;
781    }
782}
783
784/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
785/// accordingly.
786#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
787pub fn update<'a, Message: Clone>(
788    _id: Id,
789    event: &Event,
790    layout: Layout<'_>,
791    cursor: mouse::Cursor,
792    shell: &mut Shell<'_, Message>,
793    on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>,
794    on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>,
795    state: impl FnOnce() -> &'a mut State,
796) {
797    match event {
798        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
799        | Event::Touch(touch::Event::FingerPressed { .. }) => {
800            // Unfocus the button on clicks in case another widget was clicked.
801            let state = state();
802            state.unfocus();
803
804            if on_press.is_some() || on_press_down.is_some() {
805                let bounds = layout.bounds();
806
807                if cursor.is_over(bounds) {
808                    state.is_pressed = true;
809
810                    if let Some(on_press_down) = on_press_down {
811                        let msg = (on_press_down)(layout.virtual_offset(), layout.bounds());
812                        shell.publish(msg);
813                    }
814
815                    shell.capture_event();
816                    return;
817                }
818            }
819        }
820        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
821        | Event::Touch(touch::Event::FingerLifted { .. }) => {
822            if let Some(on_press) = on_press {
823                let state = state();
824
825                if state.is_pressed {
826                    state.is_pressed = false;
827
828                    let bounds = layout.bounds();
829
830                    if cursor.is_over(bounds) {
831                        let msg = (on_press)(layout.virtual_offset(), layout.bounds());
832                        shell.publish(msg);
833                    }
834
835                    shell.capture_event();
836                    return;
837                }
838            } else if on_press_down.is_some() {
839                let state = state();
840                state.is_pressed = false;
841            }
842        }
843        #[cfg(feature = "a11y")]
844        Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => {
845            let state = state();
846            if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click)
847                .then_some(on_press)
848                .flatten()
849            {
850                state.is_pressed = false;
851                let msg = (on_press)(layout.virtual_offset(), layout.bounds());
852
853                shell.publish(msg);
854            }
855            shell.capture_event();
856            return;
857        }
858        Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
859            if let Some(on_press) = on_press {
860                let state = state();
861                if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) {
862                    state.is_pressed = true;
863                    let msg = (on_press)(layout.virtual_offset(), layout.bounds());
864
865                    shell.publish(msg);
866                    shell.capture_event();
867                    return;
868                }
869            }
870        }
871        Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
872            let state = state();
873            state.is_hovered = false;
874            state.is_pressed = false;
875        }
876        _ => {}
877    }
878}
879
880#[allow(clippy::too_many_arguments)]
881pub fn draw<Renderer: iced_core::Renderer, Theme>(
882    renderer: &mut Renderer,
883    bounds: Rectangle,
884    viewport_bounds: Rectangle,
885    styling: &super::style::Style,
886    draw_contents: impl FnOnce(&mut Renderer, &Style),
887    is_image: bool,
888) where
889    Theme: super::style::Catalog,
890{
891    let doubled_border_width = styling.border_width * 2.0;
892    let doubled_outline_width = styling.outline_width * 2.0;
893
894    if styling.outline_width > 0.0 {
895        renderer.fill_quad(
896            renderer::Quad {
897                bounds: Rectangle {
898                    x: bounds.x - styling.border_width - styling.outline_width,
899                    y: bounds.y - styling.border_width - styling.outline_width,
900                    width: bounds.width + doubled_border_width + doubled_outline_width,
901                    height: bounds.height + doubled_border_width + doubled_outline_width,
902                },
903                border: Border {
904                    width: styling.outline_width,
905                    color: styling.outline_color,
906                    radius: styling.border_radius,
907                },
908                shadow: Shadow::default(),
909                snap: true,
910            },
911            Color::TRANSPARENT,
912        );
913    }
914
915    if styling.background.is_some() || styling.border_width > 0.0 {
916        if styling.shadow_offset != Vector::default() {
917            // TODO: Implement proper shadow support
918            renderer.fill_quad(
919                renderer::Quad {
920                    bounds: Rectangle {
921                        x: bounds.x + styling.shadow_offset.x,
922                        y: bounds.y + styling.shadow_offset.y,
923                        width: bounds.width,
924                        height: bounds.height,
925                    },
926                    border: Border {
927                        radius: styling.border_radius,
928                        ..Default::default()
929                    },
930                    shadow: Shadow::default(),
931                    snap: true,
932                },
933                Background::Color([0.0, 0.0, 0.0, 0.5].into()),
934            );
935        }
936
937        // Draw the button background first.
938        if let Some(background) = styling.background {
939            renderer.fill_quad(
940                renderer::Quad {
941                    bounds,
942                    border: Border {
943                        radius: styling.border_radius,
944                        ..Default::default()
945                    },
946                    shadow: Shadow::default(),
947                    snap: true,
948                },
949                background,
950            );
951        }
952
953        // Then button overlay if any.
954        if let Some(overlay) = styling.overlay {
955            renderer.fill_quad(
956                renderer::Quad {
957                    bounds,
958                    border: Border {
959                        radius: styling.border_radius,
960                        ..Default::default()
961                    },
962                    shadow: Shadow::default(),
963                    snap: true,
964                },
965                overlay,
966            );
967        }
968
969        // Then draw the button contents onto the background.
970        draw_contents(renderer, styling);
971
972        let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
973        clipped_bounds.height += styling.border_width;
974        clipped_bounds.width += 1.0;
975
976        // Finish by drawing the border above the contents.
977        renderer.with_layer(clipped_bounds, |renderer| {
978            renderer.fill_quad(
979                renderer::Quad {
980                    bounds,
981                    border: Border {
982                        width: styling.border_width,
983                        color: styling.border_color,
984                        radius: styling.border_radius,
985                    },
986                    shadow: Shadow::default(),
987                    snap: true,
988                },
989                Color::TRANSPARENT,
990            );
991        })
992    } else {
993        draw_contents(renderer, styling);
994    }
995}
996
997/// Computes the layout of a [`Button`].
998pub fn layout<Renderer>(
999    renderer: &Renderer,
1000    limits: &layout::Limits,
1001    width: Length,
1002    height: Length,
1003    padding: Padding,
1004    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
1005) -> layout::Node {
1006    let limits = limits.width(width).height(height);
1007
1008    let mut content = layout_content(renderer, &limits.shrink(padding));
1009    let padding = padding.fit(content.size(), limits.max());
1010    let size = limits
1011        .shrink(padding)
1012        .resolve(width, height, content.size())
1013        .expand(padding);
1014
1015    content = content.move_to(Point::new(padding.left, padding.top));
1016
1017    layout::Node::with_children(size, vec![content])
1018}
1019
1020/// Returns the [`mouse::Interaction`] of a [`Button`].
1021#[must_use]
1022pub fn mouse_interaction(
1023    layout: Layout<'_>,
1024    cursor: mouse::Cursor,
1025    is_enabled: bool,
1026) -> mouse::Interaction {
1027    let is_mouse_over = cursor.is_over(layout.bounds());
1028
1029    if is_mouse_over && is_enabled {
1030        mouse::Interaction::Pointer
1031    } else {
1032        mouse::Interaction::default()
1033    }
1034}
1035
1036/// Produces a [`Task`] that focuses the [`Button`] with the given [`Id`].
1037pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
1038    task::effect(Action::Widget(Box::new(operation::focusable::focus(id))))
1039}
1040
1041impl operation::Focusable for State {
1042    #[inline]
1043    fn is_focused(&self) -> bool {
1044        Self::is_focused(*self)
1045    }
1046
1047    #[inline]
1048    fn focus(&mut self) {
1049        Self::focus(self);
1050    }
1051
1052    #[inline]
1053    fn unfocus(&mut self) {
1054        Self::unfocus(self);
1055    }
1056}
1057
1058fn removal_bounds(bounds: Rectangle, offset: f32) -> Rectangle {
1059    Rectangle {
1060        x: bounds.x + bounds.width - 12.0 - offset,
1061        y: bounds.y - 12.0 + offset,
1062        width: 24.0,
1063        height: 24.0,
1064    }
1065}