diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 9eb9c16b..8af22bb2 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -77,9 +77,6 @@ pub use iced::widget::{pane_grid, PaneGrid}; #[doc(inline)] pub use iced::widget::{progress_bar, ProgressBar}; -#[doc(inline)] -pub use iced::widget::{radio, Radio}; - #[doc(inline)] pub use iced::widget::{responsive, Responsive}; @@ -261,6 +258,10 @@ pub mod popover; #[doc(inline)] pub use popover::{popover, Popover}; +pub mod radio; +#[doc(inline)] +pub use radio::{radio, Radio}; + pub mod rectangle_tracker; #[doc(inline)] pub use rectangle_tracker::{rectangle_tracker, RectangleTracker}; diff --git a/src/widget/radio.rs b/src/widget/radio.rs new file mode 100644 index 00000000..27c2a62e --- /dev/null +++ b/src/widget/radio.rs @@ -0,0 +1,400 @@ +//! Create choices using radio buttons. +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::touch; +use iced_core::widget::tree::Tree; +use iced_core::{ + Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, +}; + +pub use iced_style::radio::{Appearance, StyleSheet}; + +pub fn radio<'a, Message: Clone, Theme: StyleSheet, V, F>( + label: impl Into>, + value: V, + selected: Option, + f: F, +) -> Radio<'a, Message, Theme, crate::Renderer> +where + V: Eq + Copy, + F: FnOnce(V) -> Message, +{ + Radio::new(label, value, selected, f) +} + +/// A circular button representing a choice. +/// +/// # Example +/// ```no_run +/// # type Radio<'a, Message> = +/// # cosmic::widget::Radio<'a, Message, cosmic::Theme, cosmic::Renderer>; +/// # +/// # use cosmic::widget::text; +/// # use cosmic::iced::widget::column; +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// pub enum Choice { +/// A, +/// B, +/// C, +/// All, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// pub enum Message { +/// RadioSelected(Choice), +/// } +/// +/// let selected_choice = Some(Choice::A); +/// +/// let a = Radio::new( +/// text::heading("A"), +/// Choice::A, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let b = Radio::new( +/// text::heading("B"), +/// Choice::B, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let c = Radio::new( +/// text::heading("C"), +/// Choice::C, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let all = Radio::new( +/// column![ +/// text::heading("All"), +/// text::body("A, B and C"), +/// ], +/// Choice::All, +/// selected_choice, +/// Message::RadioSelected +/// ); +/// +/// let content = column![a, b, c, all]; +/// ``` +#[allow(missing_debug_implementations)] +pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +where + Theme: StyleSheet, + Renderer: iced_core::Renderer, +{ + is_selected: bool, + on_click: Message, + label: Element<'a, Message, Theme, Renderer>, + width: Length, + size: f32, + spacing: f32, + style: Theme::Style, +} + +impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> +where + Message: Clone, + Theme: StyleSheet, + Renderer: iced_core::Renderer, +{ + /// The default size of a [`Radio`] button. + pub const DEFAULT_SIZE: f32 = 28.0; + + /// The default spacing of a [`Radio`] button. + pub const DEFAULT_SPACING: f32 = 15.0; + + /// Creates a new [`Radio`] button. + /// + /// It expects: + /// * the value related to the [`Radio`] button + /// * the label of the [`Radio`] button + /// * the current selected value + /// * a function that will be called when the [`Radio`] is selected. It + /// receives the value of the radio and must produce a `Message`. + pub fn new(label: T, value: V, selected: Option, f: F) -> Self + where + V: Eq + Copy, + F: FnOnce(V) -> Message, + T: Into>, + { + Radio { + is_selected: Some(value) == selected, + on_click: f(value), + label: label.into(), + width: Length::Shrink, + size: Self::DEFAULT_SIZE, + spacing: Self::DEFAULT_SPACING, //15 + style: Default::default(), + } + } + + #[must_use] + /// Sets the size of the [`Radio`] button. + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into().0; + self + } + + #[must_use] + /// Sets the width of the [`Radio`] button. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + #[must_use] + /// Sets the spacing between the [`Radio`] button and the text. + pub fn spacing(mut self, spacing: impl Into) -> Self { + self.spacing = spacing.into().0; + self + } + + #[must_use] + /// Sets the style of the [`Radio`] button. + pub fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Radio<'a, Message, Theme, Renderer> +where + Message: Clone, + Theme: StyleSheet, + Renderer: iced_core::Renderer, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.label)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.children[0].diff(&mut self.label); + } + fn size(&self) -> Size { + Size { + width: self.width, + height: Length::Shrink, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + self.label + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, + ) { + self.label.as_widget().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); + } + + 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 { + let status = self.label.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().nth(1).unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + if status == event::Status::Ignored { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if cursor.is_over(layout.bounds()) { + shell.publish(self.on_click.clone()); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored + } else { + status + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let interaction = self.label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ); + + if interaction == mouse::Interaction::default() { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } else { + interaction + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let is_mouse_over = cursor.is_over(layout.bounds()); + + let mut children = layout.children(); + + let custom_style = if is_mouse_over { + theme.hovered(&self.style, self.is_selected) + } else { + theme.active(&self.style, self.is_selected) + }; + + { + let layout = children.next().unwrap(); + let bounds = layout.bounds(); + + let size = bounds.width; + let dot_size = size / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + radius: (size / 2.0).into(), + width: custom_style.border_width, + color: custom_style.border_color, + }, + ..renderer::Quad::default() + }, + custom_style.background, + ); + + if self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + dot_size / 2.0, + y: bounds.y + dot_size / 2.0, + width: bounds.width - dot_size, + height: bounds.height - dot_size, + }, + border: Border::with_radius(dot_size / 2.0), + ..renderer::Quad::default() + }, + custom_style.dot_color, + ); + } + } + + { + let label_layout = children.next().unwrap(); + self.label.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + label_layout, + cursor, + viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.label.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + ) { + self.label.as_widget().drag_destinations( + &state.children[0], + layout.children().nth(1).unwrap(), + renderer, + dnd_rectangles, + ); + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Theme: 'a + StyleSheet, + Renderer: 'a + iced_core::Renderer, +{ + fn from(radio: Radio<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> { + Element::new(radio) + } +}