diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/CHANGELOG.md | 5 | ||||
-rw-r--r-- | web/Cargo.toml | 13 | ||||
-rw-r--r-- | web/README.md | 10 | ||||
-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 |
23 files changed, 790 insertions, 253 deletions
diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index 49ddf5a3..ed02519a 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- `Button::background` takes an `Into<Background>` now instead of a `Background`. + +### Fixed +- Render not being scheduled after `Command` futures finishing. ## [0.1.0] - 2019-11-25 ### Added diff --git a/web/Cargo.toml b/web/Cargo.toml index 8c559d73..c043c697 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,11 +15,19 @@ categories = ["web-programming"] maintenance = { status = "actively-developed" } [dependencies] -iced_core = { version = "0.1.0", path = "../core", features = ["command"] } +iced_style = { version = "0.1.0-alpha", path = "../style" } dodrio = "0.1.0" wasm-bindgen = "0.2.51" wasm-bindgen-futures = "0.4" -futures = "0.3" +url = "2.0" + +[dependencies.iced_core] +version = "0.1.0" +path = "../core" + +[dependencies.iced_futures] +version = "0.1.0-alpha" +path = "../futures" [dependencies.web-sys] version = "0.3.27" @@ -31,4 +39,5 @@ features = [ "Event", "EventTarget", "InputEvent", + "KeyboardEvent", ] diff --git a/web/README.md b/web/README.md index 762f6c83..aceb4a97 100644 --- a/web/README.md +++ b/web/README.md @@ -18,7 +18,7 @@ The crate is currently a __very experimental__, simple abstraction layer over [` Add `iced_web` as a dependency in your `Cargo.toml`: ```toml -iced_web = "0.1.0-alpha" +iced_web = "0.1.0" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If @@ -35,11 +35,15 @@ For instance, let's say we want to build the [`tour` example]: ``` cd examples -cargo build --example tour --target wasm32-unknown-unknown +cargo build --package tour --target wasm32-unknown-unknown wasm-bindgen ../target/wasm32-unknown-unknown/debug/examples/tour.wasm --out-dir tour --web ``` -Then, we need to create an `.html` file to load our application: +*__Note:__ Keep in mind that Iced is still in early exploration stages and most of the work needs to happen on the native side of the ecosystem. At this stage, it is important to be able to batch work without having to constantly jump back and forth. Because of this, there is currently no requirement for the `master` branch to contain a cross-platform API at all times. If you hit an issue when building an example and want to help, it may be a good way to [start contributing]!* + +[start contributing]: ../CONTRIBUTING.md + +Once the example is compiled, we need to create an `.html` file to load our application: ```html <!DOCTYPE html> 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() + } } |