diff --git a/Cargo.toml b/Cargo.toml index 18468324..29a46688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ menu = [] quad = [] spinner = [] context_menu = [] +segmented_button = [] default = [ "badge", @@ -59,6 +60,7 @@ default = [ "context_menu", "spinner", "cupertino", + "segmented_button", ] [dependencies] @@ -105,6 +107,7 @@ members = [ "examples/spinner", "examples/context_menu", "examples/WidgetIDReturn", + "examples/segmented_button", ] [workspace.dependencies.iced] diff --git a/examples/segmented_button/Cargo.toml b/examples/segmented_button/Cargo.toml new file mode 100644 index 00000000..fcc534f0 --- /dev/null +++ b/examples/segmented_button/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "segmented_button" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +iced_aw = { workspace = true, features = [ + "segmented_button", + "icons", +] } +iced.workspace = true \ No newline at end of file diff --git a/examples/segmented_button/src/main.rs b/examples/segmented_button/src/main.rs new file mode 100644 index 00000000..9d4d2149 --- /dev/null +++ b/examples/segmented_button/src/main.rs @@ -0,0 +1,103 @@ +use iced::widget::container; +use iced::widget::{column, row, text}; +use iced::{Element, Length, Sandbox, Settings}; + +use iced_aw::native::segmented_button; +use segmented_button::SegmentedButton; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + selected_radio: Option, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + RadioSelected(Choice), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self { + selected_radio: Some(Choice::A), + } + } + + fn title(&self) -> String { + String::from("Radio - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::RadioSelected(value) => { + self.selected_radio = Some(value); + } + } + } + + fn view(&self) -> Element { + // let selected_radio = Some(Choice::A); + + // i added a row just to demonstrate that anything can be used as a child, + //in this case instead of A B C you might add icons + let a = SegmentedButton::new( + row!(text("HEAVY "), "A"), + Choice::A, + self.selected_radio, + Message::RadioSelected, + ); + + let b = SegmentedButton::new( + row!(text("MEDIUM "), "B"), + Choice::B, + self.selected_radio, + Message::RadioSelected, + ); + + let c = SegmentedButton::new( + row!(text("LIGHT "), "C"), + Choice::C, + self.selected_radio, + Message::RadioSelected, + ); + let content = column![ + row![a, b, c], + text(self.selected_radio.unwrap().to_string()) + ] + .align_items(iced::Alignment::Center); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Choice { + #[default] + A, + B, + C, +} + +impl std::fmt::Display for Choice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Choice::A => "A", + Choice::B => "B", + Choice::C => "C", + } + ) + } +} diff --git a/src/native/mod.rs b/src/native/mod.rs index 4ba345bf..306f4b18 100644 --- a/src/native/mod.rs +++ b/src/native/mod.rs @@ -125,3 +125,10 @@ pub mod context_menu; /// A context menu pub type ContextMenu<'a, Overlay, Message, Renderer> = context_menu::ContextMenu<'a, Overlay, Message, Renderer>; + +#[cfg(feature = "segmented_button")] +pub mod segmented_button; +#[cfg(feature = "segmented_button")] +/// A badge for color highlighting small information. +pub type SegmentedButton<'a, Message, Renderer> = + segmented_button::SegmentedButton<'a, Message, Renderer>; diff --git a/src/native/segmented_button.rs b/src/native/segmented_button.rs new file mode 100644 index 00000000..ef02d26e --- /dev/null +++ b/src/native/segmented_button.rs @@ -0,0 +1,290 @@ +//! Create choices using `segnmented_button` buttons. +use iced_widget::core::{ + event, + layout::{Limits, Node}, + mouse::{self, Cursor}, + renderer, touch, + widget::Tree, + Alignment, Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Point, + Rectangle, Shell, Widget, +}; + +pub use crate::style::segmented_button::StyleSheet; + +/// A `segnmented_button` for color highlighting small information. +/// +/// # Example +/// ```ignore +/// # use iced::widget::Text; +/// # use iced_aw::SegmentedButton; +/// # +/// #[derive(Debug, Clone)] +/// enum Message { +/// } +/// +/// let segmented_button = SegmentedButton::::new(Text::new("Text")); +/// ``` +#[allow(missing_debug_implementations)] +pub struct SegmentedButton<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + is_selected: bool, + on_click: Message, + /// The padding of the [`SegmentedButton`]. + padding: Padding, + /// The width of the [`SegmentedButton`]. + width: Length, + /// The height of the [`SegmentedButton`]. + height: Length, + /// The horizontal alignment of the [`SegmentedButton`] + horizontal_alignment: Alignment, + /// The vertical alignment of the [`SegmentedButton`] + vertical_alignment: Alignment, + /// The style of the [`SegmentedButton`] + style: ::Style, + /// The content [`Element`] of the [`SegmentedButton`] + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> SegmentedButton<'a, Message, Renderer> +where + Renderer: iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`SegmentedButton`](SegmentedButton) with the given content. + /// + /// It expects: + /// * the content [`Element`] to display in the [`SegmentedButton`](SegmentedButton). + pub fn new(content: T, value: V, selected: Option, f: F) -> Self + where + T: Into>, + V: Eq + Copy, + F: FnOnce(V) -> Message, + { + SegmentedButton { + is_selected: Some(value) == selected, + on_click: f(value), + padding: Padding::new(3.0), + width: Length::Shrink, + height: Length::Shrink, + horizontal_alignment: Alignment::Center, + vertical_alignment: Alignment::Center, + style: ::Style::default(), + content: content.into(), + } + } + + /// Sets the padding of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn padding(mut self, units: Padding) -> Self { + self.padding = units; + self + } + + /// Sets the width of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the horizontal alignment of the content of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn align_x(mut self, alignment: Alignment) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the vertical alignment of the content of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn align_y(mut self, alignment: Alignment) -> Self { + self.vertical_alignment = alignment; + self + } + + /// Sets the style of the [`SegmentedButton`](SegmentedButton). + #[must_use] + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } +} + +impl<'a, Message, Renderer> Widget for SegmentedButton<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + let padding = self.padding; + let limits = limits + .loose() + .width(self.width) + .height(self.height) + .pad(padding); + + let mut content = self.content.as_widget().layout(renderer, &limits.loose()); + let size = limits.resolve(content.size()); + + content.move_to(Point::new(padding.left, padding.top)); + content.align(self.horizontal_alignment, self.vertical_alignment, size); + + Node::with_children(size.pad(padding), vec![content]) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + 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 + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor: Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let mut children = layout.children(); + let is_mouse_over = bounds.contains(cursor.position().unwrap_or_default()); + let style_sheet = if is_mouse_over { + theme.hovered(&self.style) + } else { + theme.active(&self.style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: 2.0.into(), + border_width: style_sheet.border_width, + border_color: style_sheet.border_color.unwrap_or(Color::BLACK), + }, + style_sheet.background, + ); + if self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style_sheet.selected_color, + ); + } + //just for the testing as of now needs to clearup and make styling based of basecolor + if is_mouse_over && !self.is_selected { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + text_color: style_sheet.text_color, + }, + children + .next() + .expect("Graphics: Layout should have a children layout for SegmentedButton"), + cursor, + viewport, + ); + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(segmented_button: SegmentedButton<'a, Message, Renderer>) -> Self { + Self::new(segmented_button) + } +} diff --git a/src/style/mod.rs b/src/style/mod.rs index 241d2f92..4e01c332 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -61,3 +61,8 @@ pub use spinner::SpinnerStyle; pub mod context_menu; #[cfg(feature = "context_menu")] pub use context_menu::ContextMenuStyle; + +#[cfg(feature = "segmented_button")] +pub mod segmented_button; +#[cfg(feature = "segmented_button")] +pub use segmented_button::SegmentedButton; diff --git a/src/style/segmented_button.rs b/src/style/segmented_button.rs new file mode 100644 index 00000000..093144a3 --- /dev/null +++ b/src/style/segmented_button.rs @@ -0,0 +1,98 @@ +//! Use a `segmented_button` as an alternative to radio button. + +use iced_widget::{ + core::{Background, Color}, + style::Theme, +}; +/// The appearance of a [`SegmentedButton`] +#[derive(Clone, Copy, Debug)] +pub struct Appearance { + /// The background of the [`SegmentedButton`] + pub background: Background, + /// selection hightlight color + pub selected_color: Color, + + /// The border radius of the [`SegmentedButton`] + /// If no radius is specified the default one will be used. + pub border_radius: Option, + + /// The border with of the [`SegmentedButton`] + pub border_width: f32, + + /// The border color of the [`SegmentedButton`] + pub border_color: Option, + + /// The default text color of the [`SegmentedButton`] + pub text_color: Color, +} + +/// The appearance of a [`SegmentedButton`] +pub trait StyleSheet { + ///Style for the trait to use. + type Style: Default; + /// The normal appearance of a [`SegmentedButton`](crate::native::segmented_button::SegmentedButton). + fn active(&self, style: &Self::Style) -> Appearance; + + /// The appearance when the [`SegmentedButton`] + fn hovered(&self, style: &Self::Style) -> Appearance { + let active = self.active(style); + + Appearance { + background: Background::Color([0.33, 0.87, 0.33].into()), + selected_color: Color::from_rgb(0.208, 0.576, 0.961), + ..active + } + } +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: Background::Color([0.87, 0.87, 0.87].into()), + selected_color: Color::from_rgb( + 0x5E as f32 / 255.0, + 0x7C as f32 / 255.0, + 0xE2 as f32 / 255.0, + ), + border_radius: None, + border_width: 1.0, + border_color: Some([0.8, 0.8, 0.8].into()), + text_color: Color::BLACK, + } + } +} + +#[derive(Default)] +#[allow(missing_docs, clippy::missing_docs_in_private_items)] +/// Default ``SegmentedButton`` Styles +pub enum SegmentedButton { + #[default] + Default, + Custom(Box>), +} + +impl SegmentedButton { + /// Creates a custom [`SegmentedButtonStyles`] style variant. + pub fn custom(style_sheet: impl StyleSheet