use std::{ collections::{HashMap, HashSet}, hash::Hash, marker::PhantomData, str::FromStr, }; use crate::components::Block; use log::debug; use paste::paste; use sycamore::prelude::*; use crate::components::{BlockProps, Button}; pub mod prelude { pub use super::{Edit, EditProps, Editable, Editor, IntoEdit}; pub use crate::{ components::Block, edit_enum, edit_fields, edit_struct, editable, link_fields, link_variants, }; pub use paste::paste; pub use sycamore::prelude::*; } #[macro_export] macro_rules! editable { ($type:ty => $editor:ty) => { impl<'a, G: Html> Editable<'a, G> for $type { type Editor = $editor; } }; } #[macro_export] macro_rules! edit_fields { ($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => { view! { $cx, $( p { label { ($name) } ($prop.edit($cx)) } )* } }; } #[macro_export] macro_rules! link_fields { ($cx:ident, $($field:ident),* $(,)? => $state:ident as $t:ident) => { $(let $field = create_signal($cx, $state.get_untracked().$field.clone());)* create_effect($cx, || { $state.set($t { $($field: $field.get().as_ref().clone(),)* ..Default::default() }); }); }; } #[macro_export] macro_rules! edit_struct { ($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => { paste! { pub struct [<$struct Edit>]; impl<'a, G: Html> Editor<'a, G, $struct> for [<$struct Edit>] { fn edit(cx: Scope<'a>, props: EditProps<'a, $struct>) -> View { let state = props.state; link_fields!(cx, $($prop,)* => state as $struct); view! { cx, Block(title=stringify!($struct).into()) { (edit_fields!(cx, $(($name, $prop),)*)) } } } } editable!($struct => [<$struct Edit>]); } }; } #[macro_export] macro_rules! link_variants { ($cx:ident, $selected:ident => $(($var_name:ident = $variant:ident: $var_type:ty)),* $(,)? => $state:ident as $t:ident) => { let $selected = create_signal($cx, String::from("0")); $(let $var_name = if let $t::$variant(v) = $state.get_untracked().as_ref().clone() { create_signal($cx, v.clone()) } else { create_signal($cx, <$var_type>::default()) };)* create_effect($cx, || { match $selected.get().as_str() { $(stringify!($var_name) => $state.set($t::$variant($var_name.get().as_ref().clone())),)* //_ => unreachable!() _ => {} } }); }; } #[macro_export] macro_rules! edit_enum { ($enum:ident => $selected:ident => $($var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => { paste! { pub struct [<$enum Edit>]; impl<'a, G: Html> Editor<'a, G, $enum> for [<$enum Edit>] { fn edit(cx: Scope<'a>, props: EditProps<'a, $enum>) -> View { let state = props.state; link_variants!(cx, $selected => $(($var_name = $variant: $var_type),)* => state as $enum ); view! { cx, Block(title=stringify!($enum).to_string()) { select(bind:value=$selected) { $(option(value={stringify!($var_name)}, selected=true) { (stringify!($variant)) })* } (match $selected.get().as_str() { $(stringify!($var_name) => $var_name.edit(cx),)* //_ => unreachable!() _ => view! { cx, } }) } } } } editable!($enum => [<$enum Edit>]); } }; } #[derive(Prop)] pub struct EditProps<'a, T> { pub state: &'a Signal, } impl<'a, T> From<&'a Signal> for EditProps<'a, T> { fn from(state: &'a Signal) -> Self { EditProps { state } } } pub trait IntoEdit<'a, G: Html> { fn edit(self, cx: Scope<'a>) -> View; } impl<'a, G: Html, T: Editable<'a, G>> IntoEdit<'a, G> for &'a Signal where EditProps<'a, T>: From<&'a Signal>, { fn edit(self, cx: Scope<'a>) -> View { T::edit(cx, self.into()) } } pub trait Edit<'a, G: Html>: Sized { fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View; } impl<'a, G, E, Type> Edit<'a, G> for Type where G: Html, E: Editor<'a, G, Type>, Type: Editable<'a, G, Editor = E>, { fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View { E::edit(cx, props) } } pub trait Editor<'a, G: Html, Type>: Sized { fn edit(cx: Scope<'a>, props: EditProps<'a, Type>) -> View; } pub trait Editable<'a, G: Html>: Sized { type Editor: Editor<'a, G, Self>; } pub struct StringEdit; impl<'a, G: Html> Editor<'a, G, String> for StringEdit { fn edit(cx: Scope<'a>, props: EditProps<'a, String>) -> View { view! { cx, input(bind:value=props.state) } } } editable!(String => StringEdit); pub struct StubEdit; impl<'a, G: Html, T> Editor<'a, G, T> for StubEdit where T: Editable<'a, G, Editor = StubEdit>, { fn edit(cx: Scope<'a>, _props: EditProps<'a, T>) -> View { view! { cx, i { "Editor Unimplemented" } } } } impl<'a, G: Html, T: for<'b> Editable<'b, G>> Editable<'a, G> for Option { type Editor = StubEdit; } pub struct InputEdit; impl<'a, G: Html, T> Editor<'a, G, T> for InputEdit where T: Editable<'a, G, Editor = InputEdit> + FromStr + ToString + Default, { fn edit(cx: Scope<'a>, props: EditProps<'a, T>) -> View { let value = create_signal(cx, props.state.get_untracked().to_string()); create_effect(cx, || { props .state .set((*value.get()).parse().unwrap_or(T::default())) }); view! { cx, input(bind:value=value) } } } editable!(i64 => InputEdit); editable!(i32 => InputEdit); editable!(isize => InputEdit); editable!(u64 => InputEdit); editable!(u32 => InputEdit); editable!(usize => InputEdit); editable!(f64 => InputEdit); editable!(f32 => InputEdit); pub struct BoolEdit; impl<'a, G: Html> Editor<'a, G, bool> for BoolEdit { fn edit(cx: Scope<'a>, props: EditProps<'a, bool>) -> View { view! { cx, input(type="checkbox", bind:checked=props.state) } } } editable!(bool => BoolEdit); pub struct VecEdit; impl<'a, G, T, I> Editor<'a, G, I> for VecEdit where G: Html, T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a, I: IntoIterator + FromIterator + Clone, { fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View { let vec = create_signal( cx, props .state .get_untracked() .as_ref() .clone() .into_iter() .map(|x| create_signal(cx, x)) .collect::>(), ); create_effect(cx, || { props.state.set( vec.get() .as_ref() .iter() .cloned() .map(|x| x.get().as_ref().clone()) .collect(), ) }); let onadd = move |_| vec.modify().push(create_signal(cx, T::default())); let onremove = move |item: &'a Signal| { move |_| { let cloned = vec.get().as_ref().clone(); vec.set( cloned .into_iter() .filter(|x| x.get().as_ref() != item.get().as_ref()) .collect(), ); } }; Block( cx, BlockProps { title: "List".into(), children: Children::new(cx, move |_| { view! { cx, div { Indexed( iterable=vec, view=move |cx: BoundedScope<'_, 'a>, x: &'a Signal| { view! { cx, (x.edit(cx)) Button(onclick=onremove(x), icon="mdi-delete".into()) br() } }, ) Button(onclick=onadd, text="Add new".into(), icon="mdi-plus".into()) } } }), }, ) } } impl<'a, G, T> Editable<'a, G> for Vec where G: Html, T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a, { type Editor = VecEdit; } impl<'a, G, T> Editable<'a, G> for HashSet where G: Html, T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a, { type Editor = VecEdit; } impl<'a, G, K, V> Editable<'a, G> for HashMap where G: Html, K: Clone + Hash + Eq, V: Clone, (K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a, { type Editor = VecEdit; } pub struct TupleEdit; impl<'a, G: Html, A: for<'b> Editable<'b, G> + Clone, B: for<'b> Editable<'b, G> + Clone> Editor<'a, G, (A, B)> for TupleEdit { fn edit(cx: Scope<'a>, props: EditProps<'a, (A, B)>) -> View { let state = props.state; let (a, b) = state.get_untracked().as_ref().clone(); let a = create_signal(cx, a.clone()); let b = create_signal(cx, b.clone()); create_effect(cx, || { props .state .set((a.get().as_ref().clone(), b.get().as_ref().clone())) }); view! { cx, Block(title="Tuple".into()) { (a.edit(cx)) (b.edit(cx)) } } } } pub struct LabeledEdit { _t: PhantomData, } impl<'a, G, T, U> Editor<'a, G, U> for LabeledEdit where G: Html, T: for<'b> Editable<'b, G> + Clone + 'a, U: Into> + From> + Clone, { fn edit(cx: Scope<'a>, props: EditProps<'a, U>) -> View { let cloned: U = props.state.get_untracked().as_ref().clone(); let state: WithLabel = cloned.into(); let label = state.label.clone(); let inner = create_signal(cx, state.inner.clone()); { let label = label.clone(); create_effect(cx, move || { props.state.set( WithLabel { label: label.clone(), inner: inner.get().as_ref().clone(), } .into(), ) }); } let label = create_signal(cx, label); view! { cx, div { (label.get()) (inner.edit(cx)) } } } } #[derive(Clone)] pub struct WithLabel { label: String, inner: T, } impl<'a, G: Html, T: for<'b> Editable<'b, G> + Clone + 'a> Editable<'a, G> for WithLabel { type Editor = LabeledEdit; } impl<'a, G, A, B> Editable<'a, G> for (A, B) where G: Html, A: for<'b> Editable<'b, G> + Clone, B: for<'b> Editable<'b, G> + Clone, { type Editor = TupleEdit; } pub struct User(String); impl From> for User { fn from(l: WithLabel) -> Self { User(l.inner) } } impl From for WithLabel { fn from(u: User) -> Self { WithLabel { label: "User".into(), inner: u.0, } } } #[derive(Default, Clone, Debug)] pub struct Test { inner: TestInner, inner2: TestInner, } edit_struct!(Test => ("Inner", inner), ("Inner 2", inner2)); #[derive(Default, Clone, Debug)] pub struct TestInner { some_text: String, some_number: usize, } edit_struct!(TestInner => ("Text", some_text), ("Number", some_number));