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