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