From 655978f480c32bc696f0d5fe2fff834bfbf238ea Mon Sep 17 00:00:00 2001
From: Héctor Ramón Jiménez <hector0193@gmail.com>
Date: Sun, 15 Sep 2019 18:53:13 +0200
Subject: Draft nodes for missing widgets

---
 src/widget/checkbox.rs                 |  9 ++++--
 src/widget/image.rs                    |  6 ++--
 src/widget/radio.rs                    |  9 ++++--
 src/widget/slider.rs                   | 12 +++++---
 web/Cargo.toml                         |  5 ++++
 web/examples/tour/index.html           |  2 +-
 web/examples/tour/resources/ferris.png |  1 +
 web/examples/tour/src/tour.rs          |  4 +--
 web/src/widget.rs                      |  6 +---
 web/src/widget/button.rs               |  4 +--
 web/src/widget/checkbox.rs             | 39 ++++++++++++++++++++++--
 web/src/widget/column.rs               |  5 +++-
 web/src/widget/image.rs                | 25 ++++++++++++++--
 web/src/widget/radio.rs                | 40 +++++++++++++++++++++++--
 web/src/widget/row.rs                  |  5 +++-
 web/src/widget/slider.rs               | 55 +++++++++++++++++++++++++++++++---
 16 files changed, 191 insertions(+), 36 deletions(-)
 create mode 120000 web/examples/tour/resources/ferris.png

diff --git a/src/widget/checkbox.rs b/src/widget/checkbox.rs
index c60807fd..4ae167ad 100644
--- a/src/widget/checkbox.rs
+++ b/src/widget/checkbox.rs
@@ -38,9 +38,12 @@ use crate::{
 ///
 /// ![Checkbox drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/checkbox.png?raw=true)
 pub struct Checkbox<Color, Message> {
-    is_checked: bool,
-    on_toggle: Box<dyn Fn(bool) -> Message>,
-    label: String,
+    /// Whether the checkbox is checked or not
+    pub is_checked: bool,
+    /// Toggle message to fire
+    pub on_toggle: Box<dyn Fn(bool) -> Message>,
+    /// The label of the checkbox
+    pub label: String,
     label_color: Option<Color>,
 }
 
diff --git a/src/widget/image.rs b/src/widget/image.rs
index d94bfea5..8c869ab8 100644
--- a/src/widget/image.rs
+++ b/src/widget/image.rs
@@ -24,9 +24,11 @@ use std::hash::Hash;
 /// let image = Image::new(my_handle);
 /// ```
 pub struct Image<I> {
-    image: I,
+    /// The image handle
+    pub image: I,
     source: Option<Rectangle<u16>>,
-    width: Option<u16>,
+    /// The width of the image
+    pub width: Option<u16>,
     height: Option<u16>,
     style: Style,
 }
diff --git a/src/widget/radio.rs b/src/widget/radio.rs
index 28353ef4..27c0ff17 100644
--- a/src/widget/radio.rs
+++ b/src/widget/radio.rs
@@ -47,9 +47,12 @@ use std::hash::Hash;
 ///
 /// ![Radio buttons drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/radio.png?raw=true)
 pub struct Radio<Color, Message> {
-    is_selected: bool,
-    on_click: Message,
-    label: String,
+    /// Whether the radio button is selected or not
+    pub is_selected: bool,
+    /// The message to produce when the radio button is clicked
+    pub on_click: Message,
+    /// The label of the radio button
+    pub label: String,
     label_color: Option<Color>,
 }
 
diff --git a/src/widget/slider.rs b/src/widget/slider.rs
index cdec9ec4..8a0cea01 100644
--- a/src/widget/slider.rs
+++ b/src/widget/slider.rs
@@ -6,6 +6,7 @@
 //! [`State`]: struct.State.html
 use std::hash::Hash;
 use std::ops::RangeInclusive;
+use std::rc::Rc;
 
 use crate::input::{mouse, ButtonState};
 use crate::{
@@ -42,9 +43,12 @@ use crate::{
 /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
 pub struct Slider<'a, Message> {
     state: &'a mut State,
-    range: RangeInclusive<f32>,
-    value: f32,
-    on_change: Box<dyn Fn(f32) -> Message>,
+    /// The range of the slider
+    pub range: RangeInclusive<f32>,
+    /// The current value of the slider
+    pub value: f32,
+    /// The function to produce messages on change
+    pub on_change: Rc<Box<dyn Fn(f32) -> Message>>,
     style: Style,
 }
 
@@ -85,7 +89,7 @@ impl<'a, Message> Slider<'a, Message> {
             state,
             value: value.max(*range.start()).min(*range.end()),
             range,
-            on_change: Box::new(on_change),
+            on_change: Rc::new(Box::new(on_change)),
             style: Style::default().min_width(100).fill_width(),
         }
     }
diff --git a/web/Cargo.toml b/web/Cargo.toml
index 6d8c37b1..0ab89570 100644
--- a/web/Cargo.toml
+++ b/web/Cargo.toml
@@ -18,6 +18,7 @@ maintenance = { status = "actively-developed" }
 iced = { version = "0.1.0-alpha", path = ".." }
 dodrio = "0.1.0"
 futures = "0.1"
+wasm-bindgen = "0.2.50"
 
 [dependencies.web-sys]
 version = "0.3.27"
@@ -25,4 +26,8 @@ features = [
     "console",
     "Document",
     "HtmlElement",
+    "HtmlInputElement",
+    "Event",
+    "EventTarget",
+    "InputEvent",
 ]
diff --git a/web/examples/tour/index.html b/web/examples/tour/index.html
index a639a6cb..527cc54c 100644
--- a/web/examples/tour/index.html
+++ b/web/examples/tour/index.html
@@ -2,7 +2,7 @@
 <html>
   <head>
     <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
-    <title>Tour - Iced Web</title>
+    <title>Web Tour - Iced</title>
   </head>
   <body>
     <script type="module">
diff --git a/web/examples/tour/resources/ferris.png b/web/examples/tour/resources/ferris.png
new file mode 120000
index 00000000..9c4fb51c
--- /dev/null
+++ b/web/examples/tour/resources/ferris.png
@@ -0,0 +1 @@
+../../../../examples/resources/ferris.png
\ No newline at end of file
diff --git a/web/examples/tour/src/tour.rs b/web/examples/tour/src/tour.rs
index 6c24622e..9a60e092 100644
--- a/web/examples/tour/src/tour.rs
+++ b/web/examples/tour/src/tour.rs
@@ -295,8 +295,8 @@ impl<'a> Step {
             ))
             .push(Text::new(
                 "Iced does not provide a built-in renderer. This example runs \
-                 on a fairly simple renderer built on top of ggez, another \
-                 game library.",
+                 on WebAssembly using dodrio, an experimental VDOM library \
+                 for Rust.",
             ))
             .push(Text::new(
                 "You will need to interact with the UI in order to reach the \
diff --git a/web/src/widget.rs b/web/src/widget.rs
index 7af592e1..67ca8e81 100644
--- a/web/src/widget.rs
+++ b/web/src/widget.rs
@@ -25,9 +25,5 @@ pub trait Widget<Message> {
         &self,
         bump: &'b bumpalo::Bump,
         _bus: &Bus<Message>,
-    ) -> dodrio::Node<'b> {
-        use dodrio::builder::*;
-
-        div(bump).children(vec![text("WIP")]).finish()
-    }
+    ) -> dodrio::Node<'b>;
 }
diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs
index 8ccda107..63c0262a 100644
--- a/web/src/widget/button.rs
+++ b/web/src/widget/button.rs
@@ -1,8 +1,8 @@
 use crate::{Bus, Element, Widget};
-use dodrio::bumpalo;
-
 pub use iced::button::{Class, State};
 
+use dodrio::bumpalo;
+
 pub type Button<'a, Message> = iced::Button<'a, Message>;
 
 impl<'a, Message> Widget<Message> for Button<'a, Message>
diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs
index a231d801..e3c61ca7 100644
--- a/web/src/widget/checkbox.rs
+++ b/web/src/widget/checkbox.rs
@@ -1,12 +1,45 @@
-use crate::{Color, Element, Widget};
+use crate::{Bus, Color, Element, Widget};
+
+use dodrio::bumpalo;
 
 pub type Checkbox<Message> = iced::Checkbox<Color, Message>;
 
-impl<Message> Widget<Message> for Checkbox<Message> {}
+impl<Message> Widget<Message> for Checkbox<Message>
+where
+    Message: 'static + Copy,
+{
+    fn node<'b>(
+        &self,
+        bump: &'b bumpalo::Bump,
+        bus: &Bus<Message>,
+    ) -> dodrio::Node<'b> {
+        use dodrio::builder::*;
+
+        let checkbox_label = bumpalo::format!(in bump, "{}", self.label);
+
+        let event_bus = bus.clone();
+        let msg = (self.on_toggle)(!self.is_checked);
+
+        label(bump)
+            .children(vec![
+                input(bump)
+                    .attr("type", "checkbox")
+                    .bool_attr("checked", self.is_checked)
+                    .on("click", move |root, vdom, _event| {
+                        event_bus.publish(msg, root);
+
+                        vdom.schedule_render();
+                    })
+                    .finish(),
+                text(checkbox_label.into_bump_str()),
+            ])
+            .finish()
+    }
+}
 
 impl<'a, Message> From<Checkbox<Message>> for Element<'a, Message>
 where
-    Message: 'static,
+    Message: 'static + Copy,
 {
     fn from(checkbox: Checkbox<Message>) -> Element<'a, Message> {
         Element::new(checkbox)
diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs
index b3131f5e..a2b8232e 100644
--- a/web/src/widget/column.rs
+++ b/web/src/widget/column.rs
@@ -52,7 +52,10 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> {
             .map(|element| element.widget.node(bump, publish))
             .collect();
 
-        div(bump).children(children).finish()
+        div(bump)
+            .attr("style", "display: flex; flex-direction: column")
+            .children(children)
+            .finish()
     }
 }
 
diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs
index ac144fd8..a882faff 100644
--- a/web/src/widget/image.rs
+++ b/web/src/widget/image.rs
@@ -1,8 +1,29 @@
-use crate::{Element, Widget};
+use crate::{Bus, Element, Widget};
+
+use dodrio::bumpalo;
 
 pub type Image<'a> = iced::Image<&'a str>;
 
-impl<'a, Message> Widget<Message> for Image<'a> {}
+impl<'a, Message> Widget<Message> for Image<'a> {
+    fn node<'b>(
+        &self,
+        bump: &'b bumpalo::Bump,
+        _bus: &Bus<Message>,
+    ) -> dodrio::Node<'b> {
+        use dodrio::builder::*;
+
+        let src = bumpalo::format!(in bump, "{}", self.image);
+
+        let mut image = img(bump).attr("src", src.into_bump_str());
+
+        if let Some(width) = self.width {
+            let width = bumpalo::format!(in bump, "{}", width);
+            image = image.attr("width", width.into_bump_str());
+        }
+
+        image.finish()
+    }
+}
 
 impl<'a, Message> From<Image<'a>> for Element<'a, Message> {
     fn from(image: Image<'a>) -> Element<'a, Message> {
diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs
index 0c28b46f..df08a977 100644
--- a/web/src/widget/radio.rs
+++ b/web/src/widget/radio.rs
@@ -1,12 +1,46 @@
-use crate::{Color, Element, Widget};
+use crate::{Bus, Color, Element, Widget};
+
+use dodrio::bumpalo;
 
 pub type Radio<Message> = iced::Radio<Color, Message>;
 
-impl<Message> Widget<Message> for Radio<Message> {}
+impl<Message> Widget<Message> for Radio<Message>
+where
+    Message: 'static + Copy,
+{
+    fn node<'b>(
+        &self,
+        bump: &'b bumpalo::Bump,
+        bus: &Bus<Message>,
+    ) -> dodrio::Node<'b> {
+        use dodrio::builder::*;
+
+        let radio_label = bumpalo::format!(in bump, "{}", self.label);
+
+        let event_bus = bus.clone();
+        let on_click = self.on_click;
+
+        label(bump)
+            .attr("style", "display: block")
+            .children(vec![
+                input(bump)
+                    .attr("type", "radio")
+                    .bool_attr("checked", self.is_selected)
+                    .on("click", move |root, vdom, _event| {
+                        event_bus.publish(on_click, root);
+
+                        vdom.schedule_render();
+                    })
+                    .finish(),
+                text(radio_label.into_bump_str()),
+            ])
+            .finish()
+    }
+}
 
 impl<'a, Message> From<Radio<Message>> for Element<'a, Message>
 where
-    Message: 'static,
+    Message: 'static + Copy,
 {
     fn from(radio: Radio<Message>) -> Element<'a, Message> {
         Element::new(radio)
diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs
index 40fc68e3..71532245 100644
--- a/web/src/widget/row.rs
+++ b/web/src/widget/row.rs
@@ -40,7 +40,10 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> {
             .map(|element| element.widget.node(bump, publish))
             .collect();
 
-        div(bump).children(children).finish()
+        div(bump)
+            .attr("style", "display: flex; flex-direction: row")
+            .children(children)
+            .finish()
     }
 }
 
diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs
index 9c83befb..31bfcbf3 100644
--- a/web/src/widget/slider.rs
+++ b/web/src/widget/slider.rs
@@ -1,14 +1,61 @@
-use crate::{Element, Widget};
+use crate::{Bus, Element, Widget};
 
-pub use iced::slider::State;
+use dodrio::bumpalo;
 
 pub type Slider<'a, Message> = iced::Slider<'a, Message>;
 
-impl<'a, Message> Widget<Message> for Slider<'a, Message> {}
+pub use iced::slider::State;
+
+impl<'a, Message> Widget<Message> for Slider<'a, Message>
+where
+    Message: 'static + Copy,
+{
+    fn node<'b>(
+        &self,
+        bump: &'b bumpalo::Bump,
+        bus: &Bus<Message>,
+    ) -> dodrio::Node<'b> {
+        use dodrio::builder::*;
+        use wasm_bindgen::JsCast;
+
+        let (start, end) = self.range.clone().into_inner();
+
+        let min = bumpalo::format!(in bump, "{}", start);
+        let max = bumpalo::format!(in bump, "{}", end);
+        let value = bumpalo::format!(in bump, "{}", self.value);
+
+        let on_change = self.on_change.clone();
+        let event_bus = bus.clone();
+
+        // TODO: Make `step` configurable
+        label(bump)
+            .children(vec![input(bump)
+                .attr("type", "range")
+                .attr("step", "0.01")
+                .attr("min", min.into_bump_str())
+                .attr("max", max.into_bump_str())
+                .attr("value", value.into_bump_str())
+                .on("input", move |root, vdom, event| {
+                    let slider = match event.target().and_then(|t| {
+                        t.dyn_into::<web_sys::HtmlInputElement>().ok()
+                    }) {
+                        None => return,
+                        Some(slider) => slider,
+                    };
+
+                    if let Ok(value) = slider.value().parse::<f32>() {
+                        event_bus.publish(on_change(value), root);
+                        vdom.schedule_render();
+                    }
+                })
+                .finish()])
+            .finish()
+    }
+}
 
 impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message>
 where
-    Message: 'static,
+    Message: 'static + Copy,
 {
     fn from(slider: Slider<'a, Message>) -> Element<'a, Message> {
         Element::new(slider)
-- 
cgit