use iced::{ Background, Color, Element, Event, Length, Padding, Point, Rectangle, Size, Vector, event::Status, overlay, widget::button::{self, Catalog, Style, StyleFn}, }; use iced_core::{ Clipboard, Shell, Widget, layout::{self, Layout, Limits}, mouse::{self, Cursor}, renderer::{self, Quad}, widget::{ Operation, tree::{self, Tag, Tree}, }, }; #[derive(Debug, Clone, Copy)] pub struct ButtonInfo { pub position: Point, pub viewport: (f32, f32), } enum OnPress<'a, Message> { Direct(Message), Closure(Box Message + 'a>), } pub struct ButtonWithPosition<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> where Renderer: iced_core::Renderer, Theme: Catalog, { content: Element<'a, Message, Theme, Renderer>, on_press: Option>, width: Length, height: Length, padding: Padding, clip: bool, class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> ButtonWithPosition<'a, Message, Theme, Renderer> where Renderer: iced_core::Renderer, Theme: Catalog, { pub fn new(content: impl Into>) -> Self { let content = content.into(); let size = content.as_widget().size_hint(); Self { content, on_press: None, width: size.width.fluid(), height: size.height.fluid(), padding: DEFAULT_PADDING, clip: false, class: Theme::default(), } } /// Sets the width of the [`ButtonWithPosition`]. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); self } /// Sets the height of the [`ButtonWithPosition`]. pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); self } /// Sets the [`Padding`] of the [`ButtonWithPosition`]. pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); self } /// Sets the message that will be produced when the [`ButtonWithPosition`] is pressed. /// /// Unless `on_press` is called, the [`ButtonWithPosition`] will be disabled. pub fn on_press(mut self, on_press: Message) -> Self { self.on_press = Some(OnPress::Direct(on_press)); self } /// Sets the message that will be produced when the [`ButtonWithPosition`] is pressed. /// /// This custom widget also has the position included in the closure. /// /// This is analogous to [`ButtonWithPosition::on_press`], but using a closure to produce /// the message. /// /// This closure will only be called when the [`ButtonWithPosition`] is actually pressed and, /// therefore, this method is useful to reduce overhead if creating the resulting /// message is slow. pub fn on_press_with(mut self, on_press: impl Fn(ButtonInfo) -> Message + 'a) -> Self { self.on_press = Some(OnPress::Closure(Box::new(on_press))); self } pub fn on_press_with_maybe( mut self, on_press: Option Message + 'a>, ) -> Self { self.on_press = match on_press { Some(on_press) => Some(OnPress::Closure(Box::new(on_press))), None => None, }; self } /// Sets whether the contents of the [`ButtonWithPosition`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { self.clip = clip; self } /// Sets the style of the [`ButtonWithPosition`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, button::Status) -> Style + 'a) -> Self where Theme::Class<'a>: From>, { self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); self } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] struct State { is_hovered: bool, is_pressed: bool, is_focused: bool, } impl<'a, Message, Theme, Renderer> Widget for ButtonWithPosition<'a, Message, Theme, Renderer> where Message: 'a + Clone, Renderer: 'a + iced_core::Renderer, Theme: Catalog, { fn tag(&self) -> Tag { Tag::of::() } fn state(&self) -> tree::State { tree::State::new(State::default()) } fn children(&self) -> Vec { vec![tree::Tree::new(&self.content)] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)); } fn size(&self) -> Size { Size { width: self.width, height: self.height, } } fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> layout::Node { layout::padded(limits, self.width, self.height, self.padding, |limits| { self.content .as_widget() .layout(&mut tree.children[0], 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, ); }); } fn on_event( &mut self, tree: &mut Tree, event: Event, layout: Layout<'_>, cursor: Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> Status { if let 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 Status::Captured; } match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(iced::touch::Event::FingerPressed { .. }) => { if self.on_press.is_some() { let bounds = layout.bounds(); if cursor.is_over(bounds) { let state = tree.state.downcast_mut::(); state.is_pressed = true; return Status::Captured; } } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(iced::touch::Event::FingerLifted { .. }) => { if let Some(on_press) = self.on_press.as_ref() { let state = tree.state.downcast_mut::(); if state.is_pressed { state.is_pressed = false; let bounds = layout.bounds(); if cursor.is_over(bounds) { match on_press { OnPress::Direct(msg) => shell.publish(msg.clone()), OnPress::Closure(on_press) => shell.publish(on_press(ButtonInfo { position: Point::new( layout.bounds().width / 2.0 + layout.position().x, layout.bounds().height / 2.0 + layout.position().y, ), viewport: (viewport.width, viewport.height), })), } } } return Status::Captured; } } Event::Keyboard(iced::keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = self.on_press.as_ref() { let state = tree.state.downcast_mut::(); if state.is_focused && matches!( key, iced::keyboard::Key::Named(iced::keyboard::key::Named::Enter) ) { state.is_pressed = true; match on_press { OnPress::Direct(msg) => shell.publish(msg.clone()), OnPress::Closure(on_press) => shell.publish(on_press(ButtonInfo { position: Point::new( layout.bounds().width / 2.0 + layout.position().x, layout.bounds().height / 2.0 + layout.position().y, ), viewport: (viewport.width, viewport.height), })), } } return Status::Captured; } } Event::Touch(iced::touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { let state = tree.state.downcast_mut::(); state.is_hovered = false; state.is_pressed = false; } _ => {} } Status::Ignored } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, _renderer_style: &iced_core::renderer::Style, layout: iced_core::Layout<'_>, cursor: Cursor, viewport: &Rectangle, ) { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let is_mouse_over = cursor.is_over(bounds); let status = if self.on_press.is_none() { button::Status::Disabled } else if is_mouse_over { let state = tree.state.downcast_ref::(); if state.is_pressed { button::Status::Pressed } else { button::Status::Hovered } } else { button::Status::Active }; let style = theme.style(&self.class, status); if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 { renderer.fill_quad( Quad { bounds, border: style.border, shadow: style.shadow, }, style .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); } let viewport = if self.clip { bounds.intersection(viewport).unwrap_or(*viewport) } else { *viewport }; self.content.as_widget().draw( &tree.children[0], renderer, theme, &renderer::Style { text_color: style.text_color, }, content_layout, cursor, &viewport, ); } fn mouse_interaction( &self, _tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let is_mouse_over = cursor.is_over(layout.bounds()); if is_mouse_over && self.on_press.is_some() { mouse::Interaction::Pointer } else { mouse::Interaction::default() } } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, translation, ) } } impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: Clone + 'a, Theme: Catalog + 'a, Renderer: iced_core::Renderer + 'a, { fn from(button: ButtonWithPosition<'a, Message, Theme, Renderer>) -> Self { Self::new(button) } } /// The default [`Padding`] of a [`Button`]. pub(crate) const DEFAULT_PADDING: Padding = Padding { top: 5.0, bottom: 5.0, right: 10.0, left: 10.0, }; pub fn button_with_position<'a, Message, Theme, Renderer>( content: impl Into>, ) -> ButtonWithPosition<'a, Message, Theme, Renderer> where Theme: Catalog + 'a, Renderer: iced_core::Renderer, { ButtonWithPosition::new(content) }