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        &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()
335                    .layout(&mut tree.children[0], renderer, limits)
336            },
337        )
338    }
339
340    fn operate(
341        &self,
342        tree: &mut Tree,
343        layout: Layout<'_>,
344        renderer: &crate::Renderer,
345        operation: &mut dyn Operation<()>,
346    ) {
347        operation.container(None, layout.bounds(), &mut |operation| {
348            self.content.as_widget().operate(
349                &mut tree.children[0],
350                layout
351                    .children()
352                    .next()
353                    .unwrap()
354                    .with_virtual_offset(layout.virtual_offset()),
355                renderer,
356                operation,
357            );
358        });
359        let state = tree.state.downcast_mut::<State>();
360        operation.focusable(state, Some(&self.id));
361    }
362
363    fn on_event(
364        &mut self,
365        tree: &mut Tree,
366        event: Event,
367        layout: Layout<'_>,
368        cursor: mouse::Cursor,
369        renderer: &crate::Renderer,
370        clipboard: &mut dyn Clipboard,
371        shell: &mut Shell<'_, Message>,
372        viewport: &Rectangle,
373    ) -> event::Status {
374        if let Variant::Image {
375            on_remove: Some(on_remove),
376            ..
377        } = &self.variant
378        {
379            // Capture mouse/touch events on the removal button
380            match event {
381                Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
382                | Event::Touch(touch::Event::FingerPressed { .. }) => {
383                    if let Some(position) = cursor.position() {
384                        if removal_bounds(layout.bounds(), 4.0).contains(position) {
385                            shell.publish(on_remove.clone());
386                            return event::Status::Captured;
387                        }
388                    }
389                }
390
391                _ => (),
392            }
393        }
394
395        if self.content.as_widget_mut().on_event(
396            &mut tree.children[0],
397            event.clone(),
398            layout
399                .children()
400                .next()
401                .unwrap()
402                .with_virtual_offset(layout.virtual_offset()),
403            cursor,
404            renderer,
405            clipboard,
406            shell,
407            viewport,
408        ) == event::Status::Captured
409        {
410            return event::Status::Captured;
411        }
412
413        update(
414            self.id.clone(),
415            event,
416            layout,
417            cursor,
418            shell,
419            self.on_press.as_deref(),
420            self.on_press_down.as_deref(),
421            || tree.state.downcast_mut::<State>(),
422        )
423    }
424
425    #[allow(clippy::too_many_lines)]
426    fn draw(
427        &self,
428        tree: &Tree,
429        renderer: &mut crate::Renderer,
430        theme: &crate::Theme,
431        renderer_style: &renderer::Style,
432        layout: Layout<'_>,
433        cursor: mouse::Cursor,
434        viewport: &Rectangle,
435    ) {
436        let bounds = layout.bounds();
437        if !viewport.intersects(&bounds) {
438            return;
439        }
440
441        // FIXME: Why is there no content layout
442        let Some(content_layout) = layout.children().next() else {
443            return;
444        };
445
446        let mut headerbar_alpha = None;
447
448        let is_enabled =
449            self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled;
450        let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
451
452        let state = tree.state.downcast_ref::<State>();
453
454        let styling = if !is_enabled {
455            theme.disabled(&self.style)
456        } else if is_mouse_over {
457            if state.is_pressed {
458                if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
459                    headerbar_alpha = Some(0.8);
460                }
461
462                theme.pressed(state.is_focused, self.selected, &self.style)
463            } else {
464                if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
465                    headerbar_alpha = Some(0.8);
466                }
467                theme.hovered(state.is_focused, self.selected, &self.style)
468            }
469        } else {
470            if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) {
471                headerbar_alpha = Some(0.75);
472            }
473
474            theme.active(state.is_focused, self.selected, &self.style)
475        };
476
477        let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
478
479        // Menu roots should share the accent color that icons get in the header.
480        let mut text_color = if matches!(self.style, crate::theme::Button::MenuRoot) {
481            icon_color
482        } else {
483            styling.text_color.unwrap_or(renderer_style.text_color)
484        };
485
486        if let Some(alpha) = headerbar_alpha {
487            icon_color.a = alpha;
488            text_color.a = alpha;
489        }
490
491        draw::<_, crate::Theme>(
492            renderer,
493            bounds,
494            *viewport,
495            &styling,
496            |renderer, _styling| {
497                self.content.as_widget().draw(
498                    &tree.children[0],
499                    renderer,
500                    theme,
501                    &renderer::Style {
502                        icon_color,
503                        text_color,
504                        scale_factor: renderer_style.scale_factor,
505                    },
506                    content_layout.with_virtual_offset(layout.virtual_offset()),
507                    cursor,
508                    &viewport.intersection(&bounds).unwrap_or_default(),
509                );
510            },
511            matches!(self.variant, Variant::Image { .. }),
512        );
513
514        if let Variant::Image {
515            close_icon,
516            on_remove,
517        } = &self.variant
518        {
519            renderer.with_layer(*viewport, |renderer| {
520                let selection_background = theme.selection_background();
521
522                let c_rad = THEME.lock().unwrap().cosmic().corner_radii;
523
524                if self.selected {
525                    renderer.fill_quad(
526                        Quad {
527                            bounds: Rectangle {
528                                width: 24.0,
529                                height: 20.0,
530                                x: bounds.x + styling.border_width,
531                                y: bounds.y + (bounds.height - 20.0 - styling.border_width),
532                            },
533                            border: Border {
534                                radius: [
535                                    c_rad.radius_0[0],
536                                    c_rad.radius_s[1],
537                                    c_rad.radius_0[2],
538                                    c_rad.radius_s[3],
539                                ]
540                                .into(),
541                                ..Default::default()
542                            },
543                            shadow: Shadow::default(),
544                        },
545                        selection_background,
546                    );
547
548                    let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone())
549                        .color(icon_color);
550                    let bounds = Rectangle {
551                        width: 16.0,
552                        height: 16.0,
553                        x: bounds.x + 5.0 + styling.border_width,
554                        y: bounds.y + (bounds.height - 18.0 - styling.border_width),
555                    };
556                    if bounds.intersects(viewport) {
557                        iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds);
558                    }
559                }
560
561                if on_remove.is_some() {
562                    if let Some(position) = cursor.position() {
563                        if bounds.contains(position) {
564                            let bounds = removal_bounds(layout.bounds(), 4.0);
565                            renderer.fill_quad(
566                                renderer::Quad {
567                                    bounds,
568                                    shadow: Shadow::default(),
569                                    border: Border {
570                                        radius: c_rad.radius_m.into(),
571                                        ..Default::default()
572                                    },
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                            );
587                        }
588                    }
589                }
590            });
591        }
592    }
593
594    fn mouse_interaction(
595        &self,
596        _tree: &Tree,
597        layout: Layout<'_>,
598        cursor: mouse::Cursor,
599        _viewport: &Rectangle,
600        _renderer: &crate::Renderer,
601    ) -> mouse::Interaction {
602        mouse_interaction(
603            layout.with_virtual_offset(layout.virtual_offset()),
604            cursor,
605            self.on_press.is_some(),
606        )
607    }
608
609    fn overlay<'b>(
610        &'b mut self,
611        tree: &'b mut Tree,
612        layout: Layout<'_>,
613        renderer: &crate::Renderer,
614        mut translation: Vector,
615    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
616        let position = layout.bounds().position();
617        translation.x += position.x;
618        translation.y += position.y;
619        self.content.as_widget_mut().overlay(
620            &mut tree.children[0],
621            layout
622                .children()
623                .next()
624                .unwrap()
625                .with_virtual_offset(layout.virtual_offset()),
626            renderer,
627            translation,
628        )
629    }
630
631    #[cfg(feature = "a11y")]
632    /// get the a11y nodes for the widget
633    fn a11y_nodes(
634        &self,
635        layout: Layout<'_>,
636        state: &Tree,
637        p: mouse::Cursor,
638    ) -> iced_accessibility::A11yTree {
639        use iced_accessibility::{
640            A11yNode, A11yTree,
641            accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role},
642        };
643        // TODO why is state None sometimes?
644        if matches!(state.state, iced_core::widget::tree::State::None) {
645            tracing::info!("Button state is missing.");
646            return A11yTree::default();
647        }
648
649        let child_layout = layout.children().next().unwrap();
650        let child_tree = state.children.first();
651
652        let Rectangle {
653            x,
654            y,
655            width,
656            height,
657        } = layout.bounds();
658        let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64);
659        let is_hovered = state.state.downcast_ref::<State>().is_hovered;
660
661        let mut node = NodeBuilder::new(Role::Button);
662        node.add_action(Action::Focus);
663        node.add_action(Action::Default);
664        node.set_bounds(bounds);
665        if let Some(name) = self.name.as_ref() {
666            node.set_name(name.clone());
667        }
668        match self.description.as_ref() {
669            Some(iced_accessibility::Description::Id(id)) => {
670                node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
671            }
672            Some(iced_accessibility::Description::Text(text)) => {
673                node.set_description(text.clone());
674            }
675            None => {}
676        }
677
678        if let Some(label) = self.label.as_ref() {
679            node.set_labelled_by(label.clone());
680        }
681
682        if self.on_press.is_none() {
683            node.set_disabled();
684        }
685        if is_hovered {
686            node.set_hovered();
687        }
688        node.set_default_action_verb(DefaultActionVerb::Click);
689
690        if let Some(child_tree) = child_tree.map(|child_tree| {
691            self.content.as_widget().a11y_nodes(
692                child_layout.with_virtual_offset(layout.virtual_offset()),
693                child_tree,
694                p,
695            )
696        }) {
697            A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree)
698        } else {
699            A11yTree::leaf(node, self.id.clone())
700        }
701    }
702
703    fn id(&self) -> Option<Id> {
704        Some(self.id.clone())
705    }
706
707    fn set_id(&mut self, id: Id) {
708        self.id = id;
709    }
710}
711
712impl<'a, Message: Clone + 'a> From<Button<'a, Message>> for crate::Element<'a, Message> {
713    fn from(button: Button<'a, Message>) -> Self {
714        Self::new(button)
715    }
716}
717
718/// The local state of a [`Button`].
719#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
720#[allow(clippy::struct_field_names)]
721pub struct State {
722    is_hovered: bool,
723    is_pressed: bool,
724    is_focused: bool,
725}
726
727impl State {
728    /// Creates a new [`State`].
729    #[inline]
730    pub fn new() -> Self {
731        Self::default()
732    }
733
734    /// Returns whether the [`Button`] is currently focused or not.
735    #[inline]
736    pub fn is_focused(self) -> bool {
737        self.is_focused
738    }
739
740    /// Returns whether the [`Button`] is currently hovered or not.
741    #[inline]
742    pub fn is_hovered(self) -> bool {
743        self.is_hovered
744    }
745
746    /// Focuses the [`Button`].
747    #[inline]
748    pub fn focus(&mut self) {
749        self.is_focused = true;
750    }
751
752    /// Unfocuses the [`Button`].
753    #[inline]
754    pub fn unfocus(&mut self) {
755        self.is_focused = false;
756    }
757}
758
759/// Processes the given [`Event`] and updates the [`State`] of a [`Button`]
760/// accordingly.
761#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
762pub fn update<'a, Message: Clone>(
763    _id: Id,
764    event: Event,
765    layout: Layout<'_>,
766    cursor: mouse::Cursor,
767    shell: &mut Shell<'_, Message>,
768    on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>,
769    on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>,
770    state: impl FnOnce() -> &'a mut State,
771) -> event::Status {
772    match event {
773        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
774        | Event::Touch(touch::Event::FingerPressed { .. }) => {
775            // Unfocus the button on clicks in case another widget was clicked.
776            let state = state();
777            state.unfocus();
778
779            if on_press.is_some() || on_press_down.is_some() {
780                let bounds = layout.bounds();
781
782                if cursor.is_over(bounds) {
783                    state.is_pressed = true;
784
785                    if let Some(on_press_down) = on_press_down {
786                        let msg = (on_press_down)(layout.virtual_offset(), layout.bounds());
787                        shell.publish(msg);
788                    }
789
790                    return event::Status::Captured;
791                }
792            }
793        }
794        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
795        | Event::Touch(touch::Event::FingerLifted { .. }) => {
796            if let Some(on_press) = on_press.clone() {
797                let state = state();
798
799                if state.is_pressed {
800                    state.is_pressed = false;
801
802                    let bounds = layout.bounds();
803
804                    if cursor.is_over(bounds) {
805                        let msg = (on_press)(layout.virtual_offset(), layout.bounds());
806                        shell.publish(msg);
807                    }
808
809                    return event::Status::Captured;
810                }
811            } else if on_press_down.is_some() {
812                let state = state();
813                state.is_pressed = false;
814            }
815        }
816        #[cfg(feature = "a11y")]
817        Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => {
818            let state = state();
819            if let Some(Some(on_press)) = (event_id == event_id
820                && matches!(action, iced_accessibility::accesskit::Action::Default))
821            .then(|| on_press.clone())
822            {
823                state.is_pressed = false;
824                let msg = (on_press)(layout.virtual_offset(), layout.bounds());
825
826                shell.publish(msg);
827            }
828            return event::Status::Captured;
829        }
830        Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
831            if let Some(on_press) = on_press.clone() {
832                let state = state();
833                if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) {
834                    state.is_pressed = true;
835                    let msg = (on_press)(layout.virtual_offset(), layout.bounds());
836
837                    shell.publish(msg);
838                    return event::Status::Captured;
839                }
840            }
841        }
842        Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
843            let state = state();
844            state.is_hovered = false;
845            state.is_pressed = false;
846        }
847        _ => {}
848    }
849
850    event::Status::Ignored
851}
852
853#[allow(clippy::too_many_arguments)]
854pub fn draw<Renderer: iced_core::Renderer, Theme>(
855    renderer: &mut Renderer,
856    bounds: Rectangle,
857    viewport_bounds: Rectangle,
858    styling: &super::style::Style,
859    draw_contents: impl FnOnce(&mut Renderer, &Style),
860    is_image: bool,
861) where
862    Theme: super::style::Catalog,
863{
864    let doubled_border_width = styling.border_width * 2.0;
865    let doubled_outline_width = styling.outline_width * 2.0;
866
867    if styling.outline_width > 0.0 {
868        renderer.fill_quad(
869            renderer::Quad {
870                bounds: Rectangle {
871                    x: bounds.x - styling.border_width - styling.outline_width,
872                    y: bounds.y - styling.border_width - styling.outline_width,
873                    width: bounds.width + doubled_border_width + doubled_outline_width,
874                    height: bounds.height + doubled_border_width + doubled_outline_width,
875                },
876                border: Border {
877                    width: styling.outline_width,
878                    color: styling.outline_color,
879                    radius: styling.border_radius,
880                },
881                shadow: Shadow::default(),
882            },
883            Color::TRANSPARENT,
884        );
885    }
886
887    if styling.background.is_some() || styling.border_width > 0.0 {
888        if styling.shadow_offset != Vector::default() {
889            // TODO: Implement proper shadow support
890            renderer.fill_quad(
891                renderer::Quad {
892                    bounds: Rectangle {
893                        x: bounds.x + styling.shadow_offset.x,
894                        y: bounds.y + styling.shadow_offset.y,
895                        width: bounds.width,
896                        height: bounds.height,
897                    },
898                    border: Border {
899                        radius: styling.border_radius,
900                        ..Default::default()
901                    },
902                    shadow: Shadow::default(),
903                },
904                Background::Color([0.0, 0.0, 0.0, 0.5].into()),
905            );
906        }
907
908        // Draw the button background first.
909        if let Some(background) = styling.background {
910            renderer.fill_quad(
911                renderer::Quad {
912                    bounds,
913                    border: Border {
914                        radius: styling.border_radius,
915                        ..Default::default()
916                    },
917                    shadow: Shadow::default(),
918                },
919                background,
920            );
921        }
922
923        // Then button overlay if any.
924        if let Some(overlay) = styling.overlay {
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                },
934                overlay,
935            );
936        }
937
938        // Then draw the button contents onto the background.
939        draw_contents(renderer, styling);
940
941        let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
942        clipped_bounds.height += styling.border_width;
943        clipped_bounds.width += 1.0;
944
945        // Finish by drawing the border above the contents.
946        renderer.with_layer(clipped_bounds, |renderer| {
947            renderer.fill_quad(
948                renderer::Quad {
949                    bounds,
950                    border: Border {
951                        width: styling.border_width,
952                        color: styling.border_color,
953                        radius: styling.border_radius,
954                    },
955                    shadow: Shadow::default(),
956                },
957                Color::TRANSPARENT,
958            );
959        })
960    } else {
961        draw_contents(renderer, styling);
962    }
963}
964
965/// Computes the layout of a [`Button`].
966pub fn layout<Renderer>(
967    renderer: &Renderer,
968    limits: &layout::Limits,
969    width: Length,
970    height: Length,
971    padding: Padding,
972    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
973) -> layout::Node {
974    let limits = limits.width(width).height(height);
975
976    let mut content = layout_content(renderer, &limits.shrink(padding));
977    let padding = padding.fit(content.size(), limits.max());
978    let size = limits
979        .shrink(padding)
980        .resolve(width, height, content.size())
981        .expand(padding);
982
983    content = content.move_to(Point::new(padding.left, padding.top));
984
985    layout::Node::with_children(size, vec![content])
986}
987
988/// Returns the [`mouse::Interaction`] of a [`Button`].
989#[must_use]
990pub fn mouse_interaction(
991    layout: Layout<'_>,
992    cursor: mouse::Cursor,
993    is_enabled: bool,
994) -> mouse::Interaction {
995    let is_mouse_over = cursor.is_over(layout.bounds());
996
997    if is_mouse_over && is_enabled {
998        mouse::Interaction::Pointer
999    } else {
1000        mouse::Interaction::default()
1001    }
1002}
1003
1004/// Produces a [`Task`] that focuses the [`Button`] with the given [`Id`].
1005pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
1006    task::effect(Action::Widget(Box::new(operation::focusable::focus(id))))
1007}
1008
1009impl operation::Focusable for State {
1010    #[inline]
1011    fn is_focused(&self) -> bool {
1012        Self::is_focused(*self)
1013    }
1014
1015    #[inline]
1016    fn focus(&mut self) {
1017        Self::focus(self);
1018    }
1019
1020    #[inline]
1021    fn unfocus(&mut self) {
1022        Self::unfocus(self);
1023    }
1024}
1025
1026fn removal_bounds(bounds: Rectangle, offset: f32) -> Rectangle {
1027    Rectangle {
1028        x: bounds.x + bounds.width - 12.0 - offset,
1029        y: bounds.y - 12.0 + offset,
1030        width: 24.0,
1031        height: 24.0,
1032    }
1033}