├── .gitignore ├── Cargo.toml ├── README.md └── src ├── bus.rs ├── command.rs ├── command └── action.rs ├── css.rs ├── element.rs ├── hasher.rs ├── lib.rs ├── subscription.rs ├── widget.rs └── widget ├── button.rs ├── checkbox.rs ├── column.rs ├── container.rs ├── image.rs ├── progress_bar.rs ├── radio.rs ├── row.rs ├── scrollable.rs ├── slider.rs ├── space.rs ├── text.rs ├── text_input.rs └── toggler.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pkg/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .cargo/ 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iced_web" 3 | version = "0.4.0" 4 | authors = ["Héctor Ramón Jiménez "] 5 | edition = "2021" 6 | description = "A web backend for Iced" 7 | license = "MIT" 8 | repository = "https://github.com/iced-rs/iced" 9 | documentation = "https://docs.rs/iced_web" 10 | readme = "README.md" 11 | keywords = ["gui", "ui", "web", "interface", "widgets"] 12 | categories = ["web-programming"] 13 | 14 | [badges] 15 | maintenance = { status = "actively-developed" } 16 | 17 | [dependencies] 18 | iced_core = "0.4" 19 | iced_futures = "0.3" 20 | iced_style = "0.3" 21 | dodrio = "0.2" 22 | wasm-bindgen = "0.2" 23 | wasm-bindgen-futures = "0.4" 24 | url = "2.0" 25 | num-traits = "0.2" 26 | base64 = "0.13" 27 | 28 | [dependencies.web-sys] 29 | version = "0.3.27" 30 | features = [ 31 | "console", 32 | "Document", 33 | "HtmlElement", 34 | "HtmlInputElement", 35 | "Event", 36 | "EventTarget", 37 | "InputEvent", 38 | "KeyboardEvent", 39 | ] 40 | 41 | [patch.crates-io.iced_core] 42 | git = "https://github.com/iced-rs/iced.git" 43 | rev = "ba9c6168b976f92b90bf5d2106f1010cfc9c8be0" 44 | 45 | [patch.crates-io.iced_futures] 46 | git = "https://github.com/iced-rs/iced.git" 47 | rev = "ba9c6168b976f92b90bf5d2106f1010cfc9c8be0" 48 | 49 | [patch.crates-io.iced_style] 50 | git = "https://github.com/iced-rs/iced.git" 51 | rev = "ba9c6168b976f92b90bf5d2106f1010cfc9c8be0" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `iced_web` 2 | [![Documentation](https://docs.rs/iced_web/badge.svg)][documentation] 3 | [![Crates.io](https://img.shields.io/crates/v/iced_web.svg)](https://crates.io/crates/iced_web) 4 | [![License](https://img.shields.io/crates/l/iced_web.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) 5 | [![Downloads](https://img.shields.io/crates/d/iced_web.svg)](https://crates.io/crates/iced_web) 6 | [![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) 7 | [![project chat](https://img.shields.io/badge/chat-on_zulip-brightgreen.svg)](https://iced.zulipchat.com) 8 | 9 | `iced_web` takes [`iced_core`] and builds a WebAssembly runtime on top. It achieves this by introducing a `Widget` trait that can be used to produce VDOM nodes. 10 | 11 | The crate is currently a __very experimental__, simple abstraction layer over [`dodrio`]. 12 | 13 | [documentation]: https://docs.rs/iced_web 14 | [`iced_core`]: https://github.com/iced-rs/iced/tree/master/core 15 | [`dodrio`]: https://github.com/fitzgen/dodrio 16 | 17 | ## Installation 18 | Add `iced_web` as a dependency in your `Cargo.toml`: 19 | 20 | ```toml 21 | iced_web = "0.4" 22 | ``` 23 | 24 | __Iced moves fast and the `master` branch can contain breaking changes!__ If 25 | you want to learn about a specific release, check out [the release list]. 26 | 27 | [the release list]: https://github.com/iced-rs/iced_web/releases 28 | 29 | ## Usage 30 | The current build process is a bit involved, as [`wasm-pack`] does not currently [support building binary crates](https://github.com/rustwasm/wasm-pack/issues/734). 31 | 32 | Therefore, we instead build using the `wasm32-unknown-unknown` target and use the [`wasm-bindgen`] CLI to generate appropriate bindings. 33 | 34 | For instance, let's say we want to build the [`tour` example]: 35 | 36 | ``` 37 | cd examples 38 | cargo build --package tour --target wasm32-unknown-unknown 39 | wasm-bindgen ../target/wasm32-unknown-unknown/debug/tour.wasm --out-dir tour --web 40 | ``` 41 | 42 | *__Note:__ Keep in mind that Iced is still in early exploration stages and most of the work needs to happen on the native side of the ecosystem. At this stage, it is important to be able to batch work without having to constantly jump back and forth. Because of this, there is currently no requirement for the `master` branch to contain a cross-platform API at all times. If you hit an issue when building an example and want to help, it may be a good way to [start contributing]!* 43 | 44 | [start contributing]: https://github.com/iced-rs/iced/tree/master/CONTRIBUTING.md 45 | 46 | Once the example is compiled, we need to create an `.html` file to load our application: 47 | 48 | ```html 49 | 50 | 51 | 52 | 53 | 54 | Tour - Iced 55 | 56 | 57 | 62 | 63 | 64 | ``` 65 | 66 | Finally, we serve it using an HTTP server and access it with our browser. 67 | 68 | [`wasm-pack`]: https://github.com/rustwasm/wasm-pack 69 | [`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen 70 | [`tour` example]: https://github.com/iced-rs/iced/tree/master/examples/README.md#tour 71 | -------------------------------------------------------------------------------- /src/bus.rs: -------------------------------------------------------------------------------- 1 | use iced_futures::futures::channel::mpsc; 2 | use std::rc::Rc; 3 | 4 | /// A publisher of messages. 5 | /// 6 | /// It can be used to route messages back to the [`Application`]. 7 | /// 8 | /// [`Application`]: crate::Application 9 | #[allow(missing_debug_implementations)] 10 | pub struct Bus { 11 | publish: Rc ()>>, 12 | } 13 | 14 | impl Clone for Bus { 15 | fn clone(&self) -> Self { 16 | Bus { 17 | publish: self.publish.clone(), 18 | } 19 | } 20 | } 21 | 22 | impl Bus 23 | where 24 | Message: 'static, 25 | { 26 | pub(crate) fn new(publish: mpsc::UnboundedSender) -> Self { 27 | Self { 28 | publish: Rc::new(Box::new(move |message| { 29 | publish.unbounded_send(message).expect("Send message"); 30 | })), 31 | } 32 | } 33 | 34 | /// Publishes a new message for the [`Application`]. 35 | /// 36 | /// [`Application`]: crate::Application 37 | pub fn publish(&self, message: Message) { 38 | (self.publish)(message) 39 | } 40 | 41 | /// Creates a new [`Bus`] that applies the given function to the messages 42 | /// before publishing. 43 | pub fn map(&self, mapper: Rc Message>>) -> Bus 44 | where 45 | B: 'static, 46 | { 47 | let publish = self.publish.clone(); 48 | 49 | Bus { 50 | publish: Rc::new(Box::new(move |message| publish(mapper(message)))), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | mod action; 2 | 3 | pub use action::Action; 4 | 5 | use std::fmt; 6 | 7 | #[cfg(target_arch = "wasm32")] 8 | use std::future::Future; 9 | 10 | /// A set of asynchronous actions to be performed by some runtime. 11 | pub struct Command(iced_futures::Command>); 12 | 13 | impl Command { 14 | /// Creates an empty [`Command`]. 15 | /// 16 | /// In other words, a [`Command`] that does nothing. 17 | pub const fn none() -> Self { 18 | Self(iced_futures::Command::none()) 19 | } 20 | 21 | /// Creates a [`Command`] that performs a single [`Action`]. 22 | pub const fn single(action: Action) -> Self { 23 | Self(iced_futures::Command::single(action)) 24 | } 25 | 26 | /// Creates a [`Command`] that performs the action of the given future. 27 | #[cfg(target_arch = "wasm32")] 28 | pub fn perform( 29 | future: impl Future + 'static, 30 | f: impl Fn(T) -> A + 'static + Send, 31 | ) -> Command { 32 | use iced_futures::futures::FutureExt; 33 | 34 | Command::single(Action::Future(Box::pin(future.map(f)))) 35 | } 36 | 37 | /// Creates a [`Command`] that performs the actions of all the given 38 | /// commands. 39 | /// 40 | /// Once this command is run, all the commands will be executed at once. 41 | pub fn batch(commands: impl IntoIterator>) -> Self { 42 | Self(iced_futures::Command::batch( 43 | commands.into_iter().map(|Command(command)| command), 44 | )) 45 | } 46 | 47 | /// Applies a transformation to the result of a [`Command`]. 48 | #[cfg(target_arch = "wasm32")] 49 | pub fn map(self, f: impl Fn(T) -> A + 'static + Clone) -> Command 50 | where 51 | T: 'static, 52 | { 53 | let Command(command) = self; 54 | 55 | Command(command.map(move |action| action.map(f.clone()))) 56 | } 57 | 58 | /// Returns all of the actions of the [`Command`]. 59 | pub fn actions(self) -> Vec> { 60 | let Command(command) = self; 61 | 62 | command.actions() 63 | } 64 | } 65 | 66 | impl fmt::Debug for Command { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | let Command(command) = self; 69 | 70 | command.fmt(f) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/command/action.rs: -------------------------------------------------------------------------------- 1 | pub enum Action { 2 | Future(iced_futures::BoxFuture), 3 | } 4 | 5 | use std::fmt; 6 | 7 | impl Action { 8 | /// Applies a transformation to the result of a [`Command`]. 9 | #[cfg(target_arch = "wasm32")] 10 | pub fn map(self, f: impl Fn(T) -> A + 'static) -> Action 11 | where 12 | T: 'static, 13 | { 14 | use iced_futures::futures::FutureExt; 15 | 16 | match self { 17 | Self::Future(future) => Action::Future(Box::pin(future.map(f))), 18 | } 19 | } 20 | } 21 | 22 | impl fmt::Debug for Action { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Self::Future(_) => write!(f, "Action::Future"), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/css.rs: -------------------------------------------------------------------------------- 1 | //! Style your widgets. 2 | use crate::bumpalo; 3 | use crate::{Alignment, Background, Color, Length, Padding}; 4 | 5 | use std::collections::BTreeMap; 6 | 7 | /// A CSS rule of a VDOM node. 8 | #[derive(Debug)] 9 | pub enum Rule { 10 | /// Container with vertical distribution 11 | Column, 12 | 13 | /// Container with horizonal distribution 14 | Row, 15 | 16 | /// Spacing between elements 17 | Spacing(u16), 18 | 19 | /// Toggler input for a specific size 20 | Toggler(u16), 21 | } 22 | 23 | impl Rule { 24 | /// Returns the class name of the [`Rule`]. 25 | pub fn class<'a>(&self) -> String { 26 | match self { 27 | Rule::Column => String::from("c"), 28 | Rule::Row => String::from("r"), 29 | Rule::Spacing(spacing) => format!("s-{}", spacing), 30 | Rule::Toggler(size) => format!("toggler-{}", size), 31 | } 32 | } 33 | 34 | /// Returns the declaration of the [`Rule`]. 35 | pub fn declaration<'a>(&self, bump: &'a bumpalo::Bump) -> &'a str { 36 | let class = self.class(); 37 | 38 | match self { 39 | Rule::Column => { 40 | let body = "{ display: flex; flex-direction: column; }"; 41 | 42 | bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() 43 | } 44 | Rule::Row => { 45 | let body = "{ display: flex; flex-direction: row; }"; 46 | 47 | bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() 48 | } 49 | Rule::Spacing(spacing) => bumpalo::format!( 50 | in bump, 51 | ".c.{} > * {{ margin-bottom: {}px }} \ 52 | .r.{} > * {{ margin-right: {}px }} \ 53 | .c.{} > *:last-child {{ margin-bottom: 0 }} \ 54 | .r.{} > *:last-child {{ margin-right: 0 }}", 55 | class, 56 | spacing, 57 | class, 58 | spacing, 59 | class, 60 | class 61 | ) 62 | .into_bump_str(), 63 | Rule::Toggler(size) => bumpalo::format!( 64 | in bump, 65 | ".toggler-{} {{ display: flex; cursor: pointer; justify-content: space-between; }} \ 66 | .toggler-{} input {{ display:none; }} \ 67 | .toggler-{} span {{ background-color: #b1b1b1; position: relative; display: inline-flex; width:{}px; height: {}px; border-radius: {}px;}} \ 68 | .toggler-{} span > span {{ background-color: #FFFFFF; width: {}px; height: {}px; border-radius: 50%; top: 1px; left: 1px;}} \ 69 | .toggler-{}:hover span > span {{ background-color: #f1f1f1 !important; }} \ 70 | .toggler-{} input:checked + span {{ background-color: #00FF00; }} \ 71 | .toggler-{} input:checked + span > span {{ -webkit-transform: translateX({}px); -ms-transform:translateX({}px); transform: translateX({}px); }} 72 | ", 73 | // toggler 74 | size, 75 | 76 | // toggler input 77 | size, 78 | 79 | // toggler span 80 | size, 81 | size*2, 82 | size, 83 | size, 84 | 85 | // toggler span > span 86 | size, 87 | size-2, 88 | size-2, 89 | 90 | // toggler: hover + span > span 91 | size, 92 | 93 | // toggler input:checked + span 94 | size, 95 | 96 | // toggler input:checked + span > span 97 | size, 98 | size, 99 | size, 100 | size 101 | ) 102 | .into_bump_str(), 103 | } 104 | } 105 | } 106 | 107 | /// A cascading style sheet. 108 | #[derive(Debug)] 109 | pub struct Css<'a> { 110 | rules: BTreeMap, 111 | } 112 | 113 | impl<'a> Css<'a> { 114 | /// Creates an empty [`Css`]. 115 | pub fn new() -> Self { 116 | Css { 117 | rules: BTreeMap::new(), 118 | } 119 | } 120 | 121 | /// Inserts the [`Rule`] in the [`Css`], if it was not previously 122 | /// inserted. 123 | /// 124 | /// It returns the class name of the provided [`Rule`]. 125 | pub fn insert(&mut self, bump: &'a bumpalo::Bump, rule: Rule) -> String { 126 | let class = rule.class(); 127 | 128 | if !self.rules.contains_key(&class) { 129 | let _ = self.rules.insert(class.clone(), rule.declaration(bump)); 130 | } 131 | 132 | class 133 | } 134 | 135 | /// Produces the VDOM node of the [`Css`]. 136 | pub fn node(self, bump: &'a bumpalo::Bump) -> dodrio::Node<'a> { 137 | use dodrio::builder::*; 138 | 139 | let mut declarations = bumpalo::collections::Vec::new_in(bump); 140 | 141 | declarations.push(text("html { height: 100% }")); 142 | declarations.push(text( 143 | "body { height: 100%; margin: 0; padding: 0; font-family: sans-serif }", 144 | )); 145 | declarations.push(text("* { margin: 0; padding: 0 }")); 146 | declarations.push(text( 147 | "button { border: none; cursor: pointer; outline: none }", 148 | )); 149 | 150 | for declaration in self.rules.values() { 151 | declarations.push(text(*declaration)); 152 | } 153 | 154 | style(bump).children(declarations).finish() 155 | } 156 | } 157 | 158 | /// Returns the style value for the given [`Length`]. 159 | pub fn length(length: Length) -> String { 160 | match length { 161 | Length::Shrink => String::from("auto"), 162 | Length::Units(px) => format!("{}px", px), 163 | Length::Fill | Length::FillPortion(_) => String::from("100%"), 164 | } 165 | } 166 | 167 | /// Returns the style value for the given maximum length in units. 168 | pub fn max_length(units: u32) -> String { 169 | use std::u32; 170 | 171 | if units == u32::MAX { 172 | String::from("initial") 173 | } else { 174 | format!("{}px", units) 175 | } 176 | } 177 | 178 | /// Returns the style value for the given minimum length in units. 179 | pub fn min_length(units: u32) -> String { 180 | if units == 0 { 181 | String::from("initial") 182 | } else { 183 | format!("{}px", units) 184 | } 185 | } 186 | 187 | /// Returns the style value for the given [`Color`]. 188 | pub fn color(Color { r, g, b, a }: Color) -> String { 189 | format!("rgba({}, {}, {}, {})", 255.0 * r, 255.0 * g, 255.0 * b, a) 190 | } 191 | 192 | /// Returns the style value for the given [`Background`]. 193 | pub fn background(background: Background) -> String { 194 | match background { 195 | Background::Color(c) => color(c), 196 | } 197 | } 198 | 199 | /// Returns the style value for the given [`Alignment`]. 200 | pub fn alignment(alignment: Alignment) -> &'static str { 201 | match alignment { 202 | Alignment::Start => "flex-start", 203 | Alignment::Center => "center", 204 | Alignment::End => "flex-end", 205 | Alignment::Fill => "stretch", 206 | } 207 | } 208 | 209 | /// Returns the style value for the given [`Padding`]. 210 | /// 211 | /// [`Padding`]: struct.Padding.html 212 | pub fn padding(padding: Padding) -> String { 213 | format!( 214 | "{}px {}px {}px {}px", 215 | padding.top, padding.right, padding.bottom, padding.left 216 | ) 217 | } 218 | -------------------------------------------------------------------------------- /src/element.rs: -------------------------------------------------------------------------------- 1 | use crate::{Bus, Color, Css, Widget}; 2 | 3 | use dodrio::bumpalo; 4 | use std::rc::Rc; 5 | 6 | /// A generic [`Widget`]. 7 | /// 8 | /// It is useful to build composable user interfaces that do not leak 9 | /// implementation details in their __view logic__. 10 | /// 11 | /// If you have a [built-in widget], you should be able to use `Into` 12 | /// to turn it into an [`Element`]. 13 | /// 14 | /// [built-in widget]: mod@crate::widget 15 | #[allow(missing_debug_implementations)] 16 | pub struct Element<'a, Message> { 17 | pub(crate) widget: Box + 'a>, 18 | } 19 | 20 | impl<'a, Message> Element<'a, Message> { 21 | /// Create a new [`Element`] containing the given [`Widget`]. 22 | pub fn new(widget: impl Widget + 'a) -> Self { 23 | Self { 24 | widget: Box::new(widget), 25 | } 26 | } 27 | 28 | /// Applies a transformation to the produced message of the [`Element`]. 29 | /// 30 | /// This method is useful when you want to decouple different parts of your 31 | /// UI and make them __composable__. 32 | pub fn map(self, f: F) -> Element<'a, B> 33 | where 34 | Message: 'static, 35 | B: 'static, 36 | F: 'static + Fn(Message) -> B, 37 | { 38 | Element { 39 | widget: Box::new(Map::new(self.widget, f)), 40 | } 41 | } 42 | 43 | /// Marks the [`Element`] as _to-be-explained_. 44 | pub fn explain(self, _color: Color) -> Element<'a, Message> { 45 | self 46 | } 47 | 48 | /// Produces a VDOM node for the [`Element`]. 49 | pub fn node<'b>( 50 | &self, 51 | bump: &'b bumpalo::Bump, 52 | bus: &Bus, 53 | style_sheet: &mut Css<'b>, 54 | ) -> dodrio::Node<'b> { 55 | self.widget.node(bump, bus, style_sheet) 56 | } 57 | } 58 | 59 | struct Map<'a, A, B> { 60 | widget: Box + 'a>, 61 | mapper: Rc B>>, 62 | } 63 | 64 | impl<'a, A, B> Map<'a, A, B> { 65 | pub fn new(widget: Box + 'a>, mapper: F) -> Map<'a, A, B> 66 | where 67 | F: 'static + Fn(A) -> B, 68 | { 69 | Map { 70 | widget, 71 | mapper: Rc::new(Box::new(mapper)), 72 | } 73 | } 74 | } 75 | 76 | impl<'a, A, B> Widget for Map<'a, A, B> 77 | where 78 | A: 'static, 79 | B: 'static, 80 | { 81 | fn node<'b>( 82 | &self, 83 | bump: &'b bumpalo::Bump, 84 | bus: &Bus, 85 | style_sheet: &mut Css<'b>, 86 | ) -> dodrio::Node<'b> { 87 | self.widget 88 | .node(bump, &bus.map(self.mapper.clone()), style_sheet) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/hasher.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::DefaultHasher; 2 | 3 | /// The hasher used to compare subscriptions. 4 | #[derive(Debug)] 5 | pub struct Hasher(DefaultHasher); 6 | 7 | impl Default for Hasher { 8 | fn default() -> Self { 9 | Hasher(DefaultHasher::default()) 10 | } 11 | } 12 | 13 | impl core::hash::Hasher for Hasher { 14 | fn write(&mut self, bytes: &[u8]) { 15 | self.0.write(bytes) 16 | } 17 | 18 | fn finish(&self) -> u64 { 19 | self.0.finish() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A web runtime for Iced, targetting the DOM. 2 | //! 3 | //! `iced_web` takes [`iced_core`] and builds a WebAssembly runtime on top. It 4 | //! achieves this by introducing a `Widget` trait that can be used to produce 5 | //! VDOM nodes. 6 | //! 7 | //! The crate is currently a __very experimental__, simple abstraction layer 8 | //! over [`dodrio`]. 9 | //! 10 | //! [`iced_core`]: https://github.com/iced-rs/iced/tree/master/core 11 | //! [`dodrio`]: https://github.com/fitzgen/dodrio 12 | //! 13 | //! # Usage 14 | //! The current build process is a bit involved, as [`wasm-pack`] does not 15 | //! currently [support building binary crates](https://github.com/rustwasm/wasm-pack/issues/734). 16 | //! 17 | //! Therefore, we instead build using the `wasm32-unknown-unknown` target and 18 | //! use the [`wasm-bindgen`] CLI to generate appropriate bindings. 19 | //! 20 | //! For instance, let's say we want to build the [`tour` example]: 21 | //! 22 | //! ```bash 23 | //! cd examples 24 | //! cargo build --package tour --target wasm32-unknown-unknown 25 | //! wasm-bindgen ../target/wasm32-unknown-unknown/debug/tour.wasm --out-dir tour --web 26 | //! ``` 27 | //! 28 | //! Then, we need to create an `.html` file to load our application: 29 | //! 30 | //! ```html 31 | //! 32 | //! 33 | //! 34 | //! 35 | //! 36 | //! Tour - Iced 37 | //! 38 | //! 39 | //! 44 | //! 45 | //! 46 | //! ``` 47 | //! 48 | //! Finally, we serve it using an HTTP server and access it with our browser. 49 | //! 50 | //! [`wasm-pack`]: https://github.com/rustwasm/wasm-pack 51 | //! [`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen 52 | //! [`tour` example]: https://github.com/iced-rs/iced/tree/0.3/examples/tour 53 | #![doc( 54 | html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" 55 | )] 56 | #![deny(missing_docs)] 57 | #![deny(missing_debug_implementations)] 58 | #![deny(unused_results)] 59 | #![forbid(unsafe_code)] 60 | #![forbid(rust_2018_idioms)] 61 | use dodrio::bumpalo; 62 | use std::{cell::RefCell, rc::Rc}; 63 | 64 | mod bus; 65 | mod command; 66 | mod element; 67 | mod hasher; 68 | 69 | pub mod css; 70 | pub mod subscription; 71 | pub mod widget; 72 | 73 | pub use bus::Bus; 74 | pub use command::Command; 75 | pub use css::Css; 76 | pub use dodrio; 77 | pub use element::Element; 78 | pub use hasher::Hasher; 79 | pub use subscription::Subscription; 80 | 81 | pub use iced_core::alignment; 82 | pub use iced_core::keyboard; 83 | pub use iced_core::mouse; 84 | pub use iced_futures::executor; 85 | pub use iced_futures::futures; 86 | 87 | pub use iced_core::{ 88 | Alignment, Background, Color, Font, Length, Padding, Point, Rectangle, 89 | Size, Vector, 90 | }; 91 | 92 | #[doc(no_inline)] 93 | pub use widget::*; 94 | 95 | #[doc(no_inline)] 96 | pub use executor::Executor; 97 | 98 | /// An interactive web application. 99 | /// 100 | /// This trait is the main entrypoint of Iced. Once implemented, you can run 101 | /// your GUI application by simply calling [`run`](#method.run). It will take 102 | /// control of the `` and the `<body>` of the document. 103 | /// 104 | /// An [`Application`](trait.Application.html) can execute asynchronous actions 105 | /// by returning a [`Command`](struct.Command.html) in some of its methods. 106 | pub trait Application { 107 | /// The [`Executor`] that will run commands and subscriptions. 108 | type Executor: Executor; 109 | 110 | /// The type of __messages__ your [`Application`] will produce. 111 | type Message: Send; 112 | 113 | /// The data needed to initialize your [`Application`]. 114 | type Flags; 115 | 116 | /// Initializes the [`Application`]. 117 | /// 118 | /// Here is where you should return the initial state of your app. 119 | /// 120 | /// Additionally, you can return a [`Command`] if you need to perform some 121 | /// async action in the background on startup. This is useful if you want to 122 | /// load state from a file, perform an initial HTTP request, etc. 123 | fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) 124 | where 125 | Self: Sized; 126 | 127 | /// Returns the current title of the [`Application`]. 128 | /// 129 | /// This title can be dynamic! The runtime will automatically update the 130 | /// title of your application when necessary. 131 | fn title(&self) -> String; 132 | 133 | /// Handles a __message__ and updates the state of the [`Application`]. 134 | /// 135 | /// This is where you define your __update logic__. All the __messages__, 136 | /// produced by either user interactions or commands, will be handled by 137 | /// this method. 138 | /// 139 | /// Any [`Command`] returned will be executed immediately in the background. 140 | fn update(&mut self, message: Self::Message) -> Command<Self::Message>; 141 | 142 | /// Returns the widgets to display in the [`Application`]. 143 | /// 144 | /// These widgets can produce __messages__ based on user interaction. 145 | fn view(&mut self) -> Element<'_, Self::Message>; 146 | 147 | /// Returns the event [`Subscription`] for the current state of the 148 | /// application. 149 | /// 150 | /// A [`Subscription`] will be kept alive as long as you keep returning it, 151 | /// and the __messages__ produced will be handled by 152 | /// [`update`](#tymethod.update). 153 | /// 154 | /// By default, this method returns an empty [`Subscription`]. 155 | fn subscription(&self) -> Subscription<Self::Message> { 156 | Subscription::none() 157 | } 158 | 159 | /// Runs the [`Application`]. 160 | fn run(flags: Self::Flags) 161 | where 162 | Self: 'static + Sized, 163 | { 164 | use futures::stream::StreamExt; 165 | 166 | let window = web_sys::window().unwrap(); 167 | let document = window.document().unwrap(); 168 | let body = document.body().unwrap(); 169 | 170 | let (sender, receiver) = 171 | iced_futures::futures::channel::mpsc::unbounded(); 172 | 173 | let mut runtime = iced_futures::Runtime::new( 174 | Self::Executor::new().expect("Create executor"), 175 | sender.clone(), 176 | ); 177 | 178 | let (app, command) = runtime.enter(|| Self::new(flags)); 179 | 180 | let mut title = app.title(); 181 | document.set_title(&title); 182 | 183 | run_command(command, &mut runtime); 184 | 185 | let application = Rc::new(RefCell::new(app)); 186 | 187 | let instance = Instance { 188 | application: application.clone(), 189 | bus: Bus::new(sender), 190 | }; 191 | 192 | let vdom = dodrio::Vdom::new(&body, instance); 193 | 194 | let event_loop = receiver.for_each(move |message| { 195 | let (command, subscription) = runtime.enter(|| { 196 | let command = application.borrow_mut().update(message); 197 | let subscription = application.borrow().subscription(); 198 | 199 | (command, subscription) 200 | }); 201 | 202 | let new_title = application.borrow().title(); 203 | 204 | run_command(command, &mut runtime); 205 | runtime.track(subscription); 206 | 207 | if title != new_title { 208 | document.set_title(&new_title); 209 | 210 | title = new_title; 211 | } 212 | 213 | vdom.weak().schedule_render(); 214 | 215 | futures::future::ready(()) 216 | }); 217 | 218 | wasm_bindgen_futures::spawn_local(event_loop); 219 | } 220 | } 221 | 222 | struct Instance<A: Application> { 223 | application: Rc<RefCell<A>>, 224 | bus: Bus<A::Message>, 225 | } 226 | 227 | impl<'a, A> dodrio::Render<'a> for Instance<A> 228 | where 229 | A: Application, 230 | { 231 | fn render( 232 | &self, 233 | context: &mut dodrio::RenderContext<'a>, 234 | ) -> dodrio::Node<'a> { 235 | use dodrio::builder::*; 236 | 237 | let mut ui = self.application.borrow_mut(); 238 | let element = ui.view(); 239 | let mut css = Css::new(); 240 | 241 | let node = element.widget.node(context.bump, &self.bus, &mut css); 242 | 243 | div(context.bump) 244 | .attr("style", "width: 100%; height: 100%") 245 | .children(vec![css.node(context.bump), node]) 246 | .finish() 247 | } 248 | } 249 | 250 | /// An interactive embedded web application. 251 | /// 252 | /// This trait is the main entrypoint of Iced. Once implemented, you can run 253 | /// your GUI application by simply calling [`run`](#method.run). It will either 254 | /// take control of the `<body>' or of an HTML element of the document specified 255 | /// by `container_id`. 256 | /// 257 | /// An [`Embedded`](trait.Embedded.html) can execute asynchronous actions 258 | /// by returning a [`Command`](struct.Command.html) in some of its methods. 259 | pub trait Embedded { 260 | /// The [`Executor`] that will run commands and subscriptions. 261 | /// 262 | /// The [`executor::WasmBindgen`] can be a good choice for the Web. 263 | /// 264 | /// [`Executor`]: trait.Executor.html 265 | /// [`executor::Default`]: executor/struct.Default.html 266 | type Executor: Executor; 267 | 268 | /// The type of __messages__ your [`Embedded`] application will produce. 269 | /// 270 | /// [`Embedded`]: trait.Embedded.html 271 | type Message: Send; 272 | 273 | /// The data needed to initialize your [`Embedded`] application. 274 | /// 275 | /// [`Embedded`]: trait.Embedded.html 276 | type Flags; 277 | 278 | /// Initializes the [`Embedded`] application. 279 | /// 280 | /// Here is where you should return the initial state of your app. 281 | /// 282 | /// Additionally, you can return a [`Command`](struct.Command.html) if you 283 | /// need to perform some async action in the background on startup. This is 284 | /// useful if you want to load state from a file, perform an initial HTTP 285 | /// request, etc. 286 | /// 287 | /// [`Embedded`]: trait.Embedded.html 288 | fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) 289 | where 290 | Self: Sized; 291 | 292 | /// Handles a __message__ and updates the state of the [`Embedded`] 293 | /// application. 294 | /// 295 | /// This is where you define your __update logic__. All the __messages__, 296 | /// produced by either user interactions or commands, will be handled by 297 | /// this method. 298 | /// 299 | /// Any [`Command`] returned will be executed immediately in the background. 300 | /// 301 | /// [`Embedded`]: trait.Embedded.html 302 | /// [`Command`]: struct.Command.html 303 | fn update(&mut self, message: Self::Message) -> Command<Self::Message>; 304 | 305 | /// Returns the widgets to display in the [`Embedded`] application. 306 | /// 307 | /// These widgets can produce __messages__ based on user interaction. 308 | /// 309 | /// [`Embedded`]: trait.Embedded.html 310 | fn view(&mut self) -> Element<'_, Self::Message>; 311 | 312 | /// Returns the event [`Subscription`] for the current state of the embedded 313 | /// application. 314 | /// 315 | /// A [`Subscription`] will be kept alive as long as you keep returning it, 316 | /// and the __messages__ produced will be handled by 317 | /// [`update`](#tymethod.update). 318 | /// 319 | /// By default, this method returns an empty [`Subscription`]. 320 | /// 321 | /// [`Subscription`]: struct.Subscription.html 322 | fn subscription(&self) -> Subscription<Self::Message> { 323 | Subscription::none() 324 | } 325 | 326 | /// Runs the [`Embedded`] application. 327 | /// 328 | /// [`Embedded`]: trait.Embedded.html 329 | fn run(flags: Self::Flags, container_id: Option<String>) 330 | where 331 | Self: 'static + Sized, 332 | { 333 | use futures::stream::StreamExt; 334 | use wasm_bindgen::JsCast; 335 | use web_sys::HtmlElement; 336 | 337 | let window = web_sys::window().unwrap(); 338 | let document = window.document().unwrap(); 339 | let container: HtmlElement = container_id 340 | .map(|id| document.get_element_by_id(&id).unwrap()) 341 | .map(|container| { 342 | container.dyn_ref::<HtmlElement>().unwrap().to_owned() 343 | }) 344 | .unwrap_or_else(|| document.body().unwrap()); 345 | 346 | let (sender, receiver) = 347 | iced_futures::futures::channel::mpsc::unbounded(); 348 | 349 | let mut runtime = iced_futures::Runtime::new( 350 | Self::Executor::new().expect("Create executor"), 351 | sender.clone(), 352 | ); 353 | 354 | let (app, command) = runtime.enter(|| Self::new(flags)); 355 | run_command(command, &mut runtime); 356 | 357 | let application = Rc::new(RefCell::new(app)); 358 | 359 | let instance = EmbeddedInstance { 360 | application: application.clone(), 361 | bus: Bus::new(sender), 362 | }; 363 | 364 | let vdom = dodrio::Vdom::new(&container, instance); 365 | 366 | let event_loop = receiver.for_each(move |message| { 367 | let (command, subscription) = runtime.enter(|| { 368 | let command = application.borrow_mut().update(message); 369 | let subscription = application.borrow().subscription(); 370 | 371 | (command, subscription) 372 | }); 373 | 374 | run_command(command, &mut runtime); 375 | runtime.track(subscription); 376 | 377 | vdom.weak().schedule_render(); 378 | 379 | futures::future::ready(()) 380 | }); 381 | 382 | wasm_bindgen_futures::spawn_local(event_loop); 383 | } 384 | } 385 | 386 | fn run_command<Message: 'static + Send, E: Executor>( 387 | command: Command<Message>, 388 | runtime: &mut iced_futures::Runtime< 389 | Hasher, 390 | (), 391 | E, 392 | iced_futures::futures::channel::mpsc::UnboundedSender<Message>, 393 | Message, 394 | >, 395 | ) { 396 | for action in command.actions() { 397 | match action { 398 | command::Action::Future(future) => { 399 | runtime.spawn(future); 400 | } 401 | } 402 | } 403 | } 404 | 405 | struct EmbeddedInstance<A: Embedded> { 406 | application: Rc<RefCell<A>>, 407 | bus: Bus<A::Message>, 408 | } 409 | 410 | impl<'a, A> dodrio::Render<'a> for EmbeddedInstance<A> 411 | where 412 | A: Embedded, 413 | { 414 | fn render( 415 | &self, 416 | context: &mut dodrio::RenderContext<'a>, 417 | ) -> dodrio::Node<'a> { 418 | use dodrio::builder::*; 419 | 420 | let mut ui = self.application.borrow_mut(); 421 | let element = ui.view(); 422 | let mut css = Css::new(); 423 | 424 | let node = element.widget.node(context.bump, &self.bus, &mut css); 425 | 426 | div(context.bump) 427 | .attr("style", "width: 100%; height: 100%") 428 | .children(vec![css.node(context.bump), node]) 429 | .finish() 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/subscription.rs: -------------------------------------------------------------------------------- 1 | //! Listen to external events in your application. 2 | use crate::Hasher; 3 | 4 | /// A request to listen to external events. 5 | /// 6 | /// Besides performing async actions on demand with [`Command`], most 7 | /// applications also need to listen to external events passively. 8 | /// 9 | /// A [`Subscription`] is normally provided to some runtime, like a [`Command`], 10 | /// and it will generate events as long as the user keeps requesting it. 11 | /// 12 | /// For instance, you can use a [`Subscription`] to listen to a WebSocket 13 | /// connection, keyboard presses, mouse events, time ticks, etc. 14 | /// 15 | /// [`Command`]: crate::Command 16 | pub type Subscription<T> = iced_futures::Subscription<Hasher, (), T>; 17 | 18 | pub use iced_futures::subscription::Recipe; 19 | -------------------------------------------------------------------------------- /src/widget.rs: -------------------------------------------------------------------------------- 1 | //! Use the built-in widgets or create your own. 2 | //! 3 | //! # Custom widgets 4 | //! If you want to implement a custom widget, you simply need to implement the 5 | //! [`Widget`] trait. You can use the API of the built-in widgets as a guide or 6 | //! source of inspiration. 7 | //! 8 | //! # Re-exports 9 | //! For convenience, the contents of this module are available at the root 10 | //! module. Therefore, you can directly type: 11 | //! 12 | //! ``` 13 | //! use iced_web::{button, Button, Widget}; 14 | //! ``` 15 | use crate::{Bus, Css}; 16 | use dodrio::bumpalo; 17 | 18 | pub mod button; 19 | pub mod checkbox; 20 | pub mod container; 21 | pub mod image; 22 | pub mod progress_bar; 23 | pub mod radio; 24 | pub mod scrollable; 25 | pub mod slider; 26 | pub mod text_input; 27 | pub mod toggler; 28 | 29 | mod column; 30 | mod row; 31 | mod space; 32 | mod text; 33 | 34 | #[doc(no_inline)] 35 | pub use button::Button; 36 | #[doc(no_inline)] 37 | pub use scrollable::Scrollable; 38 | #[doc(no_inline)] 39 | pub use slider::Slider; 40 | #[doc(no_inline)] 41 | pub use text::Text; 42 | #[doc(no_inline)] 43 | pub use text_input::TextInput; 44 | #[doc(no_inline)] 45 | pub use toggler::Toggler; 46 | 47 | pub use checkbox::Checkbox; 48 | pub use column::Column; 49 | pub use container::Container; 50 | pub use image::Image; 51 | pub use progress_bar::ProgressBar; 52 | pub use radio::Radio; 53 | pub use row::Row; 54 | pub use space::Space; 55 | 56 | /// A component that displays information and allows interaction. 57 | /// 58 | /// If you want to build your own widgets, you will need to implement this 59 | /// trait. 60 | pub trait Widget<Message> { 61 | /// Produces a VDOM node for the [`Widget`]. 62 | fn node<'b>( 63 | &self, 64 | bump: &'b bumpalo::Bump, 65 | _bus: &Bus<Message>, 66 | style_sheet: &mut Css<'b>, 67 | ) -> dodrio::Node<'b>; 68 | } 69 | -------------------------------------------------------------------------------- /src/widget/button.rs: -------------------------------------------------------------------------------- 1 | //! Allow your users to perform actions by pressing a button. 2 | //! 3 | //! A [`Button`] has some local [`State`]. 4 | use crate::{css, Background, Bus, Css, Element, Length, Padding, Widget}; 5 | 6 | pub use iced_style::button::{Style, StyleSheet}; 7 | 8 | use dodrio::bumpalo; 9 | 10 | /// A generic widget that produces a message when pressed. 11 | /// 12 | /// ``` 13 | /// # use iced_web::{button, Button, Text}; 14 | /// # 15 | /// enum Message { 16 | /// ButtonPressed, 17 | /// } 18 | /// 19 | /// let mut state = button::State::new(); 20 | /// let button = Button::new(&mut state, Text::new("Press me!")) 21 | /// .on_press(Message::ButtonPressed); 22 | /// ``` 23 | /// 24 | /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will 25 | /// be disabled: 26 | /// 27 | /// ``` 28 | /// # use iced_web::{button, Button, Text}; 29 | /// # 30 | /// #[derive(Clone)] 31 | /// enum Message { 32 | /// ButtonPressed, 33 | /// } 34 | /// 35 | /// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { 36 | /// Button::new(state, Text::new("I'm disabled!")) 37 | /// } 38 | /// 39 | /// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { 40 | /// disabled_button(state).on_press(Message::ButtonPressed) 41 | /// } 42 | /// ``` 43 | #[allow(missing_debug_implementations)] 44 | pub struct Button<'a, Message> { 45 | content: Element<'a, Message>, 46 | on_press: Option<Message>, 47 | width: Length, 48 | #[allow(dead_code)] 49 | height: Length, 50 | min_width: u32, 51 | #[allow(dead_code)] 52 | min_height: u32, 53 | padding: Padding, 54 | style: Box<dyn StyleSheet + 'a>, 55 | } 56 | 57 | impl<'a, Message> Button<'a, Message> { 58 | /// Creates a new [`Button`] with some local [`State`] and the given 59 | /// content. 60 | pub fn new<E>(_state: &'a mut State, content: E) -> Self 61 | where 62 | E: Into<Element<'a, Message>>, 63 | { 64 | Button { 65 | content: content.into(), 66 | on_press: None, 67 | width: Length::Shrink, 68 | height: Length::Shrink, 69 | min_width: 0, 70 | min_height: 0, 71 | padding: Padding::new(5), 72 | style: Default::default(), 73 | } 74 | } 75 | 76 | /// Sets the width of the [`Button`]. 77 | pub fn width(mut self, width: Length) -> Self { 78 | self.width = width; 79 | self 80 | } 81 | 82 | /// Sets the height of the [`Button`]. 83 | pub fn height(mut self, height: Length) -> Self { 84 | self.height = height; 85 | self 86 | } 87 | 88 | /// Sets the minimum width of the [`Button`]. 89 | pub fn min_width(mut self, min_width: u32) -> Self { 90 | self.min_width = min_width; 91 | self 92 | } 93 | 94 | /// Sets the minimum height of the [`Button`]. 95 | pub fn min_height(mut self, min_height: u32) -> Self { 96 | self.min_height = min_height; 97 | self 98 | } 99 | 100 | /// Sets the [`Padding`] of the [`Button`]. 101 | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { 102 | self.padding = padding.into(); 103 | self 104 | } 105 | 106 | /// Sets the style of the [`Button`]. 107 | pub fn style(mut self, style: impl Into<Box<dyn StyleSheet + 'a>>) -> Self { 108 | self.style = style.into(); 109 | self 110 | } 111 | 112 | /// Sets the message that will be produced when the [`Button`] is pressed. 113 | /// If on_press isn't set, button will be disabled. 114 | pub fn on_press(mut self, msg: Message) -> Self { 115 | self.on_press = Some(msg); 116 | self 117 | } 118 | } 119 | 120 | /// The local state of a [`Button`]. 121 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 122 | pub struct State; 123 | 124 | impl State { 125 | /// Creates a new [`State`]. 126 | pub fn new() -> State { 127 | State::default() 128 | } 129 | } 130 | 131 | impl<'a, Message> Widget<Message> for Button<'a, Message> 132 | where 133 | Message: 'static + Clone, 134 | { 135 | fn node<'b>( 136 | &self, 137 | bump: &'b bumpalo::Bump, 138 | bus: &Bus<Message>, 139 | style_sheet: &mut Css<'b>, 140 | ) -> dodrio::Node<'b> { 141 | use dodrio::builder::*; 142 | 143 | // TODO: State-based styling 144 | let style = self.style.active(); 145 | 146 | let background = match style.background { 147 | None => String::from("none"), 148 | Some(background) => match background { 149 | Background::Color(color) => css::color(color), 150 | }, 151 | }; 152 | 153 | let mut node = button(bump) 154 | .attr( 155 | "style", 156 | bumpalo::format!( 157 | in bump, 158 | "background: {}; border-radius: {}px; width:{}; \ 159 | min-width: {}; color: {}; padding: {}", 160 | background, 161 | style.border_radius, 162 | css::length(self.width), 163 | css::min_length(self.min_width), 164 | css::color(style.text_color), 165 | css::padding(self.padding) 166 | ) 167 | .into_bump_str(), 168 | ) 169 | .children(vec![self.content.node(bump, bus, style_sheet)]); 170 | 171 | if let Some(on_press) = self.on_press.clone() { 172 | let event_bus = bus.clone(); 173 | 174 | node = node.on("click", move |_root, _vdom, _event| { 175 | event_bus.publish(on_press.clone()); 176 | }); 177 | } else { 178 | node = node.attr("disabled", ""); 179 | } 180 | 181 | node.finish() 182 | } 183 | } 184 | 185 | impl<'a, Message> From<Button<'a, Message>> for Element<'a, Message> 186 | where 187 | Message: 'static + Clone, 188 | { 189 | fn from(button: Button<'a, Message>) -> Element<'a, Message> { 190 | Element::new(button) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/widget/checkbox.rs: -------------------------------------------------------------------------------- 1 | //! Show toggle controls using checkboxes. 2 | use crate::{css, Bus, Css, Element, Length, Widget}; 3 | 4 | pub use iced_style::checkbox::{Style, StyleSheet}; 5 | 6 | use dodrio::bumpalo; 7 | use std::rc::Rc; 8 | 9 | /// A box that can be checked. 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// # use iced_web::Checkbox; 15 | /// 16 | /// pub enum Message { 17 | /// CheckboxToggled(bool), 18 | /// } 19 | /// 20 | /// let is_checked = true; 21 | /// 22 | /// Checkbox::new(is_checked, "Toggle me!", Message::CheckboxToggled); 23 | /// ``` 24 | /// 25 | /// ![Checkbox drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/checkbox.png?raw=true) 26 | #[allow(missing_debug_implementations)] 27 | pub struct Checkbox<'a, Message> { 28 | is_checked: bool, 29 | on_toggle: Rc<dyn Fn(bool) -> Message>, 30 | label: String, 31 | id: Option<String>, 32 | width: Length, 33 | #[allow(dead_code)] 34 | style_sheet: Box<dyn StyleSheet + 'a>, 35 | } 36 | 37 | impl<'a, Message> Checkbox<'a, Message> { 38 | /// Creates a new [`Checkbox`]. 39 | /// 40 | /// It expects: 41 | /// * a boolean describing whether the [`Checkbox`] is checked or not 42 | /// * the label of the [`Checkbox`] 43 | /// * a function that will be called when the [`Checkbox`] is toggled. It 44 | /// will receive the new state of the [`Checkbox`] and must produce a 45 | /// `Message`. 46 | pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self 47 | where 48 | F: 'static + Fn(bool) -> Message, 49 | { 50 | Checkbox { 51 | is_checked, 52 | on_toggle: Rc::new(f), 53 | label: label.into(), 54 | id: None, 55 | width: Length::Shrink, 56 | style_sheet: Default::default(), 57 | } 58 | } 59 | 60 | /// Sets the width of the [`Checkbox`]. 61 | pub fn width(mut self, width: Length) -> Self { 62 | self.width = width; 63 | self 64 | } 65 | 66 | /// Sets the style of the [`Checkbox`]. 67 | pub fn style( 68 | mut self, 69 | style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, 70 | ) -> Self { 71 | self.style_sheet = style_sheet.into(); 72 | self 73 | } 74 | 75 | /// Sets the id of the [`Checkbox`]. 76 | pub fn id(mut self, id: impl Into<String>) -> Self { 77 | self.id = Some(id.into()); 78 | self 79 | } 80 | } 81 | 82 | impl<'a, Message> Widget<Message> for Checkbox<'a, Message> 83 | where 84 | Message: 'static, 85 | { 86 | fn node<'b>( 87 | &self, 88 | bump: &'b bumpalo::Bump, 89 | bus: &Bus<Message>, 90 | style_sheet: &mut Css<'b>, 91 | ) -> dodrio::Node<'b> { 92 | use dodrio::builder::*; 93 | use dodrio::bumpalo::collections::String; 94 | 95 | let checkbox_label = 96 | String::from_str_in(&self.label, bump).into_bump_str(); 97 | 98 | let event_bus = bus.clone(); 99 | let on_toggle = self.on_toggle.clone(); 100 | let is_checked = self.is_checked; 101 | 102 | let row_class = style_sheet.insert(bump, css::Rule::Row); 103 | 104 | let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5)); 105 | 106 | let (label, input) = if let Some(id) = &self.id { 107 | let id = String::from_str_in(id, bump).into_bump_str(); 108 | 109 | (label(bump).attr("for", id), input(bump).attr("id", id)) 110 | } else { 111 | (label(bump), input(bump)) 112 | }; 113 | 114 | label 115 | .attr( 116 | "class", 117 | bumpalo::format!(in bump, "{} {}", row_class, spacing_class) 118 | .into_bump_str(), 119 | ) 120 | .attr( 121 | "style", 122 | bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width)) 123 | .into_bump_str(), 124 | ) 125 | .children(vec![ 126 | // TODO: Checkbox styling 127 | input 128 | .attr("type", "checkbox") 129 | .bool_attr("checked", self.is_checked) 130 | .on("click", move |_root, vdom, _event| { 131 | let msg = on_toggle(!is_checked); 132 | event_bus.publish(msg); 133 | 134 | vdom.schedule_render(); 135 | }) 136 | .finish(), 137 | text(checkbox_label), 138 | ]) 139 | .finish() 140 | } 141 | } 142 | 143 | impl<'a, Message> From<Checkbox<'a, Message>> for Element<'a, Message> 144 | where 145 | Message: 'static, 146 | { 147 | fn from(checkbox: Checkbox<'a, Message>) -> Element<'a, Message> { 148 | Element::new(checkbox) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/widget/column.rs: -------------------------------------------------------------------------------- 1 | use crate::css; 2 | use crate::{Alignment, Bus, Css, Element, Length, Padding, Widget}; 3 | 4 | use dodrio::bumpalo; 5 | use std::u32; 6 | 7 | /// A container that distributes its contents vertically. 8 | /// 9 | /// A [`Column`] will try to fill the horizontal space of its container. 10 | #[allow(missing_debug_implementations)] 11 | pub struct Column<'a, Message> { 12 | spacing: u16, 13 | padding: Padding, 14 | width: Length, 15 | height: Length, 16 | max_width: u32, 17 | max_height: u32, 18 | align_items: Alignment, 19 | children: Vec<Element<'a, Message>>, 20 | } 21 | 22 | impl<'a, Message> Column<'a, Message> { 23 | /// Creates an empty [`Column`]. 24 | pub fn new() -> Self { 25 | Self::with_children(Vec::new()) 26 | } 27 | 28 | /// Creates a [`Column`] with the given elements. 29 | pub fn with_children(children: Vec<Element<'a, Message>>) -> Self { 30 | Column { 31 | spacing: 0, 32 | padding: Padding::ZERO, 33 | width: Length::Fill, 34 | height: Length::Shrink, 35 | max_width: u32::MAX, 36 | max_height: u32::MAX, 37 | align_items: Alignment::Start, 38 | children, 39 | } 40 | } 41 | 42 | /// Sets the vertical spacing _between_ elements. 43 | /// 44 | /// Custom margins per element do not exist in Iced. You should use this 45 | /// method instead! While less flexible, it helps you keep spacing between 46 | /// elements consistent. 47 | pub fn spacing(mut self, units: u16) -> Self { 48 | self.spacing = units; 49 | self 50 | } 51 | 52 | /// Sets the [`Padding`] of the [`Column`]. 53 | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { 54 | self.padding = padding.into(); 55 | self 56 | } 57 | 58 | /// Sets the width of the [`Column`]. 59 | pub fn width(mut self, width: Length) -> Self { 60 | self.width = width; 61 | self 62 | } 63 | 64 | /// Sets the height of the [`Column`]. 65 | pub fn height(mut self, height: Length) -> Self { 66 | self.height = height; 67 | self 68 | } 69 | 70 | /// Sets the maximum width of the [`Column`]. 71 | pub fn max_width(mut self, max_width: u32) -> Self { 72 | self.max_width = max_width; 73 | self 74 | } 75 | 76 | /// Sets the maximum height of the [`Column`] in pixels. 77 | pub fn max_height(mut self, max_height: u32) -> Self { 78 | self.max_height = max_height; 79 | self 80 | } 81 | 82 | /// Sets the horizontal alignment of the contents of the [`Column`] . 83 | pub fn align_items(mut self, align: Alignment) -> Self { 84 | self.align_items = align; 85 | self 86 | } 87 | 88 | /// Adds an element to the [`Column`]. 89 | pub fn push<E>(mut self, child: E) -> Self 90 | where 91 | E: Into<Element<'a, Message>>, 92 | { 93 | self.children.push(child.into()); 94 | self 95 | } 96 | } 97 | 98 | impl<'a, Message> Widget<Message> for Column<'a, Message> { 99 | fn node<'b>( 100 | &self, 101 | bump: &'b bumpalo::Bump, 102 | publish: &Bus<Message>, 103 | style_sheet: &mut Css<'b>, 104 | ) -> dodrio::Node<'b> { 105 | use dodrio::builder::*; 106 | 107 | let children: Vec<_> = self 108 | .children 109 | .iter() 110 | .map(|element| element.widget.node(bump, publish, style_sheet)) 111 | .collect(); 112 | 113 | let column_class = style_sheet.insert(bump, css::Rule::Column); 114 | 115 | let spacing_class = 116 | style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); 117 | 118 | // TODO: Complete styling 119 | div(bump) 120 | .attr( 121 | "class", 122 | bumpalo::format!(in bump, "{} {}", column_class, spacing_class) 123 | .into_bump_str(), 124 | ) 125 | .attr("style", bumpalo::format!( 126 | in bump, 127 | "width: {}; height: {}; max-width: {}; max-height: {}; padding: {}; align-items: {}", 128 | css::length(self.width), 129 | css::length(self.height), 130 | css::max_length(self.max_width), 131 | css::max_length(self.max_height), 132 | css::padding(self.padding), 133 | css::alignment(self.align_items) 134 | ).into_bump_str() 135 | ) 136 | .children(children) 137 | .finish() 138 | } 139 | } 140 | 141 | impl<'a, Message> From<Column<'a, Message>> for Element<'a, Message> 142 | where 143 | Message: 'static, 144 | { 145 | fn from(column: Column<'a, Message>) -> Element<'a, Message> { 146 | Element::new(column) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/widget/container.rs: -------------------------------------------------------------------------------- 1 | //! Decorate content and apply alignment. 2 | use crate::alignment::{self, Alignment}; 3 | use crate::bumpalo; 4 | use crate::css; 5 | use crate::{Bus, Css, Element, Length, Padding, Widget}; 6 | 7 | pub use iced_style::container::{Style, StyleSheet}; 8 | 9 | /// An element decorating some content. 10 | /// 11 | /// It is normally used for alignment purposes. 12 | #[allow(missing_debug_implementations)] 13 | pub struct Container<'a, Message> { 14 | padding: Padding, 15 | width: Length, 16 | height: Length, 17 | max_width: u32, 18 | #[allow(dead_code)] 19 | max_height: u32, 20 | horizontal_alignment: alignment::Horizontal, 21 | vertical_alignment: alignment::Vertical, 22 | style_sheet: Box<dyn StyleSheet + 'a>, 23 | content: Element<'a, Message>, 24 | } 25 | 26 | impl<'a, Message> Container<'a, Message> { 27 | /// Creates an empty [`Container`]. 28 | pub fn new<T>(content: T) -> Self 29 | where 30 | T: Into<Element<'a, Message>>, 31 | { 32 | use std::u32; 33 | 34 | Container { 35 | padding: Padding::ZERO, 36 | width: Length::Shrink, 37 | height: Length::Shrink, 38 | max_width: u32::MAX, 39 | max_height: u32::MAX, 40 | horizontal_alignment: alignment::Horizontal::Left, 41 | vertical_alignment: alignment::Vertical::Top, 42 | style_sheet: Default::default(), 43 | content: content.into(), 44 | } 45 | } 46 | 47 | /// Sets the [`Padding`] of the [`Container`]. 48 | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { 49 | self.padding = padding.into(); 50 | self 51 | } 52 | 53 | /// Sets the width of the [`Container`]. 54 | pub fn width(mut self, width: Length) -> Self { 55 | self.width = width; 56 | self 57 | } 58 | 59 | /// Sets the height of the [`Container`]. 60 | pub fn height(mut self, height: Length) -> Self { 61 | self.height = height; 62 | self 63 | } 64 | 65 | /// Sets the maximum width of the [`Container`]. 66 | pub fn max_width(mut self, max_width: u32) -> Self { 67 | self.max_width = max_width; 68 | self 69 | } 70 | 71 | /// Sets the maximum height of the [`Container`] in pixels. 72 | pub fn max_height(mut self, max_height: u32) -> Self { 73 | self.max_height = max_height; 74 | self 75 | } 76 | 77 | /// Centers the contents in the horizontal axis of the [`Container`]. 78 | pub fn center_x(mut self) -> Self { 79 | self.horizontal_alignment = alignment::Horizontal::Center; 80 | 81 | self 82 | } 83 | 84 | /// Centers the contents in the vertical axis of the [`Container`]. 85 | pub fn center_y(mut self) -> Self { 86 | self.vertical_alignment = alignment::Vertical::Center; 87 | 88 | self 89 | } 90 | 91 | /// Sets the style of the [`Container`]. 92 | pub fn style(mut self, style: impl Into<Box<dyn StyleSheet + 'a>>) -> Self { 93 | self.style_sheet = style.into(); 94 | self 95 | } 96 | } 97 | 98 | impl<'a, Message> Widget<Message> for Container<'a, Message> 99 | where 100 | Message: 'static, 101 | { 102 | fn node<'b>( 103 | &self, 104 | bump: &'b bumpalo::Bump, 105 | bus: &Bus<Message>, 106 | style_sheet: &mut Css<'b>, 107 | ) -> dodrio::Node<'b> { 108 | use dodrio::builder::*; 109 | 110 | let column_class = style_sheet.insert(bump, css::Rule::Column); 111 | 112 | let style = self.style_sheet.style(); 113 | 114 | let node = div(bump) 115 | .attr( 116 | "class", 117 | bumpalo::format!(in bump, "{}", column_class).into_bump_str(), 118 | ) 119 | .attr( 120 | "style", 121 | bumpalo::format!( 122 | in bump, 123 | "width: {}; height: {}; max-width: {}; padding: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px", 124 | css::length(self.width), 125 | css::length(self.height), 126 | css::max_length(self.max_width), 127 | css::padding(self.padding), 128 | css::alignment(Alignment::from(self.horizontal_alignment)), 129 | css::alignment(Alignment::from(self.vertical_alignment)), 130 | style.background.map(css::background).unwrap_or(String::from("initial")), 131 | style.text_color.map(css::color).unwrap_or(String::from("inherit")), 132 | style.border_width, 133 | css::color(style.border_color), 134 | style.border_radius 135 | ) 136 | .into_bump_str(), 137 | ) 138 | .children(vec![self.content.node(bump, bus, style_sheet)]); 139 | 140 | // TODO: Complete styling 141 | 142 | node.finish() 143 | } 144 | } 145 | 146 | impl<'a, Message> From<Container<'a, Message>> for Element<'a, Message> 147 | where 148 | Message: 'static, 149 | { 150 | fn from(container: Container<'a, Message>) -> Element<'a, Message> { 151 | Element::new(container) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/widget/image.rs: -------------------------------------------------------------------------------- 1 | //! Display images in your user interface. 2 | use crate::{Bus, Css, Element, Hasher, Length, Widget}; 3 | 4 | use dodrio::bumpalo; 5 | use std::{ 6 | hash::{Hash, Hasher as _}, 7 | path::PathBuf, 8 | sync::Arc, 9 | }; 10 | 11 | /// A frame that displays an image while keeping aspect ratio. 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// # use iced_web::Image; 17 | /// 18 | /// let image = Image::new("resources/ferris.png"); 19 | /// ``` 20 | #[derive(Debug)] 21 | pub struct Image { 22 | /// The image path 23 | pub handle: Handle, 24 | 25 | /// The alt text of the image 26 | pub alt: String, 27 | 28 | /// The width of the image 29 | pub width: Length, 30 | 31 | /// The height of the image 32 | pub height: Length, 33 | } 34 | 35 | impl Image { 36 | /// Creates a new [`Image`] with the given path. 37 | pub fn new<T: Into<Handle>>(handle: T) -> Self { 38 | Image { 39 | handle: handle.into(), 40 | alt: Default::default(), 41 | width: Length::Shrink, 42 | height: Length::Shrink, 43 | } 44 | } 45 | 46 | /// Sets the width of the [`Image`] boundaries. 47 | pub fn width(mut self, width: Length) -> Self { 48 | self.width = width; 49 | self 50 | } 51 | 52 | /// Sets the height of the [`Image`] boundaries. 53 | pub fn height(mut self, height: Length) -> Self { 54 | self.height = height; 55 | self 56 | } 57 | 58 | /// Sets the alt text of the [`Image`]. 59 | pub fn alt(mut self, alt: impl Into<String>) -> Self { 60 | self.alt = alt.into(); 61 | self 62 | } 63 | } 64 | 65 | impl<Message> Widget<Message> for Image { 66 | fn node<'b>( 67 | &self, 68 | bump: &'b bumpalo::Bump, 69 | _bus: &Bus<Message>, 70 | _style_sheet: &mut Css<'b>, 71 | ) -> dodrio::Node<'b> { 72 | use dodrio::builder::*; 73 | use dodrio::bumpalo::collections::String; 74 | 75 | let src = match self.handle.data.as_ref() { 76 | Data::Path(path) => { 77 | String::from_str_in(path.to_str().unwrap_or(""), bump) 78 | } 79 | Data::Bytes(bytes) => { 80 | // The web is able to infer the kind of image, so we don't have to add a dependency on image-rs to guess the mime type. 81 | bumpalo::format!(in bump, "data:;base64,{}", base64::encode(bytes)) 82 | }, 83 | } 84 | .into_bump_str(); 85 | 86 | let alt = String::from_str_in(&self.alt, bump).into_bump_str(); 87 | 88 | let mut image = img(bump).attr("src", src).attr("alt", alt); 89 | 90 | match self.width { 91 | Length::Shrink => {} 92 | Length::Fill | Length::FillPortion(_) => { 93 | image = image.attr("width", "100%"); 94 | } 95 | Length::Units(px) => { 96 | image = image.attr( 97 | "width", 98 | bumpalo::format!(in bump, "{}px", px).into_bump_str(), 99 | ); 100 | } 101 | } 102 | 103 | // TODO: Complete styling 104 | 105 | image.finish() 106 | } 107 | } 108 | 109 | impl<'a, Message> From<Image> for Element<'a, Message> { 110 | fn from(image: Image) -> Element<'a, Message> { 111 | Element::new(image) 112 | } 113 | } 114 | 115 | /// An [`Image`] handle. 116 | #[derive(Debug, Clone)] 117 | pub struct Handle { 118 | id: u64, 119 | data: Arc<Data>, 120 | } 121 | 122 | impl Handle { 123 | /// Creates an image [`Handle`] pointing to the image of the given path. 124 | pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { 125 | Self::from_data(Data::Path(path.into())) 126 | } 127 | 128 | /// Creates an image [`Handle`] containing the image data directly. 129 | /// 130 | /// This is useful if you already have your image loaded in-memory, maybe 131 | /// because you downloaded or generated it procedurally. 132 | pub fn from_memory(bytes: Vec<u8>) -> Handle { 133 | Self::from_data(Data::Bytes(bytes)) 134 | } 135 | 136 | fn from_data(data: Data) -> Handle { 137 | let mut hasher = Hasher::default(); 138 | data.hash(&mut hasher); 139 | 140 | Handle { 141 | id: hasher.finish(), 142 | data: Arc::new(data), 143 | } 144 | } 145 | 146 | /// Returns the unique identifier of the [`Handle`]. 147 | pub fn id(&self) -> u64 { 148 | self.id 149 | } 150 | 151 | /// Returns a reference to the image [`Data`]. 152 | pub fn data(&self) -> &Data { 153 | &self.data 154 | } 155 | } 156 | 157 | impl From<String> for Handle { 158 | fn from(path: String) -> Handle { 159 | Handle::from_path(path) 160 | } 161 | } 162 | 163 | impl From<&str> for Handle { 164 | fn from(path: &str) -> Handle { 165 | Handle::from_path(path) 166 | } 167 | } 168 | 169 | /// The data of an [`Image`]. 170 | #[derive(Clone, Hash)] 171 | pub enum Data { 172 | /// A remote image 173 | Path(PathBuf), 174 | 175 | /// In-memory data 176 | Bytes(Vec<u8>), 177 | } 178 | 179 | impl std::fmt::Debug for Data { 180 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 181 | match self { 182 | Data::Path(path) => write!(f, "Path({:?})", path), 183 | Data::Bytes(_) => write!(f, "Bytes(...)"), 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/widget/progress_bar.rs: -------------------------------------------------------------------------------- 1 | //! Provide progress feedback to your users. 2 | use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; 3 | 4 | pub use iced_style::progress_bar::{Style, StyleSheet}; 5 | 6 | use std::ops::RangeInclusive; 7 | 8 | /// A bar that displays progress. 9 | /// 10 | /// # Example 11 | /// ``` 12 | /// use iced_web::ProgressBar; 13 | /// 14 | /// let value = 50.0; 15 | /// 16 | /// ProgressBar::new(0.0..=100.0, value); 17 | /// ``` 18 | /// 19 | /// ![Progress bar](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) 20 | #[allow(missing_debug_implementations)] 21 | pub struct ProgressBar<'a> { 22 | range: RangeInclusive<f32>, 23 | value: f32, 24 | width: Length, 25 | height: Option<Length>, 26 | style: Box<dyn StyleSheet + 'a>, 27 | } 28 | 29 | impl<'a> ProgressBar<'a> { 30 | /// Creates a new [`ProgressBar`]. 31 | /// 32 | /// It expects: 33 | /// * an inclusive range of possible values 34 | /// * the current value of the [`ProgressBar`] 35 | pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { 36 | ProgressBar { 37 | value: value.max(*range.start()).min(*range.end()), 38 | range, 39 | width: Length::Fill, 40 | height: None, 41 | style: Default::default(), 42 | } 43 | } 44 | 45 | /// Sets the width of the [`ProgressBar`]. 46 | pub fn width(mut self, width: Length) -> Self { 47 | self.width = width; 48 | self 49 | } 50 | 51 | /// Sets the height of the [`ProgressBar`]. 52 | pub fn height(mut self, height: Length) -> Self { 53 | self.height = Some(height); 54 | self 55 | } 56 | 57 | /// Sets the style of the [`ProgressBar`]. 58 | pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { 59 | self.style = style.into(); 60 | self 61 | } 62 | } 63 | 64 | impl<'a, Message> Widget<Message> for ProgressBar<'a> { 65 | fn node<'b>( 66 | &self, 67 | bump: &'b bumpalo::Bump, 68 | _bus: &Bus<Message>, 69 | _style_sheet: &mut Css<'b>, 70 | ) -> dodrio::Node<'b> { 71 | use dodrio::builder::*; 72 | 73 | let (range_start, range_end) = self.range.clone().into_inner(); 74 | let amount_filled = 75 | (self.value - range_start) / (range_end - range_start).max(1.0); 76 | 77 | let style = self.style.style(); 78 | 79 | let bar = div(bump) 80 | .attr( 81 | "style", 82 | bumpalo::format!( 83 | in bump, 84 | "width: {}%; height: 100%; background: {}", 85 | amount_filled * 100.0, 86 | css::background(style.bar) 87 | ) 88 | .into_bump_str(), 89 | ) 90 | .finish(); 91 | 92 | let node = div(bump).attr( 93 | "style", 94 | bumpalo::format!( 95 | in bump, 96 | "width: {}; height: {}; background: {}; border-radius: {}px; overflow: hidden;", 97 | css::length(self.width), 98 | css::length(self.height.unwrap_or(Length::Units(30))), 99 | css::background(style.background), 100 | style.border_radius 101 | ) 102 | .into_bump_str(), 103 | ).children(vec![bar]); 104 | 105 | node.finish() 106 | } 107 | } 108 | 109 | impl<'a, Message> From<ProgressBar<'a>> for Element<'a, Message> 110 | where 111 | Message: 'static, 112 | { 113 | fn from(container: ProgressBar<'a>) -> Element<'a, Message> { 114 | Element::new(container) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/widget/radio.rs: -------------------------------------------------------------------------------- 1 | //! Create choices using radio buttons. 2 | use crate::{Bus, Css, Element, Widget}; 3 | 4 | pub use iced_style::radio::{Style, StyleSheet}; 5 | 6 | use dodrio::bumpalo; 7 | 8 | /// A circular button representing a choice. 9 | /// 10 | /// # Example 11 | /// ``` 12 | /// # use iced_web::Radio; 13 | /// 14 | /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] 15 | /// pub enum Choice { 16 | /// A, 17 | /// B, 18 | /// } 19 | /// 20 | /// #[derive(Debug, Clone, Copy)] 21 | /// pub enum Message { 22 | /// RadioSelected(Choice), 23 | /// } 24 | /// 25 | /// let selected_choice = Some(Choice::A); 26 | /// 27 | /// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected); 28 | /// 29 | /// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected); 30 | /// ``` 31 | /// 32 | /// ![Radio buttons drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/radio.png?raw=true) 33 | #[allow(missing_debug_implementations)] 34 | pub struct Radio<'a, Message> { 35 | is_selected: bool, 36 | on_click: Message, 37 | label: String, 38 | id: Option<String>, 39 | name: Option<String>, 40 | #[allow(dead_code)] 41 | style_sheet: Box<dyn StyleSheet + 'a>, 42 | } 43 | 44 | impl<'a, Message> Radio<'a, Message> { 45 | /// Creates a new [`Radio`] button. 46 | /// 47 | /// It expects: 48 | /// * the value related to the [`Radio`] button 49 | /// * the label of the [`Radio`] button 50 | /// * the current selected value 51 | /// * a function that will be called when the [`Radio`] is selected. It 52 | /// receives the value of the radio and must produce a `Message`. 53 | pub fn new<F, V>( 54 | value: V, 55 | label: impl Into<String>, 56 | selected: Option<V>, 57 | f: F, 58 | ) -> Self 59 | where 60 | V: Eq + Copy, 61 | F: 'static + Fn(V) -> Message, 62 | { 63 | Radio { 64 | is_selected: Some(value) == selected, 65 | on_click: f(value), 66 | label: label.into(), 67 | id: None, 68 | name: None, 69 | style_sheet: Default::default(), 70 | } 71 | } 72 | 73 | /// Sets the style of the [`Radio`] button. 74 | pub fn style( 75 | mut self, 76 | style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, 77 | ) -> Self { 78 | self.style_sheet = style_sheet.into(); 79 | self 80 | } 81 | 82 | /// Sets the name attribute of the [`Radio`] button. 83 | pub fn name(mut self, name: impl Into<String>) -> Self { 84 | self.name = Some(name.into()); 85 | self 86 | } 87 | 88 | /// Sets the id of the [`Radio`] button. 89 | pub fn id(mut self, id: impl Into<String>) -> Self { 90 | self.id = Some(id.into()); 91 | self 92 | } 93 | } 94 | 95 | impl<'a, Message> Widget<Message> for Radio<'a, Message> 96 | where 97 | Message: 'static + Clone, 98 | { 99 | fn node<'b>( 100 | &self, 101 | bump: &'b bumpalo::Bump, 102 | bus: &Bus<Message>, 103 | _style_sheet: &mut Css<'b>, 104 | ) -> dodrio::Node<'b> { 105 | use dodrio::builder::*; 106 | use dodrio::bumpalo::collections::String; 107 | 108 | let radio_label = 109 | String::from_str_in(&self.label, bump).into_bump_str(); 110 | 111 | let event_bus = bus.clone(); 112 | let on_click = self.on_click.clone(); 113 | 114 | let (label, input) = if let Some(id) = &self.id { 115 | let id = String::from_str_in(id, bump).into_bump_str(); 116 | 117 | (label(bump).attr("for", id), input(bump).attr("id", id)) 118 | } else { 119 | (label(bump), input(bump)) 120 | }; 121 | 122 | let input = if let Some(name) = &self.name { 123 | let name = String::from_str_in(name, bump).into_bump_str(); 124 | 125 | dodrio::builder::input(bump).attr("name", name) 126 | } else { 127 | input 128 | }; 129 | 130 | // TODO: Complete styling 131 | label 132 | .attr("style", "display: block; font-size: 20px") 133 | .children(vec![ 134 | input 135 | .attr("type", "radio") 136 | .attr("style", "margin-right: 10px") 137 | .bool_attr("checked", self.is_selected) 138 | .on("click", move |_root, _vdom, _event| { 139 | event_bus.publish(on_click.clone()); 140 | }) 141 | .finish(), 142 | text(radio_label), 143 | ]) 144 | .finish() 145 | } 146 | } 147 | 148 | impl<'a, Message> From<Radio<'a, Message>> for Element<'a, Message> 149 | where 150 | Message: 'static + Clone, 151 | { 152 | fn from(radio: Radio<'a, Message>) -> Element<'a, Message> { 153 | Element::new(radio) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/widget/row.rs: -------------------------------------------------------------------------------- 1 | use crate::css; 2 | use crate::{Alignment, Bus, Css, Element, Length, Padding, Widget}; 3 | 4 | use dodrio::bumpalo; 5 | use std::u32; 6 | 7 | /// A container that distributes its contents horizontally. 8 | /// 9 | /// A [`Row`] will try to fill the horizontal space of its container. 10 | #[allow(missing_debug_implementations)] 11 | pub struct Row<'a, Message> { 12 | spacing: u16, 13 | padding: Padding, 14 | width: Length, 15 | height: Length, 16 | max_width: u32, 17 | max_height: u32, 18 | align_items: Alignment, 19 | children: Vec<Element<'a, Message>>, 20 | } 21 | 22 | impl<'a, Message> Row<'a, Message> { 23 | /// Creates an empty [`Row`]. 24 | pub fn new() -> Self { 25 | Self::with_children(Vec::new()) 26 | } 27 | 28 | /// Creates a [`Row`] with the given elements. 29 | pub fn with_children(children: Vec<Element<'a, Message>>) -> Self { 30 | Row { 31 | spacing: 0, 32 | padding: Padding::ZERO, 33 | width: Length::Fill, 34 | height: Length::Shrink, 35 | max_width: u32::MAX, 36 | max_height: u32::MAX, 37 | align_items: Alignment::Start, 38 | children, 39 | } 40 | } 41 | 42 | /// Sets the horizontal spacing _between_ elements. 43 | /// 44 | /// Custom margins per element do not exist in Iced. You should use this 45 | /// method instead! While less flexible, it helps you keep spacing between 46 | /// elements consistent. 47 | pub fn spacing(mut self, units: u16) -> Self { 48 | self.spacing = units; 49 | self 50 | } 51 | 52 | /// Sets the [`Padding`] of the [`Row`]. 53 | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { 54 | self.padding = padding.into(); 55 | self 56 | } 57 | 58 | /// Sets the width of the [`Row`]. 59 | pub fn width(mut self, width: Length) -> Self { 60 | self.width = width; 61 | self 62 | } 63 | 64 | /// Sets the height of the [`Row`]. 65 | pub fn height(mut self, height: Length) -> Self { 66 | self.height = height; 67 | self 68 | } 69 | 70 | /// Sets the maximum width of the [`Row`]. 71 | pub fn max_width(mut self, max_width: u32) -> Self { 72 | self.max_width = max_width; 73 | self 74 | } 75 | 76 | /// Sets the maximum height of the [`Row`]. 77 | pub fn max_height(mut self, max_height: u32) -> Self { 78 | self.max_height = max_height; 79 | self 80 | } 81 | 82 | /// Sets the vertical alignment of the contents of the [`Row`] . 83 | pub fn align_items(mut self, align: Alignment) -> Self { 84 | self.align_items = align; 85 | self 86 | } 87 | 88 | /// Adds an [`Element`] to the [`Row`]. 89 | pub fn push<E>(mut self, child: E) -> Self 90 | where 91 | E: Into<Element<'a, Message>>, 92 | { 93 | self.children.push(child.into()); 94 | self 95 | } 96 | } 97 | 98 | impl<'a, Message> Widget<Message> for Row<'a, Message> { 99 | fn node<'b>( 100 | &self, 101 | bump: &'b bumpalo::Bump, 102 | publish: &Bus<Message>, 103 | style_sheet: &mut Css<'b>, 104 | ) -> dodrio::Node<'b> { 105 | use dodrio::builder::*; 106 | 107 | let children: Vec<_> = self 108 | .children 109 | .iter() 110 | .map(|element| element.widget.node(bump, publish, style_sheet)) 111 | .collect(); 112 | 113 | let row_class = style_sheet.insert(bump, css::Rule::Row); 114 | 115 | let spacing_class = 116 | style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); 117 | 118 | // TODO: Complete styling 119 | div(bump) 120 | .attr( 121 | "class", 122 | bumpalo::format!(in bump, "{} {}", row_class, spacing_class) 123 | .into_bump_str(), 124 | ) 125 | .attr("style", bumpalo::format!( 126 | in bump, 127 | "width: {}; height: {}; max-width: {}; max-height: {}; padding: {}; align-items: {}", 128 | css::length(self.width), 129 | css::length(self.height), 130 | css::max_length(self.max_width), 131 | css::max_length(self.max_height), 132 | css::padding(self.padding), 133 | css::alignment(self.align_items) 134 | ).into_bump_str() 135 | ) 136 | .children(children) 137 | .finish() 138 | } 139 | } 140 | 141 | impl<'a, Message> From<Row<'a, Message>> for Element<'a, Message> 142 | where 143 | Message: 'static, 144 | { 145 | fn from(column: Row<'a, Message>) -> Element<'a, Message> { 146 | Element::new(column) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/widget/scrollable.rs: -------------------------------------------------------------------------------- 1 | //! Navigate an endless amount of content with a scrollbar. 2 | use crate::bumpalo; 3 | use crate::css; 4 | use crate::{Alignment, Bus, Column, Css, Element, Length, Padding, Widget}; 5 | 6 | pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; 7 | 8 | /// A widget that can vertically display an infinite amount of content with a 9 | /// scrollbar. 10 | #[allow(missing_debug_implementations)] 11 | pub struct Scrollable<'a, Message> { 12 | width: Length, 13 | height: Length, 14 | max_height: u32, 15 | content: Column<'a, Message>, 16 | #[allow(dead_code)] 17 | style_sheet: Box<dyn StyleSheet + 'a>, 18 | } 19 | 20 | impl<'a, Message> Scrollable<'a, Message> { 21 | /// Creates a new [`Scrollable`] with the given [`State`]. 22 | pub fn new(_state: &'a mut State) -> Self { 23 | use std::u32; 24 | 25 | Scrollable { 26 | width: Length::Fill, 27 | height: Length::Shrink, 28 | max_height: u32::MAX, 29 | content: Column::new(), 30 | style_sheet: Default::default(), 31 | } 32 | } 33 | 34 | /// Sets the vertical spacing _between_ elements. 35 | /// 36 | /// Custom margins per element do not exist in Iced. You should use this 37 | /// method instead! While less flexible, it helps you keep spacing between 38 | /// elements consistent. 39 | pub fn spacing(mut self, units: u16) -> Self { 40 | self.content = self.content.spacing(units); 41 | self 42 | } 43 | 44 | /// Sets the [`Padding`] of the [`Scrollable`]. 45 | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { 46 | self.content = self.content.padding(padding); 47 | self 48 | } 49 | 50 | /// Sets the width of the [`Scrollable`]. 51 | pub fn width(mut self, width: Length) -> Self { 52 | self.width = width; 53 | self 54 | } 55 | 56 | /// Sets the height of the [`Scrollable`]. 57 | pub fn height(mut self, height: Length) -> Self { 58 | self.height = height; 59 | self 60 | } 61 | 62 | /// Sets the maximum width of the [`Scrollable`]. 63 | pub fn max_width(mut self, max_width: u32) -> Self { 64 | self.content = self.content.max_width(max_width); 65 | self 66 | } 67 | 68 | /// Sets the maximum height of the [`Scrollable`] in pixels. 69 | pub fn max_height(mut self, max_height: u32) -> Self { 70 | self.max_height = max_height; 71 | self 72 | } 73 | 74 | /// Sets the horizontal alignment of the contents of the [`Scrollable`] . 75 | pub fn align_items(mut self, align_items: Alignment) -> Self { 76 | self.content = self.content.align_items(align_items); 77 | self 78 | } 79 | 80 | /// Sets the style of the [`Scrollable`] . 81 | pub fn style( 82 | mut self, 83 | style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, 84 | ) -> Self { 85 | self.style_sheet = style_sheet.into(); 86 | self 87 | } 88 | 89 | /// Adds an element to the [`Scrollable`]. 90 | pub fn push<E>(mut self, child: E) -> Self 91 | where 92 | E: Into<Element<'a, Message>>, 93 | { 94 | self.content = self.content.push(child); 95 | self 96 | } 97 | } 98 | 99 | impl<'a, Message> Widget<Message> for Scrollable<'a, Message> 100 | where 101 | Message: 'static, 102 | { 103 | fn node<'b>( 104 | &self, 105 | bump: &'b bumpalo::Bump, 106 | bus: &Bus<Message>, 107 | style_sheet: &mut Css<'b>, 108 | ) -> dodrio::Node<'b> { 109 | use dodrio::builder::*; 110 | 111 | let width = css::length(self.width); 112 | let height = css::length(self.height); 113 | 114 | // TODO: Scrollbar styling 115 | 116 | let node = div(bump) 117 | .attr( 118 | "style", 119 | bumpalo::format!( 120 | in bump, 121 | "width: {}; height: {}; max-height: {}px; overflow: auto", 122 | width, 123 | height, 124 | self.max_height 125 | ) 126 | .into_bump_str(), 127 | ) 128 | .children(vec![self.content.node(bump, bus, style_sheet)]); 129 | 130 | node.finish() 131 | } 132 | } 133 | 134 | impl<'a, Message> From<Scrollable<'a, Message>> for Element<'a, Message> 135 | where 136 | Message: 'static, 137 | { 138 | fn from(scrollable: Scrollable<'a, Message>) -> Element<'a, Message> { 139 | Element::new(scrollable) 140 | } 141 | } 142 | 143 | /// The local state of a [`Scrollable`]. 144 | #[derive(Debug, Clone, Copy, Default)] 145 | pub struct State; 146 | 147 | impl State { 148 | /// Creates a new [`State`] with the scrollbar located at the top. 149 | pub fn new() -> Self { 150 | State::default() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/widget/slider.rs: -------------------------------------------------------------------------------- 1 | //! Display an interactive selector of a single value from a range of values. 2 | //! 3 | //! A [`Slider`] has some local [`State`]. 4 | use crate::{Bus, Css, Element, Length, Widget}; 5 | 6 | pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; 7 | 8 | use dodrio::bumpalo; 9 | use std::{ops::RangeInclusive, rc::Rc}; 10 | 11 | /// An horizontal bar and a handle that selects a single value from a range of 12 | /// values. 13 | /// 14 | /// A [`Slider`] will try to fill the horizontal space of its container. 15 | /// 16 | /// The [`Slider`] range of numeric values is generic and its step size defaults 17 | /// to 1 unit. 18 | /// 19 | /// # Example 20 | /// ``` 21 | /// # use iced_web::{slider, Slider}; 22 | /// # 23 | /// pub enum Message { 24 | /// SliderChanged(f32), 25 | /// } 26 | /// 27 | /// let state = &mut slider::State::new(); 28 | /// let value = 50.0; 29 | /// 30 | /// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); 31 | /// ``` 32 | /// 33 | /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) 34 | #[allow(missing_debug_implementations)] 35 | pub struct Slider<'a, T, Message> { 36 | _state: &'a mut State, 37 | range: RangeInclusive<T>, 38 | step: T, 39 | value: T, 40 | on_change: Rc<Box<dyn Fn(T) -> Message>>, 41 | #[allow(dead_code)] 42 | width: Length, 43 | #[allow(dead_code)] 44 | style_sheet: Box<dyn StyleSheet + 'a>, 45 | } 46 | 47 | impl<'a, T, Message> Slider<'a, T, Message> 48 | where 49 | T: Copy + From<u8> + std::cmp::PartialOrd, 50 | { 51 | /// Creates a new [`Slider`]. 52 | /// 53 | /// It expects: 54 | /// * the local [`State`] of the [`Slider`] 55 | /// * an inclusive range of possible values 56 | /// * the current value of the [`Slider`] 57 | /// * a function that will be called when the [`Slider`] is dragged. 58 | /// It receives the new value of the [`Slider`] and must produce a 59 | /// `Message`. 60 | pub fn new<F>( 61 | state: &'a mut State, 62 | range: RangeInclusive<T>, 63 | value: T, 64 | on_change: F, 65 | ) -> Self 66 | where 67 | F: 'static + Fn(T) -> Message, 68 | { 69 | let value = if value >= *range.start() { 70 | value 71 | } else { 72 | *range.start() 73 | }; 74 | 75 | let value = if value <= *range.end() { 76 | value 77 | } else { 78 | *range.end() 79 | }; 80 | 81 | Slider { 82 | _state: state, 83 | value, 84 | range, 85 | step: T::from(1), 86 | on_change: Rc::new(Box::new(on_change)), 87 | width: Length::Fill, 88 | style_sheet: Default::default(), 89 | } 90 | } 91 | 92 | /// Sets the width of the [`Slider`]. 93 | pub fn width(mut self, width: Length) -> Self { 94 | self.width = width; 95 | self 96 | } 97 | 98 | /// Sets the style of the [`Slider`]. 99 | pub fn style( 100 | mut self, 101 | style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, 102 | ) -> Self { 103 | self.style_sheet = style_sheet.into(); 104 | self 105 | } 106 | 107 | /// Sets the step size of the [`Slider`]. 108 | pub fn step(mut self, step: T) -> Self { 109 | self.step = step; 110 | self 111 | } 112 | } 113 | 114 | impl<'a, T, Message> Widget<Message> for Slider<'a, T, Message> 115 | where 116 | T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, 117 | Message: 'static, 118 | { 119 | fn node<'b>( 120 | &self, 121 | bump: &'b bumpalo::Bump, 122 | bus: &Bus<Message>, 123 | _style_sheet: &mut Css<'b>, 124 | ) -> dodrio::Node<'b> { 125 | use dodrio::builder::*; 126 | use wasm_bindgen::JsCast; 127 | 128 | let (start, end) = self.range.clone().into_inner(); 129 | 130 | let min = bumpalo::format!(in bump, "{}", start.into()); 131 | let max = bumpalo::format!(in bump, "{}", end.into()); 132 | let value = bumpalo::format!(in bump, "{}", self.value.into()); 133 | let step = bumpalo::format!(in bump, "{}", self.step.into()); 134 | 135 | let on_change = self.on_change.clone(); 136 | let event_bus = bus.clone(); 137 | 138 | // TODO: Styling 139 | input(bump) 140 | .attr("type", "range") 141 | .attr("step", step.into_bump_str()) 142 | .attr("min", min.into_bump_str()) 143 | .attr("max", max.into_bump_str()) 144 | .attr("value", value.into_bump_str()) 145 | .attr("style", "width: 100%") 146 | .on("input", move |_root, _vdom, event| { 147 | let slider = match event.target().and_then(|t| { 148 | t.dyn_into::<web_sys::HtmlInputElement>().ok() 149 | }) { 150 | None => return, 151 | Some(slider) => slider, 152 | }; 153 | 154 | if let Ok(value) = slider.value().parse::<f64>() { 155 | if let Some(value) = T::from_f64(value) { 156 | event_bus.publish(on_change(value)); 157 | } 158 | } 159 | }) 160 | .finish() 161 | } 162 | } 163 | 164 | impl<'a, T, Message> From<Slider<'a, T, Message>> for Element<'a, Message> 165 | where 166 | T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, 167 | Message: 'static, 168 | { 169 | fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message> { 170 | Element::new(slider) 171 | } 172 | } 173 | 174 | /// The local state of a [`Slider`]. 175 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 176 | pub struct State; 177 | 178 | impl State { 179 | /// Creates a new [`State`]. 180 | pub fn new() -> Self { 181 | Self 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/widget/space.rs: -------------------------------------------------------------------------------- 1 | use crate::{css, Bus, Css, Element, Length, Widget}; 2 | use dodrio::bumpalo; 3 | 4 | /// An amount of empty space. 5 | /// 6 | /// It can be useful if you want to fill some space with nothing. 7 | #[derive(Debug)] 8 | pub struct Space { 9 | width: Length, 10 | height: Length, 11 | } 12 | 13 | impl Space { 14 | /// Creates an amount of empty [`Space`] with the given width and height. 15 | pub fn new(width: Length, height: Length) -> Self { 16 | Space { width, height } 17 | } 18 | 19 | /// Creates an amount of horizontal [`Space`]. 20 | pub fn with_width(width: Length) -> Self { 21 | Space { 22 | width, 23 | height: Length::Shrink, 24 | } 25 | } 26 | 27 | /// Creates an amount of vertical [`Space`]. 28 | pub fn with_height(height: Length) -> Self { 29 | Space { 30 | width: Length::Shrink, 31 | height, 32 | } 33 | } 34 | } 35 | 36 | impl<'a, Message> Widget<Message> for Space { 37 | fn node<'b>( 38 | &self, 39 | bump: &'b bumpalo::Bump, 40 | _publish: &Bus<Message>, 41 | _css: &mut Css<'b>, 42 | ) -> dodrio::Node<'b> { 43 | use dodrio::builder::*; 44 | 45 | let width = css::length(self.width); 46 | let height = css::length(self.height); 47 | 48 | let style = bumpalo::format!( 49 | in bump, 50 | "width: {}; height: {};", 51 | width, 52 | height 53 | ); 54 | 55 | div(bump).attr("style", style.into_bump_str()).finish() 56 | } 57 | } 58 | 59 | impl<'a, Message> From<Space> for Element<'a, Message> { 60 | fn from(space: Space) -> Element<'a, Message> { 61 | Element::new(space) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/widget/text.rs: -------------------------------------------------------------------------------- 1 | use crate::alignment; 2 | use crate::css; 3 | use crate::{Bus, Color, Css, Element, Font, Length, Widget}; 4 | use dodrio::bumpalo; 5 | 6 | /// A paragraph of text. 7 | /// 8 | /// # Example 9 | /// 10 | /// ``` 11 | /// # use iced_web::Text; 12 | /// 13 | /// Text::new("I <3 iced!") 14 | /// .size(40); 15 | /// ``` 16 | #[derive(Debug, Clone)] 17 | pub struct Text { 18 | content: String, 19 | size: Option<u16>, 20 | color: Option<Color>, 21 | font: Font, 22 | width: Length, 23 | height: Length, 24 | horizontal_alignment: alignment::Horizontal, 25 | vertical_alignment: alignment::Vertical, 26 | } 27 | 28 | impl Text { 29 | /// Create a new fragment of [`Text`] with the given contents. 30 | pub fn new<T: Into<String>>(label: T) -> Self { 31 | Text { 32 | content: label.into(), 33 | size: None, 34 | color: None, 35 | font: Font::Default, 36 | width: Length::Shrink, 37 | height: Length::Shrink, 38 | horizontal_alignment: alignment::Horizontal::Left, 39 | vertical_alignment: alignment::Vertical::Top, 40 | } 41 | } 42 | 43 | /// Sets the size of the [`Text`]. 44 | pub fn size(mut self, size: u16) -> Self { 45 | self.size = Some(size); 46 | self 47 | } 48 | 49 | /// Sets the [`Color`] of the [`Text`]. 50 | pub fn color<C: Into<Color>>(mut self, color: C) -> Self { 51 | self.color = Some(color.into()); 52 | self 53 | } 54 | 55 | /// Sets the [`Font`] of the [`Text`]. 56 | pub fn font(mut self, font: Font) -> Self { 57 | self.font = font; 58 | self 59 | } 60 | 61 | /// Sets the width of the [`Text`] boundaries. 62 | pub fn width(mut self, width: Length) -> Self { 63 | self.width = width; 64 | self 65 | } 66 | 67 | /// Sets the height of the [`Text`] boundaries. 68 | pub fn height(mut self, height: Length) -> Self { 69 | self.height = height; 70 | self 71 | } 72 | 73 | /// Sets the [`HorizontalAlignment`] of the [`Text`]. 74 | pub fn horizontal_alignment( 75 | mut self, 76 | alignment: alignment::Horizontal, 77 | ) -> Self { 78 | self.horizontal_alignment = alignment; 79 | self 80 | } 81 | 82 | /// Sets the [`VerticalAlignment`] of the [`Text`]. 83 | pub fn vertical_alignment( 84 | mut self, 85 | alignment: alignment::Vertical, 86 | ) -> Self { 87 | self.vertical_alignment = alignment; 88 | self 89 | } 90 | } 91 | 92 | impl<'a, Message> Widget<Message> for Text { 93 | fn node<'b>( 94 | &self, 95 | bump: &'b bumpalo::Bump, 96 | _publish: &Bus<Message>, 97 | _style_sheet: &mut Css<'b>, 98 | ) -> dodrio::Node<'b> { 99 | use dodrio::builder::*; 100 | 101 | let content = { 102 | use dodrio::bumpalo::collections::String; 103 | 104 | String::from_str_in(&self.content, bump) 105 | }; 106 | 107 | let color = self 108 | .color 109 | .map(css::color) 110 | .unwrap_or(String::from("inherit")); 111 | 112 | let width = css::length(self.width); 113 | let height = css::length(self.height); 114 | 115 | let text_align = match self.horizontal_alignment { 116 | alignment::Horizontal::Left => "left", 117 | alignment::Horizontal::Center => "center", 118 | alignment::Horizontal::Right => "right", 119 | }; 120 | 121 | let style = bumpalo::format!( 122 | in bump, 123 | "width: {}; height: {}; font-size: {}px; color: {}; \ 124 | text-align: {}; font-family: {}", 125 | width, 126 | height, 127 | self.size.unwrap_or(20), 128 | color, 129 | text_align, 130 | match self.font { 131 | Font::Default => "inherit", 132 | Font::External { name, .. } => name, 133 | } 134 | ); 135 | 136 | // TODO: Complete styling 137 | p(bump) 138 | .attr("style", style.into_bump_str()) 139 | .children(vec![text(content.into_bump_str())]) 140 | .finish() 141 | } 142 | } 143 | 144 | impl<'a, Message> From<Text> for Element<'a, Message> { 145 | fn from(text: Text) -> Element<'a, Message> { 146 | Element::new(text) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/widget/text_input.rs: -------------------------------------------------------------------------------- 1 | //! Display fields that can be filled with text. 2 | //! 3 | //! A [`TextInput`] has some local [`State`]. 4 | use crate::{bumpalo, css, Bus, Css, Element, Length, Padding, Widget}; 5 | 6 | pub use iced_style::text_input::{Style, StyleSheet}; 7 | 8 | use std::{rc::Rc, u32}; 9 | 10 | /// A field that can be filled with text. 11 | /// 12 | /// # Example 13 | /// ``` 14 | /// # use iced_web::{text_input, TextInput}; 15 | /// # 16 | /// enum Message { 17 | /// TextInputChanged(String), 18 | /// } 19 | /// 20 | /// let mut state = text_input::State::new(); 21 | /// let value = "Some text"; 22 | /// 23 | /// let input = TextInput::new( 24 | /// &mut state, 25 | /// "This is the placeholder...", 26 | /// value, 27 | /// Message::TextInputChanged, 28 | /// ); 29 | /// ``` 30 | #[allow(missing_debug_implementations)] 31 | pub struct TextInput<'a, Message> { 32 | _state: &'a mut State, 33 | placeholder: String, 34 | value: String, 35 | is_secure: bool, 36 | width: Length, 37 | max_width: u32, 38 | padding: Padding, 39 | size: Option<u16>, 40 | on_change: Rc<Box<dyn Fn(String) -> Message>>, 41 | on_submit: Option<Message>, 42 | style_sheet: Box<dyn StyleSheet + 'a>, 43 | } 44 | 45 | impl<'a, Message> TextInput<'a, Message> { 46 | /// Creates a new [`TextInput`]. 47 | /// 48 | /// It expects: 49 | /// - some [`State`] 50 | /// - a placeholder 51 | /// - the current value 52 | /// - a function that produces a message when the [`TextInput`] changes 53 | pub fn new<F>( 54 | state: &'a mut State, 55 | placeholder: &str, 56 | value: &str, 57 | on_change: F, 58 | ) -> Self 59 | where 60 | F: 'static + Fn(String) -> Message, 61 | { 62 | Self { 63 | _state: state, 64 | placeholder: String::from(placeholder), 65 | value: String::from(value), 66 | is_secure: false, 67 | width: Length::Fill, 68 | max_width: u32::MAX, 69 | padding: Padding::ZERO, 70 | size: None, 71 | on_change: Rc::new(Box::new(on_change)), 72 | on_submit: None, 73 | style_sheet: Default::default(), 74 | } 75 | } 76 | 77 | /// Converts the [`TextInput`] into a secure password input. 78 | pub fn password(mut self) -> Self { 79 | self.is_secure = true; 80 | self 81 | } 82 | 83 | /// Sets the width of the [`TextInput`]. 84 | pub fn width(mut self, width: Length) -> Self { 85 | self.width = width; 86 | self 87 | } 88 | 89 | /// Sets the maximum width of the [`TextInput`]. 90 | pub fn max_width(mut self, max_width: u32) -> Self { 91 | self.max_width = max_width; 92 | self 93 | } 94 | 95 | /// Sets the [`Padding`] of the [`TextInput`]. 96 | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { 97 | self.padding = padding.into(); 98 | self 99 | } 100 | 101 | /// Sets the text size of the [`TextInput`]. 102 | pub fn size(mut self, size: u16) -> Self { 103 | self.size = Some(size); 104 | self 105 | } 106 | 107 | /// Sets the message that should be produced when the [`TextInput`] is 108 | /// focused and the enter key is pressed. 109 | pub fn on_submit(mut self, message: Message) -> Self { 110 | self.on_submit = Some(message); 111 | self 112 | } 113 | 114 | /// Sets the style of the [`TextInput`]. 115 | pub fn style( 116 | mut self, 117 | style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, 118 | ) -> Self { 119 | self.style_sheet = style_sheet.into(); 120 | self 121 | } 122 | } 123 | 124 | impl<'a, Message> Widget<Message> for TextInput<'a, Message> 125 | where 126 | Message: 'static + Clone, 127 | { 128 | fn node<'b>( 129 | &self, 130 | bump: &'b bumpalo::Bump, 131 | bus: &Bus<Message>, 132 | _style_sheet: &mut Css<'b>, 133 | ) -> dodrio::Node<'b> { 134 | use dodrio::builder::*; 135 | use wasm_bindgen::JsCast; 136 | 137 | let placeholder = { 138 | use dodrio::bumpalo::collections::String; 139 | 140 | String::from_str_in(&self.placeholder, bump).into_bump_str() 141 | }; 142 | 143 | let value = { 144 | use dodrio::bumpalo::collections::String; 145 | 146 | String::from_str_in(&self.value, bump).into_bump_str() 147 | }; 148 | 149 | let on_change = self.on_change.clone(); 150 | let on_submit = self.on_submit.clone(); 151 | let input_event_bus = bus.clone(); 152 | let submit_event_bus = bus.clone(); 153 | let style = self.style_sheet.active(); 154 | 155 | input(bump) 156 | .attr( 157 | "style", 158 | bumpalo::format!( 159 | in bump, 160 | "width: {}; max-width: {}; padding: {}; font-size: {}px; \ 161 | background: {}; border-width: {}px; border-color: {}; \ 162 | border-radius: {}px; color: {}", 163 | css::length(self.width), 164 | css::max_length(self.max_width), 165 | css::padding(self.padding), 166 | self.size.unwrap_or(20), 167 | css::background(style.background), 168 | style.border_width, 169 | css::color(style.border_color), 170 | style.border_radius, 171 | css::color(self.style_sheet.value_color()) 172 | ) 173 | .into_bump_str(), 174 | ) 175 | .attr("placeholder", placeholder) 176 | .attr("value", value) 177 | .attr("type", if self.is_secure { "password" } else { "text" }) 178 | .on("input", move |_root, _vdom, event| { 179 | let text_input = match event.target().and_then(|t| { 180 | t.dyn_into::<web_sys::HtmlInputElement>().ok() 181 | }) { 182 | None => return, 183 | Some(text_input) => text_input, 184 | }; 185 | 186 | input_event_bus.publish(on_change(text_input.value())); 187 | }) 188 | .on("keypress", move |_root, _vdom, event| { 189 | if let Some(on_submit) = on_submit.clone() { 190 | let event = 191 | event.unchecked_into::<web_sys::KeyboardEvent>(); 192 | 193 | match event.key_code() { 194 | 13 => { 195 | submit_event_bus.publish(on_submit); 196 | } 197 | _ => {} 198 | } 199 | } 200 | }) 201 | .finish() 202 | } 203 | } 204 | 205 | impl<'a, Message> From<TextInput<'a, Message>> for Element<'a, Message> 206 | where 207 | Message: 'static + Clone, 208 | { 209 | fn from(text_input: TextInput<'a, Message>) -> Element<'a, Message> { 210 | Element::new(text_input) 211 | } 212 | } 213 | 214 | /// The state of a [`TextInput`]. 215 | #[derive(Debug, Clone, Copy, Default)] 216 | pub struct State; 217 | 218 | impl State { 219 | /// Creates a new [`State`], representing an unfocused [`TextInput`]. 220 | pub fn new() -> Self { 221 | Self::default() 222 | } 223 | 224 | /// Creates a new [`State`], representing a focused [`TextInput`]. 225 | pub fn focused() -> Self { 226 | // TODO 227 | Self::default() 228 | } 229 | 230 | /// Selects all the content of the [`TextInput`]. 231 | pub fn select_all(&mut self) { 232 | // TODO 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/widget/toggler.rs: -------------------------------------------------------------------------------- 1 | //! Show toggle controls using togglers. 2 | use crate::{css, Bus, Css, Element, Length, Widget}; 3 | 4 | pub use iced_style::toggler::{Style, StyleSheet}; 5 | 6 | use dodrio::bumpalo; 7 | use std::rc::Rc; 8 | 9 | /// A toggler that can be toggled. 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// # use iced_web::Toggler; 15 | /// 16 | /// pub enum Message { 17 | /// TogglerToggled(bool), 18 | /// } 19 | /// 20 | /// let is_active = true; 21 | /// 22 | /// Toggler::new(is_active, String::from("Toggle me!"), Message::TogglerToggled); 23 | /// ``` 24 | /// 25 | #[allow(missing_debug_implementations)] 26 | pub struct Toggler<Message> { 27 | is_active: bool, 28 | on_toggle: Rc<dyn Fn(bool) -> Message>, 29 | label: Option<String>, 30 | id: Option<String>, 31 | width: Length, 32 | style: Box<dyn StyleSheet>, 33 | } 34 | 35 | impl<Message> Toggler<Message> { 36 | /// Creates a new [`Toggler`]. 37 | /// 38 | /// It expects: 39 | /// * a boolean describing whether the [`Toggler`] is active or not 40 | /// * An optional label for the [`Toggler`] 41 | /// * a function that will be called when the [`Toggler`] is toggled. It 42 | /// will receive the new state of the [`Toggler`] and must produce a 43 | /// `Message`. 44 | /// 45 | /// [`Toggler`]: struct.Toggler.html 46 | pub fn new<F>( 47 | is_active: bool, 48 | label: impl Into<Option<String>>, 49 | f: F, 50 | ) -> Self 51 | where 52 | F: 'static + Fn(bool) -> Message, 53 | { 54 | Toggler { 55 | is_active, 56 | on_toggle: Rc::new(f), 57 | label: label.into(), 58 | id: None, 59 | width: Length::Shrink, 60 | style: Default::default(), 61 | } 62 | } 63 | 64 | /// Sets the width of the [`Toggler`]. 65 | /// 66 | /// [`Toggler`]: struct.Toggler.html 67 | pub fn width(mut self, width: Length) -> Self { 68 | self.width = width; 69 | self 70 | } 71 | 72 | /// Sets the style of the [`Toggler`]. 73 | /// 74 | /// [`Toggler`]: struct.Toggler.html 75 | pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { 76 | self.style = style.into(); 77 | self 78 | } 79 | 80 | /// Sets the id of the [`Toggler`]. 81 | /// 82 | /// [`Toggler`]: struct.Toggler.html 83 | pub fn id(mut self, id: impl Into<String>) -> Self { 84 | self.id = Some(id.into()); 85 | self 86 | } 87 | } 88 | 89 | impl<Message> Widget<Message> for Toggler<Message> 90 | where 91 | Message: 'static, 92 | { 93 | fn node<'b>( 94 | &self, 95 | bump: &'b bumpalo::Bump, 96 | bus: &Bus<Message>, 97 | style_sheet: &mut Css<'b>, 98 | ) -> dodrio::Node<'b> { 99 | use dodrio::builder::*; 100 | use dodrio::bumpalo::collections::String; 101 | 102 | let toggler_label = &self 103 | .label 104 | .as_ref() 105 | .map(|label| String::from_str_in(&label, bump).into_bump_str()); 106 | 107 | let event_bus = bus.clone(); 108 | let on_toggle = self.on_toggle.clone(); 109 | let is_active = self.is_active; 110 | 111 | let row_class = style_sheet.insert(bump, css::Rule::Row); 112 | let toggler_class = style_sheet.insert(bump, css::Rule::Toggler(16)); 113 | 114 | let (label, input) = if let Some(id) = &self.id { 115 | let id = String::from_str_in(id, bump).into_bump_str(); 116 | 117 | (label(bump).attr("for", id), input(bump).attr("id", id)) 118 | } else { 119 | (label(bump), input(bump)) 120 | }; 121 | 122 | let checkbox = input 123 | .attr("type", "checkbox") 124 | .bool_attr("checked", self.is_active) 125 | .on("click", move |_root, vdom, _event| { 126 | let msg = on_toggle(!is_active); 127 | event_bus.publish(msg); 128 | 129 | vdom.schedule_render(); 130 | }) 131 | .finish(); 132 | 133 | let toggler = span(bump).children(vec![span(bump).finish()]).finish(); 134 | 135 | label 136 | .attr( 137 | "class", 138 | bumpalo::format!(in bump, "{} {}", row_class, toggler_class) 139 | .into_bump_str(), 140 | ) 141 | .attr( 142 | "style", 143 | bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width)) 144 | .into_bump_str() 145 | ) 146 | .children( 147 | if let Some(label) = toggler_label { 148 | vec![ 149 | text(label), 150 | checkbox, 151 | toggler, 152 | ] 153 | } else { 154 | vec![ 155 | checkbox, 156 | toggler, 157 | ] 158 | } 159 | ) 160 | .finish() 161 | } 162 | } 163 | 164 | impl<'a, Message> From<Toggler<Message>> for Element<'a, Message> 165 | where 166 | Message: 'static, 167 | { 168 | fn from(toggler: Toggler<Message>) -> Element<'a, Message> { 169 | Element::new(toggler) 170 | } 171 | } 172 | --------------------------------------------------------------------------------