Compare commits
1 Commits
2ddbfc2fdb
...
a0a3ebb856
Author | SHA1 | Date |
---|---|---|
Daan Vanoverloop | a0a3ebb856 |
|
@ -274,15 +274,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "convert_case"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
@ -478,17 +469,6 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "displaydoc"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenv"
|
name = "dotenv"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
@ -1156,27 +1136,11 @@ dependencies = [
|
||||||
name = "lan_party_core"
|
name = "lan_party_core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
|
||||||
"lan_party_macros",
|
|
||||||
"log",
|
|
||||||
"paste",
|
"paste",
|
||||||
"rocket",
|
"rocket",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"sycamore",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lan_party_macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"convert_case",
|
|
||||||
"paste",
|
|
||||||
"quote",
|
|
||||||
"sycamore",
|
|
||||||
"syn",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
members = [
|
members = [
|
||||||
"backend",
|
"backend",
|
||||||
"core",
|
"core",
|
||||||
"web",
|
"web"
|
||||||
"macros"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,10 @@ edition = "2021"
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
openapi = ["dep:schemars"]
|
openapi = ["dep:schemars"]
|
||||||
rocket = ["dep:rocket", "serde"]
|
rocket = ["dep:rocket", "serde"]
|
||||||
sycamore = ["dep:sycamore", "dep:web-sys", "dep:lan_party_macros"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
schemars = { version = "0.8", optional = true }
|
schemars = { version = "0.8", optional = true }
|
||||||
serde = { version = "1", optional = true }
|
serde = { version = "1", optional = true }
|
||||||
rocket = { version = "0.5.0-rc.2", features = ["json"], optional = true }
|
rocket = { version = "0.5.0-rc.2", features = ["json"], optional = true }
|
||||||
sycamore = { version = "0.8.1", features = ["serde", "suspense"], optional = true }
|
|
||||||
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"], optional = true }
|
|
||||||
lan_party_macros = { path = "../macros", optional = true }
|
|
||||||
paste = "1"
|
paste = "1"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
displaydoc = "0.2"
|
|
||||||
log = "0.4"
|
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
//use log::debug;
|
|
||||||
use sycamore::prelude::*;
|
|
||||||
use web_sys::Event;
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct ButtonProps<F: FnMut(Event)> {
|
|
||||||
pub onclick: F,
|
|
||||||
#[builder(default)]
|
|
||||||
pub text: String,
|
|
||||||
#[builder(default)]
|
|
||||||
pub icon: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: ButtonProps<F>) -> View<G> {
|
|
||||||
let mut icon_class = String::from("mdi ");
|
|
||||||
|
|
||||||
if !props.icon.is_empty() {
|
|
||||||
icon_class.push_str(&props.icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
button(on:click=props.onclick) {
|
|
||||||
span(class=icon_class)
|
|
||||||
span { (props.text) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct TableProps<'a, G: Html> {
|
|
||||||
pub headers: Vec<String>,
|
|
||||||
|
|
||||||
pub children: Children<'a, G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
|
||||||
let children = props.children.call(cx);
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
table {
|
|
||||||
thead {
|
|
||||||
tr {
|
|
||||||
(View::new_fragment(props.headers.iter().cloned().map(|header| view! { cx,
|
|
||||||
th(scope="col") {
|
|
||||||
(header)
|
|
||||||
}
|
|
||||||
}).collect()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
(children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct BlockProps<'a, G: Html> {
|
|
||||||
pub title: String,
|
|
||||||
pub children: Children<'a, G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Block<'a, G: Html>(cx: Scope<'a>, props: BlockProps<'a, G>) -> View<G> {
|
|
||||||
let children = props.children.call(cx);
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
details {
|
|
||||||
summary { (props.title) }
|
|
||||||
p { (children) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
476
core/src/edit.rs
476
core/src/edit.rs
|
@ -1,476 +0,0 @@
|
||||||
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 + std::fmt::Debug + '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 + std::fmt::Debug + '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 + std::fmt::Debug + '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 + std::fmt::Debug + '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));
|
|
|
@ -1,10 +1,4 @@
|
||||||
#[cfg(feature = "sycamore")]
|
|
||||||
use crate::edit::prelude::*;
|
|
||||||
use crate::util::PartyError;
|
use crate::util::PartyError;
|
||||||
#[cfg(feature = "sycamore")]
|
|
||||||
use crate::view::prelude::*;
|
|
||||||
#[cfg(feature = "sycamore")]
|
|
||||||
use lan_party_macros::{WebEdit, WebView};
|
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -22,10 +16,9 @@ pub struct EventOutcome {
|
||||||
/// # Event
|
/// # Event
|
||||||
///
|
///
|
||||||
/// An event in which participants can win or lose points
|
/// An event in which participants can win or lose points
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebView))]
|
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
/// Has this event concluded?
|
/// Has this event concluded?
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
@ -36,6 +29,7 @@ pub struct Event {
|
||||||
/// Description of the event
|
/// Description of the event
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
/// Event type
|
||||||
pub event_type: EventType,
|
pub event_type: EventType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,11 +51,8 @@ impl Event {
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct EventSpec {
|
pub struct EventSpec {
|
||||||
/// Name of the event
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Description of the event
|
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub event_type: EventTypeSpec,
|
pub event_type: EventTypeSpec,
|
||||||
}
|
}
|
||||||
|
@ -75,7 +66,6 @@ macro_rules! events {
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum EventTypeSpec {
|
pub enum EventTypeSpec {
|
||||||
$($name($module::[<$name Spec>]),)*
|
$($name($module::[<$name Spec>]),)*
|
||||||
}
|
}
|
||||||
|
@ -86,7 +76,6 @@ macro_rules! events {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum EventUpdate {
|
pub enum EventUpdate {
|
||||||
$($name($module::[<$name Update>]),)*
|
$($name($module::[<$name Update>]),)*
|
||||||
}
|
}
|
||||||
|
@ -94,10 +83,9 @@ macro_rules! events {
|
||||||
/// # EventType
|
/// # EventType
|
||||||
///
|
///
|
||||||
/// An enumeration of event types
|
/// An enumeration of event types
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum EventType {
|
pub enum EventType {
|
||||||
$($name($module::$name),)*
|
$($name($module::$name),)*
|
||||||
}
|
}
|
||||||
|
@ -161,10 +149,9 @@ pub trait EventTrait {
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct Test {
|
pub struct Test {
|
||||||
pub num_players: i64,
|
pub num_players: i64,
|
||||||
}
|
}
|
||||||
|
@ -172,15 +159,13 @@ pub mod test {
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct TestSpec {
|
pub struct TestSpec {
|
||||||
pub num_players: i64,
|
pub num_players: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct TestUpdate {
|
pub struct TestUpdate {
|
||||||
pub win_game: bool,
|
pub win_game: bool,
|
||||||
}
|
}
|
||||||
|
@ -220,10 +205,9 @@ pub mod team_game {
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct TeamGame {
|
pub struct TeamGame {
|
||||||
/// Map of teams with a name as key and an array of players as value
|
/// Map of teams with a name as key and an array of players as value
|
||||||
pub teams: HashMap<String, Vec<String>>,
|
pub teams: HashMap<String, Vec<String>>,
|
||||||
|
@ -235,7 +219,6 @@ pub mod team_game {
|
||||||
#[derive(Clone, Debug, PartialEq, Default)]
|
#[derive(Clone, Debug, PartialEq, Default)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct TeamGameSpec {
|
pub struct TeamGameSpec {
|
||||||
/// Map of teams with a name as key and an array of players as value
|
/// Map of teams with a name as key and an array of players as value
|
||||||
pub teams: HashMap<String, Vec<String>>,
|
pub teams: HashMap<String, Vec<String>>,
|
||||||
|
@ -256,38 +239,21 @@ pub mod team_game {
|
||||||
pub lose_rewards: Vec<i64>,
|
pub lose_rewards: Vec<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct TeamGameUpdateSetTeam {
|
|
||||||
pub team: String,
|
|
||||||
pub members: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum TeamGameUpdateInner {
|
pub enum TeamGameUpdateInner {
|
||||||
/// Add or replace a team with the given name and array of members
|
/// Add or replace a team with the given name and array of members
|
||||||
SetTeam(TeamGameUpdateSetTeam),
|
SetTeam { team: String, members: Vec<String> },
|
||||||
|
|
||||||
/// Remove team with given name
|
/// Remove team with given name
|
||||||
RemoveTeam(String),
|
RemoveTeam(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TeamGameUpdateInner {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetTeam(TeamGameUpdateSetTeam::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum TeamGameFfaInheritedUpdate {
|
pub enum TeamGameFfaInheritedUpdate {
|
||||||
/// Change the ranking and scores
|
/// Change the ranking and scores
|
||||||
Ranking(FreeForAllGameUpdateRanking),
|
Ranking(FreeForAllGameUpdateRanking),
|
||||||
|
@ -295,34 +261,17 @@ pub mod team_game {
|
||||||
Rewards(FreeForAllGameUpdateRewards),
|
Rewards(FreeForAllGameUpdateRewards),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TeamGameFfaInheritedUpdate {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum TeamGameUpdate {
|
pub enum TeamGameUpdate {
|
||||||
/// # Team
|
|
||||||
///
|
|
||||||
/// Team specific updates
|
/// Team specific updates
|
||||||
Team(TeamGameUpdateInner),
|
Team(TeamGameUpdateInner),
|
||||||
/// # Other
|
|
||||||
///
|
|
||||||
/// Inherited from FreeForAllGame
|
/// Inherited from FreeForAllGame
|
||||||
Ffa(TeamGameFfaInheritedUpdate),
|
Ffa(TeamGameFfaInheritedUpdate),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TeamGameUpdate {
|
|
||||||
fn default() -> Self {
|
|
||||||
TeamGameUpdate::Team(TeamGameUpdateInner::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventTrait for TeamGame {
|
impl EventTrait for TeamGame {
|
||||||
type Spec = TeamGameSpec;
|
type Spec = TeamGameSpec;
|
||||||
type Update = TeamGameUpdate;
|
type Update = TeamGameUpdate;
|
||||||
|
@ -351,12 +300,12 @@ pub mod team_game {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TeamGameUpdate::Team(update) => match update {
|
TeamGameUpdate::Team(update) => match update {
|
||||||
TeamGameUpdateInner::SetTeam(u) => {
|
TeamGameUpdateInner::SetTeam { team, members } => {
|
||||||
self.ffa_game
|
self.ffa_game
|
||||||
.apply_update(FreeForAllGameUpdate::Participants(
|
.apply_update(FreeForAllGameUpdate::Participants(
|
||||||
FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()),
|
FreeForAllGameUpdateParticipants::AddParticipant(team.clone()),
|
||||||
))?;
|
))?;
|
||||||
self.teams.insert(u.team, u.members);
|
self.teams.insert(team, members);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
TeamGameUpdateInner::RemoveTeam(team) => {
|
TeamGameUpdateInner::RemoveTeam(team) => {
|
||||||
|
@ -398,7 +347,6 @@ pub mod free_for_all_game {
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameRanking {
|
pub enum FreeForAllGameRanking {
|
||||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||||
/// place, etc.)
|
/// place, etc.)
|
||||||
|
@ -407,12 +355,6 @@ pub mod free_for_all_game {
|
||||||
Scores(HashMap<String, i64>),
|
Scores(HashMap<String, i64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeForAllGameRanking {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(Vec::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FreeForAllGameRanking {
|
impl FreeForAllGameRanking {
|
||||||
pub fn is_valid(&self, participants: &HashSet<String>) -> bool {
|
pub fn is_valid(&self, participants: &HashSet<String>) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
@ -422,10 +364,9 @@ pub mod free_for_all_game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct FreeForAllGame {
|
pub struct FreeForAllGame {
|
||||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||||
/// place, etc.)
|
/// place, etc.)
|
||||||
|
@ -447,7 +388,6 @@ pub mod free_for_all_game {
|
||||||
#[derive(Clone, Debug, PartialEq, Default)]
|
#[derive(Clone, Debug, PartialEq, Default)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub struct FreeForAllGameSpec {
|
pub struct FreeForAllGameSpec {
|
||||||
/// Array of user ids that participate in the game
|
/// Array of user ids that participate in the game
|
||||||
pub participants: HashSet<String>,
|
pub participants: HashSet<String>,
|
||||||
|
@ -465,7 +405,6 @@ pub mod free_for_all_game {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdateRanking {
|
pub enum FreeForAllGameUpdateRanking {
|
||||||
/// Replace the current ranking with the given ranking
|
/// Replace the current ranking with the given ranking
|
||||||
SetRanking(FreeForAllGameRanking),
|
SetRanking(FreeForAllGameRanking),
|
||||||
|
@ -474,16 +413,9 @@ pub mod free_for_all_game {
|
||||||
ScoreDelta(HashMap<String, i64>),
|
ScoreDelta(HashMap<String, i64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdateRanking {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetRanking(FreeForAllGameRanking::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdateRewards {
|
pub enum FreeForAllGameUpdateRewards {
|
||||||
/// Set rewards for winning the game
|
/// Set rewards for winning the game
|
||||||
SetWinRewards(Vec<i64>),
|
SetWinRewards(Vec<i64>),
|
||||||
|
@ -492,16 +424,9 @@ pub mod free_for_all_game {
|
||||||
SetLoseRewards(Vec<i64>),
|
SetLoseRewards(Vec<i64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdateRewards {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetWinRewards(Vec::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdateParticipants {
|
pub enum FreeForAllGameUpdateParticipants {
|
||||||
/// Set list of participants participating in the game
|
/// Set list of participants participating in the game
|
||||||
SetParticipants(HashSet<String>),
|
SetParticipants(HashSet<String>),
|
||||||
|
@ -513,17 +438,10 @@ pub mod free_for_all_game {
|
||||||
RemoveParticipant(String),
|
RemoveParticipant(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdateParticipants {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::SetParticipants(HashSet::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
|
||||||
pub enum FreeForAllGameUpdate {
|
pub enum FreeForAllGameUpdate {
|
||||||
/// Change the ranking and scores
|
/// Change the ranking and scores
|
||||||
Ranking(FreeForAllGameUpdateRanking),
|
Ranking(FreeForAllGameUpdateRanking),
|
||||||
|
@ -533,12 +451,6 @@ pub mod free_for_all_game {
|
||||||
Participants(FreeForAllGameUpdateParticipants),
|
Participants(FreeForAllGameUpdateParticipants),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeForAllGameUpdate {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventTrait for FreeForAllGame {
|
impl EventTrait for FreeForAllGame {
|
||||||
type Spec = FreeForAllGameSpec;
|
type Spec = FreeForAllGameSpec;
|
||||||
type Update = FreeForAllGameUpdate;
|
type Update = FreeForAllGameUpdate;
|
||||||
|
@ -648,9 +560,3 @@ impl Default for EventTypeSpec {
|
||||||
Self::Test(test::TestSpec::default())
|
Self::Test(test::TestSpec::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EventUpdate {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Test(test::TestUpdate::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,13 +2,4 @@ pub mod event;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
#[cfg(feature = "sycamore")]
|
|
||||||
pub mod edit;
|
|
||||||
|
|
||||||
#[cfg(feature = "sycamore")]
|
|
||||||
pub mod view;
|
|
||||||
|
|
||||||
#[cfg(feature = "sycamore")]
|
|
||||||
pub mod components;
|
|
||||||
|
|
||||||
pub use util::PartyError;
|
pub use util::PartyError;
|
||||||
|
|
205
core/src/view.rs
205
core/src/view.rs
|
@ -1,205 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
hash::Hash,
|
|
||||||
marker::PhantomData,
|
|
||||||
};
|
|
||||||
use sycamore::view::IntoView as _;
|
|
||||||
|
|
||||||
use sycamore::{builder::prelude::*, prelude::*};
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::{IntoView, ViewProps, Viewable, Viewer, WebView};
|
|
||||||
pub use crate::components::Block;
|
|
||||||
pub use paste::paste;
|
|
||||||
pub use sycamore::prelude::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
use crate::components::{Block, BlockProps};
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! viewable {
|
|
||||||
($type:ty => $viewer:ty) => {
|
|
||||||
impl<'a, G: Html> Viewable<'a, G> for $type {
|
|
||||||
type Viewer = $viewer;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Prop)]
|
|
||||||
pub struct ViewProps<'a, T> {
|
|
||||||
pub state: &'a T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> From<&'a T> for ViewProps<'a, T> {
|
|
||||||
fn from(state: &'a T) -> Self {
|
|
||||||
ViewProps { state }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoView<'a, G: Html> {
|
|
||||||
fn view(self, cx: Scope<'a>) -> View<G>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G: Html, T: Viewable<'a, G>> IntoView<'a, G> for &'a T
|
|
||||||
where
|
|
||||||
ViewProps<'a, T>: From<&'a T>,
|
|
||||||
{
|
|
||||||
fn view(self, cx: Scope<'a>) -> View<G> {
|
|
||||||
T::view(cx, self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait WebView<'a, G: Html>: Sized {
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, Self>) -> View<G>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G, E, Type> WebView<'a, G> for Type
|
|
||||||
where
|
|
||||||
G: Html,
|
|
||||||
E: Viewer<'a, G, Type>,
|
|
||||||
Type: Viewable<'a, G, Viewer = E>,
|
|
||||||
{
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, Self>) -> View<G> {
|
|
||||||
E::view(cx, props)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Viewer<'a, G: Html, Type>: Sized {
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, Type>) -> View<G>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Viewable<'a, G: Html>: Sized {
|
|
||||||
type Viewer: Viewer<'a, G, Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StringView;
|
|
||||||
|
|
||||||
impl<'a, G: Html, T: 'a> Viewer<'a, G, T> for StringView
|
|
||||||
where
|
|
||||||
T: sycamore::view::IntoView<G>,
|
|
||||||
{
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, T>) -> View<G> {
|
|
||||||
view! { cx,
|
|
||||||
(props.state.create())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewable!(String => StringView);
|
|
||||||
|
|
||||||
viewable!(i64 => StringView);
|
|
||||||
viewable!(i32 => StringView);
|
|
||||||
viewable!(isize => StringView);
|
|
||||||
|
|
||||||
viewable!(u64 => StringView);
|
|
||||||
viewable!(u32 => StringView);
|
|
||||||
viewable!(usize => StringView);
|
|
||||||
|
|
||||||
viewable!(f64 => StringView);
|
|
||||||
viewable!(f32 => StringView);
|
|
||||||
|
|
||||||
pub struct BoolView;
|
|
||||||
|
|
||||||
impl<'a, G: Html> Viewer<'a, G, bool> for BoolView {
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, bool>) -> View<G> {
|
|
||||||
view! { cx,
|
|
||||||
input(type="checkbox", checked=*props.state, disabled=true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewable!(bool => BoolView);
|
|
||||||
|
|
||||||
pub struct VecView;
|
|
||||||
|
|
||||||
impl<'a, G, T, I> Viewer<'a, G, I> for VecView
|
|
||||||
where
|
|
||||||
G: Html,
|
|
||||||
T: for<'b> Viewable<'b, G> + Clone + PartialEq + 'a,
|
|
||||||
I: IntoIterator<Item = T> + FromIterator<T> + Clone,
|
|
||||||
{
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, I>) -> View<G> {
|
|
||||||
Block(
|
|
||||||
cx,
|
|
||||||
BlockProps {
|
|
||||||
title: "List".into(),
|
|
||||||
children: Children::new(cx, move |_| {
|
|
||||||
view! { cx,
|
|
||||||
//Block(title="List".into()) {
|
|
||||||
div {
|
|
||||||
(View::new_fragment(props.state.clone().into_iter().map(|x| create_ref(cx, x)).map(|x| {
|
|
||||||
div().c(x.view(cx)).c(br()).view(cx)
|
|
||||||
}).collect()))
|
|
||||||
}
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G, T> Viewable<'a, G> for Vec<T>
|
|
||||||
where
|
|
||||||
G: Html,
|
|
||||||
T: for<'b> Viewable<'b, G> + Clone + PartialEq + 'a,
|
|
||||||
{
|
|
||||||
type Viewer = VecView;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G, T> Viewable<'a, G> for HashSet<T>
|
|
||||||
where
|
|
||||||
G: Html,
|
|
||||||
T: for<'b> Viewable<'b, G> + Clone + PartialEq + Hash + Eq + 'a,
|
|
||||||
{
|
|
||||||
type Viewer = VecView;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G, K, V> Viewable<'a, G> for HashMap<K, V>
|
|
||||||
where
|
|
||||||
G: Html,
|
|
||||||
K: Clone + Hash + Eq,
|
|
||||||
V: Clone,
|
|
||||||
(K, V): for<'b> Viewable<'b, G> + Clone + PartialEq + Hash + Eq + 'a,
|
|
||||||
{
|
|
||||||
type Viewer = VecView;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TupleView;
|
|
||||||
|
|
||||||
impl<'a, G: Html, A: for<'b> Viewable<'b, G> + Clone, B: for<'b> Viewable<'b, G> + Clone>
|
|
||||||
Viewer<'a, G, (A, B)> for TupleView
|
|
||||||
{
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, (A, B)>) -> View<G> {
|
|
||||||
view! { cx,
|
|
||||||
Block(title="Tuple".into()) {
|
|
||||||
(props.state.0.view(cx))
|
|
||||||
(props.state.1.view(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G, A, B> Viewable<'a, G> for (A, B)
|
|
||||||
where
|
|
||||||
G: Html,
|
|
||||||
A: for<'b> Viewable<'b, G> + Clone,
|
|
||||||
B: for<'b> Viewable<'b, G> + Clone,
|
|
||||||
{
|
|
||||||
type Viewer = TupleView;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OptionView;
|
|
||||||
|
|
||||||
impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewer<'a, G, Option<T>> for OptionView {
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, Option<T>>) -> View<G> {
|
|
||||||
match props.state {
|
|
||||||
Some(x) => view! { cx, (x.view(cx)) },
|
|
||||||
None => view! { cx, "None" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G: Html, T: for<'b> Viewable<'b, G> + Clone> Viewable<'a, G> for Option<T> {
|
|
||||||
type Viewer = OptionView;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "lan_party_macros"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
|
||||||
quote = "1.0"
|
|
||||||
sycamore = { version = "0.8.1", features = ["serde", "suspense"] }
|
|
||||||
paste = "1.0"
|
|
||||||
convert_case = "0.6"
|
|
||||||
#lan_party_core = { path = "../core", features = ["sycamore"] }
|
|
|
@ -1,505 +0,0 @@
|
||||||
mod edit;
|
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
|
||||||
use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum Documentation {
|
|
||||||
Title(String),
|
|
||||||
Description(String),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Documentation {
|
|
||||||
fn parse(attr: &Attribute) -> Documentation {
|
|
||||||
if !attr.path.is_ident("doc") {
|
|
||||||
return Documentation::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = attr.tokens.to_string();
|
|
||||||
|
|
||||||
let text = text.trim_matches(|c: char| c == '\"' || c == '=' || c.is_whitespace());
|
|
||||||
|
|
||||||
match text.get(0..1) {
|
|
||||||
Some("#") => Documentation::Title(text.trim_matches('#').trim().into()),
|
|
||||||
Some(&_) => Documentation::Description(text.trim().into()),
|
|
||||||
None => Documentation::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_title_description(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
|
||||||
let docs: Vec<_> = attrs.iter().map(Documentation::parse).collect();
|
|
||||||
|
|
||||||
let mut title = None;
|
|
||||||
let mut description: Option<String> = None;
|
|
||||||
|
|
||||||
for doc in docs {
|
|
||||||
match doc {
|
|
||||||
Documentation::Title(t) => title = Some(t),
|
|
||||||
Documentation::Description(d) => {
|
|
||||||
if description.is_some() {
|
|
||||||
description.as_mut().unwrap().push(' ');
|
|
||||||
} else {
|
|
||||||
let _ = description.insert(String::new());
|
|
||||||
}
|
|
||||||
description.as_mut().unwrap().push_str(&d);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(title, description)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ItemProps {
|
|
||||||
name: Ident,
|
|
||||||
title: String,
|
|
||||||
description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StructField {
|
|
||||||
name: Ident,
|
|
||||||
name_str: String,
|
|
||||||
title: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EnumVariant {
|
|
||||||
variant: Ident,
|
|
||||||
variant_lower: Ident,
|
|
||||||
inner: Type,
|
|
||||||
title: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|
||||||
let ItemProps {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let fields = s.fields.iter().map(|f| {
|
|
||||||
let name = f
|
|
||||||
.ident
|
|
||||||
.as_ref()
|
|
||||||
.expect("each struct field must be named")
|
|
||||||
.clone();
|
|
||||||
let name_str = name.to_string();
|
|
||||||
let (title, description) = get_title_description(&f.attrs);
|
|
||||||
|
|
||||||
StructField {
|
|
||||||
name_str,
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let fields_view = fields.clone().map(|f| {
|
|
||||||
let title = f.title.unwrap_or(f.name_str.to_case(Case::Title));
|
|
||||||
let description = if let Some(d) = f.description {
|
|
||||||
quote! {
|
|
||||||
span(class="description") {
|
|
||||||
" " #d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {}
|
|
||||||
};
|
|
||||||
let name = f.name;
|
|
||||||
quote! {
|
|
||||||
p {
|
|
||||||
label { (#title) br() #description }
|
|
||||||
(#name.edit(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let signals = fields.clone().map(|f| {
|
|
||||||
let name = f.name;
|
|
||||||
quote! {
|
|
||||||
let #name = create_signal(cx, state.get_untracked().#name.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let effect_fields = fields.clone().map(|f| {
|
|
||||||
let name = f.name;
|
|
||||||
quote! {
|
|
||||||
#name: #name.get().as_ref().clone()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let state = props.state;
|
|
||||||
|
|
||||||
#(#signals)*
|
|
||||||
|
|
||||||
create_effect(cx, || {
|
|
||||||
state.set(#name {
|
|
||||||
#(#effect_fields,)*
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
Block(title=#title.to_string()) {
|
|
||||||
p(class="description") {
|
|
||||||
#description
|
|
||||||
}
|
|
||||||
#(#fields_view)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|
||||||
let ItemProps {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let variants = e.variants.iter().map(|v| {
|
|
||||||
let variant = v.ident.clone();
|
|
||||||
|
|
||||||
let inner = match &v.fields {
|
|
||||||
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(),
|
|
||||||
_ => unimplemented!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (title, description) = get_title_description(&v.attrs);
|
|
||||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
|
||||||
EnumVariant {
|
|
||||||
variant_lower,
|
|
||||||
variant,
|
|
||||||
inner,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let first = variants.clone().next().unwrap().variant_lower;
|
|
||||||
let first_str = first.to_string();
|
|
||||||
|
|
||||||
let options = variants.clone().map(|v| {
|
|
||||||
let lower = v.variant_lower;
|
|
||||||
let title = v
|
|
||||||
.title
|
|
||||||
.unwrap_or(v.variant.to_string().to_case(Case::Title));
|
|
||||||
let selected = first == lower;
|
|
||||||
quote! { option(value={stringify!(#lower)}, selected=#selected) { (#title) } }
|
|
||||||
});
|
|
||||||
|
|
||||||
let view_match = variants.clone().map(|v| {
|
|
||||||
let lower = v.variant_lower;
|
|
||||||
let lower_str = format!("{}", lower);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#lower_str => #lower.edit(cx)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let view_description = variants.clone().map(|v| {
|
|
||||||
let lower = v.variant_lower;
|
|
||||||
let lower_str = format!("{}", lower);
|
|
||||||
let description = if let Some(d) = v.description {
|
|
||||||
quote! {
|
|
||||||
span(class="description") {
|
|
||||||
" " #d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {}
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#lower_str => view! { cx, #description }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let signals = variants.clone().map(
|
|
||||||
|EnumVariant {
|
|
||||||
variant,
|
|
||||||
variant_lower,
|
|
||||||
inner,
|
|
||||||
..
|
|
||||||
}| {
|
|
||||||
quote! {
|
|
||||||
let #variant_lower = if let #name::#variant(v) = state.get_untracked().as_ref().clone() {
|
|
||||||
create_signal(cx, v.clone())
|
|
||||||
} else {
|
|
||||||
create_signal(cx, <#inner>::default())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let effect_match = variants.clone().map(|v| {
|
|
||||||
let lower = v.variant_lower;
|
|
||||||
let variant = v.variant;
|
|
||||||
let lower_str = format!("{}", lower);
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#lower_str => state.set(#name::#variant(#lower.get().as_ref().clone()))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let state = props.state;
|
|
||||||
|
|
||||||
let selected = create_signal(cx, String::from(#first_str));
|
|
||||||
|
|
||||||
#(#signals)*
|
|
||||||
|
|
||||||
create_effect(cx, || {
|
|
||||||
match selected.get().as_str() {
|
|
||||||
#(#effect_match,)*
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
Block(title=#title.to_string()) {
|
|
||||||
p(class="description") {
|
|
||||||
#description
|
|
||||||
}
|
|
||||||
select(bind:value=selected) {
|
|
||||||
#(#options)*
|
|
||||||
}
|
|
||||||
(match selected.get().as_str() {
|
|
||||||
#(#view_description,)*
|
|
||||||
_ => view! { cx, }
|
|
||||||
})
|
|
||||||
(match selected.get().as_str() {
|
|
||||||
#(#view_match,)*
|
|
||||||
_ => view! { cx, }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn struct_view(props: &ItemProps, s: DataStruct) -> TokenStream2 {
|
|
||||||
let ItemProps {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let fields = s.fields.iter().map(|f| {
|
|
||||||
let name = f
|
|
||||||
.ident
|
|
||||||
.as_ref()
|
|
||||||
.expect("each struct field must be named")
|
|
||||||
.clone();
|
|
||||||
let name_str = name.to_string();
|
|
||||||
let (title, description) = get_title_description(&f.attrs);
|
|
||||||
|
|
||||||
StructField {
|
|
||||||
name_str,
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let refs = fields.clone().map(|f| {
|
|
||||||
let name = f.name;
|
|
||||||
quote! {
|
|
||||||
let #name = create_ref(cx, state.#name.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let fields_view = fields.clone().map(|f| {
|
|
||||||
let title = f.title.unwrap_or(f.name_str.to_case(Case::Title));
|
|
||||||
let description = if let Some(d) = f.description {
|
|
||||||
quote! {
|
|
||||||
span(class="description") {
|
|
||||||
" " #d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {}
|
|
||||||
};
|
|
||||||
let name = f.name;
|
|
||||||
quote! {
|
|
||||||
p {
|
|
||||||
label { (#title) br() #description }
|
|
||||||
(#name.view(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let state = props.state;
|
|
||||||
|
|
||||||
#(#refs)*
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
Block(title=#title.to_string()) {
|
|
||||||
p(class="description") {
|
|
||||||
#description
|
|
||||||
}
|
|
||||||
#(#fields_view)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enum_view(props: &ItemProps, e: DataEnum) -> TokenStream2 {
|
|
||||||
let ItemProps {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let variants = e.variants.iter().map(|v| {
|
|
||||||
let variant = v.ident.clone();
|
|
||||||
|
|
||||||
let inner = match &v.fields {
|
|
||||||
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(),
|
|
||||||
_ => unimplemented!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (title, description) = get_title_description(&v.attrs);
|
|
||||||
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
|
||||||
EnumVariant {
|
|
||||||
variant_lower,
|
|
||||||
variant,
|
|
||||||
inner,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let view_match = variants.clone().map(|v| {
|
|
||||||
let variant = v.variant;
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#name::#variant(x) => x.view(cx)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let view_description = variants.clone().map(|v| {
|
|
||||||
let variant = v.variant;
|
|
||||||
let description = if let Some(d) = v.description {
|
|
||||||
quote! {
|
|
||||||
span(class="description") {
|
|
||||||
" " #d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {}
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#name::#variant(_) => view! { cx, #description }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let state = props.state;
|
|
||||||
|
|
||||||
view! { cx,
|
|
||||||
Block(title=#title.to_string()) {
|
|
||||||
p(class="description") {
|
|
||||||
#description
|
|
||||||
}
|
|
||||||
(match state {
|
|
||||||
#(#view_description,)*
|
|
||||||
_ => view! { cx, }
|
|
||||||
})
|
|
||||||
(match state {
|
|
||||||
#(#view_match,)*
|
|
||||||
_ => view! { cx, }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(WebEdit)]
|
|
||||||
pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
|
||||||
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
|
||||||
|
|
||||||
let name = input.ident;
|
|
||||||
let edit_ident = format_ident!("{}Edit", name);
|
|
||||||
|
|
||||||
let (t, d) = get_title_description(&input.attrs);
|
|
||||||
|
|
||||||
let title = t.unwrap_or(name.to_string());
|
|
||||||
let description = d;
|
|
||||||
|
|
||||||
let props = ItemProps {
|
|
||||||
name: name.clone(),
|
|
||||||
title: title.clone(),
|
|
||||||
description: description.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inner = match input.data {
|
|
||||||
syn::Data::Struct(s) => struct_edit(&props, s),
|
|
||||||
syn::Data::Enum(e) => enum_edit(&props, e),
|
|
||||||
_ => unimplemented!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = quote! {
|
|
||||||
pub struct #edit_ident;
|
|
||||||
|
|
||||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
|
||||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
|
||||||
#inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G: Html> Editable<'a, G> for #name {
|
|
||||||
type Editor = #edit_ident;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TokenStream::from(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(WebView)]
|
|
||||||
pub fn web_view(tokens: TokenStream) -> TokenStream {
|
|
||||||
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
|
||||||
|
|
||||||
let name = input.ident;
|
|
||||||
let view_ident = format_ident!("{}View", name);
|
|
||||||
|
|
||||||
let (t, d) = get_title_description(&input.attrs);
|
|
||||||
|
|
||||||
let title = t.unwrap_or(name.to_string());
|
|
||||||
let description = d;
|
|
||||||
|
|
||||||
let props = ItemProps {
|
|
||||||
name: name.clone(),
|
|
||||||
title: title.clone(),
|
|
||||||
description: description.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inner = match input.data {
|
|
||||||
syn::Data::Struct(s) => struct_view(&props, s),
|
|
||||||
syn::Data::Enum(e) => enum_view(&props, e),
|
|
||||||
_ => unimplemented!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = quote! {
|
|
||||||
pub struct #view_ident;
|
|
||||||
|
|
||||||
impl<'a, G: Html> Viewer<'a, G, #name> for #view_ident {
|
|
||||||
fn view(cx: Scope<'a>, props: ViewProps<'a, #name>) -> View<G> {
|
|
||||||
#inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, G: Html> Viewable<'a, G> for #name {
|
|
||||||
type Viewer = #view_ident;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TokenStream::from(res)
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ yew = "0.19"
|
||||||
yew-router = "0.16"
|
yew-router = "0.16"
|
||||||
#yew-router = { git = "https://github.com/yewstack/yew" }
|
#yew-router = { git = "https://github.com/yewstack/yew" }
|
||||||
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] }
|
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] }
|
||||||
lan_party_core = { path = "../core", features = ["serde", "sycamore"] }
|
lan_party_core = { path = "../core", features = ["serde"] }
|
||||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<!--<link data-trunk href="tailwind.css" rel="css">-->
|
<!--<link data-trunk href="tailwind.css" rel="css">-->
|
||||||
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
|
<link rel="stylesheet" href="/simple.min-d15f5ff500b4c62a.css">
|
||||||
<link rel="stylesheet" href="/style-5e1a36be9e479fbe.css">
|
<link rel="stylesheet" href="/style-13bbcf1f99e2f75c.css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
||||||
<title>LAN Party</title>
|
<title>LAN Party</title>
|
||||||
|
|
||||||
<link rel="preload" href="/index-61979c95126900f4_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
<link rel="preload" href="/index-175ab564dbb350a3_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
<link rel="modulepreload" href="/index-61979c95126900f4.js"></head>
|
<link rel="modulepreload" href="/index-175ab564dbb350a3.js"></head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">import init from '/index-61979c95126900f4.js';init('/index-61979c95126900f4_bg.wasm');</script></body></html>
|
<script type="module">import init from '/index-175ab564dbb350a3.js';init('/index-175ab564dbb350a3_bg.wasm');</script></body></html>
|
|
@ -0,0 +1,401 @@
|
||||||
|
use std::{cell::RefCell, marker::PhantomData, str::FromStr};
|
||||||
|
|
||||||
|
use crate::components::Block;
|
||||||
|
use lan_party_core::event::{
|
||||||
|
free_for_all_game::FreeForAllGameSpec, team_game::TeamGameSpec, test::TestSpec, EventSpec,
|
||||||
|
EventTypeSpec,
|
||||||
|
};
|
||||||
|
use log::debug;
|
||||||
|
use paste::paste;
|
||||||
|
use sycamore::{builder::prelude::*, component::Prop, prelude::*};
|
||||||
|
|
||||||
|
macro_rules! editable {
|
||||||
|
($type:ty => $editor:ty) => {
|
||||||
|
impl<'s: 'a, 'a, G: Html> Editable<'s, 'a, G> for $type {
|
||||||
|
type Editor = $editor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! edit_fields {
|
||||||
|
($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => {
|
||||||
|
view! { $cx,
|
||||||
|
$(
|
||||||
|
p {
|
||||||
|
label { ($name) }
|
||||||
|
($prop.edit($cx))
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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_rules! edit_struct {
|
||||||
|
($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => {
|
||||||
|
paste! {
|
||||||
|
pub struct [<$struct Edit>];
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G: Html> Editor<'s, '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_rules! link_variants {
|
||||||
|
($cx:ident, $selected:ident => $($index:literal: $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, || {
|
||||||
|
debug!("{:#?}", $selected.get());
|
||||||
|
match $selected.get().as_str() {
|
||||||
|
$(stringify!($index) => $state.set($t::$variant($var_name.get().as_ref().clone())),)*
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! edit_enum {
|
||||||
|
($enum:ident => $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => {
|
||||||
|
paste! {
|
||||||
|
pub struct [<$enum Edit>];
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G: Html> Editor<'s, '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 =>
|
||||||
|
$($index: $var_name = $variant: $var_type,)*
|
||||||
|
=> state as $enum
|
||||||
|
);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
Block(title=stringify!($enum).to_string()) {
|
||||||
|
select(bind:value=$selected) {
|
||||||
|
$(option(value={stringify!($index)}, selected=$index==0) { (stringify!($variant)) })*
|
||||||
|
}
|
||||||
|
(match $selected.get().as_str() {
|
||||||
|
$(stringify!($index) => $var_name.edit(cx),)*
|
||||||
|
_ => unreachable!()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<'s: 'a, 'a, G: Html> {
|
||||||
|
fn edit(self, cx: BoundedScope<'a, 's>) -> View<G>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G: Html, T: Editable<'s, 'a, G> + 'a> IntoEdit<'s, 'a, G> for &'a Signal<T>
|
||||||
|
where
|
||||||
|
EditProps<'a, T>: From<&'a Signal<T>>,
|
||||||
|
{
|
||||||
|
fn edit(self, cx: BoundedScope<'a, 's>) -> View<G> {
|
||||||
|
T::edit(cx, self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Edit<'s: 'a, 'a, G: Html>: Sized {
|
||||||
|
fn edit(cx: BoundedScope<'a, 's>, props: EditProps<'a, Self>) -> View<G>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G, E, Type> Edit<'s, 'a, G> for Type
|
||||||
|
where
|
||||||
|
G: Html,
|
||||||
|
E: Editor<'s, 'a, G, Type>,
|
||||||
|
Type: Editable<'s, 'a, G, Editor = E> + 'a,
|
||||||
|
{
|
||||||
|
fn edit(cx: BoundedScope<'a, 's>, props: EditProps<'a, Self>) -> View<G> {
|
||||||
|
E::edit(cx, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Editor<'s: 'a, 'a, G: Html, Type: 'a>: Sized {
|
||||||
|
fn edit(cx: BoundedScope<'a, 's>, props: EditProps<'a, Type>) -> View<G>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Editable<'s: 'a, 'a, G: Html>: Sized + 'a {
|
||||||
|
type Editor: Editor<'s, 'a, G, Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_struct!(EventSpec => ("Name", name), ("Description", description), ("Event type", event_type));
|
||||||
|
|
||||||
|
edit_enum!(EventTypeSpec => selected =>
|
||||||
|
0: test = Test: TestSpec,
|
||||||
|
1: team_game = TeamGame: TeamGameSpec,
|
||||||
|
2: free_for_all_game = FreeForAllGame: FreeForAllGameSpec
|
||||||
|
);
|
||||||
|
|
||||||
|
edit_struct!(TestSpec => ("Number of players", num_players));
|
||||||
|
edit_struct!(TeamGameSpec => ("Win rewards", win_rewards));
|
||||||
|
edit_struct!(FreeForAllGameSpec => );
|
||||||
|
|
||||||
|
pub struct StringEdit;
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G: Html> Editor<'s, '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<'s: 'a, 'a, G: Html, T> Editor<'s, 'a, G, T> for StubEdit
|
||||||
|
where
|
||||||
|
T: Editable<'s, 'a, G, Editor = StubEdit>,
|
||||||
|
{
|
||||||
|
fn edit(cx: Scope<'a>, _props: EditProps<'a, T>) -> View<G> {
|
||||||
|
view! { cx,
|
||||||
|
i { "Editor Unimplemented" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InputEdit;
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G: Html, T> Editor<'s, 'a, G, T> for InputEdit
|
||||||
|
where
|
||||||
|
T: Editable<'s, '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<'s: 'a, 'a, G: Html> Editor<'s, '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<'s: 'a, 'a, G, T, I> Editor<'s, 'a, G, I> for VecEdit
|
||||||
|
where
|
||||||
|
G: Html,
|
||||||
|
T: Editable<'s, 'a, G> + Clone + PartialEq + 'a,
|
||||||
|
I: IntoIterator<Item = T> + FromIterator<T> + Clone + 'a,
|
||||||
|
{
|
||||||
|
fn edit(cx: BoundedScope<'a, 's>, props: EditProps<'a, I>) -> View<G> {
|
||||||
|
let vec: &'a Signal<Vec<&'a Signal<T>>> = create_signal(
|
||||||
|
cx,
|
||||||
|
props
|
||||||
|
.state
|
||||||
|
.get_untracked()
|
||||||
|
.as_ref()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| create_signal(cx, x))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
//let signal = create_signal(cx, 0);
|
||||||
|
//let vec2 = vec.get().as_ref().clone();
|
||||||
|
//let signal = create_ref(cx, vec.get(0).unwrap());
|
||||||
|
|
||||||
|
create_effect(cx, || {
|
||||||
|
props.state.set(
|
||||||
|
vec.get()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|x| x.get().as_ref().clone())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let test = Indexed(
|
||||||
|
cx,
|
||||||
|
IndexedProps::builder()
|
||||||
|
.iterable(vec)
|
||||||
|
.view(|cx: BoundedScope<'_, '_>, x: &'a Signal<T>| {
|
||||||
|
//view! { cx, (T::edit(cx, EditProps { state: x })) }
|
||||||
|
T::edit(cx, EditProps { state: x })
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
test
|
||||||
|
|
||||||
|
/*
|
||||||
|
//view! { cx, "Vec" }
|
||||||
|
view! { cx,
|
||||||
|
Indexed(
|
||||||
|
iterable=vec,
|
||||||
|
view=|cx: BoundedScope<'_, 'a>, x: &'a Signal<T>| {
|
||||||
|
view! { cx,
|
||||||
|
//(T::edit(cx, EditProps { state: x }))
|
||||||
|
(something_with_signal(cx, EditProps { state: x }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
view! { cx,
|
||||||
|
(View::new_fragment((vec.get().try_borrow().unwrap().clone().into_iter().map(|s: &'a Signal<T>| {
|
||||||
|
view! { cx, (T::edit(cx, s.into())) }
|
||||||
|
})).collect()))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[component]
|
||||||
|
pub fn VecTest<
|
||||||
|
'a,
|
||||||
|
G: Html,
|
||||||
|
T: 'a + PartialEq + Clone + Editable<'a, G, Editor = E>,
|
||||||
|
E: Editor<'a, G, T>,
|
||||||
|
>(
|
||||||
|
cx: Scope<'a>,
|
||||||
|
props: EditProps<'a, Vec<T>>,
|
||||||
|
) -> View<G> {
|
||||||
|
/*
|
||||||
|
let signals: &'a Signal<Vec<&'a Signal<String>>> = create_signal(
|
||||||
|
cx,
|
||||||
|
vec![
|
||||||
|
create_signal(cx, String::new()),
|
||||||
|
create_signal(cx, String::new()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
let signals = create_signal(
|
||||||
|
cx,
|
||||||
|
props
|
||||||
|
.state
|
||||||
|
.get()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|s| create_signal(cx, s))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
Indexed(
|
||||||
|
iterable=signals,
|
||||||
|
view=|cx: BoundedScope<'_, 'a>, signal: &'a Signal<T>| {
|
||||||
|
view! { cx,
|
||||||
|
//(something_with_signal(cx, SignalWrapper(signal)))
|
||||||
|
//(E::edit(cx, EditProps { state: signal }))
|
||||||
|
//(T::edit(cx, EditProps { state: signal }))
|
||||||
|
//(signal.edit(cx))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub struct SignalWrapper<'a, T>(&'a Signal<T>);
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn something_with_signal<'a, G: Html, T: 'a>(
|
||||||
|
cx: Scope<'a>,
|
||||||
|
signal: EditProps<'a, T>,
|
||||||
|
) -> View<G> {
|
||||||
|
drop(signal);
|
||||||
|
view! { cx, "Something" }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s: 'a, 'a, G, T> Editable<'s, 'a, G> for Vec<T>
|
||||||
|
where
|
||||||
|
G: Html,
|
||||||
|
T: Editable<'s, 'a, G> + Clone + PartialEq + 'a,
|
||||||
|
{
|
||||||
|
type Editor = VecEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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));
|
|
@ -1,8 +1,12 @@
|
||||||
|
pub mod event;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use web_sys::Event;
|
use web_sys::Event;
|
||||||
|
use yew::use_effect;
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct ButtonProps<F: FnMut(Event)> {
|
pub struct Props<F: FnMut(Event)> {
|
||||||
pub onclick: F,
|
pub onclick: F,
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
@ -11,7 +15,7 @@ pub struct ButtonProps<F: FnMut(Event)> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: ButtonProps<F>) -> View<G> {
|
pub fn Button<'a, G: Html, F: 'a + FnMut(Event)>(cx: Scope<'a>, props: Props<F>) -> View<G> {
|
||||||
let mut icon_class = String::from("mdi ");
|
let mut icon_class = String::from("mdi ");
|
||||||
|
|
||||||
if !props.icon.is_empty() {
|
if !props.icon.is_empty() {
|
||||||
|
@ -57,8 +61,8 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Prop)]
|
||||||
pub struct BlockProps<'a, G: Html> {
|
pub struct BlockProps<'a, G: Html> {
|
||||||
pub title: String,
|
title: String,
|
||||||
pub children: Children<'a, G>,
|
children: Children<'a, G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|
|
@ -1,70 +1,21 @@
|
||||||
use lan_party_core::{
|
use crate::components::event::{IntoEdit, Test};
|
||||||
edit::IntoEdit,
|
use lan_party_core::event::EventSpec;
|
||||||
event::{Event, EventSpec, EventUpdate},
|
|
||||||
view::IntoView,
|
|
||||||
};
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use reqwasm::http::Method;
|
use sycamore::prelude::*;
|
||||||
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::components::{
|
||||||
components::{Block, Button},
|
event::{Edit, EditProps},
|
||||||
util::api_request,
|
Block, Button,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
let event_spec = create_signal(cx, EventSpec::default());
|
let event_spec = create_signal(cx, EventSpec::default());
|
||||||
let event_update = create_signal(cx, EventUpdate::default());
|
|
||||||
|
|
||||||
let events: &'a Signal<Vec<Event>> = create_signal(cx, Vec::<Event>::new());
|
|
||||||
|
|
||||||
spawn_local_scoped(cx, async move {
|
|
||||||
events.set(
|
|
||||||
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
|
||||||
.await
|
|
||||||
.map(|inner| inner.unwrap())
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let onadd = move |_| {
|
|
||||||
spawn_local_scoped(cx, async move {
|
|
||||||
let new_event = api_request::<EventSpec, Event>(
|
|
||||||
Method::POST,
|
|
||||||
"/event",
|
|
||||||
Some((*event_spec).get().as_ref().clone()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
debug!("{:#?}", new_event);
|
|
||||||
events.modify().push(new_event.unwrap());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
Block(title="Events".into()) {
|
|
||||||
Keyed(
|
|
||||||
iterable=&events,
|
|
||||||
view=move |cx, event| {
|
|
||||||
let event = create_ref(cx, event);
|
|
||||||
view! { cx,
|
|
||||||
(event.view(cx))
|
|
||||||
br()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key=|event| (event.name.clone()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
br()
|
|
||||||
Block(title="Create new event".into()) {
|
Block(title="Create new event".into()) {
|
||||||
(event_spec.edit(cx))
|
(event_spec.edit(cx))
|
||||||
Button(icon="mdi-check".into(), onclick=onadd)
|
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_spec.get()))
|
||||||
}
|
|
||||||
br()
|
|
||||||
Block(title="Update an event".into()) {
|
|
||||||
(event_update.edit(cx))
|
|
||||||
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,9 @@ pub fn UsersPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
users.modify().push(user.unwrap());
|
let mut cloned = (*users).get().as_ref().clone();
|
||||||
|
cloned.push(user.unwrap());
|
||||||
|
users.set(cloned);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -58,11 +58,3 @@ textarea:focus, input:focus{
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
grid-template-columns: 1fr min(60rem, 90%) 1fr;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue