diff options
Diffstat (limited to 'web/src')
-rw-r--r-- | web/src/bus.rs | 32 | ||||
-rw-r--r-- | web/src/css.rs (renamed from web/src/style.rs) | 85 | ||||
-rw-r--r-- | web/src/element.rs | 14 | ||||
-rw-r--r-- | web/src/hasher.rs | 21 | ||||
-rw-r--r-- | web/src/lib.rs | 139 | ||||
-rw-r--r-- | web/src/subscription.rs | 19 | ||||
-rw-r--r-- | web/src/widget.rs | 16 | ||||
-rw-r--r-- | web/src/widget/button.rs | 70 | ||||
-rw-r--r-- | web/src/widget/checkbox.rs | 63 | ||||
-rw-r--r-- | web/src/widget/column.rs | 23 | ||||
-rw-r--r-- | web/src/widget/container.rs | 44 | ||||
-rw-r--r-- | web/src/widget/image.rs | 93 | ||||
-rw-r--r-- | web/src/widget/progress_bar.rs | 124 | ||||
-rw-r--r-- | web/src/widget/radio.rs | 23 | ||||
-rw-r--r-- | web/src/widget/row.rs | 23 | ||||
-rw-r--r-- | web/src/widget/scrollable.rs | 26 | ||||
-rw-r--r-- | web/src/widget/slider.rs | 27 | ||||
-rw-r--r-- | web/src/widget/space.rs | 69 | ||||
-rw-r--r-- | web/src/widget/text.rs | 24 | ||||
-rw-r--r-- | web/src/widget/text_input.rs | 80 |
20 files changed, 767 insertions, 248 deletions
diff --git a/web/src/bus.rs b/web/src/bus.rs index 1b650b28..c66e9659 100644 --- a/web/src/bus.rs +++ b/web/src/bus.rs @@ -1,5 +1,4 @@ -use crate::Instance; - +use iced_futures::futures::channel::mpsc; use std::rc::Rc; /// A publisher of messages. @@ -8,21 +7,26 @@ use std::rc::Rc; /// /// [`Application`]: trait.Application.html #[allow(missing_debug_implementations)] -#[derive(Clone)] pub struct Bus<Message> { - publish: Rc<Box<dyn Fn(Message, &mut dyn dodrio::RootRender)>>, + publish: Rc<Box<dyn Fn(Message) -> ()>>, +} + +impl<Message> Clone for Bus<Message> { + fn clone(&self) -> Self { + Bus { + publish: self.publish.clone(), + } + } } impl<Message> Bus<Message> where - Message: 'static + Clone, + Message: 'static, { - pub(crate) fn new() -> Self { + pub(crate) fn new(publish: mpsc::UnboundedSender<Message>) -> Self { Self { - publish: Rc::new(Box::new(|message, root| { - let app = root.unwrap_mut::<Instance<Message>>(); - - app.update(message) + publish: Rc::new(Box::new(move |message| { + publish.unbounded_send(message).expect("Send message"); })), } } @@ -30,8 +34,8 @@ where /// Publishes a new message for the [`Application`]. /// /// [`Application`]: trait.Application.html - pub fn publish(&self, message: Message, root: &mut dyn dodrio::RootRender) { - (self.publish)(message, root); + pub fn publish(&self, message: Message) { + (self.publish)(message) } /// Creates a new [`Bus`] that applies the given function to the messages @@ -45,9 +49,7 @@ where let publish = self.publish.clone(); Bus { - publish: Rc::new(Box::new(move |message, root| { - publish(mapper(message), root) - })), + publish: Rc::new(Box::new(move |message| publish(mapper(message)))), } } } diff --git a/web/src/style.rs b/web/src/css.rs index 2fb8602a..6a307770 100644 --- a/web/src/style.rs +++ b/web/src/css.rs @@ -1,11 +1,11 @@ //! Style your widgets. -use crate::{bumpalo, Align, Color, Length}; +use crate::{bumpalo, Align, Background, Color, Length}; use std::collections::BTreeMap; -/// The style of a VDOM node. +/// A CSS rule of a VDOM node. #[derive(Debug)] -pub enum Style { +pub enum Rule { /// Container with vertical distribution Column, @@ -19,16 +19,16 @@ pub enum Style { Spacing(u16), } -impl Style { +impl Rule { /// Returns the class name of the [`Style`]. /// /// [`Style`]: enum.Style.html pub fn class<'a>(&self) -> String { match self { - Style::Column => String::from("c"), - Style::Row => String::from("r"), - Style::Padding(padding) => format!("p-{}", padding), - Style::Spacing(spacing) => format!("s-{}", spacing), + Rule::Column => String::from("c"), + Rule::Row => String::from("r"), + Rule::Padding(padding) => format!("p-{}", padding), + Rule::Spacing(spacing) => format!("s-{}", spacing), } } @@ -39,24 +39,24 @@ impl Style { let class = self.class(); match self { - Style::Column => { + Rule::Column => { let body = "{ display: flex; flex-direction: column; }"; bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() } - Style::Row => { + Rule::Row => { let body = "{ display: flex; flex-direction: row; }"; bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() } - Style::Padding(padding) => bumpalo::format!( + Rule::Padding(padding) => bumpalo::format!( in bump, ".{} {{ box-sizing: border-box; padding: {}px }}", class, padding ) .into_bump_str(), - Style::Spacing(spacing) => bumpalo::format!( + Rule::Spacing(spacing) => bumpalo::format!( in bump, ".c.{} > * {{ margin-bottom: {}px }} \ .r.{} > * {{ margin-right: {}px }} \ @@ -74,34 +74,34 @@ impl Style { } } -/// A sheet of styles. +/// A cascading style sheet. #[derive(Debug)] -pub struct Sheet<'a> { - styles: BTreeMap<String, &'a str>, +pub struct Css<'a> { + rules: BTreeMap<String, &'a str>, } -impl<'a> Sheet<'a> { +impl<'a> Css<'a> { /// Creates an empty style [`Sheet`]. /// /// [`Sheet`]: struct.Sheet.html pub fn new() -> Self { - Self { - styles: BTreeMap::new(), + Css { + rules: BTreeMap::new(), } } - /// Inserts the [`Style`] in the [`Sheet`], if it was not previously + /// Inserts the [`rule`] in the [`Sheet`], if it was not previously /// inserted. /// - /// It returns the class name of the provided [`Style`]. + /// It returns the class name of the provided [`Rule`]. /// /// [`Sheet`]: struct.Sheet.html - /// [`Style`]: enum.Style.html - pub fn insert(&mut self, bump: &'a bumpalo::Bump, style: Style) -> String { - let class = style.class(); + /// [`Rule`]: enum.Rule.html + pub fn insert(&mut self, bump: &'a bumpalo::Bump, rule: Rule) -> String { + let class = rule.class(); - if !self.styles.contains_key(&class) { - let _ = self.styles.insert(class.clone(), style.declaration(bump)); + if !self.rules.contains_key(&class) { + let _ = self.rules.insert(class.clone(), rule.declaration(bump)); } class @@ -119,12 +119,12 @@ impl<'a> Sheet<'a> { declarations.push(text( "body { height: 100%; margin: 0; padding: 0; font-family: sans-serif }", )); - declarations.push(text("p { margin: 0 }")); + declarations.push(text("* { margin: 0; padding: 0 }")); declarations.push(text( "button { border: none; cursor: pointer; outline: none }", )); - for declaration in self.styles.values() { + for declaration in self.rules.values() { declarations.push(text(*declaration)); } @@ -139,7 +139,27 @@ pub fn length(length: Length) -> String { match length { Length::Shrink => String::from("auto"), Length::Units(px) => format!("{}px", px), - Length::Fill => String::from("100%"), + Length::Fill | Length::FillPortion(_) => String::from("100%"), + } +} + +/// Returns the style value for the given maximum length in units. +pub fn max_length(units: u32) -> String { + use std::u32; + + if units == u32::MAX { + String::from("initial") + } else { + format!("{}px", units) + } +} + +/// Returns the style value for the given minimum length in units. +pub fn min_length(units: u32) -> String { + if units == 0 { + String::from("initial") + } else { + format!("{}px", units) } } @@ -150,6 +170,15 @@ pub fn color(Color { r, g, b, a }: Color) -> String { format!("rgba({}, {}, {}, {})", 255.0 * r, 255.0 * g, 255.0 * b, a) } +/// Returns the style value for the given [`Background`]. +/// +/// [`Background`]: ../struct.Background.html +pub fn background(background: Background) -> String { + match background { + Background::Color(c) => color(c), + } +} + /// Returns the style value for the given [`Align`]. /// /// [`Align`]: ../enum.Align.html diff --git a/web/src/element.rs b/web/src/element.rs index 85fa7c34..93e73713 100644 --- a/web/src/element.rs +++ b/web/src/element.rs @@ -1,4 +1,4 @@ -use crate::{style, Bus, Color, Widget}; +use crate::{Bus, Color, Css, Widget}; use dodrio::bumpalo; use std::rc::Rc; @@ -38,8 +38,8 @@ impl<'a, Message> Element<'a, Message> { /// [`Element`]: struct.Element.html pub fn map<F, B>(self, f: F) -> Element<'a, B> where - Message: 'static + Clone, - B: 'static + Clone, + Message: 'static, + B: 'static, F: 'static + Fn(Message) -> B, { Element { @@ -57,7 +57,7 @@ impl<'a, Message> Element<'a, Message> { &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { self.widget.node(bump, bus, style_sheet) } @@ -82,14 +82,14 @@ impl<'a, A, B> Map<'a, A, B> { impl<'a, A, B> Widget<B> for Map<'a, A, B> where - A: 'static + Clone, - B: 'static + Clone, + A: 'static, + B: 'static, { fn node<'b>( &self, bump: &'b bumpalo::Bump, bus: &Bus<B>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { self.widget .node(bump, &bus.map(self.mapper.clone()), style_sheet) diff --git a/web/src/hasher.rs b/web/src/hasher.rs new file mode 100644 index 00000000..1a28a2f9 --- /dev/null +++ b/web/src/hasher.rs @@ -0,0 +1,21 @@ +use std::collections::hash_map::DefaultHasher; + +/// The hasher used to compare subscriptions. +#[derive(Debug)] +pub struct Hasher(DefaultHasher); + +impl Default for Hasher { + fn default() -> Self { + Hasher(DefaultHasher::default()) + } +} + +impl core::hash::Hasher for Hasher { + fn write(&mut self, bytes: &[u8]) { + self.0.write(bytes) + } + + fn finish(&self) -> u64 { + self.0.finish() + } +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 8239ffc9..258ad9e7 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -54,27 +54,37 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] use dodrio::bumpalo; use std::{cell::RefCell, rc::Rc}; mod bus; mod element; +mod hasher; -pub mod style; +pub mod css; +pub mod subscription; pub mod widget; pub use bus::Bus; +pub use css::Css; pub use dodrio; pub use element::Element; +pub use hasher::Hasher; pub use iced_core::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - VerticalAlignment, + Align, Background, Color, Font, HorizontalAlignment, Length, Point, Size, + Vector, VerticalAlignment, }; -pub use style::Style; +pub use iced_futures::{executor, futures, Command}; +pub use subscription::Subscription; + +#[doc(no_inline)] pub use widget::*; +#[doc(no_inline)] +pub use executor::Executor; + /// An interactive web application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -87,7 +97,15 @@ pub trait Application { /// The type of __messages__ your [`Application`] will produce. /// /// [`Application`]: trait.Application.html - type Message: Clone; + type Message: Send; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [`executor::WasmBindgen`] can be a good choice for the Web. + /// + /// [`Executor`]: trait.Executor.html + /// [`executor::Default`]: executor/struct.Default.html + type Executor: Executor; /// Initializes the [`Application`]. /// @@ -130,6 +148,20 @@ pub trait Application { /// [`Application`]: trait.Application.html fn view(&mut self) -> Element<'_, Self::Message>; + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + /// Runs the [`Application`]. /// /// [`Application`]: trait.Application.html @@ -137,71 +169,66 @@ pub trait Application { where Self: 'static + Sized, { - let (app, command) = Self::new(); - let mut instance = Instance::new(app); + use futures::stream::StreamExt; - instance.spawn(command); + let (app, command) = Self::new(); let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - document.set_title(&instance.title); - let body = document.body().unwrap(); - let vdom = dodrio::Vdom::new(&body, instance); - vdom.forget(); - } -} + let mut title = app.title(); + document.set_title(&title); -#[derive(Clone)] -struct Instance<Message> { - title: String, - ui: Rc<RefCell<Box<dyn Application<Message = Message>>>>, -} + let (sender, receiver) = + iced_futures::futures::channel::mpsc::unbounded(); -impl<Message> Instance<Message> -where - Message: 'static + Clone, -{ - fn new(ui: impl Application<Message = Message> + 'static) -> Self { - Self { - title: ui.title(), - ui: Rc::new(RefCell::new(Box::new(ui))), - } - } + let mut runtime = iced_futures::Runtime::new( + Self::Executor::new().expect("Create executor"), + sender.clone(), + ); + runtime.spawn(command); - fn update(&mut self, message: Message) { - let command = self.ui.borrow_mut().update(message); - let title = self.ui.borrow().title(); + let application = Rc::new(RefCell::new(app)); - self.spawn(command); + let instance = Instance { + application: application.clone(), + bus: Bus::new(sender), + }; - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); + let vdom = dodrio::Vdom::new(&body, instance); - if self.title != title { - document.set_title(&title); + let event_loop = receiver.for_each(move |message| { + let command = application.borrow_mut().update(message); + let subscription = application.borrow().subscription(); + let new_title = application.borrow().title(); - self.title = title; - } - } + runtime.spawn(command); + runtime.track(subscription); + + if title != new_title { + document.set_title(&new_title); - fn spawn(&mut self, command: Command<Message>) { - use futures::FutureExt; + title = new_title; + } - for future in command.futures() { - let mut instance = self.clone(); - let future = future.map(move |message| instance.update(message)); + vdom.weak().schedule_render(); - wasm_bindgen_futures::spawn_local(future); - } + futures::future::ready(()) + }); + + wasm_bindgen_futures::spawn_local(event_loop); } } -impl<Message> dodrio::Render for Instance<Message> +struct Instance<A: Application> { + application: Rc<RefCell<A>>, + bus: Bus<A::Message>, +} + +impl<A> dodrio::Render for Instance<A> where - Message: 'static + Clone, + A: Application, { fn render<'a, 'bump>( &'a self, @@ -212,15 +239,15 @@ where { use dodrio::builder::*; - let mut ui = self.ui.borrow_mut(); + let mut ui = self.application.borrow_mut(); let element = ui.view(); - let mut style_sheet = style::Sheet::new(); + let mut css = Css::new(); - let node = element.widget.node(bump, &Bus::new(), &mut style_sheet); + let node = element.widget.node(bump, &self.bus, &mut css); div(bump) .attr("style", "width: 100%; height: 100%") - .children(vec![style_sheet.node(bump), node]) + .children(vec![css.node(bump), node]) .finish() } } diff --git a/web/src/subscription.rs b/web/src/subscription.rs new file mode 100644 index 00000000..6b8415c0 --- /dev/null +++ b/web/src/subscription.rs @@ -0,0 +1,19 @@ +//! Listen to external events in your application. +use crate::Hasher; + +/// A request to listen to external events. +/// +/// Besides performing async actions on demand with [`Command`], most +/// applications also need to listen to external events passively. +/// +/// A [`Subscription`] is normally provided to some runtime, like a [`Command`], +/// and it will generate events as long as the user keeps requesting it. +/// +/// For instance, you can use a [`Subscription`] to listen to a WebSocket +/// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// [`Command`]: ../struct.Command.html +/// [`Subscription`]: struct.Subscription.html +pub type Subscription<T> = iced_futures::Subscription<Hasher, (), T>; + +pub use iced_futures::subscription::Recipe; diff --git a/web/src/widget.rs b/web/src/widget.rs index b0e16692..025cf22f 100644 --- a/web/src/widget.rs +++ b/web/src/widget.rs @@ -14,20 +14,22 @@ //! ``` //! //! [`Widget`]: trait.Widget.html -use crate::{style, Bus}; +use crate::{Bus, Css}; use dodrio::bumpalo; pub mod button; +pub mod checkbox; +pub mod container; +pub mod image; +pub mod progress_bar; +pub mod radio; pub mod scrollable; pub mod slider; pub mod text_input; -mod checkbox; mod column; -mod container; -mod image; -mod radio; mod row; +mod space; mod text; #[doc(no_inline)] @@ -45,8 +47,10 @@ pub use checkbox::Checkbox; pub use column::Column; pub use container::Container; pub use image::Image; +pub use progress_bar::ProgressBar; pub use radio::Radio; pub use row::Row; +pub use space::Space; /// A component that displays information and allows interaction. /// @@ -62,6 +66,6 @@ pub trait Widget<Message> { &self, bump: &'b bumpalo::Bump, _bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b>; } diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs index 889c0ab1..3a5afe60 100644 --- a/web/src/widget/button.rs +++ b/web/src/widget/button.rs @@ -4,7 +4,9 @@ //! //! [`Button`]: struct.Button.html //! [`State`]: struct.State.html -use crate::{style, Background, Bus, Element, Length, Style, Widget}; +use crate::{css, Background, Bus, Css, Element, Length, Widget}; + +pub use iced_style::button::{Style, StyleSheet}; use dodrio::bumpalo; @@ -26,10 +28,11 @@ pub struct Button<'a, Message> { content: Element<'a, Message>, on_press: Option<Message>, width: Length, + height: Length, min_width: u32, + min_height: u32, padding: u16, - background: Option<Background>, - border_radius: u16, + style: Box<dyn StyleSheet>, } impl<'a, Message> Button<'a, Message> { @@ -46,10 +49,11 @@ impl<'a, Message> Button<'a, Message> { content: content.into(), on_press: None, width: Length::Shrink, + height: Length::Shrink, min_width: 0, - padding: 0, - background: None, - border_radius: 0, + min_height: 0, + padding: 5, + style: Default::default(), } } @@ -61,6 +65,14 @@ impl<'a, Message> Button<'a, Message> { self } + /// Sets the height of the [`Button`]. + /// + /// [`Button`]: struct.Button.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + /// Sets the minimum width of the [`Button`]. /// /// [`Button`]: struct.Button.html @@ -69,28 +81,27 @@ impl<'a, Message> Button<'a, Message> { self } - /// Sets the padding of the [`Button`]. + /// Sets the minimum height of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + pub fn min_height(mut self, min_height: u32) -> Self { + self.min_height = min_height; self } - /// Sets the [`Background`] of the [`Button`]. + /// Sets the padding of the [`Button`]. /// /// [`Button`]: struct.Button.html - /// [`Background`]: ../../struct.Background.html - pub fn background(mut self, background: Background) -> Self { - self.background = Some(background); + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; self } - /// Sets the border radius of the [`Button`]. + /// Sets the style of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn border_radius(mut self, border_radius: u16) -> Self { - self.border_radius = border_radius; + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); self } @@ -126,17 +137,20 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + // TODO: State-based styling + let style = self.style.active(); + let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); - let background = match self.background { + let background = match style.background { None => String::from("none"), Some(background) => match background { - Background::Color(color) => style::color(color), + Background::Color(color) => css::color(color), }, }; @@ -149,10 +163,12 @@ where "style", bumpalo::format!( in bump, - "background: {}; border-radius: {}px; min-width: {}px", + "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}", background, - self.border_radius, - self.min_width + style.border_radius, + css::length(self.width), + css::min_length(self.min_width), + css::color(style.text_color) ) .into_bump_str(), ) @@ -161,15 +177,11 @@ where if let Some(on_press) = self.on_press.clone() { let event_bus = bus.clone(); - node = node.on("click", move |root, vdom, _event| { - event_bus.publish(on_press.clone(), root); - - vdom.schedule_render(); + node = node.on("click", move |_root, _vdom, _event| { + event_bus.publish(on_press.clone()); }); } - // TODO: Complete styling - node.finish() } } diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs index b81a0d52..0657ccfb 100644 --- a/web/src/widget/checkbox.rs +++ b/web/src/widget/checkbox.rs @@ -1,6 +1,10 @@ -use crate::{style, Bus, Color, Element, Widget}; +//! Show toggle controls using checkboxes. +use crate::{css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::checkbox::{Style, StyleSheet}; use dodrio::bumpalo; +use std::rc::Rc; /// A box that can be checked. /// @@ -22,9 +26,10 @@ use dodrio::bumpalo; #[allow(missing_debug_implementations)] pub struct Checkbox<Message> { is_checked: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Rc<dyn Fn(bool) -> Message>, label: String, - label_color: Option<Color>, + width: Length, + style: Box<dyn StyleSheet>, } impl<Message> Checkbox<Message> { @@ -44,51 +49,77 @@ impl<Message> Checkbox<Message> { { Checkbox { is_checked, - on_toggle: Box::new(f), + on_toggle: Rc::new(f), label: String::from(label), - label_color: None, + width: Length::Shrink, + style: Default::default(), } } - /// Sets the color of the label of the [`Checkbox`]. + /// Sets the width of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the style of the [`Checkbox`]. /// /// [`Checkbox`]: struct.Checkbox.html - pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); self } } impl<Message> Widget<Message> for Checkbox<Message> where - Message: 'static + Clone, + Message: 'static, { fn node<'b>( &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; let checkbox_label = bumpalo::format!(in bump, "{}", self.label); let event_bus = bus.clone(); - let msg = (self.on_toggle)(!self.is_checked); + let on_toggle = self.on_toggle.clone(); + let is_checked = self.is_checked; + + let row_class = style_sheet.insert(bump, css::Rule::Row); + + let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5)); - // TODO: Complete styling label(bump) + .attr( + "class", + bumpalo::format!(in bump, "{} {}", row_class, spacing_class) + .into_bump_str(), + ) + .attr( + "style", + bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width)) + .into_bump_str(), + ) .children(vec![ + // TODO: Checkbox styling input(bump) .attr("type", "checkbox") .bool_attr("checked", self.is_checked) - .on("click", move |root, vdom, _event| { - event_bus.publish(msg.clone(), root); + .on("click", move |_root, vdom, _event| { + let msg = on_toggle(!is_checked); + event_bus.publish(msg); vdom.schedule_render(); }) .finish(), - text(checkbox_label.into_bump_str()), + span(bump).children(vec![ + text(checkbox_label.into_bump_str())]).finish(), ]) .finish() } @@ -96,7 +127,7 @@ where impl<'a, Message> From<Checkbox<Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(checkbox: Checkbox<Message>) -> Element<'a, Message> { Element::new(checkbox) diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs index cc850f5f..6454ffba 100644 --- a/web/src/widget/column.rs +++ b/web/src/widget/column.rs @@ -1,4 +1,4 @@ -use crate::{style, Align, Bus, Element, Length, Style, Widget}; +use crate::{css, Align, Bus, Css, Element, Length, Widget}; use dodrio::bumpalo; use std::u32; @@ -112,7 +112,7 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { &self, bump: &'b bumpalo::Bump, publish: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -122,16 +122,13 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { .map(|element| element.widget.node(bump, publish, style_sheet)) .collect(); - let column_class = style_sheet.insert(bump, Style::Column); + let column_class = style_sheet.insert(bump, css::Rule::Column); let spacing_class = - style_sheet.insert(bump, Style::Spacing(self.spacing)); + style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); - - let width = style::length(self.width); - let height = style::length(self.height); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); // TODO: Complete styling div(bump) @@ -142,10 +139,12 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px", - width, - height, - self.max_width + "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}", + css::length(self.width), + css::length(self.height), + css::max_length(self.max_width), + css::max_length(self.max_height), + css::align(self.align_items) ).into_bump_str() ) .children(children) diff --git a/web/src/widget/container.rs b/web/src/widget/container.rs index 25e4ebf8..8e4318f9 100644 --- a/web/src/widget/container.rs +++ b/web/src/widget/container.rs @@ -1,4 +1,7 @@ -use crate::{bumpalo, style, Align, Bus, Element, Length, Style, Widget}; +//! Decorate content and apply alignment. +use crate::{bumpalo, css, Align, Bus, Css, Element, Length, Widget}; + +pub use iced_style::container::{Style, StyleSheet}; /// An element decorating some content. /// @@ -11,6 +14,7 @@ pub struct Container<'a, Message> { max_height: u32, horizontal_alignment: Align, vertical_alignment: Align, + style_sheet: Box<dyn StyleSheet>, content: Element<'a, Message>, } @@ -31,6 +35,7 @@ impl<'a, Message> Container<'a, Message> { max_height: u32::MAX, horizontal_alignment: Align::Start, vertical_alignment: Align::Start, + style_sheet: Default::default(), content: content.into(), } } @@ -84,6 +89,14 @@ impl<'a, Message> Container<'a, Message> { self } + + /// Sets the style of the [`Container`]. + /// + /// [`Container`]: struct.Container.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style_sheet = style.into(); + self + } } impl<'a, Message> Widget<Message> for Container<'a, Message> @@ -94,17 +107,13 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let column_class = style_sheet.insert(bump, Style::Column); + let column_class = style_sheet.insert(bump, css::Rule::Column); - let width = style::length(self.width); - let height = style::length(self.height); - - let align_items = style::align(self.horizontal_alignment); - let justify_content = style::align(self.vertical_alignment); + let style = self.style_sheet.style(); let node = div(bump) .attr( @@ -115,12 +124,17 @@ where "style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px; align-items: {}; justify-content: {}", - width, - height, - self.max_width, - align_items, - justify_content + "width: {}; height: {}; max-width: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px", + css::length(self.width), + css::length(self.height), + css::max_length(self.max_width), + css::align(self.horizontal_alignment), + css::align(self.vertical_alignment), + style.background.map(css::background).unwrap_or(String::from("initial")), + style.text_color.map(css::color).unwrap_or(String::from("inherit")), + style.border_width, + css::color(style.border_color), + style.border_radius ) .into_bump_str(), ) @@ -134,7 +148,7 @@ where impl<'a, Message> From<Container<'a, Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(container: Container<'a, Message>) -> Element<'a, Message> { Element::new(container) diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs index ed8b7ecf..029ab352 100644 --- a/web/src/widget/image.rs +++ b/web/src/widget/image.rs @@ -1,6 +1,12 @@ -use crate::{style, Bus, Element, Length, Widget}; +//! Display images in your user interface. +use crate::{Bus, Css, Element, Hasher, Length, Widget}; use dodrio::bumpalo; +use std::{ + hash::{Hash, Hasher as _}, + path::PathBuf, + sync::Arc, +}; /// A frame that displays an image while keeping aspect ratio. /// @@ -14,7 +20,7 @@ use dodrio::bumpalo; #[derive(Debug)] pub struct Image { /// The image path - pub path: String, + pub handle: Handle, /// The width of the image pub width: Length, @@ -27,9 +33,9 @@ impl Image { /// Creates a new [`Image`] with the given path. /// /// [`Image`]: struct.Image.html - pub fn new<T: Into<String>>(path: T) -> Self { + pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { - path: path.into(), + handle: handle.into(), width: Length::Shrink, height: Length::Shrink, } @@ -57,17 +63,19 @@ impl<Message> Widget<Message> for Image { &self, bump: &'b bumpalo::Bump, _bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let src = bumpalo::format!(in bump, "{}", self.path); + let src = bumpalo::format!(in bump, "{}", match self.handle.data.as_ref() { + Data::Path(path) => path.to_str().unwrap_or("") + }); let mut image = img(bump).attr("src", src.into_bump_str()); match self.width { Length::Shrink => {} - Length::Fill => { + Length::Fill | Length::FillPortion(_) => { image = image.attr("width", "100%"); } Length::Units(px) => { @@ -89,3 +97,74 @@ impl<'a, Message> From<Image> for Element<'a, Message> { Element::new(image) } } + +/// An [`Image`] handle. +/// +/// [`Image`]: struct.Image.html +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc<Data>, +} + +impl Handle { + /// Creates an image [`Handle`] pointing to the image of the given path. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data: Arc::new(data), + } + } + + /// Returns the unique identifier of the [`Handle`]. + /// + /// [`Handle`]: struct.Handle.html + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the image [`Data`]. + /// + /// [`Data`]: enum.Data.html + pub fn data(&self) -> &Data { + &self.data + } +} + +impl From<String> for Handle { + fn from(path: String) -> Handle { + Handle::from_path(path) + } +} + +impl From<&str> for Handle { + fn from(path: &str) -> Handle { + Handle::from_path(path) + } +} + +/// The data of an [`Image`]. +/// +/// [`Image`]: struct.Image.html +#[derive(Clone, Hash)] +pub enum Data { + /// A remote image + Path(PathBuf), +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({:?})", path), + } + } +} diff --git a/web/src/widget/progress_bar.rs b/web/src/widget/progress_bar.rs new file mode 100644 index 00000000..856203c0 --- /dev/null +++ b/web/src/widget/progress_bar.rs @@ -0,0 +1,124 @@ +//! Provide progress feedback to your users. +use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::progress_bar::{Style, StyleSheet}; + +use std::ops::RangeInclusive; + +/// A bar that displays progress. +/// +/// # Example +/// ``` +/// use iced_web::ProgressBar; +/// +/// let value = 50.0; +/// +/// ProgressBar::new(0.0..=100.0, value); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct ProgressBar { + range: RangeInclusive<f32>, + value: f32, + width: Length, + height: Option<Length>, + style: Box<dyn StyleSheet>, +} + +impl ProgressBar { + /// Creates a new [`ProgressBar`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`ProgressBar`] + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { + ProgressBar { + value: value.max(*range.start()).min(*range.end()), + range, + width: Length::Fill, + height: None, + style: Default::default(), + } + } + + /// Sets the width of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn height(mut self, height: Length) -> Self { + self.height = Some(height); + self + } + + /// Sets the style of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message> Widget<Message> for ProgressBar { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _bus: &Bus<Message>, + _style_sheet: &mut Css<'b>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let (range_start, range_end) = self.range.clone().into_inner(); + let amount_filled = + (self.value - range_start) / (range_end - range_start).max(1.0); + + let style = self.style.style(); + + let bar = div(bump) + .attr( + "style", + bumpalo::format!( + in bump, + "width: {}%; height: 100%; background: {}", + amount_filled * 100.0, + css::background(style.bar) + ) + .into_bump_str(), + ) + .finish(); + + let node = div(bump).attr( + "style", + bumpalo::format!( + in bump, + "width: {}; height: {}; background: {}; border-radius: {}px; overflow: hidden;", + css::length(self.width), + css::length(self.height.unwrap_or(Length::Units(30))), + css::background(style.background), + style.border_radius + ) + .into_bump_str(), + ).children(vec![bar]); + + node.finish() + } +} + +impl<'a, Message> From<ProgressBar> for Element<'a, Message> +where + Message: 'static, +{ + fn from(container: ProgressBar) -> Element<'a, Message> { + Element::new(container) + } +} diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs index 4e7d02b8..e00e26db 100644 --- a/web/src/widget/radio.rs +++ b/web/src/widget/radio.rs @@ -1,4 +1,7 @@ -use crate::{style, Bus, Color, Element, Widget}; +//! Create choices using radio buttons. +use crate::{Bus, Css, Element, Widget}; + +pub use iced_style::radio::{Style, StyleSheet}; use dodrio::bumpalo; @@ -32,7 +35,7 @@ pub struct Radio<Message> { is_selected: bool, on_click: Message, label: String, - label_color: Option<Color>, + style: Box<dyn StyleSheet>, } impl<Message> Radio<Message> { @@ -55,15 +58,15 @@ impl<Message> Radio<Message> { is_selected: Some(value) == selected, on_click: f(value), label: String::from(label), - label_color: None, + style: Default::default(), } } - /// Sets the `Color` of the label of the [`Radio`]. + /// Sets the style of the [`Radio`] button. /// /// [`Radio`]: struct.Radio.html - pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); self } } @@ -76,7 +79,7 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -93,10 +96,8 @@ where .attr("type", "radio") .attr("style", "margin-right: 10px") .bool_attr("checked", self.is_selected) - .on("click", move |root, vdom, _event| { - event_bus.publish(on_click.clone(), root); - - vdom.schedule_render(); + .on("click", move |_root, _vdom, _event| { + event_bus.publish(on_click.clone()); }) .finish(), text(radio_label.into_bump_str()), diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs index e47478be..02035113 100644 --- a/web/src/widget/row.rs +++ b/web/src/widget/row.rs @@ -1,4 +1,4 @@ -use crate::{style, Align, Bus, Element, Length, Style, Widget}; +use crate::{css, Align, Bus, Css, Element, Length, Widget}; use dodrio::bumpalo; use std::u32; @@ -113,7 +113,7 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { &self, bump: &'b bumpalo::Bump, publish: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -123,16 +123,13 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { .map(|element| element.widget.node(bump, publish, style_sheet)) .collect(); - let row_class = style_sheet.insert(bump, Style::Row); + let row_class = style_sheet.insert(bump, css::Rule::Row); let spacing_class = - style_sheet.insert(bump, Style::Spacing(self.spacing)); + style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); - - let width = style::length(self.width); - let height = style::length(self.height); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); // TODO: Complete styling div(bump) @@ -143,10 +140,12 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px", - width, - height, - self.max_width + "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}", + css::length(self.width), + css::length(self.height), + css::max_length(self.max_width), + css::max_length(self.max_height), + css::align(self.align_items) ).into_bump_str() ) .children(children) diff --git a/web/src/widget/scrollable.rs b/web/src/widget/scrollable.rs index 710bb70a..07b38aad 100644 --- a/web/src/widget/scrollable.rs +++ b/web/src/widget/scrollable.rs @@ -1,5 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. -use crate::{bumpalo, style, Align, Bus, Column, Element, Length, Widget}; +use crate::{bumpalo, css, Align, Bus, Column, Css, Element, Length, Widget}; + +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. @@ -9,6 +11,7 @@ pub struct Scrollable<'a, Message> { height: Length, max_height: u32, content: Column<'a, Message>, + style: Box<dyn StyleSheet>, } impl<'a, Message> Scrollable<'a, Message> { @@ -24,6 +27,7 @@ impl<'a, Message> Scrollable<'a, Message> { height: Length::Shrink, max_height: u32::MAX, content: Column::new(), + style: Default::default(), } } @@ -85,6 +89,14 @@ impl<'a, Message> Scrollable<'a, Message> { self } + /// Sets the style of the [`Scrollable`] . + /// + /// [`Scrollable`]: struct.Scrollable.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } + /// Adds an element to the [`Scrollable`]. /// /// [`Scrollable`]: struct.Scrollable.html @@ -105,12 +117,14 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let width = style::length(self.width); - let height = style::length(self.height); + let width = css::length(self.width); + let height = css::length(self.height); + + // TODO: Scrollbar styling let node = div(bump) .attr( @@ -126,15 +140,13 @@ where ) .children(vec![self.content.node(bump, bus, style_sheet)]); - // TODO: Complete styling - node.finish() } } impl<'a, Message> From<Scrollable<'a, Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(scrollable: Scrollable<'a, Message>) -> Element<'a, Message> { Element::new(scrollable) diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs index 5b203e07..5aa6439e 100644 --- a/web/src/widget/slider.rs +++ b/web/src/widget/slider.rs @@ -4,7 +4,9 @@ //! //! [`Slider`]: struct.Slider.html //! [`State`]: struct.State.html -use crate::{style, Bus, Element, Length, Widget}; +use crate::{Bus, Css, Element, Length, Widget}; + +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; use dodrio::bumpalo; use std::{ops::RangeInclusive, rc::Rc}; @@ -38,6 +40,7 @@ pub struct Slider<'a, Message> { value: f32, on_change: Rc<Box<dyn Fn(f32) -> Message>>, width: Length, + style: Box<dyn StyleSheet>, } impl<'a, Message> Slider<'a, Message> { @@ -68,6 +71,7 @@ impl<'a, Message> Slider<'a, Message> { range, on_change: Rc::new(Box::new(on_change)), width: Length::Fill, + style: Default::default(), } } @@ -78,17 +82,25 @@ impl<'a, Message> Slider<'a, Message> { self.width = width; self } + + /// Sets the style of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } } impl<'a, Message> Widget<Message> for Slider<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn node<'b>( &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; use wasm_bindgen::JsCast; @@ -103,7 +115,7 @@ where let event_bus = bus.clone(); // TODO: Make `step` configurable - // TODO: Complete styling + // TODO: Styling input(bump) .attr("type", "range") .attr("step", "0.01") @@ -111,7 +123,7 @@ where .attr("max", max.into_bump_str()) .attr("value", value.into_bump_str()) .attr("style", "width: 100%") - .on("input", move |root, vdom, event| { + .on("input", move |_root, _vdom, event| { let slider = match event.target().and_then(|t| { t.dyn_into::<web_sys::HtmlInputElement>().ok() }) { @@ -120,8 +132,7 @@ where }; if let Ok(value) = slider.value().parse::<f32>() { - event_bus.publish(on_change(value), root); - vdom.schedule_render(); + event_bus.publish(on_change(value)); } }) .finish() @@ -130,7 +141,7 @@ where impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(slider: Slider<'a, Message>) -> Element<'a, Message> { Element::new(slider) diff --git a/web/src/widget/space.rs b/web/src/widget/space.rs new file mode 100644 index 00000000..4ce52595 --- /dev/null +++ b/web/src/widget/space.rs @@ -0,0 +1,69 @@ +use crate::{css, Bus, Css, Element, Length, Widget}; +use dodrio::bumpalo; + +/// An amount of empty space. +/// +/// It can be useful if you want to fill some space with nothing. +#[derive(Debug)] +pub struct Space { + width: Length, + height: Length, +} + +impl Space { + /// Creates an amount of empty [`Space`] with the given width and height. + /// + /// [`Space`]: struct.Space.html + pub fn new(width: Length, height: Length) -> Self { + Space { width, height } + } + + /// Creates an amount of horizontal [`Space`]. + /// + /// [`Space`]: struct.Space.html + pub fn with_width(width: Length) -> Self { + Space { + width, + height: Length::Shrink, + } + } + + /// Creates an amount of vertical [`Space`]. + /// + /// [`Space`]: struct.Space.html + pub fn with_height(height: Length) -> Self { + Space { + width: Length::Shrink, + height, + } + } +} + +impl<'a, Message> Widget<Message> for Space { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _publish: &Bus<Message>, + _css: &mut Css<'b>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let width = css::length(self.width); + let height = css::length(self.height); + + let style = bumpalo::format!( + in bump, + "width: {}; height: {};", + width, + height + ); + + div(bump).attr("style", style.into_bump_str()).finish() + } +} + +impl<'a, Message> From<Space> for Element<'a, Message> { + fn from(space: Space) -> Element<'a, Message> { + Element::new(space) + } +} diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs index 6194a12e..3ec565a8 100644 --- a/web/src/widget/text.rs +++ b/web/src/widget/text.rs @@ -1,5 +1,5 @@ use crate::{ - style, Bus, Color, Element, Font, HorizontalAlignment, Length, + css, Bus, Color, Css, Element, Font, HorizontalAlignment, Length, VerticalAlignment, Widget, }; use dodrio::bumpalo; @@ -36,7 +36,7 @@ impl Text { size: None, color: None, font: Font::Default, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, horizontal_alignment: HorizontalAlignment::Left, vertical_alignment: VerticalAlignment::Top, @@ -112,12 +112,18 @@ impl<'a, Message> Widget<Message> for Text { &self, bump: &'b bumpalo::Bump, _publish: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; let content = bumpalo::format!(in bump, "{}", self.content); - let color = style::color(self.color.unwrap_or(Color::BLACK)); + let color = self + .color + .map(css::color) + .unwrap_or(String::from("inherit")); + + let width = css::length(self.width); + let height = css::length(self.height); let text_align = match self.horizontal_alignment { HorizontalAlignment::Left => "left", @@ -127,10 +133,16 @@ impl<'a, Message> Widget<Message> for Text { let style = bumpalo::format!( in bump, - "font-size: {}px; color: {}; text-align: {}", + "width: {}; height: {}; font-size: {}px; color: {}; text-align: {}; font-family: {}", + width, + height, self.size.unwrap_or(20), color, - text_align + text_align, + match self.font { + Font::Default => "inherit", + Font::External { name, .. } => name, + } ); // TODO: Complete styling diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs index d6357512..3fa458bd 100644 --- a/web/src/widget/text_input.rs +++ b/web/src/widget/text_input.rs @@ -4,8 +4,11 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html -use crate::{bumpalo, style, Bus, Element, Length, Style, Widget}; -use std::rc::Rc; +use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::text_input::{Style, StyleSheet}; + +use std::{rc::Rc, u32}; /// A field that can be filled with text. /// @@ -32,12 +35,14 @@ pub struct TextInput<'a, Message> { _state: &'a mut State, placeholder: String, value: String, + is_secure: bool, width: Length, - max_width: Length, + max_width: u32, padding: u16, size: Option<u16>, on_change: Rc<Box<dyn Fn(String) -> Message>>, on_submit: Option<Message>, + style_sheet: Box<dyn StyleSheet>, } impl<'a, Message> TextInput<'a, Message> { @@ -64,15 +69,25 @@ impl<'a, Message> TextInput<'a, Message> { _state: state, placeholder: String::from(placeholder), value: String::from(value), + is_secure: false, width: Length::Fill, - max_width: Length::Shrink, + max_width: u32::MAX, padding: 0, size: None, on_change: Rc::new(Box::new(on_change)), on_submit: None, + style_sheet: Default::default(), } } + /// Converts the [`TextInput`] into a secure password input. + /// + /// [`TextInput`]: struct.TextInput.html + pub fn password(mut self) -> Self { + self.is_secure = true; + self + } + /// Sets the width of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html @@ -84,7 +99,7 @@ impl<'a, Message> TextInput<'a, Message> { /// Sets the maximum width of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html - pub fn max_width(mut self, max_width: Length) -> Self { + pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } @@ -113,6 +128,14 @@ impl<'a, Message> TextInput<'a, Message> { self.on_submit = Some(message); self } + + /// Sets the style of the [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style_sheet = style.into(); + self + } } impl<'a, Message> Widget<Message> for TextInput<'a, Message> @@ -123,16 +146,19 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; use wasm_bindgen::JsCast; let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); let on_change = self.on_change.clone(); - let event_bus = bus.clone(); + let on_submit = self.on_submit.clone(); + let input_event_bus = bus.clone(); + let submit_event_bus = bus.clone(); + let style = self.style_sheet.active(); input(bump) .attr( @@ -143,8 +169,15 @@ where "style", bumpalo::format!( in bump, - "font-size: {}px", - self.size.unwrap_or(20) + "width: {}; max-width: {}; font-size: {}px; background: {}; border-width: {}px; border-color: {}; border-radius: {}px; color: {}", + css::length(self.width), + css::max_length(self.max_width), + self.size.unwrap_or(20), + css::background(style.background), + style.border_width, + css::color(style.border_color), + style.border_radius, + css::color(self.style_sheet.value_color()) ) .into_bump_str(), ) @@ -157,7 +190,11 @@ where "value", bumpalo::format!(in bump, "{}", self.value).into_bump_str(), ) - .on("input", move |root, vdom, event| { + .attr( + "type", + bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(), + ) + .on("input", move |_root, _vdom, event| { let text_input = match event.target().and_then(|t| { t.dyn_into::<web_sys::HtmlInputElement>().ok() }) { @@ -165,8 +202,17 @@ where Some(text_input) => text_input, }; - event_bus.publish(on_change(text_input.value()), root); - vdom.schedule_render(); + input_event_bus.publish(on_change(text_input.value())); + }) + .on("keypress", move |_root, _vdom, event| { + if let Some(on_submit) = on_submit.clone() { + let event = event.unchecked_into::<web_sys::KeyboardEvent>(); + + match event.key_code() { + 13 => { submit_event_bus.publish(on_submit); } + _ => {} + } + } }) .finish() } @@ -194,4 +240,12 @@ impl State { pub fn new() -> Self { Self::default() } + + /// Creates a new [`State`], representing a focused [`TextInput`]. + /// + /// [`State`]: struct.State.html + pub fn focused() -> Self { + // TODO + Self::default() + } } |