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