diff --git a/Cargo.toml b/Cargo.toml index 7bfce09a..39c5957a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,16 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -# Enables the Image widget +# Enables the `Image` widget image = ["iced_wgpu/image"] -# Enables the Svg widget +# Enables the `Svg` widget svg = ["iced_wgpu/svg"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] +# Enables `tokio` as the `executor::Default` on native platforms +tokio = ["iced_futures/tokio"] +# Enables `async-std` as the `executor::Default` on native platforms +async-std = ["iced_futures/async-std"] [badges] maintenance = { status = "actively-developed" } @@ -45,6 +49,9 @@ members = [ "examples/tour", ] +[dependencies] +iced_futures = { version = "0.1.0-alpha", path = "futures" } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_winit = { version = "0.1.0-alpha", path = "winit" } iced_wgpu = { version = "0.1.0", path = "wgpu" } diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 76a3a82f..c1e3edb5 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -6,9 +6,13 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["image"] } -iced_futures = { path = "../../futures", features = ["async-std"] } -surf = "1.0" -rand = "0.7" +iced = { path = "../..", features = ["image", "debug", "tokio"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +rand = { version = "0.7", features = ["wasm-bindgen"] } + +[dependencies.reqwest] +version = "0.10" +git = "https://github.com/hecrj/reqwest.git" +branch = "feature/wasm-deserialize-json" +features = ["json"] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 283437b2..4449b901 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -27,7 +27,7 @@ enum Message { } impl Application for Pokedex { - type Executor = iced_futures::executor::AsyncStd; + type Executor = iced::executor::Default; type Message = Message; fn new() -> (Pokedex, Command) { @@ -79,6 +79,7 @@ impl Application for Pokedex { fn view(&mut self) -> Element { let content = match self { Pokedex::Loading => Column::new() + .width(Length::Shrink) .push(Text::new("Searching for Pokémon...").size(40)), Pokedex::Loaded { pokemon, search } => Column::new() .max_width(500) @@ -166,19 +167,21 @@ impl Pokemon { } let id = { - let mut rng = rand::thread_rng(); + let mut rng = rand::rngs::OsRng::default(); rng.gen_range(0, Pokemon::TOTAL) }; - let url = format!("https://pokeapi.co/api/v2/pokemon-species/{}", id); - let sprite = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); + let fetch_entry = async { + let url = + format!("https://pokeapi.co/api/v2/pokemon-species/{}", id); - let (entry, sprite): (Entry, _) = futures::future::try_join( - surf::get(&url).recv_json(), - surf::get(&sprite).recv_bytes(), - ) - .await?; + reqwest::get(&url).await?.json().await + }; + + let (entry, image): (Entry, _) = + futures::future::try_join(fetch_entry, Self::fetch_image(id)) + .await?; let description = entry .flavor_text_entries @@ -195,9 +198,23 @@ impl Pokemon { .chars() .map(|c| if c.is_control() { ' ' } else { c }) .collect(), - image: image::Handle::from_memory(sprite), + image, }) } + + async fn fetch_image(id: u16) -> Result { + let url = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); + + #[cfg(not(target_arch = "wasm32"))] + { + let bytes = reqwest::get(&url).await?.bytes().await?; + + Ok(image::Handle::from_memory(bytes.as_ref().to_vec())) + } + + #[cfg(target_arch = "wasm32")] + Ok(image::Handle::from_path(url)) + } } #[derive(Debug, Clone)] @@ -206,9 +223,9 @@ enum Error { LanguageError, } -impl From for Error { - fn from(exception: surf::Exception) -> Error { - dbg!(&exception); +impl From for Error { + fn from(error: reqwest::Error) -> Error { + dbg!(&error); Error::APIError } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 50095ec7..47408624 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -104,6 +104,7 @@ impl Sandbox for Styling { "Toggle me!", Message::CheckboxToggled, ) + .width(Length::Fill) .style(self.theme); let content = Column::new() diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 21acd5d6..f945cde5 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,13 +6,18 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_futures = { path = "../../futures", features = ["async-std"] } -async-std = "1.0" +iced = { path = "../..", features = ["async-std"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-std = "1.0" directories = "2.0" +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3", features = ["Window", "Storage"] } +wasm-timer = "0.2" + [package.metadata.deb] assets = [ ["target/release/todos", "usr/bin/iced-todos", "755"], diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index bfae5e88..7e866b19 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -38,7 +38,7 @@ enum Message { } impl Application for Todos { - type Executor = iced_futures::executor::AsyncStd; + type Executor = iced::executor::Default; type Message = Message; fn new() -> (Todos, Command) { @@ -377,6 +377,7 @@ impl Controls { ) .push( Row::new() + .width(Length::Shrink) .spacing(10) .push(filter_button( all_button, @@ -493,6 +494,7 @@ enum SaveError { FormatError, } +#[cfg(not(target_arch = "wasm32"))] impl SavedState { fn path() -> std::path::PathBuf { let mut path = if let Some(project_dirs) = @@ -555,6 +557,41 @@ impl SavedState { } } +#[cfg(target_arch = "wasm32")] +impl SavedState { + fn storage() -> Option { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result { + let storage = Self::storage().ok_or(LoadError::FileError)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::FileError)? + .ok_or(LoadError::FileError)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::FileError)?; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::WriteError)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + mod style { use iced::{button, Background, Color, Vector}; diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 45105c31..96749e90 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -8,6 +8,3 @@ publish = false [dependencies] iced = { path = "../..", features = ["image", "debug"] } env_logger = "0.7" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2.51" diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 43c7e50f..800254ed 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -779,16 +779,3 @@ mod style { } } } - -// This should be gracefully handled by Iced in the future. Probably using our -// own proc macro, or maybe the whole process is streamlined by `wasm-pack` at -// some point. -#[cfg(target_arch = "wasm32")] -mod wasm { - use wasm_bindgen::prelude::*; - - #[wasm_bindgen(start)] - pub fn run() { - super::main() - } -} diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 91860e1e..483e60cb 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -19,12 +19,12 @@ log = "0.4" [dependencies.futures] version = "0.3" -[dependencies.tokio] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "0.2" optional = true -features = ["rt-core"] +features = ["rt-core", "rt-threaded"] -[dependencies.async-std] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std] version = "1.0" optional = true diff --git a/futures/src/command.rs b/futures/src/command.rs index e7885fb8..26f58fde 100644 --- a/futures/src/command.rs +++ b/futures/src/command.rs @@ -1,100 +1,11 @@ -use futures::future::{BoxFuture, Future, FutureExt}; +#[cfg(not(target_arch = "wasm32"))] +mod native; -/// A collection of async operations. -/// -/// You should be able to turn a future easily into a [`Command`], either by -/// using the `From` trait or [`Command::perform`]. -/// -/// [`Command`]: struct.Command.html -pub struct Command { - futures: Vec>, -} +#[cfg(not(target_arch = "wasm32"))] +pub use native::Command; -impl Command { - /// Creates an empty [`Command`]. - /// - /// In other words, a [`Command`] that does nothing. - /// - /// [`Command`]: struct.Command.html - pub fn none() -> Self { - Self { - futures: Vec::new(), - } - } +#[cfg(target_arch = "wasm32")] +mod web; - /// Creates a [`Command`] that performs the action of the given future. - /// - /// [`Command`]: struct.Command.html - pub fn perform( - future: impl Future + 'static + Send, - f: impl Fn(T) -> A + 'static + Send, - ) -> Command { - Command { - futures: vec![future.map(f).boxed()], - } - } - - /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: struct.Command.html - pub fn map( - mut self, - f: impl Fn(T) -> A + 'static + Send + Sync, - ) -> Command - where - T: 'static, - { - let f = std::sync::Arc::new(f); - - Command { - futures: self - .futures - .drain(..) - .map(|future| { - let f = f.clone(); - - future.map(move |result| f(result)).boxed() - }) - .collect(), - } - } - - /// Creates a [`Command`] that performs the actions of all the given - /// commands. - /// - /// Once this command is run, all the commands will be exectued at once. - /// - /// [`Command`]: struct.Command.html - pub fn batch(commands: impl IntoIterator>) -> Self { - Self { - futures: commands - .into_iter() - .flat_map(|command| command.futures) - .collect(), - } - } - - /// Converts a [`Command`] into its underlying list of futures. - /// - /// [`Command`]: struct.Command.html - pub fn futures(self) -> Vec> { - self.futures - } -} - -impl From for Command -where - A: Future + 'static + Send, -{ - fn from(future: A) -> Self { - Self { - futures: vec![future.boxed()], - } - } -} - -impl std::fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Command").finish() - } -} +#[cfg(target_arch = "wasm32")] +pub use web::Command; diff --git a/futures/src/command/native.rs b/futures/src/command/native.rs new file mode 100644 index 00000000..38cb4e06 --- /dev/null +++ b/futures/src/command/native.rs @@ -0,0 +1,100 @@ +use futures::future::{BoxFuture, Future, FutureExt}; + +/// A collection of async operations. +/// +/// You should be able to turn a future easily into a [`Command`], either by +/// using the `From` trait or [`Command::perform`]. +/// +/// [`Command`]: struct.Command.html +pub struct Command { + futures: Vec>, +} + +impl Command { + /// Creates an empty [`Command`]. + /// + /// In other words, a [`Command`] that does nothing. + /// + /// [`Command`]: struct.Command.html + pub fn none() -> Self { + Self { + futures: Vec::new(), + } + } + + /// Creates a [`Command`] that performs the action of the given future. + /// + /// [`Command`]: struct.Command.html + pub fn perform( + future: impl Future + 'static + Send, + f: impl Fn(T) -> A + 'static + Send, + ) -> Command { + Command { + futures: vec![future.map(f).boxed()], + } + } + + /// Applies a transformation to the result of a [`Command`]. + /// + /// [`Command`]: struct.Command.html + pub fn map( + mut self, + f: impl Fn(T) -> A + 'static + Send + Sync, + ) -> Command + where + T: 'static, + { + let f = std::sync::Arc::new(f); + + Command { + futures: self + .futures + .drain(..) + .map(|future| { + let f = f.clone(); + + future.map(move |result| f(result)).boxed() + }) + .collect(), + } + } + + /// Creates a [`Command`] that performs the actions of all the given + /// commands. + /// + /// Once this command is run, all the commands will be executed at once. + /// + /// [`Command`]: struct.Command.html + pub fn batch(commands: impl IntoIterator>) -> Self { + Self { + futures: commands + .into_iter() + .flat_map(|command| command.futures) + .collect(), + } + } + + /// Converts a [`Command`] into its underlying list of futures. + /// + /// [`Command`]: struct.Command.html + pub fn futures(self) -> Vec> { + self.futures + } +} + +impl From for Command +where + A: Future + 'static + Send, +{ + fn from(future: A) -> Self { + Self { + futures: vec![future.boxed()], + } + } +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").finish() + } +} diff --git a/futures/src/command/web.rs b/futures/src/command/web.rs new file mode 100644 index 00000000..11b46b90 --- /dev/null +++ b/futures/src/command/web.rs @@ -0,0 +1,101 @@ +use futures::future::{Future, FutureExt}; +use std::pin::Pin; + +/// A collection of async operations. +/// +/// You should be able to turn a future easily into a [`Command`], either by +/// using the `From` trait or [`Command::perform`]. +/// +/// [`Command`]: struct.Command.html +pub struct Command { + futures: Vec + 'static>>>, +} + +impl Command { + /// Creates an empty [`Command`]. + /// + /// In other words, a [`Command`] that does nothing. + /// + /// [`Command`]: struct.Command.html + pub fn none() -> Self { + Self { + futures: Vec::new(), + } + } + + /// Creates a [`Command`] that performs the action of the given future. + /// + /// [`Command`]: struct.Command.html + pub fn perform( + future: impl Future + 'static, + f: impl Fn(T) -> A + 'static, + ) -> Command { + Command { + futures: vec![future.map(f).boxed_local()], + } + } + + /// Applies a transformation to the result of a [`Command`]. + /// + /// [`Command`]: struct.Command.html + pub fn map( + mut self, + f: impl Fn(T) -> A + 'static + Send + Sync + Unpin, + ) -> Command + where + T: 'static, + { + let f = std::sync::Arc::new(f); + + Command { + futures: self + .futures + .drain(..) + .map(|future| { + let f = f.clone(); + + future.map(move |result| f(result)).boxed_local() + }) + .collect(), + } + } + + /// Creates a [`Command`] that performs the actions of all the given + /// commands. + /// + /// Once this command is run, all the commands will be executed at once. + /// + /// [`Command`]: struct.Command.html + pub fn batch(commands: impl IntoIterator>) -> Self { + Self { + futures: commands + .into_iter() + .flat_map(|command| command.futures) + .collect(), + } + } + + /// Converts a [`Command`] into its underlying list of futures. + /// + /// [`Command`]: struct.Command.html + pub fn futures(self) -> Vec + 'static>>> { + self.futures + } +} + +impl From for Command +where + A: Future + 'static, +{ + fn from(future: A) -> Self { + Self { + futures: vec![future.boxed_local()], + } + } +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").finish() + } +} diff --git a/futures/src/executor.rs b/futures/src/executor.rs index c2b9cc72..5378c0b3 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -1,13 +1,13 @@ //! Choose your preferred executor to power a runtime. mod null; -#[cfg(feature = "thread-pool")] +#[cfg(all(not(target_arch = "wasm32"), feature = "thread-pool"))] mod thread_pool; -#[cfg(feature = "tokio")] +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] mod tokio; -#[cfg(feature = "async-std")] +#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] mod async_std; #[cfg(target_arch = "wasm32")] @@ -15,13 +15,13 @@ mod wasm_bindgen; pub use null::Null; -#[cfg(feature = "thread-pool")] +#[cfg(all(not(target_arch = "wasm32"), feature = "thread-pool"))] pub use thread_pool::ThreadPool; -#[cfg(feature = "tokio")] +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] pub use self::tokio::Tokio; -#[cfg(feature = "async-std")] +#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] pub use self::async_std::AsyncStd; #[cfg(target_arch = "wasm32")] @@ -41,8 +41,15 @@ pub trait Executor: Sized { /// Spawns a future in the [`Executor`]. /// /// [`Executor`]: trait.Executor.html + #[cfg(not(target_arch = "wasm32"))] fn spawn(&self, future: impl Future + Send + 'static); + /// Spawns a local future in the [`Executor`]. + /// + /// [`Executor`]: trait.Executor.html + #[cfg(target_arch = "wasm32")] + fn spawn(&self, future: impl Future + 'static); + /// Runs the given closure inside the [`Executor`]. /// /// Some executors, like `tokio`, require some global state to be in place diff --git a/futures/src/executor/null.rs b/futures/src/executor/null.rs index 6d5cf982..65e2e2df 100644 --- a/futures/src/executor/null.rs +++ b/futures/src/executor/null.rs @@ -11,5 +11,9 @@ impl Executor for Null { Ok(Self) } + #[cfg(not(target_arch = "wasm32"))] fn spawn(&self, _future: impl Future + Send + 'static) {} + + #[cfg(target_arch = "wasm32")] + fn spawn(&self, _future: impl Future + 'static) {} } diff --git a/futures/src/executor/wasm_bindgen.rs b/futures/src/executor/wasm_bindgen.rs index 69b7c7e2..94d694c8 100644 --- a/futures/src/executor/wasm_bindgen.rs +++ b/futures/src/executor/wasm_bindgen.rs @@ -9,10 +9,7 @@ impl Executor for WasmBindgen { Ok(Self) } - fn spawn( - &self, - future: impl futures::Future + Send + 'static, - ) { + fn spawn(&self, future: impl futures::Future + 'static) { wasm_bindgen_futures::spawn_local(future); } } diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 3be45a26..ede529dc 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -73,11 +73,13 @@ where for future in futures { let mut sender = self.sender.clone(); - self.executor.spawn(future.then(|message| async move { + let future = future.then(|message| async move { let _ = sender.send(message).await; () - })); + }); + + self.executor.spawn(future); } } diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 1efe4570..200401f9 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,5 +1,4 @@ //! Display images in your user interface. - use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; use std::{ diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 7e212add..04118755 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -9,6 +9,8 @@ use crate::{ layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; + +use std::u32; use unicode_segmentation::UnicodeSegmentation; /// A field that can be filled with text. @@ -43,7 +45,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { is_secure: bool, font: Font, width: Length, - max_width: Length, + max_width: u32, padding: u16, size: Option, on_change: Box Message>, @@ -78,7 +80,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { is_secure: false, font: Font::Default, width: Length::Fill, - max_width: Length::Shrink, + max_width: u32::MAX, padding: 0, size: None, on_change: Box::new(on_change), @@ -114,7 +116,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// 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 } @@ -178,6 +180,7 @@ where let limits = limits .pad(padding) .width(self.width) + .max_width(self.max_width) .height(Length::Units(text_size)); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); diff --git a/src/executor.rs b/src/executor.rs index cbbd8283..e31bd93d 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -5,36 +5,49 @@ pub use platform::Default; #[cfg(not(target_arch = "wasm32"))] mod platform { - use iced_winit::{executor::ThreadPool, futures, Executor}; + use iced_futures::{executor, futures}; + + #[cfg(feature = "tokio")] + type Executor = executor::Tokio; + + #[cfg(all(not(feature = "tokio"), feature = "async-std"))] + type Executor = executor::AsyncStd; + + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + type Executor = executor::ThreadPool; /// A default cross-platform executor. /// - /// - On native platforms, it will use a `iced_futures::executor::ThreadPool`. + /// - On native platforms, it will use: + /// - `iced_futures::executor::Tokio` when the `tokio` feature is enabled. + /// - `iced_futures::executor::AsyncStd` when the `async-std` feature is + /// enabled. + /// - `iced_futures::executor::ThreadPool` otherwise. /// - On the Web, it will use `iced_futures::executor::WasmBindgen`. #[derive(Debug)] - pub struct Default(ThreadPool); + pub struct Default(Executor); - impl Executor for Default { + impl super::Executor for Default { fn new() -> Result { - Ok(Default(ThreadPool::new()?)) + Ok(Default(Executor::new()?)) } fn spawn( &self, future: impl futures::Future + Send + 'static, ) { - self.0.spawn(future); + let _ = self.0.spawn(future); } } } #[cfg(target_arch = "wasm32")] mod platform { - use iced_web::{executor::WasmBindgen, futures, Executor}; + use iced_futures::{executor::WasmBindgen, futures, Executor}; /// A default cross-platform executor. /// - /// - On native platforms, it will use a `iced_futures::executor::ThreadPool`. + /// - On native platforms, it will use `iced_futures::executor::ThreadPool`. /// - On the Web, it will use `iced_futures::executor::WasmBindgen`. #[derive(Debug)] pub struct Default(WasmBindgen); @@ -44,10 +57,7 @@ mod platform { Ok(Default(WasmBindgen::new()?)) } - fn spawn( - &self, - future: impl futures::Future + Send + 'static, - ) { + fn spawn(&self, future: impl futures::Future + 'static) { self.0.spawn(future); } } diff --git a/web/Cargo.toml b/web/Cargo.toml index 46953863..c043c697 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,9 +15,11 @@ categories = ["web-programming"] maintenance = { status = "actively-developed" } [dependencies] +iced_style = { version = "0.1.0-alpha", path = "../style" } dodrio = "0.1.0" wasm-bindgen = "0.2.51" wasm-bindgen-futures = "0.4" +url = "2.0" [dependencies.iced_core] version = "0.1.0" @@ -37,4 +39,5 @@ features = [ "Event", "EventTarget", "InputEvent", + "KeyboardEvent", ] diff --git a/web/src/style.rs b/web/src/css.rs similarity index 64% rename from web/src/style.rs rename to web/src/css.rs index 4f72b22c..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, +pub struct Css<'a> { + rules: BTreeMap, } -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)); } @@ -143,6 +143,26 @@ pub fn length(length: Length) -> String { } } +/// 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) + } +} + /// Returns the style value for the given [`Color`]. /// /// [`Color`]: ../struct.Color.html @@ -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 0315d7d6..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; @@ -57,7 +57,7 @@ impl<'a, Message> Element<'a, Message> { &self, bump: &'b bumpalo::Bump, bus: &Bus, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { self.widget.node(bump, bus, style_sheet) } @@ -89,7 +89,7 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - 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/lib.rs b/web/src/lib.rs index 0b9c0c3d..7b54a07a 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -63,11 +63,12 @@ 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; @@ -76,7 +77,6 @@ pub use iced_core::{ VerticalAlignment, }; pub use iced_futures::{executor, futures, Command}; -pub use style::Style; pub use subscription::Subscription; #[doc(no_inline)] @@ -241,13 +241,13 @@ where 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, &self.bus, &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/widget.rs b/web/src/widget.rs index 0ac536bd..025cf22f 100644 --- a/web/src/widget.rs +++ b/web/src/widget.rs @@ -14,19 +14,20 @@ //! ``` //! //! [`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; @@ -46,6 +47,7 @@ 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; @@ -64,6 +66,6 @@ pub trait Widget { &self, bump: &'b bumpalo::Bump, _bus: &Bus, - 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 6fef48ce..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, width: Length, + height: Length, min_width: u32, + min_height: u32, padding: u16, - background: Option, - border_radius: u16, + style: Box, } 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,6 +81,14 @@ impl<'a, Message> Button<'a, Message> { self } + /// Sets the minimum height of the [`Button`]. + /// + /// [`Button`]: struct.Button.html + pub fn min_height(mut self, min_height: u32) -> Self { + self.min_height = min_height; + self + } + /// Sets the padding of the [`Button`]. /// /// [`Button`]: struct.Button.html @@ -77,20 +97,11 @@ impl<'a, Message> Button<'a, Message> { self } - /// Sets the [`Background`] of the [`Button`]. + /// Sets the style of the [`Button`]. /// /// [`Button`]: struct.Button.html - /// [`Background`]: ../../struct.Background.html - pub fn background>(mut self, background: T) -> Self { - self.background = Some(background.into()); - self - } - - /// Sets the border radius 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>) -> Self { + self.style = style.into(); self } @@ -126,18 +137,20 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let width = style::length(self.width); - let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); + // TODO: State-based styling + let style = self.style.active(); - let background = match self.background { + let padding_class = + style_sheet.insert(bump, css::Rule::Padding(self.padding)); + + 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), }, }; @@ -150,11 +163,12 @@ where "style", bumpalo::format!( in bump, - "background: {}; border-radius: {}px; width:{}; min-width: {}px", + "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}", background, - self.border_radius, - width, - self.min_width + style.border_radius, + css::length(self.width), + css::min_length(self.min_width), + css::color(style.text_color) ) .into_bump_str(), ) @@ -168,8 +182,6 @@ where }); } - // TODO: Complete styling - node.finish() } } diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs index 1e864875..0657ccfb 100644 --- a/web/src/widget/checkbox.rs +++ b/web/src/widget/checkbox.rs @@ -1,4 +1,7 @@ -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; @@ -25,7 +28,8 @@ pub struct Checkbox { is_checked: bool, on_toggle: Rc Message>, label: String, - label_color: Option, + width: Length, + style: Box, } impl Checkbox { @@ -47,15 +51,24 @@ impl Checkbox { is_checked, 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 label_color>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the style of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn style(mut self, style: impl Into>) -> Self { + self.style = style.into(); self } } @@ -68,7 +81,7 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - _style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -78,9 +91,23 @@ where let on_toggle = self.on_toggle.clone(); let is_checked = self.is_checked; - // TODO: Complete styling + let row_class = style_sheet.insert(bump, css::Rule::Row); + + let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5)); + 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) @@ -91,7 +118,8 @@ where vdom.schedule_render(); }) .finish(), - text(checkbox_label.into_bump_str()), + span(bump).children(vec![ + text(checkbox_label.into_bump_str())]).finish(), ]) .finish() } diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs index 9aa988ff..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; @@ -28,7 +28,7 @@ impl<'a, Message> Column<'a, Message> { Column { spacing: 0, padding: 0, - width: Length::Shrink, + width: Length::Fill, height: Length::Shrink, max_width: u32::MAX, max_height: u32::MAX, @@ -112,7 +112,7 @@ impl<'a, Message> Widget for Column<'a, Message> { &self, bump: &'b bumpalo::Bump, publish: &Bus, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -122,18 +122,13 @@ impl<'a, Message> Widget 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); - - let align_items = style::align(self.align_items); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); // TODO: Complete styling div(bump) @@ -144,12 +139,12 @@ impl<'a, Message> Widget for Column<'a, Message> { ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px; max-height: {}px; align-items: {}", - width, - height, - self.max_width, - self.max_height, - align_items + "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 bdc88979..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, 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>) -> Self { + self.style_sheet = style.into(); + self + } } impl<'a, Message> Widget for Container<'a, Message> @@ -94,17 +107,13 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - 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(), ) diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs index 413b663e..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>(path: T) -> Self { + pub fn new>(handle: T) -> Self { Image { - path: path.into(), + handle: handle.into(), width: Length::Shrink, height: Length::Shrink, } @@ -57,11 +63,13 @@ impl Widget for Image { &self, bump: &'b bumpalo::Bump, _bus: &Bus, - _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()); @@ -89,3 +97,74 @@ impl<'a, Message> From for Element<'a, Message> { Element::new(image) } } + +/// An [`Image`] handle. +/// +/// [`Image`]: struct.Image.html +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc, +} + +impl Handle { + /// Creates an image [`Handle`] pointing to the image of the given path. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_path>(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 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); +/// ``` +/// +/// ![Progress bar](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) +#[allow(missing_debug_implementations)] +pub struct ProgressBar { + range: RangeInclusive, + value: f32, + width: Length, + height: Option, + style: Box, +} + +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, 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>) -> Self { + self.style = style.into(); + self + } +} + +impl Widget for ProgressBar { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _bus: &Bus, + _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 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 6dd0ad45..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 { is_selected: bool, on_click: Message, label: String, - label_color: Option, + style: Box, } impl Radio { @@ -55,15 +58,15 @@ impl Radio { 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>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn style(mut self, style: impl Into>) -> Self { + self.style = style.into(); self } } @@ -76,7 +79,7 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs index c26cb91b..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; @@ -28,7 +28,7 @@ impl<'a, Message> Row<'a, Message> { Row { spacing: 0, padding: 0, - width: Length::Shrink, + width: Length::Fill, height: Length::Shrink, max_width: u32::MAX, max_height: u32::MAX, @@ -113,7 +113,7 @@ impl<'a, Message> Widget for Row<'a, Message> { &self, bump: &'b bumpalo::Bump, publish: &Bus, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -123,18 +123,13 @@ impl<'a, Message> Widget 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); - - let justify_content = style::align(self.align_items); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); // TODO: Complete styling div(bump) @@ -145,12 +140,12 @@ impl<'a, Message> Widget for Row<'a, Message> { ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px; max-height: {}px; justify-content: {}", - width, - height, - self.max_width, - self.max_height, - justify_content + "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 f146e007..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, } 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>) -> 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, - 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,8 +140,6 @@ where ) .children(vec![self.content.node(bump, bus, style_sheet)]); - // TODO: Complete styling - node.finish() } } diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs index 25c57933..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 Message>>, width: Length, + style: Box, } 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,6 +82,14 @@ 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>) -> Self { + self.style = style.into(); + self + } } impl<'a, Message> Widget for Slider<'a, Message> @@ -88,7 +100,7 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - _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") diff --git a/web/src/widget/space.rs b/web/src/widget/space.rs index baf4c80b..4ce52595 100644 --- a/web/src/widget/space.rs +++ b/web/src/widget/space.rs @@ -1,4 +1,4 @@ -use crate::{style, Bus, Element, Length, Widget}; +use crate::{css, Bus, Css, Element, Length, Widget}; use dodrio::bumpalo; /// An amount of empty space. @@ -44,12 +44,12 @@ impl<'a, Message> Widget for Space { &self, bump: &'b bumpalo::Bump, _publish: &Bus, - _style_sheet: &mut style::Sheet<'b>, + _css: &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); let style = bumpalo::format!( in bump, diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs index 5b0bee55..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; @@ -112,15 +112,18 @@ impl<'a, Message> Widget for Text { &self, bump: &'b bumpalo::Bump, _publish: &Bus, - _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 = style::length(self.width); - let height = style::length(self.height); + let width = css::length(self.width); + let height = css::length(self.height); let text_align = match self.horizontal_alignment { HorizontalAlignment::Left => "left", @@ -130,12 +133,16 @@ impl<'a, Message> Widget for Text { let style = bumpalo::format!( in bump, - "width: {}; height: {}; 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 078e05b2..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. /// @@ -34,11 +37,12 @@ pub struct TextInput<'a, Message> { value: String, is_secure: bool, width: Length, - max_width: Length, + max_width: u32, padding: u16, size: Option, on_change: Rc Message>>, on_submit: Option, + style_sheet: Box, } impl<'a, Message> TextInput<'a, Message> { @@ -67,11 +71,12 @@ impl<'a, Message> TextInput<'a, Message> { 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(), } } @@ -94,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 } @@ -123,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>) -> Self { + self.style_sheet = style.into(); + self + } } impl<'a, Message> Widget for TextInput<'a, Message> @@ -133,18 +146,19 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; use wasm_bindgen::JsCast; - let width = style::length(self.width); - let max_width = style::length(self.max_width); 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( @@ -155,10 +169,15 @@ where "style", bumpalo::format!( in bump, - "width: {}; max-width: {}; font-size: {}px", - width, - max_width, - 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(), ) @@ -183,7 +202,17 @@ where Some(text_input) => text_input, }; - event_bus.publish(on_change(text_input.value())); + 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::(); + + match event.key_code() { + 13 => { submit_event_bus.publish(on_submit); } + _ => {} + } + } }) .finish() } @@ -211,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() + } }