// Copyright 2019 H�ctor Ram�n, Iced contributors // Copyright 2023 System76 // SPDX-License-Identifier: MIT //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. use iced_runtime::core::widget::Id; use iced_runtime::{keyboard, Command}; use iced_core::event::{self, Event}; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer::{self, Quad}; use iced_core::touch; use iced_core::widget::tree::{self, Tree}; use iced_core::widget::Operation; use iced_core::{layout, svg}; use iced_core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; use iced_renderer::core::widget::{operation, OperationOutputWrapper}; pub use super::style::{Appearance, StyleSheet}; struct Selected { icon: svg::Handle, } /// A generic widget that produces a message when pressed. /// /// ```no_run /// # type Button<'a, Message> = /// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer>; /// # /// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } /// /// let button = Button::new("Press me!").on_press(Message::ButtonPressed); /// ``` /// /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will /// be disabled: /// /// ``` /// # type Button<'a, Message> = /// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer>; /// # /// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } /// /// fn disabled_button<'a>() -> Button<'a, Message> { /// Button::new("I'm disabled!") /// } /// /// fn enabled_button<'a>() -> Button<'a, Message> { /// disabled_button().on_press(Message::ButtonPressed) /// } /// ``` #[allow(missing_debug_implementations)] #[must_use] pub struct Button<'a, Message, Renderer> where Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { id: Id, #[cfg(feature = "a11y")] name: Option>, #[cfg(feature = "a11y")] description: Option>, #[cfg(feature = "a11y")] label: Option>, content: Element<'a, Message, Renderer>, on_press: Option, width: Length, height: Length, padding: Padding, selected: Option, style: ::Style, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> where Renderer: iced_core::Renderer, Renderer::Theme: StyleSheet, { /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into>) -> Self { Button { id: Id::unique(), #[cfg(feature = "a11y")] name: None, #[cfg(feature = "a11y")] description: None, #[cfg(feature = "a11y")] label: None, content: content.into(), on_press: None, width: Length::Shrink, height: Length::Shrink, padding: Padding::new(5.0), selected: None, style: ::Style::default(), } } /// Sets the [`Id`] of the [`Button`]. pub fn id(mut self, id: Id) -> Self { self.id = id; self } /// Sets the width of the [`Button`]. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); self } /// Sets the height of the [`Button`]. pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); self } /// Sets the [`Padding`] of the [`Button`]. pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); self } /// Sets the message that will be produced when the [`Button`] is pressed. /// /// Unless `on_press` is called, the [`Button`] will be disabled. pub fn on_press(mut self, on_press: Message) -> Self { self.on_press = Some(on_press); self } /// Sets the message that will be produced when the [`Button`] is pressed, /// if `Some`. /// /// If `None`, the [`Button`] will be disabled. pub fn on_press_maybe(mut self, on_press: Option) -> Self { self.on_press = on_press; self } /// Sets the widget to a selected state. /// /// Displays a selection indicator on image buttons. pub fn selected(mut self, selected: bool) -> Self { self.selected = selected.then(|| Selected { icon: crate::widget::icon::from_name("object-select-symbolic") .size(16) .icon() .into_svg_handle() .unwrap_or_else(|| { let bytes: &'static [u8] = &[]; iced_core::svg::Handle::from_memory(bytes) }), }); self } /// Sets the style variant of this [`Button`]. pub fn style(mut self, style: ::Style) -> Self { self.style = style; self } #[cfg(feature = "a11y")] /// Sets the name of the [`Button`]. pub fn name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } #[cfg(feature = "a11y")] /// Sets the description of the [`Button`]. pub fn description_widget(mut self, description: &T) -> Self { self.description = Some(iced_accessibility::Description::Id( description.description(), )); self } #[cfg(feature = "a11y")] /// Sets the description of the [`Button`]. pub fn description(mut self, description: impl Into>) -> Self { self.description = Some(iced_accessibility::Description::Text(description.into())); self } #[cfg(feature = "a11y")] /// Sets the label of the [`Button`]. pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { self.label = Some(label.label().into_iter().map(|l| l.into()).collect()); self } } impl<'a, Message, Renderer> Widget for Button<'a, Message, Renderer> where Message: 'a + Clone, Renderer: 'a + iced_core::Renderer + svg::Renderer, Renderer::Theme: StyleSheet, { fn tag(&self) -> tree::Tag { tree::Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::new()) } fn children(&self) -> Vec { vec![Tree::new(&self.content)] } fn diff(&mut self, tree: &mut Tree) { tree.diff_children(std::slice::from_mut(&mut self.content)); } fn width(&self) -> Length { self.width } fn height(&self) -> Length { self.height } fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { layout( renderer, limits, self.width, self.height, self.padding, |renderer, limits| self.content.as_widget().layout(renderer, limits), ) } fn operate( &self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), renderer, operation, ); }); let state = tree.state.downcast_mut::(); operation.focusable(state, Some(&self.id)); } fn on_event( &mut self, tree: &mut Tree, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { if let event::Status::Captured = self.content.as_widget_mut().on_event( &mut tree.children[0], event.clone(), layout.children().next().unwrap(), cursor, renderer, clipboard, shell, viewport, ) { return event::Status::Captured; } update( self.id.clone(), event, layout, cursor, shell, &self.on_press, || tree.state.downcast_mut::(), ) } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let styling = draw( renderer, bounds, cursor, self.on_press.is_some(), self.selected.is_some(), theme, &self.style, || tree.state.downcast_ref::(), |renderer, styling| { self.content.as_widget().draw( &tree.children[0], renderer, theme, &renderer::Style { icon_color: styling.icon_color.unwrap_or(renderer_style.icon_color), text_color: styling.text_color.unwrap_or(renderer_style.icon_color), scale_factor: renderer_style.scale_factor, }, content_layout, cursor, &bounds, ); }, ); if let Some(ref selected) = self.selected { renderer.fill_quad( Quad { bounds: Rectangle { width: 24.0, height: 20.0, x: bounds.x + styling.border_width, y: bounds.y + (bounds.height - 20.0 - styling.border_width), }, border_radius: [0.0, 8.0, 0.0, 8.0].into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, theme.selection_background(), ); iced_core::svg::Renderer::draw( renderer, selected.icon.clone(), styling.icon_color, Rectangle { width: 16.0, height: 16.0, x: bounds.x + 5.0 + styling.border_width, y: bounds.y + (bounds.height - 18.0 - styling.border_width), }, ); } } fn mouse_interaction( &self, _tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { mouse_interaction(layout, cursor, self.on_press.is_some()) } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, ) } #[cfg(feature = "a11y")] /// get the a11y nodes for the widget fn a11y_nodes( &self, layout: Layout<'_>, state: &Tree, p: mouse::Cursor, ) -> iced_accessibility::A11yTree { use iced_accessibility::{ accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role}, A11yNode, A11yTree, }; let child_layout = layout.children().next().unwrap(); let child_tree = &state.children[0]; let child_tree = self .content .as_widget() .a11y_nodes(child_layout, &child_tree, p); let Rectangle { x, y, width, height, } = layout.bounds(); let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64); let is_hovered = state.state.downcast_ref::().is_hovered; let mut node = NodeBuilder::new(Role::Button); node.add_action(Action::Focus); node.add_action(Action::Default); node.set_bounds(bounds); if let Some(name) = self.name.as_ref() { node.set_name(name.clone()); } match self.description.as_ref() { Some(iced_accessibility::Description::Id(id)) => { node.set_described_by( id.iter() .cloned() .map(|id| NodeId::from(id)) .collect::>(), ); } Some(iced_accessibility::Description::Text(text)) => { node.set_description(text.clone()); } None => {} } if let Some(label) = self.label.as_ref() { node.set_labelled_by(label.clone()); } if self.on_press.is_none() { node.set_disabled() } if is_hovered { node.set_hovered() } node.set_default_action_verb(DefaultActionVerb::Click); A11yTree::node_with_child_tree(A11yNode::new(node, self.id.clone()), child_tree) } fn id(&self) -> Option { Some(self.id.clone()) } fn set_id(&mut self, id: Id) { self.id = id; } } impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: Clone + 'a, Renderer: iced_core::Renderer + svg::Renderer + 'a, Renderer::Theme: StyleSheet, { fn from(button: Button<'a, Message, Renderer>) -> Self { Self::new(button) } } /// The local state of a [`Button`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_hovered: bool, is_pressed: bool, is_focused: bool, } impl State { /// Creates a new [`State`]. pub fn new() -> State { State::default() } /// Returns whether the [`Button`] is currently focused or not. pub fn is_focused(self) -> bool { self.is_focused } /// Returns whether the [`Button`] is currently hovered or not. pub fn is_hovered(self) -> bool { self.is_hovered } /// Focuses the [`Button`]. pub fn focus(&mut self) { self.is_focused = true; } /// Unfocuses the [`Button`]. pub fn unfocus(&mut self) { self.is_focused = false; } } /// Processes the given [`Event`] and updates the [`State`] of a [`Button`] /// accordingly. #[allow(clippy::needless_pass_by_value)] pub fn update<'a, Message: Clone>( _id: Id, event: Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_press: &Option, state: impl FnOnce() -> &'a mut State, ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if on_press.is_some() { let bounds = layout.bounds(); if cursor.is_over(bounds) { let state = state(); state.is_pressed = true; return event::Status::Captured; } } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) => { if let Some(on_press) = on_press.clone() { let state = state(); if state.is_pressed { state.is_pressed = false; let bounds = layout.bounds(); if cursor.is_over(bounds) { shell.publish(on_press); } return event::Status::Captured; } } } #[cfg(feature = "a11y")] Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); if let Some(Some(on_press)) = (id == event_id && matches!(action, iced_accessibility::accesskit::Action::Default)) .then(|| on_press.clone()) { state.is_pressed = false; shell.publish(on_press); } return event::Status::Captured; } Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { if let Some(on_press) = on_press.clone() { let state = state(); if state.is_focused && key_code == keyboard::KeyCode::Enter { state.is_pressed = true; shell.publish(on_press); return event::Status::Captured; } } } Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { let state = state(); state.is_hovered = false; state.is_pressed = false; } _ => {} } event::Status::Ignored } #[allow(clippy::too_many_arguments)] pub fn draw<'a, Renderer: iced_core::Renderer>( renderer: &mut Renderer, bounds: Rectangle, cursor: mouse::Cursor, is_enabled: bool, is_selected: bool, style_sheet: &dyn StyleSheet