Merge pull request #180 from hecrj/feature/web-styling

Custom styling for `iced_web`
This commit is contained in:
Héctor Ramón 2020-02-06 10:21:52 -06:00 committed by GitHub
commit 97c308076f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 884 additions and 343 deletions

View File

@ -12,12 +12,16 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"] categories = ["gui"]
[features] [features]
# Enables the Image widget # Enables the `Image` widget
image = ["iced_wgpu/image"] image = ["iced_wgpu/image"]
# Enables the Svg widget # Enables the `Svg` widget
svg = ["iced_wgpu/svg"] svg = ["iced_wgpu/svg"]
# Enables a debug view in native platforms (press F12) # Enables a debug view in native platforms (press F12)
debug = ["iced_winit/debug"] 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] [badges]
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
@ -45,6 +49,9 @@ members = [
"examples/tour", "examples/tour",
] ]
[dependencies]
iced_futures = { version = "0.1.0-alpha", path = "futures" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
iced_winit = { version = "0.1.0-alpha", path = "winit" } iced_winit = { version = "0.1.0-alpha", path = "winit" }
iced_wgpu = { version = "0.1.0", path = "wgpu" } iced_wgpu = { version = "0.1.0", path = "wgpu" }

View File

@ -6,9 +6,13 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["image"] } iced = { path = "../..", features = ["image", "debug", "tokio"] }
iced_futures = { path = "../../futures", features = ["async-std"] }
surf = "1.0"
rand = "0.7"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" 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"]

View File

@ -27,7 +27,7 @@ enum Message {
} }
impl Application for Pokedex { impl Application for Pokedex {
type Executor = iced_futures::executor::AsyncStd; type Executor = iced::executor::Default;
type Message = Message; type Message = Message;
fn new() -> (Pokedex, Command<Message>) { fn new() -> (Pokedex, Command<Message>) {
@ -79,6 +79,7 @@ impl Application for Pokedex {
fn view(&mut self) -> Element<Message> { fn view(&mut self) -> Element<Message> {
let content = match self { let content = match self {
Pokedex::Loading => Column::new() Pokedex::Loading => Column::new()
.width(Length::Shrink)
.push(Text::new("Searching for Pokémon...").size(40)), .push(Text::new("Searching for Pokémon...").size(40)),
Pokedex::Loaded { pokemon, search } => Column::new() Pokedex::Loaded { pokemon, search } => Column::new()
.max_width(500) .max_width(500)
@ -166,18 +167,20 @@ impl Pokemon {
} }
let id = { let id = {
let mut rng = rand::thread_rng(); let mut rng = rand::rngs::OsRng::default();
rng.gen_range(0, Pokemon::TOTAL) rng.gen_range(0, Pokemon::TOTAL)
}; };
let url = format!("https://pokeapi.co/api/v2/pokemon-species/{}", id); let fetch_entry = async {
let sprite = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); let url =
format!("https://pokeapi.co/api/v2/pokemon-species/{}", id);
let (entry, sprite): (Entry, _) = futures::future::try_join( reqwest::get(&url).await?.json().await
surf::get(&url).recv_json(), };
surf::get(&sprite).recv_bytes(),
) let (entry, image): (Entry, _) =
futures::future::try_join(fetch_entry, Self::fetch_image(id))
.await?; .await?;
let description = entry let description = entry
@ -195,9 +198,23 @@ impl Pokemon {
.chars() .chars()
.map(|c| if c.is_control() { ' ' } else { c }) .map(|c| if c.is_control() { ' ' } else { c })
.collect(), .collect(),
image: image::Handle::from_memory(sprite), image,
}) })
} }
async fn fetch_image(id: u16) -> Result<image::Handle, reqwest::Error> {
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)] #[derive(Debug, Clone)]
@ -206,9 +223,9 @@ enum Error {
LanguageError, LanguageError,
} }
impl From<surf::Exception> for Error { impl From<reqwest::Error> for Error {
fn from(exception: surf::Exception) -> Error { fn from(error: reqwest::Error) -> Error {
dbg!(&exception); dbg!(&error);
Error::APIError Error::APIError
} }

View File

@ -104,6 +104,7 @@ impl Sandbox for Styling {
"Toggle me!", "Toggle me!",
Message::CheckboxToggled, Message::CheckboxToggled,
) )
.width(Length::Fill)
.style(self.theme); .style(self.theme);
let content = Column::new() let content = Column::new()

View File

@ -6,13 +6,18 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../.." } iced = { path = "../..", features = ["async-std"] }
iced_futures = { path = "../../futures", features = ["async-std"] }
async-std = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-std = "1.0"
directories = "2.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] [package.metadata.deb]
assets = [ assets = [
["target/release/todos", "usr/bin/iced-todos", "755"], ["target/release/todos", "usr/bin/iced-todos", "755"],

View File

@ -38,7 +38,7 @@ enum Message {
} }
impl Application for Todos { impl Application for Todos {
type Executor = iced_futures::executor::AsyncStd; type Executor = iced::executor::Default;
type Message = Message; type Message = Message;
fn new() -> (Todos, Command<Message>) { fn new() -> (Todos, Command<Message>) {
@ -377,6 +377,7 @@ impl Controls {
) )
.push( .push(
Row::new() Row::new()
.width(Length::Shrink)
.spacing(10) .spacing(10)
.push(filter_button( .push(filter_button(
all_button, all_button,
@ -493,6 +494,7 @@ enum SaveError {
FormatError, FormatError,
} }
#[cfg(not(target_arch = "wasm32"))]
impl SavedState { impl SavedState {
fn path() -> std::path::PathBuf { fn path() -> std::path::PathBuf {
let mut path = if let Some(project_dirs) = let mut path = if let Some(project_dirs) =
@ -555,6 +557,41 @@ impl SavedState {
} }
} }
#[cfg(target_arch = "wasm32")]
impl SavedState {
fn storage() -> Option<web_sys::Storage> {
let window = web_sys::window()?;
window.local_storage().ok()?
}
async fn load() -> Result<SavedState, LoadError> {
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 { mod style {
use iced::{button, Background, Color, Vector}; use iced::{button, Background, Color, Vector};

View File

@ -8,6 +8,3 @@ publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["image", "debug"] } iced = { path = "../..", features = ["image", "debug"] }
env_logger = "0.7" env_logger = "0.7"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2.51"

View File

@ -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()
}
}

View File

@ -19,12 +19,12 @@ log = "0.4"
[dependencies.futures] [dependencies.futures]
version = "0.3" version = "0.3"
[dependencies.tokio] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2" version = "0.2"
optional = true 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" version = "1.0"
optional = true optional = true

View File

@ -1,100 +1,11 @@
use futures::future::{BoxFuture, Future, FutureExt}; #[cfg(not(target_arch = "wasm32"))]
mod native;
/// A collection of async operations. #[cfg(not(target_arch = "wasm32"))]
/// pub use native::Command;
/// 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<T> {
futures: Vec<BoxFuture<'static, T>>,
}
impl<T> Command<T> { #[cfg(target_arch = "wasm32")]
/// Creates an empty [`Command`]. mod web;
///
/// 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. #[cfg(target_arch = "wasm32")]
/// pub use web::Command;
/// [`Command`]: struct.Command.html
pub fn perform<A>(
future: impl Future<Output = T> + 'static + Send,
f: impl Fn(T) -> A + 'static + Send,
) -> Command<A> {
Command {
futures: vec![future.map(f).boxed()],
}
}
/// Applies a transformation to the result of a [`Command`].
///
/// [`Command`]: struct.Command.html
pub fn map<A>(
mut self,
f: impl Fn(T) -> A + 'static + Send + Sync,
) -> Command<A>
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<Item = Command<T>>) -> 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<BoxFuture<'static, T>> {
self.futures
}
}
impl<T, A> From<A> for Command<T>
where
A: Future<Output = T> + 'static + Send,
{
fn from(future: A) -> Self {
Self {
futures: vec![future.boxed()],
}
}
}
impl<T> std::fmt::Debug for Command<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command").finish()
}
}

View File

@ -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<T> {
futures: Vec<BoxFuture<'static, T>>,
}
impl<T> Command<T> {
/// 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<A>(
future: impl Future<Output = T> + 'static + Send,
f: impl Fn(T) -> A + 'static + Send,
) -> Command<A> {
Command {
futures: vec![future.map(f).boxed()],
}
}
/// Applies a transformation to the result of a [`Command`].
///
/// [`Command`]: struct.Command.html
pub fn map<A>(
mut self,
f: impl Fn(T) -> A + 'static + Send + Sync,
) -> Command<A>
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<Item = Command<T>>) -> 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<BoxFuture<'static, T>> {
self.futures
}
}
impl<T, A> From<A> for Command<T>
where
A: Future<Output = T> + 'static + Send,
{
fn from(future: A) -> Self {
Self {
futures: vec![future.boxed()],
}
}
}
impl<T> std::fmt::Debug for Command<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command").finish()
}
}

101
futures/src/command/web.rs Normal file
View File

@ -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<T> {
futures: Vec<Pin<Box<dyn Future<Output = T> + 'static>>>,
}
impl<T> Command<T> {
/// 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<A>(
future: impl Future<Output = T> + 'static,
f: impl Fn(T) -> A + 'static,
) -> Command<A> {
Command {
futures: vec![future.map(f).boxed_local()],
}
}
/// Applies a transformation to the result of a [`Command`].
///
/// [`Command`]: struct.Command.html
pub fn map<A>(
mut self,
f: impl Fn(T) -> A + 'static + Send + Sync + Unpin,
) -> Command<A>
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<Item = Command<T>>) -> 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<Pin<Box<dyn Future<Output = T> + 'static>>> {
self.futures
}
}
impl<T, A> From<A> for Command<T>
where
A: Future<Output = T> + 'static,
{
fn from(future: A) -> Self {
Self {
futures: vec![future.boxed_local()],
}
}
}
impl<T> std::fmt::Debug for Command<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command").finish()
}
}

View File

@ -1,13 +1,13 @@
//! Choose your preferred executor to power a runtime. //! Choose your preferred executor to power a runtime.
mod null; mod null;
#[cfg(feature = "thread-pool")] #[cfg(all(not(target_arch = "wasm32"), feature = "thread-pool"))]
mod thread_pool; mod thread_pool;
#[cfg(feature = "tokio")] #[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
mod tokio; mod tokio;
#[cfg(feature = "async-std")] #[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))]
mod async_std; mod async_std;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -15,13 +15,13 @@ mod wasm_bindgen;
pub use null::Null; pub use null::Null;
#[cfg(feature = "thread-pool")] #[cfg(all(not(target_arch = "wasm32"), feature = "thread-pool"))]
pub use thread_pool::ThreadPool; pub use thread_pool::ThreadPool;
#[cfg(feature = "tokio")] #[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
pub use self::tokio::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; pub use self::async_std::AsyncStd;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -41,8 +41,15 @@ pub trait Executor: Sized {
/// Spawns a future in the [`Executor`]. /// Spawns a future in the [`Executor`].
/// ///
/// [`Executor`]: trait.Executor.html /// [`Executor`]: trait.Executor.html
#[cfg(not(target_arch = "wasm32"))]
fn spawn(&self, future: impl Future<Output = ()> + Send + 'static); fn spawn(&self, future: impl Future<Output = ()> + Send + 'static);
/// Spawns a local future in the [`Executor`].
///
/// [`Executor`]: trait.Executor.html
#[cfg(target_arch = "wasm32")]
fn spawn(&self, future: impl Future<Output = ()> + 'static);
/// Runs the given closure inside the [`Executor`]. /// Runs the given closure inside the [`Executor`].
/// ///
/// Some executors, like `tokio`, require some global state to be in place /// Some executors, like `tokio`, require some global state to be in place

View File

@ -11,5 +11,9 @@ impl Executor for Null {
Ok(Self) Ok(Self)
} }
#[cfg(not(target_arch = "wasm32"))]
fn spawn(&self, _future: impl Future<Output = ()> + Send + 'static) {} fn spawn(&self, _future: impl Future<Output = ()> + Send + 'static) {}
#[cfg(target_arch = "wasm32")]
fn spawn(&self, _future: impl Future<Output = ()> + 'static) {}
} }

View File

@ -9,10 +9,7 @@ impl Executor for WasmBindgen {
Ok(Self) Ok(Self)
} }
fn spawn( fn spawn(&self, future: impl futures::Future<Output = ()> + 'static) {
&self,
future: impl futures::Future<Output = ()> + Send + 'static,
) {
wasm_bindgen_futures::spawn_local(future); wasm_bindgen_futures::spawn_local(future);
} }
} }

View File

@ -73,11 +73,13 @@ where
for future in futures { for future in futures {
let mut sender = self.sender.clone(); 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; let _ = sender.send(message).await;
() ()
})); });
self.executor.spawn(future);
} }
} }

View File

@ -1,5 +1,4 @@
//! Display images in your user interface. //! Display images in your user interface.
use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
use std::{ use std::{

View File

@ -9,6 +9,8 @@ use crate::{
layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point,
Rectangle, Size, Widget, Rectangle, Size, Widget,
}; };
use std::u32;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
/// A field that can be filled with text. /// A field that can be filled with text.
@ -43,7 +45,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {
is_secure: bool, is_secure: bool,
font: Font, font: Font,
width: Length, width: Length,
max_width: Length, max_width: u32,
padding: u16, padding: u16,
size: Option<u16>, size: Option<u16>,
on_change: Box<dyn Fn(String) -> Message>, on_change: Box<dyn Fn(String) -> Message>,
@ -78,7 +80,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
is_secure: false, is_secure: false,
font: Font::Default, font: Font::Default,
width: Length::Fill, width: Length::Fill,
max_width: Length::Shrink, max_width: u32::MAX,
padding: 0, padding: 0,
size: None, size: None,
on_change: Box::new(on_change), 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`]. /// Sets the maximum width of the [`TextInput`].
/// ///
/// [`TextInput`]: struct.TextInput.html /// [`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.max_width = max_width;
self self
} }
@ -178,6 +180,7 @@ where
let limits = limits let limits = limits
.pad(padding) .pad(padding)
.width(self.width) .width(self.width)
.max_width(self.max_width)
.height(Length::Units(text_size)); .height(Length::Units(text_size));
let mut text = layout::Node::new(limits.resolve(Size::ZERO)); let mut text = layout::Node::new(limits.resolve(Size::ZERO));

View File

@ -5,36 +5,49 @@ pub use platform::Default;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod platform { 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. /// 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`. /// - On the Web, it will use `iced_futures::executor::WasmBindgen`.
#[derive(Debug)] #[derive(Debug)]
pub struct Default(ThreadPool); pub struct Default(Executor);
impl Executor for Default { impl super::Executor for Default {
fn new() -> Result<Self, futures::io::Error> { fn new() -> Result<Self, futures::io::Error> {
Ok(Default(ThreadPool::new()?)) Ok(Default(Executor::new()?))
} }
fn spawn( fn spawn(
&self, &self,
future: impl futures::Future<Output = ()> + Send + 'static, future: impl futures::Future<Output = ()> + Send + 'static,
) { ) {
self.0.spawn(future); let _ = self.0.spawn(future);
} }
} }
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
mod platform { mod platform {
use iced_web::{executor::WasmBindgen, futures, Executor}; use iced_futures::{executor::WasmBindgen, futures, Executor};
/// A default cross-platform 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`. /// - On the Web, it will use `iced_futures::executor::WasmBindgen`.
#[derive(Debug)] #[derive(Debug)]
pub struct Default(WasmBindgen); pub struct Default(WasmBindgen);
@ -44,10 +57,7 @@ mod platform {
Ok(Default(WasmBindgen::new()?)) Ok(Default(WasmBindgen::new()?))
} }
fn spawn( fn spawn(&self, future: impl futures::Future<Output = ()> + 'static) {
&self,
future: impl futures::Future<Output = ()> + Send + 'static,
) {
self.0.spawn(future); self.0.spawn(future);
} }
} }

View File

@ -15,9 +15,11 @@ categories = ["web-programming"]
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
iced_style = { version = "0.1.0-alpha", path = "../style" }
dodrio = "0.1.0" dodrio = "0.1.0"
wasm-bindgen = "0.2.51" wasm-bindgen = "0.2.51"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
url = "2.0"
[dependencies.iced_core] [dependencies.iced_core]
version = "0.1.0" version = "0.1.0"
@ -37,4 +39,5 @@ features = [
"Event", "Event",
"EventTarget", "EventTarget",
"InputEvent", "InputEvent",
"KeyboardEvent",
] ]

View File

@ -1,11 +1,11 @@
//! Style your widgets. //! Style your widgets.
use crate::{bumpalo, Align, Color, Length}; use crate::{bumpalo, Align, Background, Color, Length};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The style of a VDOM node. /// A CSS rule of a VDOM node.
#[derive(Debug)] #[derive(Debug)]
pub enum Style { pub enum Rule {
/// Container with vertical distribution /// Container with vertical distribution
Column, Column,
@ -19,16 +19,16 @@ pub enum Style {
Spacing(u16), Spacing(u16),
} }
impl Style { impl Rule {
/// Returns the class name of the [`Style`]. /// Returns the class name of the [`Style`].
/// ///
/// [`Style`]: enum.Style.html /// [`Style`]: enum.Style.html
pub fn class<'a>(&self) -> String { pub fn class<'a>(&self) -> String {
match self { match self {
Style::Column => String::from("c"), Rule::Column => String::from("c"),
Style::Row => String::from("r"), Rule::Row => String::from("r"),
Style::Padding(padding) => format!("p-{}", padding), Rule::Padding(padding) => format!("p-{}", padding),
Style::Spacing(spacing) => format!("s-{}", spacing), Rule::Spacing(spacing) => format!("s-{}", spacing),
} }
} }
@ -39,24 +39,24 @@ impl Style {
let class = self.class(); let class = self.class();
match self { match self {
Style::Column => { Rule::Column => {
let body = "{ display: flex; flex-direction: column; }"; let body = "{ display: flex; flex-direction: column; }";
bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str()
} }
Style::Row => { Rule::Row => {
let body = "{ display: flex; flex-direction: row; }"; let body = "{ display: flex; flex-direction: row; }";
bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str()
} }
Style::Padding(padding) => bumpalo::format!( Rule::Padding(padding) => bumpalo::format!(
in bump, in bump,
".{} {{ box-sizing: border-box; padding: {}px }}", ".{} {{ box-sizing: border-box; padding: {}px }}",
class, class,
padding padding
) )
.into_bump_str(), .into_bump_str(),
Style::Spacing(spacing) => bumpalo::format!( Rule::Spacing(spacing) => bumpalo::format!(
in bump, in bump,
".c.{} > * {{ margin-bottom: {}px }} \ ".c.{} > * {{ margin-bottom: {}px }} \
.r.{} > * {{ margin-right: {}px }} \ .r.{} > * {{ margin-right: {}px }} \
@ -74,34 +74,34 @@ impl Style {
} }
} }
/// A sheet of styles. /// A cascading style sheet.
#[derive(Debug)] #[derive(Debug)]
pub struct Sheet<'a> { pub struct Css<'a> {
styles: BTreeMap<String, &'a str>, rules: BTreeMap<String, &'a str>,
} }
impl<'a> Sheet<'a> { impl<'a> Css<'a> {
/// Creates an empty style [`Sheet`]. /// Creates an empty style [`Sheet`].
/// ///
/// [`Sheet`]: struct.Sheet.html /// [`Sheet`]: struct.Sheet.html
pub fn new() -> Self { pub fn new() -> Self {
Self { Css {
styles: BTreeMap::new(), 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. /// inserted.
/// ///
/// It returns the class name of the provided [`Style`]. /// It returns the class name of the provided [`Rule`].
/// ///
/// [`Sheet`]: struct.Sheet.html /// [`Sheet`]: struct.Sheet.html
/// [`Style`]: enum.Style.html /// [`Rule`]: enum.Rule.html
pub fn insert(&mut self, bump: &'a bumpalo::Bump, style: Style) -> String { pub fn insert(&mut self, bump: &'a bumpalo::Bump, rule: Rule) -> String {
let class = style.class(); let class = rule.class();
if !self.styles.contains_key(&class) { if !self.rules.contains_key(&class) {
let _ = self.styles.insert(class.clone(), style.declaration(bump)); let _ = self.rules.insert(class.clone(), rule.declaration(bump));
} }
class class
@ -119,12 +119,12 @@ impl<'a> Sheet<'a> {
declarations.push(text( declarations.push(text(
"body { height: 100%; margin: 0; padding: 0; font-family: sans-serif }", "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( declarations.push(text(
"button { border: none; cursor: pointer; outline: none }", "button { border: none; cursor: pointer; outline: none }",
)); ));
for declaration in self.styles.values() { for declaration in self.rules.values() {
declarations.push(text(*declaration)); 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`]. /// Returns the style value for the given [`Color`].
/// ///
/// [`Color`]: ../struct.Color.html /// [`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) 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`]. /// Returns the style value for the given [`Align`].
/// ///
/// [`Align`]: ../enum.Align.html /// [`Align`]: ../enum.Align.html

View File

@ -1,4 +1,4 @@
use crate::{style, Bus, Color, Widget}; use crate::{Bus, Color, Css, Widget};
use dodrio::bumpalo; use dodrio::bumpalo;
use std::rc::Rc; use std::rc::Rc;
@ -57,7 +57,7 @@ impl<'a, Message> Element<'a, Message> {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
self.widget.node(bump, bus, style_sheet) self.widget.node(bump, bus, style_sheet)
} }
@ -89,7 +89,7 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<B>, bus: &Bus<B>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
self.widget self.widget
.node(bump, &bus.map(self.mapper.clone()), style_sheet) .node(bump, &bus.map(self.mapper.clone()), style_sheet)

View File

@ -63,11 +63,12 @@ mod bus;
mod element; mod element;
mod hasher; mod hasher;
pub mod style; pub mod css;
pub mod subscription; pub mod subscription;
pub mod widget; pub mod widget;
pub use bus::Bus; pub use bus::Bus;
pub use css::Css;
pub use dodrio; pub use dodrio;
pub use element::Element; pub use element::Element;
pub use hasher::Hasher; pub use hasher::Hasher;
@ -76,7 +77,6 @@ pub use iced_core::{
VerticalAlignment, VerticalAlignment,
}; };
pub use iced_futures::{executor, futures, Command}; pub use iced_futures::{executor, futures, Command};
pub use style::Style;
pub use subscription::Subscription; pub use subscription::Subscription;
#[doc(no_inline)] #[doc(no_inline)]
@ -241,13 +241,13 @@ where
let mut ui = self.application.borrow_mut(); let mut ui = self.application.borrow_mut();
let element = ui.view(); 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) div(bump)
.attr("style", "width: 100%; height: 100%") .attr("style", "width: 100%; height: 100%")
.children(vec![style_sheet.node(bump), node]) .children(vec![css.node(bump), node])
.finish() .finish()
} }
} }

View File

@ -14,19 +14,20 @@
//! ``` //! ```
//! //!
//! [`Widget`]: trait.Widget.html //! [`Widget`]: trait.Widget.html
use crate::{style, Bus}; use crate::{Bus, Css};
use dodrio::bumpalo; use dodrio::bumpalo;
pub mod button; pub mod button;
pub mod checkbox;
pub mod container;
pub mod image;
pub mod progress_bar;
pub mod radio;
pub mod scrollable; pub mod scrollable;
pub mod slider; pub mod slider;
pub mod text_input; pub mod text_input;
mod checkbox;
mod column; mod column;
mod container;
mod image;
mod radio;
mod row; mod row;
mod space; mod space;
mod text; mod text;
@ -46,6 +47,7 @@ pub use checkbox::Checkbox;
pub use column::Column; pub use column::Column;
pub use container::Container; pub use container::Container;
pub use image::Image; pub use image::Image;
pub use progress_bar::ProgressBar;
pub use radio::Radio; pub use radio::Radio;
pub use row::Row; pub use row::Row;
pub use space::Space; pub use space::Space;
@ -64,6 +66,6 @@ pub trait Widget<Message> {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
_bus: &Bus<Message>, _bus: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b>; ) -> dodrio::Node<'b>;
} }

View File

@ -4,7 +4,9 @@
//! //!
//! [`Button`]: struct.Button.html //! [`Button`]: struct.Button.html
//! [`State`]: struct.State.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; use dodrio::bumpalo;
@ -26,10 +28,11 @@ pub struct Button<'a, Message> {
content: Element<'a, Message>, content: Element<'a, Message>,
on_press: Option<Message>, on_press: Option<Message>,
width: Length, width: Length,
height: Length,
min_width: u32, min_width: u32,
min_height: u32,
padding: u16, padding: u16,
background: Option<Background>, style: Box<dyn StyleSheet>,
border_radius: u16,
} }
impl<'a, Message> Button<'a, Message> { impl<'a, Message> Button<'a, Message> {
@ -46,10 +49,11 @@ impl<'a, Message> Button<'a, Message> {
content: content.into(), content: content.into(),
on_press: None, on_press: None,
width: Length::Shrink, width: Length::Shrink,
height: Length::Shrink,
min_width: 0, min_width: 0,
padding: 0, min_height: 0,
background: None, padding: 5,
border_radius: 0, style: Default::default(),
} }
} }
@ -61,6 +65,14 @@ impl<'a, Message> Button<'a, Message> {
self 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`]. /// Sets the minimum width of the [`Button`].
/// ///
/// [`Button`]: struct.Button.html /// [`Button`]: struct.Button.html
@ -69,6 +81,14 @@ impl<'a, Message> Button<'a, Message> {
self 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`]. /// Sets the padding of the [`Button`].
/// ///
/// [`Button`]: struct.Button.html /// [`Button`]: struct.Button.html
@ -77,20 +97,11 @@ impl<'a, Message> Button<'a, Message> {
self self
} }
/// Sets the [`Background`] of the [`Button`]. /// Sets the style of the [`Button`].
/// ///
/// [`Button`]: struct.Button.html /// [`Button`]: struct.Button.html
/// [`Background`]: ../../struct.Background.html pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
pub fn background<T: Into<Background>>(mut self, background: T) -> Self { self.style = style.into();
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;
self self
} }
@ -126,18 +137,20 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
let width = style::length(self.width); // TODO: State-based styling
let padding_class = let style = self.style.active();
style_sheet.insert(bump, Style::Padding(self.padding));
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"), None => String::from("none"),
Some(background) => match background { Some(background) => match background {
Background::Color(color) => style::color(color), Background::Color(color) => css::color(color),
}, },
}; };
@ -150,11 +163,12 @@ where
"style", "style",
bumpalo::format!( bumpalo::format!(
in bump, in bump,
"background: {}; border-radius: {}px; width:{}; min-width: {}px", "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}",
background, background,
self.border_radius, style.border_radius,
width, css::length(self.width),
self.min_width css::min_length(self.min_width),
css::color(style.text_color)
) )
.into_bump_str(), .into_bump_str(),
) )
@ -168,8 +182,6 @@ where
}); });
} }
// TODO: Complete styling
node.finish() node.finish()
} }
} }

View File

@ -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 dodrio::bumpalo;
use std::rc::Rc; use std::rc::Rc;
@ -25,7 +28,8 @@ pub struct Checkbox<Message> {
is_checked: bool, is_checked: bool,
on_toggle: Rc<dyn Fn(bool) -> Message>, on_toggle: Rc<dyn Fn(bool) -> Message>,
label: String, label: String,
label_color: Option<Color>, width: Length,
style: Box<dyn StyleSheet>,
} }
impl<Message> Checkbox<Message> { impl<Message> Checkbox<Message> {
@ -47,15 +51,24 @@ impl<Message> Checkbox<Message> {
is_checked, is_checked,
on_toggle: Rc::new(f), on_toggle: Rc::new(f),
label: String::from(label), 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 /// [`Checkbox`]: struct.Checkbox.html
pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { pub fn width(mut self, width: Length) -> Self {
self.label_color = Some(color.into()); self.width = width;
self
}
/// Sets the style of the [`Checkbox`].
///
/// [`Checkbox`]: struct.Checkbox.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self self
} }
} }
@ -68,7 +81,7 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
_style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
@ -78,9 +91,23 @@ where
let on_toggle = self.on_toggle.clone(); let on_toggle = self.on_toggle.clone();
let is_checked = self.is_checked; 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) 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![ .children(vec![
// TODO: Checkbox styling
input(bump) input(bump)
.attr("type", "checkbox") .attr("type", "checkbox")
.bool_attr("checked", self.is_checked) .bool_attr("checked", self.is_checked)
@ -91,7 +118,8 @@ where
vdom.schedule_render(); vdom.schedule_render();
}) })
.finish(), .finish(),
text(checkbox_label.into_bump_str()), span(bump).children(vec![
text(checkbox_label.into_bump_str())]).finish(),
]) ])
.finish() .finish()
} }

View File

@ -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 dodrio::bumpalo;
use std::u32; use std::u32;
@ -28,7 +28,7 @@ impl<'a, Message> Column<'a, Message> {
Column { Column {
spacing: 0, spacing: 0,
padding: 0, padding: 0,
width: Length::Shrink, width: Length::Fill,
height: Length::Shrink, height: Length::Shrink,
max_width: u32::MAX, max_width: u32::MAX,
max_height: u32::MAX, max_height: u32::MAX,
@ -112,7 +112,7 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
publish: &Bus<Message>, publish: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
@ -122,18 +122,13 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> {
.map(|element| element.widget.node(bump, publish, style_sheet)) .map(|element| element.widget.node(bump, publish, style_sheet))
.collect(); .collect();
let column_class = style_sheet.insert(bump, Style::Column); let column_class = style_sheet.insert(bump, css::Rule::Column);
let spacing_class = let spacing_class =
style_sheet.insert(bump, Style::Spacing(self.spacing)); style_sheet.insert(bump, css::Rule::Spacing(self.spacing));
let padding_class = let padding_class =
style_sheet.insert(bump, Style::Padding(self.padding)); style_sheet.insert(bump, css::Rule::Padding(self.padding));
let width = style::length(self.width);
let height = style::length(self.height);
let align_items = style::align(self.align_items);
// TODO: Complete styling // TODO: Complete styling
div(bump) div(bump)
@ -144,12 +139,12 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> {
) )
.attr("style", bumpalo::format!( .attr("style", bumpalo::format!(
in bump, in bump,
"width: {}; height: {}; max-width: {}px; max-height: {}px; align-items: {}", "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}",
width, css::length(self.width),
height, css::length(self.height),
self.max_width, css::max_length(self.max_width),
self.max_height, css::max_length(self.max_height),
align_items css::align(self.align_items)
).into_bump_str() ).into_bump_str()
) )
.children(children) .children(children)

View File

@ -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. /// An element decorating some content.
/// ///
@ -11,6 +14,7 @@ pub struct Container<'a, Message> {
max_height: u32, max_height: u32,
horizontal_alignment: Align, horizontal_alignment: Align,
vertical_alignment: Align, vertical_alignment: Align,
style_sheet: Box<dyn StyleSheet>,
content: Element<'a, Message>, content: Element<'a, Message>,
} }
@ -31,6 +35,7 @@ impl<'a, Message> Container<'a, Message> {
max_height: u32::MAX, max_height: u32::MAX,
horizontal_alignment: Align::Start, horizontal_alignment: Align::Start,
vertical_alignment: Align::Start, vertical_alignment: Align::Start,
style_sheet: Default::default(),
content: content.into(), content: content.into(),
} }
} }
@ -84,6 +89,14 @@ impl<'a, Message> Container<'a, Message> {
self 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> impl<'a, Message> Widget<Message> for Container<'a, Message>
@ -94,17 +107,13 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; 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 style = self.style_sheet.style();
let height = style::length(self.height);
let align_items = style::align(self.horizontal_alignment);
let justify_content = style::align(self.vertical_alignment);
let node = div(bump) let node = div(bump)
.attr( .attr(
@ -115,12 +124,17 @@ where
"style", "style",
bumpalo::format!( bumpalo::format!(
in bump, in bump,
"width: {}; height: {}; max-width: {}px; align-items: {}; justify-content: {}", "width: {}; height: {}; max-width: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px",
width, css::length(self.width),
height, css::length(self.height),
self.max_width, css::max_length(self.max_width),
align_items, css::align(self.horizontal_alignment),
justify_content 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(), .into_bump_str(),
) )

View File

@ -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 dodrio::bumpalo;
use std::{
hash::{Hash, Hasher as _},
path::PathBuf,
sync::Arc,
};
/// A frame that displays an image while keeping aspect ratio. /// A frame that displays an image while keeping aspect ratio.
/// ///
@ -14,7 +20,7 @@ use dodrio::bumpalo;
#[derive(Debug)] #[derive(Debug)]
pub struct Image { pub struct Image {
/// The image path /// The image path
pub path: String, pub handle: Handle,
/// The width of the image /// The width of the image
pub width: Length, pub width: Length,
@ -27,9 +33,9 @@ impl Image {
/// Creates a new [`Image`] with the given path. /// Creates a new [`Image`] with the given path.
/// ///
/// [`Image`]: struct.Image.html /// [`Image`]: struct.Image.html
pub fn new<T: Into<String>>(path: T) -> Self { pub fn new<T: Into<Handle>>(handle: T) -> Self {
Image { Image {
path: path.into(), handle: handle.into(),
width: Length::Shrink, width: Length::Shrink,
height: Length::Shrink, height: Length::Shrink,
} }
@ -57,11 +63,13 @@ impl<Message> Widget<Message> for Image {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
_bus: &Bus<Message>, _bus: &Bus<Message>,
_style_sheet: &mut style::Sheet<'b>, _style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; 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()); let mut image = img(bump).attr("src", src.into_bump_str());
@ -89,3 +97,74 @@ impl<'a, Message> From<Image> for Element<'a, Message> {
Element::new(image) 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),
}
}
}

View File

@ -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<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)
}
}

View File

@ -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; use dodrio::bumpalo;
@ -32,7 +35,7 @@ pub struct Radio<Message> {
is_selected: bool, is_selected: bool,
on_click: Message, on_click: Message,
label: String, label: String,
label_color: Option<Color>, style: Box<dyn StyleSheet>,
} }
impl<Message> Radio<Message> { impl<Message> Radio<Message> {
@ -55,15 +58,15 @@ impl<Message> Radio<Message> {
is_selected: Some(value) == selected, is_selected: Some(value) == selected,
on_click: f(value), on_click: f(value),
label: String::from(label), 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 /// [`Radio`]: struct.Radio.html
pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.label_color = Some(color.into()); self.style = style.into();
self self
} }
} }
@ -76,7 +79,7 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
_style_sheet: &mut style::Sheet<'b>, _style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;

View File

@ -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 dodrio::bumpalo;
use std::u32; use std::u32;
@ -28,7 +28,7 @@ impl<'a, Message> Row<'a, Message> {
Row { Row {
spacing: 0, spacing: 0,
padding: 0, padding: 0,
width: Length::Shrink, width: Length::Fill,
height: Length::Shrink, height: Length::Shrink,
max_width: u32::MAX, max_width: u32::MAX,
max_height: u32::MAX, max_height: u32::MAX,
@ -113,7 +113,7 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
publish: &Bus<Message>, publish: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
@ -123,18 +123,13 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> {
.map(|element| element.widget.node(bump, publish, style_sheet)) .map(|element| element.widget.node(bump, publish, style_sheet))
.collect(); .collect();
let row_class = style_sheet.insert(bump, Style::Row); let row_class = style_sheet.insert(bump, css::Rule::Row);
let spacing_class = let spacing_class =
style_sheet.insert(bump, Style::Spacing(self.spacing)); style_sheet.insert(bump, css::Rule::Spacing(self.spacing));
let padding_class = let padding_class =
style_sheet.insert(bump, Style::Padding(self.padding)); style_sheet.insert(bump, css::Rule::Padding(self.padding));
let width = style::length(self.width);
let height = style::length(self.height);
let justify_content = style::align(self.align_items);
// TODO: Complete styling // TODO: Complete styling
div(bump) div(bump)
@ -145,12 +140,12 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> {
) )
.attr("style", bumpalo::format!( .attr("style", bumpalo::format!(
in bump, in bump,
"width: {}; height: {}; max-width: {}px; max-height: {}px; justify-content: {}", "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}",
width, css::length(self.width),
height, css::length(self.height),
self.max_width, css::max_length(self.max_width),
self.max_height, css::max_length(self.max_height),
justify_content css::align(self.align_items)
).into_bump_str() ).into_bump_str()
) )
.children(children) .children(children)

View File

@ -1,5 +1,7 @@
//! Navigate an endless amount of content with a scrollbar. //! 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 /// A widget that can vertically display an infinite amount of content with a
/// scrollbar. /// scrollbar.
@ -9,6 +11,7 @@ pub struct Scrollable<'a, Message> {
height: Length, height: Length,
max_height: u32, max_height: u32,
content: Column<'a, Message>, content: Column<'a, Message>,
style: Box<dyn StyleSheet>,
} }
impl<'a, Message> Scrollable<'a, Message> { impl<'a, Message> Scrollable<'a, Message> {
@ -24,6 +27,7 @@ impl<'a, Message> Scrollable<'a, Message> {
height: Length::Shrink, height: Length::Shrink,
max_height: u32::MAX, max_height: u32::MAX,
content: Column::new(), content: Column::new(),
style: Default::default(),
} }
} }
@ -85,6 +89,14 @@ impl<'a, Message> Scrollable<'a, Message> {
self 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`]. /// Adds an element to the [`Scrollable`].
/// ///
/// [`Scrollable`]: struct.Scrollable.html /// [`Scrollable`]: struct.Scrollable.html
@ -105,12 +117,14 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
let width = style::length(self.width); let width = css::length(self.width);
let height = style::length(self.height); let height = css::length(self.height);
// TODO: Scrollbar styling
let node = div(bump) let node = div(bump)
.attr( .attr(
@ -126,8 +140,6 @@ where
) )
.children(vec![self.content.node(bump, bus, style_sheet)]); .children(vec![self.content.node(bump, bus, style_sheet)]);
// TODO: Complete styling
node.finish() node.finish()
} }
} }

View File

@ -4,7 +4,9 @@
//! //!
//! [`Slider`]: struct.Slider.html //! [`Slider`]: struct.Slider.html
//! [`State`]: struct.State.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 dodrio::bumpalo;
use std::{ops::RangeInclusive, rc::Rc}; use std::{ops::RangeInclusive, rc::Rc};
@ -38,6 +40,7 @@ pub struct Slider<'a, Message> {
value: f32, value: f32,
on_change: Rc<Box<dyn Fn(f32) -> Message>>, on_change: Rc<Box<dyn Fn(f32) -> Message>>,
width: Length, width: Length,
style: Box<dyn StyleSheet>,
} }
impl<'a, Message> Slider<'a, Message> { impl<'a, Message> Slider<'a, Message> {
@ -68,6 +71,7 @@ impl<'a, Message> Slider<'a, Message> {
range, range,
on_change: Rc::new(Box::new(on_change)), on_change: Rc::new(Box::new(on_change)),
width: Length::Fill, width: Length::Fill,
style: Default::default(),
} }
} }
@ -78,6 +82,14 @@ impl<'a, Message> Slider<'a, Message> {
self.width = width; self.width = width;
self 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> impl<'a, Message> Widget<Message> for Slider<'a, Message>
@ -88,7 +100,7 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
_style_sheet: &mut style::Sheet<'b>, _style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -103,7 +115,7 @@ where
let event_bus = bus.clone(); let event_bus = bus.clone();
// TODO: Make `step` configurable // TODO: Make `step` configurable
// TODO: Complete styling // TODO: Styling
input(bump) input(bump)
.attr("type", "range") .attr("type", "range")
.attr("step", "0.01") .attr("step", "0.01")

View File

@ -1,4 +1,4 @@
use crate::{style, Bus, Element, Length, Widget}; use crate::{css, Bus, Css, Element, Length, Widget};
use dodrio::bumpalo; use dodrio::bumpalo;
/// An amount of empty space. /// An amount of empty space.
@ -44,12 +44,12 @@ impl<'a, Message> Widget<Message> for Space {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
_publish: &Bus<Message>, _publish: &Bus<Message>,
_style_sheet: &mut style::Sheet<'b>, _css: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
let width = style::length(self.width); let width = css::length(self.width);
let height = style::length(self.height); let height = css::length(self.height);
let style = bumpalo::format!( let style = bumpalo::format!(
in bump, in bump,

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
style, Bus, Color, Element, Font, HorizontalAlignment, Length, css, Bus, Color, Css, Element, Font, HorizontalAlignment, Length,
VerticalAlignment, Widget, VerticalAlignment, Widget,
}; };
use dodrio::bumpalo; use dodrio::bumpalo;
@ -112,15 +112,18 @@ impl<'a, Message> Widget<Message> for Text {
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
_publish: &Bus<Message>, _publish: &Bus<Message>,
_style_sheet: &mut style::Sheet<'b>, _style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
let content = bumpalo::format!(in bump, "{}", self.content); 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 width = css::length(self.width);
let height = style::length(self.height); let height = css::length(self.height);
let text_align = match self.horizontal_alignment { let text_align = match self.horizontal_alignment {
HorizontalAlignment::Left => "left", HorizontalAlignment::Left => "left",
@ -130,12 +133,16 @@ impl<'a, Message> Widget<Message> for Text {
let style = bumpalo::format!( let style = bumpalo::format!(
in bump, in bump,
"width: {}; height: {}; font-size: {}px; color: {}; text-align: {}", "width: {}; height: {}; font-size: {}px; color: {}; text-align: {}; font-family: {}",
width, width,
height, height,
self.size.unwrap_or(20), self.size.unwrap_or(20),
color, color,
text_align text_align,
match self.font {
Font::Default => "inherit",
Font::External { name, .. } => name,
}
); );
// TODO: Complete styling // TODO: Complete styling

View File

@ -4,8 +4,11 @@
//! //!
//! [`TextInput`]: struct.TextInput.html //! [`TextInput`]: struct.TextInput.html
//! [`State`]: struct.State.html //! [`State`]: struct.State.html
use crate::{bumpalo, style, Bus, Element, Length, Style, Widget}; use crate::{bumpalo, css, Bus, Css, Element, Length, Widget};
use std::rc::Rc;
pub use iced_style::text_input::{Style, StyleSheet};
use std::{rc::Rc, u32};
/// A field that can be filled with text. /// A field that can be filled with text.
/// ///
@ -34,11 +37,12 @@ pub struct TextInput<'a, Message> {
value: String, value: String,
is_secure: bool, is_secure: bool,
width: Length, width: Length,
max_width: Length, max_width: u32,
padding: u16, padding: u16,
size: Option<u16>, size: Option<u16>,
on_change: Rc<Box<dyn Fn(String) -> Message>>, on_change: Rc<Box<dyn Fn(String) -> Message>>,
on_submit: Option<Message>, on_submit: Option<Message>,
style_sheet: Box<dyn StyleSheet>,
} }
impl<'a, Message> TextInput<'a, Message> { impl<'a, Message> TextInput<'a, Message> {
@ -67,11 +71,12 @@ impl<'a, Message> TextInput<'a, Message> {
value: String::from(value), value: String::from(value),
is_secure: false, is_secure: false,
width: Length::Fill, width: Length::Fill,
max_width: Length::Shrink, max_width: u32::MAX,
padding: 0, padding: 0,
size: None, size: None,
on_change: Rc::new(Box::new(on_change)), on_change: Rc::new(Box::new(on_change)),
on_submit: None, on_submit: None,
style_sheet: Default::default(),
} }
} }
@ -94,7 +99,7 @@ impl<'a, Message> TextInput<'a, Message> {
/// Sets the maximum width of the [`TextInput`]. /// Sets the maximum width of the [`TextInput`].
/// ///
/// [`TextInput`]: struct.TextInput.html /// [`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.max_width = max_width;
self self
} }
@ -123,6 +128,14 @@ impl<'a, Message> TextInput<'a, Message> {
self.on_submit = Some(message); self.on_submit = Some(message);
self 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> impl<'a, Message> Widget<Message> for TextInput<'a, Message>
@ -133,18 +146,19 @@ where
&self, &self,
bump: &'b bumpalo::Bump, bump: &'b bumpalo::Bump,
bus: &Bus<Message>, bus: &Bus<Message>,
style_sheet: &mut style::Sheet<'b>, style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> { ) -> dodrio::Node<'b> {
use dodrio::builder::*; use dodrio::builder::*;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
let width = style::length(self.width);
let max_width = style::length(self.max_width);
let padding_class = 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 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) input(bump)
.attr( .attr(
@ -155,10 +169,15 @@ where
"style", "style",
bumpalo::format!( bumpalo::format!(
in bump, in bump,
"width: {}; max-width: {}; font-size: {}px", "width: {}; max-width: {}; font-size: {}px; background: {}; border-width: {}px; border-color: {}; border-radius: {}px; color: {}",
width, css::length(self.width),
max_width, css::max_length(self.max_width),
self.size.unwrap_or(20) 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(), .into_bump_str(),
) )
@ -183,7 +202,17 @@ where
Some(text_input) => text_input, 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::<web_sys::KeyboardEvent>();
match event.key_code() {
13 => { submit_event_bus.publish(on_submit); }
_ => {}
}
}
}) })
.finish() .finish()
} }
@ -211,4 +240,12 @@ impl State {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
/// Creates a new [`State`], representing a focused [`TextInput`].
///
/// [`State`]: struct.State.html
pub fn focused() -> Self {
// TODO
Self::default()
}
} }