diff --git a/Cargo.lock b/Cargo.lock index b4935c6..088865c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,7 @@ dependencies = [ [[package]] name = "respo" -version = "0.0.14" +version = "0.0.15" dependencies = [ "cirru_parser", "js-sys", diff --git a/README.md b/README.md index 4041aed..a8a0325 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,82 @@ > > Respo was initially designed to work in a dynamic language with persistent data and HMR(hot code replacement), which is dramatically different from Rust. So this is more like an experiment. -Docs https://docs.rs/respo +- Docs https://docs.rs/respo +- Live Demo https://r.tiye.me/Respo/respo.rs/ ### Usage -A preview example, to delare a store: +Here is some preview of DOM syntax: + +```rust +Ok( + div() + .class(ui_global()) + .add_style(RespoStyle::default().padding(12.0).to_owned()) + .add_children([ + comp_counter(&states.pick("counter"), store.counted)?, + comp_panel(&states.pick("panel"))?, + comp_todolist(memo_caches, &states.pick("todolist"), &store.tasks)?, + ]) + .to_owned(), +) +``` + +CSS-in-Rust: + +```rust +static_styles!( + style_remove_button, + ( + "$0".to_owned(), + RespoStyle::default() + .width(CssSize::Px(16.0)) + .height(CssSize::Px(16.0)) + .margin(4.) + .cursor("pointer".to_owned()) + .margin4(0.0, 0.0, 0.0, 16.0) + .color(CssColor::Hsl(0, 90, 90)), + ), + ("$0:hover".to_owned(), RespoStyle::default().color(CssColor::Hsl(0, 90, 80))), +); +``` + +Builtin styles, [demonstrated](http://ui.respo-mvc.org/): + +| function | usages | +| ------------------ | ------------------------------- | +| `ui_global` | global styles | +| `ui_fullscreen` | fullscreen styles | +| `ui_button` | button styles | +| `ui_input` | input styles | +| `ui_textarea` | textarea styles | +| `ui_link` | link styles | +| `ui_flex` | `flex:1` styles | +| `ui_expand` | `flex:1` styles with scrolls | +| `ui_center` | flexbox center styles | +| `ui_row` | flexbox row styles | +| `ui_column` | flexbox column styles | +| `ui_row_center` | flexbox row center styles | +| `ui_column_center` | flexbox column center styles | +| `ui_row_around` | flexbox row around styles | +| `ui_column_around` | flexbox column around styles | +| `ui_row_evenly` | flexbox row evenly styles | +| `ui_column_evenly` | flexbox column evenly styles | +| `ui_row_parted` | flexbox row between styles | +| `ui_column_parted` | flexbox column between styles | +| `ui_row_middle` | flexbox row between styles | +| `ui_column_middle` | flexbox column between styles | +| `ui_font_code` | code font family | +| `ui_font_normal` | normal font family(Hind) | +| `ui_font_fancy` | fancy font family(Josefin Sans) | + +There are several dialog components in the demo. Syntax is not nice enough, so I'm not advertising it. But they work relatively good. + +For more components, read code in `src/app/`, they are just variants like `RespoNode::Component(..)`. It may be sugared in the future, not determined yet. + +### Store abstraction + +Declaring a store: ```rust #[derive(Debug, Clone, Deserialize, Serialize)] @@ -48,7 +119,7 @@ impl RespoStore for Store { } ``` -To declare an app: +Declaring an app: ```rust struct App { @@ -94,7 +165,7 @@ impl RespoApp for App { } ``` -mount app: +Mounting app: ```rust let app = App { @@ -110,27 +181,6 @@ let app = App { app.render_loop().expect("app render"); ``` -CSS-in-Rust: - -```rust -static_styles!( - style_remove_button, - ( - "$0".to_owned(), - RespoStyle::default() - .width(CssSize::Px(16.0)) - .height(CssSize::Px(16.0)) - .margin(4.) - .cursor("pointer".to_owned()) - .margin4(0.0, 0.0, 0.0, 16.0) - .color(CssColor::Hsl(0, 90, 90)), - ), - ("$0:hover".to_owned(), RespoStyle::default().color(CssColor::Hsl(0, 90, 80))), -); -``` - -For components, read code in `src/app/`, they are just variants like `RespoNode::Component(..)`. It may be sugared in the future, not decided yet. - ### License Apache License 2.0 . diff --git a/demo_respo/src/plugins.rs b/demo_respo/src/plugins.rs index ee60488..a6ecd7c 100644 --- a/demo_respo/src/plugins.rs +++ b/demo_respo/src/plugins.rs @@ -1,13 +1,15 @@ use std::fmt::Debug; +use respo::RespoEvent; use respo::{space, ui::ui_row_parted, RespoStyle}; use serde::{Deserialize, Serialize}; use respo::{button, div, span, ui::ui_button, util, DispatchFn, RespoNode, StatesTree}; use respo::dialog::{ - AlertOptions, AlertPlugin, AlertPluginInterface, ConfirmOptions, ConfirmPlugin, ConfirmPluginInterface, ModalOptions, ModalPlugin, - ModalPluginInterface, ModalRenderer, PromptOptions, PromptPlugin, PromptPluginInterface, PromptValidator, + AlertOptions, AlertPlugin, AlertPluginInterface, ConfirmOptions, ConfirmPlugin, ConfirmPluginInterface, DrawerOptions, DrawerPlugin, + DrawerPluginInterface, DrawerRenderer, ModalOptions, ModalPlugin, ModalPluginInterface, ModalRenderer, PromptOptions, PromptPlugin, + PromptPluginInterface, PromptValidator, }; use super::store::*; @@ -147,6 +149,48 @@ pub fn comp_plugins_demo(states: &StatesTree) -> Result<RespoNode<ActionOp>, Str } }; + // declare drawer + + let drawer_plugin = DrawerPlugin::new( + states.pick("drawer"), + DrawerOptions { + title: Some(String::from("Modal demo")), + render: DrawerRenderer::new(|close_drawer: _| { + let handler = move |_e: _, dispatch: DispatchFn<ActionOp>| { + respo::util::log!("on modal handle"); + close_drawer(dispatch) + }; + Ok( + div() + .style(RespoStyle::default().padding(8.0).to_owned()) + .children([ + div() + .children([span().inner_text("content in custom drawer").to_owned()]) + .to_owned(), + div() + .class(ui_row_parted()) + .children([span(), button().class(ui_button()).inner_text("close").on_click(handler).to_owned()]) + .to_owned(), + ]) + .to_owned(), + ) + }), + ..DrawerOptions::default() + }, + )? + .share_with_ref(); + + let on_drawer = { + let drawer_plugin = drawer_plugin.clone(); + move |e: RespoEvent, dispatch: DispatchFn<_>| -> Result<(), String> { + util::log!("click {:?}", e); + + drawer_plugin.show(dispatch)?; + + Ok(()) + } + }; + Ok( div() .children([ @@ -168,12 +212,19 @@ pub fn comp_plugins_demo(states: &StatesTree) -> Result<RespoNode<ActionOp>, Str .inner_text("Try Custom Modal") .on_click(on_modal) .to_owned(), + space(Some(8), None), + button() + .class(ui_button()) + .inner_text("Try Custom Drawer") + .on_click(on_drawer) + .to_owned(), ]) .to_owned(), alert_plugin.render()?, confirm_plugin.render()?, prompt_plugin.render()?, modal_plugin.render()?, + drawer_plugin.render()?, ]) .to_owned(), ) diff --git a/respo/Cargo.toml b/respo/Cargo.toml index f6e681f..7fc3c72 100644 --- a/respo/Cargo.toml +++ b/respo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "respo" -version = "0.0.14" +version = "0.0.15" edition = "2021" description = "a tiny virtual DOM library migrated from ClojureScript" license = "Apache-2.0" diff --git a/respo/src/dialog.rs b/respo/src/dialog.rs index 001be5d..2f06727 100644 --- a/respo/src/dialog.rs +++ b/respo/src/dialog.rs @@ -2,6 +2,7 @@ mod alert; mod confirm; +mod drawer; mod modal; mod prompt; @@ -17,6 +18,7 @@ pub(crate) const BUTTON_NAME: &str = "dialog-button"; pub use alert::{AlertOptions, AlertPlugin, AlertPluginInterface}; pub use confirm::{ConfirmOptions, ConfirmPlugin, ConfirmPluginInterface}; +pub use drawer::{DrawerOptions, DrawerPlugin, DrawerPluginInterface, DrawerRenderer}; pub use modal::{ModalOptions, ModalPlugin, ModalPluginInterface, ModalRenderer}; pub use prompt::{PromptOptions, PromptPlugin, PromptPluginInterface, PromptValidator}; @@ -71,7 +73,7 @@ pub(crate) fn effect_fade(args: Vec<RespoEffectArg>, effect_type: RespoEffectTyp let card = cloned.first_child().unwrap(); let card_style = card.dyn_ref::<HtmlElement>().unwrap().style(); card_style.set_property("transition-duration", "240ms").unwrap(); - card_style.set_property("transform", "scale(0.94) translate(0px,-20px)").unwrap(); + card_style.set_property("transform", "translate(100%,0px)").unwrap(); }); window .set_timeout_with_callback_and_timeout_and_arguments_0(immediate_call.as_ref().unchecked_ref(), 10) @@ -98,12 +100,12 @@ pub(crate) fn effect_fade(args: Vec<RespoEffectArg>, effect_type: RespoEffectTyp let style = target.dyn_ref::<HtmlElement>().unwrap().style(); let card_style = target.first_child().unwrap().dyn_ref::<HtmlElement>().unwrap().style(); style.set_property("opacity", "0").unwrap(); - card_style.set_property("transform", "scale(0.94)").unwrap(); + card_style.set_property("transform", "translate(100%, 0px)").unwrap(); let call = Closure::once(move || { style.set_property("transition-duration", "240ms").unwrap(); card_style.set_property("transition-duration", "240ms").unwrap(); style.set_property("opacity", "1").unwrap(); - card_style.set_property("transform", "scale(1) translate(0px,0px)").unwrap(); + card_style.set_property("transform", "translate(0px,0px)").unwrap(); }); let window = web_sys::window().unwrap(); window @@ -126,7 +128,6 @@ static_styles!( .background_color(CssColor::Hsla(0.0, 30.0, 10.0, 0.6)) .position(CssPosition::Fixed) .z_index(999) - .padding(16.0) ) ); @@ -136,14 +137,17 @@ static_styles!( "$0".to_owned(), RespoStyle::default() .background_color(CssColor::Hsl(0, 0, 100)) - .max_width(CssSize::Px(600.0)) - .width(CssSize::Percent(100.)) - .max_height(CssSize::Vh(80.0)) + .max_width(CssSize::Vw(50.0)) + .width(CssSize::Px(400.)) + .height(CssSize::Vh(100.0)) .overflow(CssOverflow::Auto) - .border_radius(3.0) .color(CssColor::Hsl(0, 0, 0)) - .insert("margin", "auto".to_owned()) - .padding(16.0) + .top(CssSize::Px(0.)) + .right(CssSize::Px(0.)) + .bottom(CssSize::Px(0.)) + .position(CssPosition::Absolute) + .box_shadow(-2., 0., 12., 0., CssColor::Hsla(0., 0., 0., 0.2)) + .transform_property("transform, opacity".to_owned()) ) ); diff --git a/respo/src/dialog/drawer.rs b/respo/src/dialog/drawer.rs new file mode 100644 index 0000000..0a3281a --- /dev/null +++ b/respo/src/dialog/drawer.rs @@ -0,0 +1,222 @@ +use std::fmt::Debug; + +use std::marker::PhantomData; +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; + +use crate::dialog::{css_backdrop, css_card}; +use crate::ui::{ui_center, ui_column, ui_fullscreen, ui_global}; + +use crate::{div, space, span, CssLineHeight, CssPosition, DispatchFn, RespoAction, RespoEvent, RespoNode, RespoStyle, StatesTree}; + +use crate::dialog::effect_fade; + +/// The options for custom drawer. +#[derive(Debug, Clone, Default)] +pub struct DrawerOptions<T> +where + T: Debug + Clone, +{ + /// inline style for backdrop + pub backdrop_style: RespoStyle, + /// inline style for card + pub card_style: RespoStyle, + /// title of the drawer, defaults to `drawer` + pub title: Option<String>, + /// render body + pub render: DrawerRenderer<T>, +} + +type DrawerRendererFn<T> = dyn Fn(Rc<dyn Fn(DispatchFn<T>) -> Result<(), String>>) -> Result<RespoNode<T>, String>; + +/// wraps render function +#[derive(Clone)] +pub struct DrawerRenderer<T>(Rc<DrawerRendererFn<T>>) +where + T: Debug + Clone; + +impl<T> Debug for DrawerRenderer<T> +where + T: Debug + Clone, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "(&DrawerRenderer ..)") + } +} + +impl<T> Default for DrawerRenderer<T> +where + T: Debug + Clone, +{ + fn default() -> Self { + Self(Rc::new(|_close: _| Ok(div()))) + } +} + +impl<T> DrawerRenderer<T> +where + T: Debug + Clone, +{ + pub fn new<V>(renderer: V) -> Self + where + V: Fn(Rc<dyn Fn(DispatchFn<T>) -> Result<(), String>>) -> Result<RespoNode<T>, String> + 'static, + { + Self(Rc::new(renderer)) + } + + pub fn run<V>(&self, close: V) -> Result<RespoNode<T>, String> + where + V: Fn(DispatchFn<T>) -> Result<(), String> + 'static, + { + (self.0)(Rc::new(close)) + } +} + +fn comp_drawer<T, U>(options: DrawerOptions<T>, show: bool, on_close: U) -> Result<RespoNode<T>, String> +where + U: Fn(DispatchFn<T>) -> Result<(), String> + 'static, + T: Clone + Debug, +{ + let close = Rc::new(on_close); + let close2 = close.clone(); + + Ok( + RespoNode::new_component( + "drawer", + div() + .style(RespoStyle::default().position(CssPosition::Absolute).to_owned()) + .children([if show { + div() + .class_list(&[ui_fullscreen(), ui_center(), css_backdrop()]) + .style(options.backdrop_style) + .on_click(move |e, dispatch| -> Result<(), String> { + if let RespoEvent::Click { original_event, .. } = e { + // stop propagation to prevent closing the drawer + original_event.stop_propagation(); + } + close(dispatch)?; + Ok(()) + }) + .children([div() + .class_list(&[ui_column(), ui_global(), css_card()]) + .style(RespoStyle::default().padding(0.0).line_height(CssLineHeight::Px(32.0)).to_owned()) + .style(options.card_style) + .on_click(move |e, _dispatch| -> Result<(), String> { + // nothing to do + if let RespoEvent::Click { original_event, .. } = e { + // stop propagation to prevent closing the drawer + original_event.stop_propagation(); + } + Ok(()) + }) + .children([div() + .class(ui_column()) + .children([ + div() + .class(ui_center()) + .children([span().inner_text(options.title.unwrap_or_else(|| "Drawer".to_owned())).to_owned()]) + .to_owned(), + space(None, Some(8)), + options.render.run(move |dispatch| -> Result<(), String> { + let close = close2.clone(); + close(dispatch)?; + Ok(()) + })?, + ]) + .to_owned()]) + .to_owned()]) + .to_owned() + } else { + span().attribute("data-name", "placeholder").to_owned() + }]) + .to_owned(), + ) + // .effect(&[show], effect_focus) + .effect(&[show], effect_fade) + .share_with_ref(), + ) +} + +/// provides the interfaces to component of custom drawer dialog +pub trait DrawerPluginInterface<T> +where + T: Debug + Clone + RespoAction, +{ + /// renders UI + fn render(&self) -> Result<RespoNode<T>, String> + where + T: Clone + Debug; + /// to show drawer + fn show(&self, dispatch: DispatchFn<T>) -> Result<(), String>; + /// to close drawer + fn close(&self, dispatch: DispatchFn<T>) -> Result<(), String>; + + fn new(states: StatesTree, options: DrawerOptions<T>) -> Result<Self, String> + where + Self: std::marker::Sized; + + /// share it with `Rc` + fn share_with_ref(&self) -> Rc<Self>; +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct DrawerPluginState { + show: bool, +} + +/// a drawer that you can render you down card body +#[derive(Debug, Clone)] +pub struct DrawerPlugin<T> +where + T: Clone + Debug, +{ + state: DrawerPluginState, + options: DrawerOptions<T>, + /// tracking content to display + cursor: Vec<String>, + phantom: PhantomData<T>, +} + +impl<T> DrawerPluginInterface<T> for DrawerPlugin<T> +where + T: Clone + Debug + RespoAction, +{ + fn render(&self) -> Result<RespoNode<T>, String> { + let cursor = self.cursor.clone(); + + comp_drawer(self.options.to_owned(), self.state.show, move |dispatch: DispatchFn<_>| { + let s = DrawerPluginState { show: false }; + dispatch.run_state(&cursor, s)?; + Ok(()) + }) + } + fn show(&self, dispatch: DispatchFn<T>) -> Result<(), String> { + let s = DrawerPluginState { show: true }; + dispatch.run_state(&self.cursor, s)?; + Ok(()) + } + fn close(&self, dispatch: DispatchFn<T>) -> Result<(), String> { + let s = DrawerPluginState { show: false }; + dispatch.run_state(&self.cursor, s)?; + Ok(()) + } + + fn new(states: StatesTree, options: DrawerOptions<T>) -> Result<Self, String> { + let cursor = states.path(); + let state: DrawerPluginState = states.data.cast_or_default()?; + + let instance = Self { + state, + options, + cursor, + phantom: PhantomData, + }; + + Ok(instance) + } + + fn share_with_ref(&self) -> Rc<Self> { + Rc::new(self.clone()) + } +} diff --git a/respo/src/respo/css.rs b/respo/src/respo/css.rs index 21d1044..3801533 100644 --- a/respo/src/respo/css.rs +++ b/respo/src/respo/css.rs @@ -302,7 +302,7 @@ impl Display for CssSize { Self::Px(v) => write!(f, "{}px", v), Self::Percent(v) => write!(f, "{}%", v), Self::Vw(v) => write!(f, "{}vw", v), - Self::Vh(v) => write!(f, "{}wh", v), + Self::Vh(v) => write!(f, "{}vh", v), Self::Custom(v) => write!(f, "{}", v), } }