diff options
Diffstat (limited to 'web/src')
-rw-r--r-- | web/src/bus.rs | 40 | ||||
-rw-r--r-- | web/src/element.rs | 62 | ||||
-rw-r--r-- | web/src/lib.rs | 74 | ||||
-rw-r--r-- | web/src/widget.rs | 35 | ||||
-rw-r--r-- | web/src/widget/button.rs | 45 | ||||
-rw-r--r-- | web/src/widget/checkbox.rs | 48 | ||||
-rw-r--r-- | web/src/widget/column.rs | 36 | ||||
-rw-r--r-- | web/src/widget/image.rs | 42 | ||||
-rw-r--r-- | web/src/widget/radio.rs | 49 | ||||
-rw-r--r-- | web/src/widget/row.rs | 36 | ||||
-rw-r--r-- | web/src/widget/slider.rs | 62 | ||||
-rw-r--r-- | web/src/widget/text.rs | 29 |
12 files changed, 558 insertions, 0 deletions
diff --git a/web/src/bus.rs b/web/src/bus.rs new file mode 100644 index 00000000..d76466f5 --- /dev/null +++ b/web/src/bus.rs @@ -0,0 +1,40 @@ +use crate::Application; + +use std::rc::Rc; + +#[derive(Clone)] +pub struct Bus<Message> { + publish: Rc<Box<dyn Fn(Message, &mut dyn dodrio::RootRender)>>, +} + +impl<Message> Bus<Message> +where + Message: 'static, +{ + pub fn new() -> Self { + Self { + publish: Rc::new(Box::new(|message, root| { + let app = root.unwrap_mut::<Application<Message>>(); + + app.update(message) + })), + } + } + + pub fn publish(&self, message: Message, root: &mut dyn dodrio::RootRender) { + (self.publish)(message, root); + } + + pub fn map<B>(&self, mapper: Rc<Box<dyn Fn(B) -> Message>>) -> Bus<B> + where + B: 'static, + { + let publish = self.publish.clone(); + + Bus { + publish: Rc::new(Box::new(move |message, root| { + publish(mapper(message), root) + })), + } + } +} diff --git a/web/src/element.rs b/web/src/element.rs new file mode 100644 index 00000000..8270d8db --- /dev/null +++ b/web/src/element.rs @@ -0,0 +1,62 @@ +use crate::{Bus, Color, Widget}; + +use dodrio::bumpalo; +use std::rc::Rc; + +pub struct Element<'a, Message> { + pub(crate) widget: Box<dyn Widget<Message> + 'a>, +} + +impl<'a, Message> Element<'a, Message> { + pub fn new(widget: impl Widget<Message> + 'a) -> Self { + Self { + widget: Box::new(widget), + } + } + + pub fn explain(self, _color: Color) -> Element<'a, Message> { + self + } + + pub fn map<F, B>(self, f: F) -> Element<'a, B> + where + Message: 'static, + B: 'static, + F: 'static + Fn(Message) -> B, + { + Element { + widget: Box::new(Map::new(self.widget, f)), + } + } +} + +struct Map<'a, A, B> { + widget: Box<dyn Widget<A> + 'a>, + mapper: Rc<Box<dyn Fn(A) -> B>>, +} + +impl<'a, A, B> Map<'a, A, B> { + pub fn new<F>(widget: Box<dyn Widget<A> + 'a>, mapper: F) -> Map<'a, A, B> + where + F: 'static + Fn(A) -> B, + { + Map { + widget, + mapper: Rc::new(Box::new(mapper)), + } + } +} + +impl<'a, A, B> Widget<B> for Map<'a, A, B> +where + A: 'static, + B: 'static, +{ + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + bus: &Bus<B>, + ) -> dodrio::Node<'b> { + self.widget.node(bump, &bus.map(self.mapper.clone())) + } +} diff --git a/web/src/lib.rs b/web/src/lib.rs new file mode 100644 index 00000000..caf17df5 --- /dev/null +++ b/web/src/lib.rs @@ -0,0 +1,74 @@ +use dodrio::bumpalo; +use futures::Future; +use std::cell::RefCell; + +mod bus; +mod element; +pub mod widget; + +pub use bus::Bus; +pub use element::Element; +pub use iced_core::{Align, Color, Justify, Length}; +pub use widget::*; + +pub trait UserInterface { + type Message; + + fn update( + &mut self, + message: Self::Message, + ) -> Option<Box<dyn Future<Output = Self::Message>>>; + + fn view(&mut self) -> Element<Self::Message>; + + fn run(self) + where + Self: 'static + Sized, + { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let app = Application::new(self); + + let vdom = dodrio::Vdom::new(&body, app); + vdom.forget(); + } +} + +struct Application<Message> { + ui: RefCell<Box<dyn UserInterface<Message = Message>>>, +} + +impl<Message> Application<Message> { + fn new(ui: impl UserInterface<Message = Message> + 'static) -> Self { + Self { + ui: RefCell::new(Box::new(ui)), + } + } + + fn update(&mut self, message: Message) { + let mut ui = self.ui.borrow_mut(); + + // TODO: Resolve futures and publish resulting messages + let _ = ui.update(message); + } +} + +impl<Message> dodrio::Render for Application<Message> +where + Message: 'static, +{ + fn render<'a, 'bump>( + &'a self, + bump: &'bump bumpalo::Bump, + ) -> dodrio::Node<'bump> + where + 'a: 'bump, + { + let mut ui = self.ui.borrow_mut(); + let element = ui.view(); + + element.widget.node(bump, &Bus::new()) + } +} diff --git a/web/src/widget.rs b/web/src/widget.rs new file mode 100644 index 00000000..88b2efc9 --- /dev/null +++ b/web/src/widget.rs @@ -0,0 +1,35 @@ +use crate::Bus; +use dodrio::bumpalo; + +pub mod button; +pub mod slider; +pub mod text; + +mod checkbox; +mod column; +mod image; +mod radio; +mod row; + +#[doc(no_inline)] +pub use button::Button; + +#[doc(no_inline)] +pub use slider::Slider; + +#[doc(no_inline)] +pub use text::Text; + +pub use checkbox::Checkbox; +pub use column::Column; +pub use image::Image; +pub use radio::Radio; +pub use row::Row; + +pub trait Widget<Message> { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _bus: &Bus<Message>, + ) -> dodrio::Node<'b>; +} diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs new file mode 100644 index 00000000..23a4165a --- /dev/null +++ b/web/src/widget/button.rs @@ -0,0 +1,45 @@ +use crate::{Bus, Element, Widget}; + +use dodrio::bumpalo; + +pub use iced_core::button::*; + +impl<'a, Message> Widget<Message> for Button<'a, Message> +where + Message: 'static + Copy, +{ + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + bus: &Bus<Message>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let label = bumpalo::format!(in bump, "{}", self.label); + + let mut node = button(bump).children(vec![text(label.into_bump_str())]); + + if let Some(on_press) = self.on_press { + let event_bus = bus.clone(); + + node = node.on("click", move |root, vdom, _event| { + event_bus.publish(on_press, root); + + vdom.schedule_render(); + }); + } + + // TODO: Complete styling + + node.finish() + } +} + +impl<'a, Message> From<Button<'a, Message>> for Element<'a, Message> +where + Message: 'static + Copy, +{ + fn from(button: Button<'a, Message>) -> Element<'a, Message> { + Element::new(button) + } +} diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs new file mode 100644 index 00000000..72f0a2aa --- /dev/null +++ b/web/src/widget/checkbox.rs @@ -0,0 +1,48 @@ +use crate::{Bus, Element, Widget}; + +use dodrio::bumpalo; + +pub use iced_core::Checkbox; + +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); + + // TODO: Complete styling + 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 + 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 new file mode 100644 index 00000000..becd6bc6 --- /dev/null +++ b/web/src/widget/column.rs @@ -0,0 +1,36 @@ +use crate::{Bus, Element, Widget}; + +use dodrio::bumpalo; + +pub type Column<'a, Message> = iced_core::Column<Element<'a, Message>>; + +impl<'a, Message> Widget<Message> for Column<'a, Message> { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + publish: &Bus<Message>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let children: Vec<_> = self + .children + .iter() + .map(|element| element.widget.node(bump, publish)) + .collect(); + + // TODO: Complete styling + div(bump) + .attr("style", "display: flex; flex-direction: column") + .children(children) + .finish() + } +} + +impl<'a, Message> From<Column<'a, Message>> for Element<'a, Message> +where + Message: 'static, +{ + fn from(column: Column<'a, Message>) -> Element<'a, Message> { + Element::new(column) + } +} diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs new file mode 100644 index 00000000..fd4ff0df --- /dev/null +++ b/web/src/widget/image.rs @@ -0,0 +1,42 @@ +use crate::{Bus, Element, Length, Widget}; + +use dodrio::bumpalo; + +pub type Image<'a> = iced_core::Image<&'a str>; + +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.handle); + + let mut image = img(bump).attr("src", src.into_bump_str()); + + match self.width { + Length::Shrink => {} + Length::Fill => { + image = image.attr("width", "100%"); + } + Length::Units(px) => { + image = image.attr( + "width", + bumpalo::format!(in bump, "{}px", px).into_bump_str(), + ); + } + } + + // TODO: Complete styling + + image.finish() + } +} + +impl<'a, Message> From<Image<'a>> for Element<'a, Message> { + fn from(image: Image<'a>) -> Element<'a, Message> { + Element::new(image) + } +} diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs new file mode 100644 index 00000000..d249ad26 --- /dev/null +++ b/web/src/widget/radio.rs @@ -0,0 +1,49 @@ +use crate::{Bus, Element, Widget}; + +use dodrio::bumpalo; + +pub use iced_core::Radio; + +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; + + // TODO: Complete styling + 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 + 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 new file mode 100644 index 00000000..cf6ae594 --- /dev/null +++ b/web/src/widget/row.rs @@ -0,0 +1,36 @@ +use crate::{Bus, Element, Widget}; + +use dodrio::bumpalo; + +pub type Row<'a, Message> = iced_core::Row<Element<'a, Message>>; + +impl<'a, Message> Widget<Message> for Row<'a, Message> { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + publish: &Bus<Message>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let children: Vec<_> = self + .children + .iter() + .map(|element| element.widget.node(bump, publish)) + .collect(); + + // TODO: Complete styling + div(bump) + .attr("style", "display: flex; flex-direction: row") + .children(children) + .finish() + } +} + +impl<'a, Message> From<Row<'a, Message>> for Element<'a, Message> +where + Message: 'static, +{ + fn from(column: Row<'a, Message>) -> Element<'a, Message> { + Element::new(column) + } +} diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs new file mode 100644 index 00000000..54b2fdf6 --- /dev/null +++ b/web/src/widget/slider.rs @@ -0,0 +1,62 @@ +use crate::{Bus, Element, Widget}; + +use dodrio::bumpalo; + +pub use iced_core::slider::*; + +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 + // TODO: Complete styling + 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 + Copy, +{ + fn from(slider: Slider<'a, Message>) -> Element<'a, Message> { + Element::new(slider) + } +} diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs new file mode 100644 index 00000000..41ccd6fc --- /dev/null +++ b/web/src/widget/text.rs @@ -0,0 +1,29 @@ +use crate::{Bus, Element, Widget}; +use dodrio::bumpalo; + +pub use iced_core::text::*; + +impl<'a, Message> Widget<Message> for Text { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _publish: &Bus<Message>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let content = bumpalo::format!(in bump, "{}", self.content); + let size = bumpalo::format!(in bump, "font-size: {}px", self.size.unwrap_or(20)); + + // TODO: Complete styling + p(bump) + .attr("style", size.into_bump_str()) + .children(vec![text(content.into_bump_str())]) + .finish() + } +} + +impl<'a, Message> From<Text> for Element<'a, Message> { + fn from(text: Text) -> Element<'a, Message> { + Element::new(text) + } +} |