diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index 190ea9c0..e34d267f 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -20,6 +20,7 @@ pub mod scrollable; pub mod slider; pub mod svg; pub mod text_input; +pub mod toggler; pub mod tooltip; mod column; @@ -50,6 +51,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; pub use column::Column; diff --git a/graphics/src/widget/toggler.rs b/graphics/src/widget/toggler.rs new file mode 100644 index 00000000..a258443e --- /dev/null +++ b/graphics/src/widget/toggler.rs @@ -0,0 +1,94 @@ +//! Show toggle controls using togglers. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::toggler; +use iced_native::Rectangle; + +pub use iced_style::toggler::{Style, StyleSheet}; + +/// Makes sure that the border radius of the toggler looks good at every size. +const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; + +/// The space ratio between the background Quad and the Toggler bounds, and +/// between the background Quad and foreground Quad. +const SPACE_RATIO: f32 = 0.05; + +/// A toggler that can be toggled. +/// +/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. +pub type Toggler = + iced_native::Toggler>; + +impl toggler::Renderer for Renderer +where + B: Backend + backend::Text, +{ + type Style = Box; + + const DEFAULT_SIZE: u16 = 20; + + fn draw( + &mut self, + bounds: Rectangle, + is_active: bool, + is_mouse_over: bool, + (label, _): Self::Output, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered(is_active) + } else { + style_sheet.active(is_active) + }; + + let border_radius = bounds.height as f32 / BORDER_RADIUS_RATIO; + let space = SPACE_RATIO * bounds.height as f32; + + let toggler_background_bounds = Rectangle { + x: bounds.x + space, + y: bounds.y + space, + width: bounds.width - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + let toggler_background = Primitive::Quad { + bounds: toggler_background_bounds, + background: style.background.into(), + border_radius, + border_width: 1.0, + border_color: style.background_border.unwrap_or(style.background), + }; + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + if is_active { + bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + } else { + 2.0 * space + }, + y: bounds.y + (2.0 * space), + width: bounds.height - (4.0 * space), + height: bounds.height - (4.0 * space), + }; + + let toggler_foreground = Primitive::Quad { + bounds: toggler_foreground_bounds, + background: style.foreground.into(), + border_radius, + border_width: 1.0, + border_color: style.foreground_border.unwrap_or(style.foreground), + }; + + ( + Primitive::Group { + primitives: vec![label, toggler_background, toggler_foreground], + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 28746585..89bb9433 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,6 +1,6 @@ use crate::{ button, checkbox, column, container, pane_grid, progress_bar, radio, row, - scrollable, slider, text, text_input, Color, Element, Font, + scrollable, slider, text, text_input, toggler, Color, Element, Font, HorizontalAlignment, Layout, Padding, Point, Rectangle, Renderer, Size, VerticalAlignment, }; @@ -288,3 +288,19 @@ impl pane_grid::Renderer for Null { ) { } } + +impl toggler::Renderer for Null { + type Style = (); + + const DEFAULT_SIZE: u16 = 20; + + fn draw( + &mut self, + _bounds: Rectangle, + _is_checked: bool, + _is_mouse_over: bool, + _label: Self::Output, + _style: &Self::Style, + ) { + } +} diff --git a/native/src/widget.rs b/native/src/widget.rs index 791c53a3..759fe71a 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -36,6 +36,7 @@ pub mod space; pub mod svg; pub mod text; pub mod text_input; +pub mod toggler; pub mod tooltip; #[doc(no_inline)] @@ -73,6 +74,8 @@ pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; use crate::event::{self, Event}; diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs new file mode 100644 index 00000000..250abac4 --- /dev/null +++ b/native/src/widget/toggler.rs @@ -0,0 +1,262 @@ +//! Show toggle controls using togglers. +use std::hash::Hash; + +use crate::{ + event, layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, + HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, + VerticalAlignment, Widget, +}; + +/// A toggler widget +/// +/// # Example +/// +/// ``` +/// # type Toggler = iced_native::Toggler; +/// # +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, "Toggle me!", |b| Message::TogglerToggled(b)) +/// ``` +/// +#[allow(missing_debug_implementations)] +pub struct Toggler { + is_active: bool, + on_toggle: Box Message>, + label: String, + width: Length, + size: u16, + text_size: Option, + font: Renderer::Font, + style: Renderer::Style, +} + +impl + Toggler +{ + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * the label of the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn new(is_active: bool, label: impl Into, f: F) -> Self + where + F: 'static + Fn(bool) -> Message, + { + Toggler { + is_active, + on_toggle: Box::new(f), + label: label.into(), + width: Length::Fill, + size: ::DEFAULT_SIZE, + text_size: None, + font: Renderer::Font::default(), + style: Renderer::Style::default(), + } + } + + /// Sets the size of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the text size o the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + /// + /// [`Toggler`]: struct.Toggler.html + /// [`Font`]: ../../struct.Font.html + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); + self + } +} + +impl Widget for Toggler +where + Renderer: self::Renderer + text::Renderer + row::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + Row::<(), Renderer>::new() + .width(self.width) + .align_items(Align::Center) + .push( + Text::new(&self.label) + .font(self.font) + .width(self.width) + .size(self.text_size.unwrap_or(renderer.default_size())), + ) + .push( + Row::new() + .width(Length::Units(2 * self.size)) + .height(Length::Units(self.size)), + ) + .layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + messages: &mut Vec, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = layout.bounds().contains(cursor_position); + + if mouse_over { + messages.push((self.on_toggle)(!self.is_active)); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let bounds = layout.bounds(); + let mut children = layout.children(); + + let label_layout = children.next().unwrap(); + let toggler_layout = children.next().unwrap(); + let toggler_bounds = toggler_layout.bounds(); + + let label = text::Renderer::draw( + renderer, + defaults, + label_layout.bounds(), + &self.label, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + None, + HorizontalAlignment::Left, + VerticalAlignment::Center, + ); + + let is_mouse_over = bounds.contains(cursor_position); + + self::Renderer::draw( + renderer, + toggler_bounds, + self.is_active, + is_mouse_over, + label, + &self.style, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::().hash(state); + + self.label.hash(state) + } +} + +/// The renderer of a [`Toggler`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Toggler`] in your user interface. +/// +/// [`Toggler`]: struct.Toggler.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// The default size of a [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + const DEFAULT_SIZE: u16; + + /// Draws a [`Toggler`]. + /// + /// It receives: + /// * the bounds of the [`Toggler`] + /// * whether the [`Toggler`] is activated or not + /// * whether the mouse is over the [`Toggler`] or not + /// * the drawn label of the [`Toggler`] + /// * the style of the [`Toggler`] + /// + /// [`Toggler`]: struct.Toggler.html + fn draw( + &mut self, + bounds: Rectangle, + is_active: bool, + is_mouse_over: bool, + label: Self::Output, + style: &Self::Style, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Message: 'a, +{ + fn from( + toggler: Toggler, + ) -> Element<'a, Message, Renderer> { + Element::new(toggler) + } +} diff --git a/src/widget.rs b/src/widget.rs index eac50d57..db052106 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -17,8 +17,8 @@ mod platform { pub use crate::renderer::widget::{ button, checkbox, container, pane_grid, pick_list, progress_bar, radio, - rule, scrollable, slider, text_input, tooltip, Column, Row, Space, - Text, + rule, scrollable, slider, text_input, toggler, tooltip, Column, Row, + Space, Text, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] @@ -53,7 +53,7 @@ mod platform { button::Button, checkbox::Checkbox, container::Container, image::Image, pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider, - svg::Svg, text_input::TextInput, tooltip::Tooltip, + svg::Svg, text_input::TextInput, toggler::Toggler, tooltip::Tooltip, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] diff --git a/style/src/lib.rs b/style/src/lib.rs index f09b5f9d..08d9f044 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -18,3 +18,4 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; diff --git a/style/src/toggler.rs b/style/src/toggler.rs new file mode 100644 index 00000000..5a155123 --- /dev/null +++ b/style/src/toggler.rs @@ -0,0 +1,57 @@ +//! Show toggle controls using togglers. +use iced_core::Color; + +/// The appearance of a toggler. +#[derive(Debug)] +pub struct Style { + pub background: Color, + pub background_border: Option, + pub foreground: Color, + pub foreground_border: Option, +} + +/// A set of rules that dictate the style of a toggler. +pub trait StyleSheet { + fn active(&self, is_active: bool) -> Style; + + fn hovered(&self, is_active: bool) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self, is_active: bool) -> Style { + Style { + background: if is_active { + Color::from_rgb(0.0, 1.0, 0.0) + } else { + Color::from_rgb(0.7, 0.7, 0.7) + }, + background_border: None, + foreground: Color::WHITE, + foreground_border: None, + } + } + + fn hovered(&self, is_active: bool) -> Style { + Style { + foreground: Color::from_rgb(0.95, 0.95, 0.95), + ..self.active(is_active) + } + } +} + +impl std::default::Default for Box { + fn default() -> Self { + Box::new(Default) + } +} + +impl From for Box +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index 304bb726..a575d036 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -20,6 +20,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; pub mod tooltip; #[doc(no_inline)] @@ -45,6 +46,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; #[cfg(feature = "canvas")] diff --git a/wgpu/src/widget/toggler.rs b/wgpu/src/widget/toggler.rs new file mode 100644 index 00000000..dfcf759b --- /dev/null +++ b/wgpu/src/widget/toggler.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using togglers. +use crate::Renderer; + +pub use iced_graphics::toggler::{Style, StyleSheet}; + +/// A toggler that can be toggled +/// +/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. +pub type Toggler = iced_native::Toggler;