476 lines
12 KiB
Rust
476 lines
12 KiB
Rust
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<G> {
|
|
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<G> {
|
|
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<T>,
|
|
}
|
|
|
|
impl<'a, T> From<&'a Signal<T>> for EditProps<'a, T> {
|
|
fn from(state: &'a Signal<T>) -> Self {
|
|
EditProps { state }
|
|
}
|
|
}
|
|
|
|
pub trait IntoEdit<'a, G: Html> {
|
|
fn edit(self, cx: Scope<'a>) -> View<G>;
|
|
}
|
|
|
|
impl<'a, G: Html, T: Editable<'a, G>> IntoEdit<'a, G> for &'a Signal<T>
|
|
where
|
|
EditProps<'a, T>: From<&'a Signal<T>>,
|
|
{
|
|
fn edit(self, cx: Scope<'a>) -> View<G> {
|
|
T::edit(cx, self.into())
|
|
}
|
|
}
|
|
|
|
pub trait Edit<'a, G: Html>: Sized {
|
|
fn edit(cx: Scope<'a>, props: EditProps<'a, Self>) -> View<G>;
|
|
}
|
|
|
|
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<G> {
|
|
E::edit(cx, props)
|
|
}
|
|
}
|
|
|
|
pub trait Editor<'a, G: Html, Type>: Sized {
|
|
fn edit(cx: Scope<'a>, props: EditProps<'a, Type>) -> View<G>;
|
|
}
|
|
|
|
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<G> {
|
|
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<G> {
|
|
view! { cx,
|
|
i { "Editor Unimplemented" }
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, G: Html, T: for<'b> Editable<'b, G>> Editable<'a, G> for Option<T> {
|
|
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<G> {
|
|
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<G> {
|
|
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<Item = T> + FromIterator<T> + Clone,
|
|
{
|
|
fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View<G> {
|
|
let vec = create_signal(
|
|
cx,
|
|
props
|
|
.state
|
|
.get_untracked()
|
|
.as_ref()
|
|
.clone()
|
|
.into_iter()
|
|
.map(|x| create_signal(cx, x))
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
|
|
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<T>| {
|
|
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<T>| {
|
|
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<T>
|
|
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<T>
|
|
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<K, V>
|
|
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<G> {
|
|
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> {
|
|
_t: PhantomData<T>,
|
|
}
|
|
|
|
impl<'a, G, T, U> Editor<'a, G, U> for LabeledEdit<T>
|
|
where
|
|
G: Html,
|
|
T: for<'b> Editable<'b, G> + Clone + 'a,
|
|
U: Into<WithLabel<T>> + From<WithLabel<T>> + Clone,
|
|
{
|
|
fn edit(cx: Scope<'a>, props: EditProps<'a, U>) -> View<G> {
|
|
let cloned: U = props.state.get_untracked().as_ref().clone();
|
|
let state: WithLabel<T> = 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<T: Clone> {
|
|
label: String,
|
|
inner: T,
|
|
}
|
|
|
|
impl<'a, G: Html, T: for<'b> Editable<'b, G> + Clone + 'a> Editable<'a, G> for WithLabel<T> {
|
|
type Editor = LabeledEdit<T>;
|
|
}
|
|
|
|
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<WithLabel<String>> for User {
|
|
fn from(l: WithLabel<String>) -> Self {
|
|
User(l.inner)
|
|
}
|
|
}
|
|
|
|
impl From<User> for WithLabel<String> {
|
|
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));
|