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