diff --git a/web/src/css.rs b/web/src/css.rs index 66c363f2..21f51f85 100644 --- a/web/src/css.rs +++ b/web/src/css.rs @@ -14,6 +14,9 @@ pub enum Rule { /// Spacing between elements Spacing(u16), + + /// Toggler input for a specific size + Toggler(u16), } impl Rule { @@ -23,6 +26,7 @@ impl Rule { Rule::Column => String::from("c"), Rule::Row => String::from("r"), Rule::Spacing(spacing) => format!("s-{}", spacing), + Rule::Toggler(size) => format!("toggler-{}", size), } } @@ -55,6 +59,46 @@ impl Rule { class ) .into_bump_str(), + Rule::Toggler(size) => bumpalo::format!( + in bump, + ".toggler-{} {{ display: flex; cursor: pointer; justify-content: space-between; }} \ + .toggler-{} input {{ display:none; }} \ + .toggler-{} span {{ background-color: #b1b1b1; position: relative; display: inline-flex; width:{}px; height: {}px; border-radius: {}px;}} \ + .toggler-{} span > span {{ background-color: #FFFFFF; width: {}px; height: {}px; border-radius: 50%; top: 1px; left: 1px;}} \ + .toggler-{}:hover span > span {{ background-color: #f1f1f1 !important; }} \ + .toggler-{} input:checked + span {{ background-color: #00FF00; }} \ + .toggler-{} input:checked + span > span {{ -webkit-transform: translateX({}px); -ms-transform:translateX({}px); transform: translateX({}px); }} + ", + // toggler + size, + + // toggler input + size, + + // toggler span + size, + size*2, + size, + size, + + // toggler span > span + size, + size-2, + size-2, + + // toggler: hover + span > span + size, + + // toggler input:checked + span + size, + + // toggler input:checked + span > span + size, + size, + size, + size + ) + .into_bump_str(), } } } diff --git a/web/src/widget.rs b/web/src/widget.rs index 023f5f13..4cb0a9cc 100644 --- a/web/src/widget.rs +++ b/web/src/widget.rs @@ -24,6 +24,7 @@ pub mod radio; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; mod column; mod row; @@ -40,6 +41,8 @@ pub use slider::Slider; pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use toggler::Toggler; pub use checkbox::Checkbox; pub use column::Column; diff --git a/web/src/widget/toggler.rs b/web/src/widget/toggler.rs new file mode 100644 index 00000000..5bcc1fc7 --- /dev/null +++ b/web/src/widget/toggler.rs @@ -0,0 +1,168 @@ +//! Show toggle controls using togglers. +use crate::{css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::toggler::{Style, StyleSheet}; + +use dodrio::bumpalo; +use std::rc::Rc; + +/// A toggler that can be toggled. +/// +/// # Example +/// +/// ``` +/// # use iced_web::Toggler; +/// +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, String::from("Toggle me!"), Message::TogglerToggled); +/// ``` +/// +#[allow(missing_debug_implementations)] +pub struct Toggler { + is_active: bool, + on_toggle: Rc Message>, + label: Option, + id: Option, + width: Length, + style: Box, +} + +impl Toggler { + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is active or not + /// * An optional label for 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: Rc::new(f), + label: label.into(), + id: None, + width: Length::Shrink, + style: Default::default(), + } + } + + /// Sets the width of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + 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 + } + + /// Sets the id of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } +} + +impl Widget for Toggler +where + Message: 'static, +{ + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + bus: &Bus, + style_sheet: &mut Css<'b>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + use dodrio::bumpalo::collections::String; + + let toggler_label = &self.label.as_ref().map(|label| { + String::from_str_in(&label, bump).into_bump_str() + }); + + let event_bus = bus.clone(); + let on_toggle = self.on_toggle.clone(); + let is_active = self.is_active; + + let row_class = style_sheet.insert(bump, css::Rule::Row); + let toggler_class = style_sheet.insert(bump, css::Rule::Toggler(16)); + + let (label, input) = if let Some(id) = &self.id { + let id = String::from_str_in(id, bump).into_bump_str(); + + (label(bump).attr("for", id), input(bump).attr("id", id)) + } else { + (label(bump), input(bump)) + }; + + let checkbox = input + .attr("type", "checkbox") + .bool_attr("checked", self.is_active) + .on("click", move |_root, vdom, _event| { + let msg = on_toggle(!is_active); + event_bus.publish(msg); + + vdom.schedule_render(); + }) + .finish(); + + let toggler = span(bump) + .children(vec![span(bump).finish()]) + .finish(); + + label + .attr( + "class", + bumpalo::format!(in bump, "{} {}", row_class, toggler_class) + .into_bump_str(), + ) + .attr( + "style", + bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width)) + .into_bump_str() + ) + .children( + if let Some(label) = toggler_label { + vec![ + text(label), + checkbox, + toggler, + ] + } else { + vec![ + checkbox, + toggler, + ] + } + ) + .finish() + } +} + +impl<'a, Message> From> for Element<'a, Message> +where + Message: 'static, +{ + fn from(toggler: Toggler) -> Element<'a, Message> { + Element::new(toggler) + } +} \ No newline at end of file