Procedural macros
This commit is contained in:
parent
004885e6e7
commit
59dfb89ee6
|
@ -274,6 +274,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "cookie"
|
||||
version = "0.16.0"
|
||||
|
@ -469,6 +478,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
|
@ -1136,11 +1156,26 @@ dependencies = [
|
|||
name = "lan_party_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"lan_party_macros",
|
||||
"paste",
|
||||
"rocket",
|
||||
"schemars",
|
||||
"serde",
|
||||
"sycamore",
|
||||
"thiserror",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lan_party_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"paste",
|
||||
"quote",
|
||||
"sycamore",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
members = [
|
||||
"backend",
|
||||
"core",
|
||||
"web"
|
||||
"web",
|
||||
"macros"
|
||||
]
|
||||
|
||||
|
|
|
@ -9,10 +9,15 @@ edition = "2021"
|
|||
serde = ["dep:serde"]
|
||||
openapi = ["dep:schemars"]
|
||||
rocket = ["dep:rocket", "serde"]
|
||||
sycamore = ["dep:sycamore", "dep:web-sys", "dep:lan_party_macros"]
|
||||
|
||||
[dependencies]
|
||||
schemars = { version = "0.8", optional = true }
|
||||
serde = { version = "1", 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"
|
||||
thiserror = "1.0"
|
||||
displaydoc = "0.2"
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
//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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,16 +6,22 @@ use std::{
|
|||
};
|
||||
|
||||
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::*, prelude::*};
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use super::{BlockProps, Button, ButtonProps};
|
||||
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 {
|
||||
|
@ -24,6 +30,7 @@ macro_rules! editable {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! edit_fields {
|
||||
($cx:ident, $(($name:expr, $prop:expr)),* $(,)?) => {
|
||||
view! { $cx,
|
||||
|
@ -37,6 +44,7 @@ macro_rules! edit_fields {
|
|||
};
|
||||
}
|
||||
|
||||
#[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());)*
|
||||
|
@ -50,6 +58,7 @@ macro_rules! link_fields {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! edit_struct {
|
||||
($struct:ident => $(($name:expr, $prop:ident)),* $(,)?) => {
|
||||
paste! {
|
||||
|
@ -72,8 +81,9 @@ macro_rules! edit_struct {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! link_variants {
|
||||
($cx:ident, $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)? => $state:ident as $t:ident) => {
|
||||
($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() {
|
||||
|
@ -83,17 +93,18 @@ macro_rules! link_variants {
|
|||
};)*
|
||||
|
||||
create_effect($cx, || {
|
||||
debug!("{:#?}", $selected.get());
|
||||
match $selected.get().as_str() {
|
||||
$(stringify!($index) => $state.set($t::$variant($var_name.get().as_ref().clone())),)*
|
||||
_ => unreachable!()
|
||||
$(stringify!($var_name) => $state.set($t::$variant($var_name.get().as_ref().clone())),)*
|
||||
//_ => unreachable!()
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! edit_enum {
|
||||
($enum:ident => $selected:ident => $($index:literal: $var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => {
|
||||
($enum:ident => $selected:ident => $($var_name:ident = $variant:ident: $var_type:ty),* $(,)?) => {
|
||||
paste! {
|
||||
pub struct [<$enum Edit>];
|
||||
|
||||
|
@ -102,18 +113,19 @@ macro_rules! edit_enum {
|
|||
let state = props.state;
|
||||
|
||||
link_variants!(cx, $selected =>
|
||||
$($index: $var_name = $variant: $var_type,)*
|
||||
$(($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)) })*
|
||||
$(option(value={stringify!($var_name)}, selected=true) { (stringify!($variant)) })*
|
||||
}
|
||||
(match $selected.get().as_str() {
|
||||
$(stringify!($index) => $var_name.edit(cx),)*
|
||||
_ => unreachable!()
|
||||
$(stringify!($var_name) => $var_name.edit(cx),)*
|
||||
//_ => unreachable!()
|
||||
_ => view! { cx, }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -172,18 +184,6 @@ pub trait Editable<'a, G: Html>: Sized {
|
|||
type Editor: Editor<'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 => ("Teams", teams), ("Win rewards", win_rewards), ("Lose rewards", lose_rewards));
|
||||
edit_struct!(FreeForAllGameSpec => ("Participants", participants), ("Win rewards", win_rewards), ("Lose rewards", lose_rewards));
|
||||
|
||||
pub struct StringEdit;
|
||||
|
||||
impl<'a, G: Html> Editor<'a, G, String> for StringEdit {
|
||||
|
@ -209,6 +209,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -258,7 +262,7 @@ pub struct VecEdit;
|
|||
impl<'a, G, T, I> Editor<'a, G, I> for VecEdit
|
||||
where
|
||||
G: Html,
|
||||
T: Editable<'a, G> + Clone + PartialEq + Default + 'a,
|
||||
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> {
|
||||
|
@ -287,13 +291,39 @@ where
|
|||
|
||||
let onadd = move |_| vec.modify().push(create_signal(cx, T::default()));
|
||||
|
||||
Block(
|
||||
cx,
|
||||
BlockProps {
|
||||
title: "List".into(),
|
||||
children: Children::new(cx, move |_| {
|
||||
view! { cx,
|
||||
//Block(title="List".into()) {
|
||||
div {
|
||||
Indexed(
|
||||
iterable=vec,
|
||||
view=|cx: BoundedScope<'_, 'a>, x: &'a Signal<T>| {
|
||||
view! { cx,
|
||||
(x.edit(cx))
|
||||
br()
|
||||
}
|
||||
},
|
||||
)
|
||||
Button(onclick=onadd, text="Add new".into(), icon="mdi-plus".into())
|
||||
}
|
||||
//}
|
||||
}
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
/*
|
||||
Block(
|
||||
cx,
|
||||
BlockProps {
|
||||
title: "List".into(),
|
||||
children: Children::new(cx, move |_| {
|
||||
div()
|
||||
.dyn_c(move || {
|
||||
.dyn_c_scoped(move |cx| {
|
||||
View::new_fragment(
|
||||
vec.get()
|
||||
.as_ref()
|
||||
|
@ -315,13 +345,14 @@ where
|
|||
}),
|
||||
},
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, G, T> Editable<'a, G> for Vec<T>
|
||||
where
|
||||
G: Html,
|
||||
T: Editable<'a, G> + Clone + PartialEq + Default + 'a,
|
||||
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a,
|
||||
{
|
||||
type Editor = VecEdit;
|
||||
}
|
||||
|
@ -329,7 +360,7 @@ where
|
|||
impl<'a, G, T> Editable<'a, G> for HashSet<T>
|
||||
where
|
||||
G: Html,
|
||||
T: Editable<'a, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||
{
|
||||
type Editor = VecEdit;
|
||||
}
|
||||
|
@ -339,15 +370,15 @@ where
|
|||
G: Html,
|
||||
K: Clone + Hash + Eq,
|
||||
V: Clone,
|
||||
(K, V): Editable<'a, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||
(K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
|
||||
{
|
||||
type Editor = VecEdit;
|
||||
}
|
||||
|
||||
pub struct TupleEdit;
|
||||
|
||||
impl<'a, 'b, G: Html, A: Editable<'a, G> + Clone, B: Editable<'a, G> + Clone> Editor<'a, G, (A, B)>
|
||||
for 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;
|
||||
|
@ -362,30 +393,91 @@ impl<'a, 'b, G: Html, A: Editable<'a, G> + Clone, B: Editable<'a, G> + Clone> Ed
|
|||
.set((a.get().as_ref().clone(), b.get().as_ref().clone()))
|
||||
});
|
||||
|
||||
Block(
|
||||
cx,
|
||||
BlockProps {
|
||||
title: "Tuple".into(),
|
||||
children: Children::new(cx, move |_| {
|
||||
div()
|
||||
.dyn_c(move || a.edit(cx))
|
||||
.dyn_c(move || b.edit(cx))
|
||||
.view(cx)
|
||||
}),
|
||||
},
|
||||
)
|
||||
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: Editable<'a, G> + Clone,
|
||||
B: Editable<'a, G> + Clone,
|
||||
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,
|
|
@ -1,4 +1,8 @@
|
|||
#[cfg(feature = "sycamore")]
|
||||
use crate::edit::prelude::*;
|
||||
use crate::util::PartyError;
|
||||
#[cfg(feature = "sycamore")]
|
||||
use lan_party_macros::WebEdit;
|
||||
use paste::paste;
|
||||
#[cfg(feature = "openapi")]
|
||||
use schemars::JsonSchema;
|
||||
|
@ -51,6 +55,7 @@ impl Event {
|
|||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct EventSpec {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
|
@ -66,6 +71,7 @@ macro_rules! events {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum EventTypeSpec {
|
||||
$($name($module::[<$name Spec>]),)*
|
||||
}
|
||||
|
@ -76,6 +82,7 @@ macro_rules! events {
|
|||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum EventUpdate {
|
||||
$($name($module::[<$name Update>]),)*
|
||||
}
|
||||
|
@ -86,6 +93,7 @@ macro_rules! events {
|
|||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum EventType {
|
||||
$($name($module::$name),)*
|
||||
}
|
||||
|
@ -149,9 +157,10 @@ pub trait EventTrait {
|
|||
pub mod test {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct Test {
|
||||
pub num_players: i64,
|
||||
}
|
||||
|
@ -159,13 +168,15 @@ pub mod test {
|
|||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct TestSpec {
|
||||
pub num_players: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct TestUpdate {
|
||||
pub win_game: bool,
|
||||
}
|
||||
|
@ -205,9 +216,10 @@ pub mod team_game {
|
|||
*,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct TeamGame {
|
||||
/// Map of teams with a name as key and an array of players as value
|
||||
pub teams: HashMap<String, Vec<String>>,
|
||||
|
@ -219,6 +231,7 @@ pub mod team_game {
|
|||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct TeamGameSpec {
|
||||
/// Map of teams with a name as key and an array of players as value
|
||||
pub teams: HashMap<String, Vec<String>>,
|
||||
|
@ -239,21 +252,38 @@ pub mod team_game {
|
|||
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))]
|
||||
pub struct TeamGameUpdateSetTeam {
|
||||
pub team: String,
|
||||
pub members: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum TeamGameUpdateInner {
|
||||
/// Add or replace a team with the given name and array of members
|
||||
SetTeam { team: String, members: Vec<String> },
|
||||
SetTeam(TeamGameUpdateSetTeam),
|
||||
|
||||
/// Remove team with given name
|
||||
RemoveTeam(String),
|
||||
}
|
||||
|
||||
impl Default for TeamGameUpdateInner {
|
||||
fn default() -> Self {
|
||||
Self::SetTeam(TeamGameUpdateSetTeam::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum TeamGameFfaInheritedUpdate {
|
||||
/// Change the ranking and scores
|
||||
Ranking(FreeForAllGameUpdateRanking),
|
||||
|
@ -261,10 +291,17 @@ pub mod team_game {
|
|||
Rewards(FreeForAllGameUpdateRewards),
|
||||
}
|
||||
|
||||
impl Default for TeamGameFfaInheritedUpdate {
|
||||
fn default() -> Self {
|
||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum TeamGameUpdate {
|
||||
/// Team specific updates
|
||||
Team(TeamGameUpdateInner),
|
||||
|
@ -272,6 +309,12 @@ pub mod team_game {
|
|||
Ffa(TeamGameFfaInheritedUpdate),
|
||||
}
|
||||
|
||||
impl Default for TeamGameUpdate {
|
||||
fn default() -> Self {
|
||||
TeamGameUpdate::Team(TeamGameUpdateInner::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for TeamGame {
|
||||
type Spec = TeamGameSpec;
|
||||
type Update = TeamGameUpdate;
|
||||
|
@ -300,12 +343,12 @@ pub mod team_game {
|
|||
}
|
||||
},
|
||||
TeamGameUpdate::Team(update) => match update {
|
||||
TeamGameUpdateInner::SetTeam { team, members } => {
|
||||
TeamGameUpdateInner::SetTeam(u) => {
|
||||
self.ffa_game
|
||||
.apply_update(FreeForAllGameUpdate::Participants(
|
||||
FreeForAllGameUpdateParticipants::AddParticipant(team.clone()),
|
||||
FreeForAllGameUpdateParticipants::AddParticipant(u.team.clone()),
|
||||
))?;
|
||||
self.teams.insert(team, members);
|
||||
self.teams.insert(u.team, u.members);
|
||||
Ok(())
|
||||
}
|
||||
TeamGameUpdateInner::RemoveTeam(team) => {
|
||||
|
@ -347,6 +390,7 @@ pub mod free_for_all_game {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum FreeForAllGameRanking {
|
||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||
/// place, etc.)
|
||||
|
@ -355,6 +399,12 @@ pub mod free_for_all_game {
|
|||
Scores(HashMap<String, i64>),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameRanking {
|
||||
fn default() -> Self {
|
||||
Self::Ranking(Vec::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl FreeForAllGameRanking {
|
||||
pub fn is_valid(&self, participants: &HashSet<String>) -> bool {
|
||||
match self {
|
||||
|
@ -364,9 +414,10 @@ pub mod free_for_all_game {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct FreeForAllGame {
|
||||
/// Ranking of participants by user name or team name (first element is first place, second element is second
|
||||
/// place, etc.)
|
||||
|
@ -388,6 +439,7 @@ pub mod free_for_all_game {
|
|||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub struct FreeForAllGameSpec {
|
||||
/// Array of user ids that participate in the game
|
||||
pub participants: HashSet<String>,
|
||||
|
@ -405,6 +457,7 @@ pub mod free_for_all_game {
|
|||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum FreeForAllGameUpdateRanking {
|
||||
/// Replace the current ranking with the given ranking
|
||||
SetRanking(FreeForAllGameRanking),
|
||||
|
@ -413,9 +466,16 @@ pub mod free_for_all_game {
|
|||
ScoreDelta(HashMap<String, i64>),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameUpdateRanking {
|
||||
fn default() -> Self {
|
||||
Self::SetRanking(FreeForAllGameRanking::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum FreeForAllGameUpdateRewards {
|
||||
/// Set rewards for winning the game
|
||||
SetWinRewards(Vec<i64>),
|
||||
|
@ -424,9 +484,16 @@ pub mod free_for_all_game {
|
|||
SetLoseRewards(Vec<i64>),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameUpdateRewards {
|
||||
fn default() -> Self {
|
||||
Self::SetWinRewards(Vec::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum FreeForAllGameUpdateParticipants {
|
||||
/// Set list of participants participating in the game
|
||||
SetParticipants(HashSet<String>),
|
||||
|
@ -438,10 +505,17 @@ pub mod free_for_all_game {
|
|||
RemoveParticipant(String),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameUpdateParticipants {
|
||||
fn default() -> Self {
|
||||
Self::SetParticipants(HashSet::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "openapi", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
#[cfg_attr(feature = "sycamore", derive(WebEdit))]
|
||||
pub enum FreeForAllGameUpdate {
|
||||
/// Change the ranking and scores
|
||||
Ranking(FreeForAllGameUpdateRanking),
|
||||
|
@ -451,6 +525,12 @@ pub mod free_for_all_game {
|
|||
Participants(FreeForAllGameUpdateParticipants),
|
||||
}
|
||||
|
||||
impl Default for FreeForAllGameUpdate {
|
||||
fn default() -> Self {
|
||||
Self::Ranking(FreeForAllGameUpdateRanking::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for FreeForAllGame {
|
||||
type Spec = FreeForAllGameSpec;
|
||||
type Update = FreeForAllGameUpdate;
|
||||
|
@ -560,3 +640,9 @@ impl Default for EventTypeSpec {
|
|||
Self::Test(test::TestSpec::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventUpdate {
|
||||
fn default() -> Self {
|
||||
Self::Test(test::TestUpdate::default())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,10 @@ pub mod event;
|
|||
pub mod user;
|
||||
pub mod util;
|
||||
|
||||
#[cfg(feature = "sycamore")]
|
||||
pub mod edit;
|
||||
|
||||
#[cfg(feature = "sycamore")]
|
||||
pub mod components;
|
||||
|
||||
pub use util::PartyError;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
[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"] }
|
|
@ -0,0 +1,172 @@
|
|||
mod edit;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::Fields;
|
||||
|
||||
#[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 editable = quote! {
|
||||
pub struct #edit_ident;
|
||||
|
||||
impl<'a, G: Html> Editable<'a, G> for #name {
|
||||
type Editor = #edit_ident;
|
||||
}
|
||||
};
|
||||
|
||||
let derived = match input.data {
|
||||
syn::Data::Struct(s) => {
|
||||
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();
|
||||
(name_str, name)
|
||||
});
|
||||
|
||||
let fields_view = fields.clone().map(|(name_str, name)| {
|
||||
let title = name_str.to_case(Case::Title);
|
||||
quote! {
|
||||
p {
|
||||
label { (#title) }
|
||||
(#name.edit(cx))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let signals = fields.clone().map(|(_name_str, name)| {
|
||||
quote! {
|
||||
let #name = create_signal(cx, state.get_untracked().#name.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let effect_fields = fields.clone().map(|(_name_str, name)| {
|
||||
quote! {
|
||||
#name: #name.get().as_ref().clone()
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
||||
let state = props.state;
|
||||
|
||||
#(#signals)*
|
||||
|
||||
create_effect(cx, || {
|
||||
state.set(#name {
|
||||
#(#effect_fields,)*
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
Block(title=stringify!(#name).to_string()) {
|
||||
#(#fields_view)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
syn::Data::Enum(e) => {
|
||||
//dbg!(&e);
|
||||
|
||||
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 variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
|
||||
(variant_lower, variant, inner)
|
||||
});
|
||||
|
||||
let first = variants.clone().next().unwrap().0;
|
||||
|
||||
let options = variants.clone().map(|(lower, variant, _inner)| {
|
||||
let selected = first == lower;
|
||||
quote! { option(value={stringify!(#lower)}, selected=#selected) { (stringify!(#variant)) } }
|
||||
});
|
||||
|
||||
let view_match = variants.clone().map(|(lower, _variant, _inner)| {
|
||||
let lower_str = format!("{}", lower);
|
||||
|
||||
quote! {
|
||||
#lower_str => #lower.edit(cx)
|
||||
}
|
||||
});
|
||||
|
||||
let signals = variants.clone().map(|(lower, variant, inner)| {
|
||||
quote! {
|
||||
let #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(|(lower, variant, _inner)| {
|
||||
let lower_str = format!("{}", lower);
|
||||
|
||||
quote! {
|
||||
#lower_str => state.set(#name::#variant(#lower.get().as_ref().clone()))
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
||||
let state = props.state;
|
||||
|
||||
let selected = create_signal(cx, String::from("0"));
|
||||
|
||||
#(#signals)*
|
||||
|
||||
create_effect(cx, || {
|
||||
match selected.get().as_str() {
|
||||
#(#effect_match,)*
|
||||
_ => {}
|
||||
//_ => unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
Block(title=stringify!(#name).to_string()) {
|
||||
select(bind:value=selected) {
|
||||
#(#options)*
|
||||
}
|
||||
(match selected.get().as_str() {
|
||||
#(#view_match,)*
|
||||
_ => view! { cx, }
|
||||
//_ => unreachable!()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
println!("{}", &derived.to_string());
|
||||
TokenStream::from(quote! {
|
||||
#derived
|
||||
|
||||
#editable
|
||||
})
|
||||
}
|
|
@ -11,7 +11,7 @@ yew = "0.19"
|
|||
yew-router = "0.16"
|
||||
#yew-router = { git = "https://github.com/yewstack/yew" }
|
||||
web-sys = { version = "0.3", features = ["Request", "RequestInit", "RequestMode", "Response", "Headers", "HtmlSelectElement"] }
|
||||
lan_party_core = { path = "../core", features = ["serde"] }
|
||||
lan_party_core = { path = "../core", features = ["serde", "sycamore"] }
|
||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
serde = "1"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<title>LAN Party</title>
|
||||
|
||||
<link rel="preload" href="/index-ca59edff96de869_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-ca59edff96de869.js"></head>
|
||||
<link rel="preload" href="/index-7e6e148776e9cebb_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="modulepreload" href="/index-7e6e148776e9cebb.js"></head>
|
||||
<body>
|
||||
<script type="module">import init from '/index-ca59edff96de869.js';init('/index-ca59edff96de869_bg.wasm');</script></body></html>
|
||||
<script type="module">import init from '/index-7e6e148776e9cebb.js';init('/index-7e6e148776e9cebb_bg.wasm');</script></body></html>
|
|
@ -1,9 +1,5 @@
|
|||
pub mod event;
|
||||
|
||||
use log::debug;
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::Event;
|
||||
use yew::use_effect;
|
||||
|
||||
#[derive(Prop)]
|
||||
pub struct ButtonProps<F: FnMut(Event)> {
|
||||
|
@ -61,8 +57,8 @@ pub fn Table<'a, G: Html>(cx: Scope<'a>, props: TableProps<'a, G>) -> View<G> {
|
|||
|
||||
#[derive(Prop)]
|
||||
pub struct BlockProps<'a, G: Html> {
|
||||
title: String,
|
||||
children: Children<'a, G>,
|
||||
pub title: String,
|
||||
pub children: Children<'a, G>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
use crate::components::event::{IntoEdit, Test};
|
||||
use lan_party_core::event::EventSpec;
|
||||
use lan_party_core::{
|
||||
edit::IntoEdit,
|
||||
event::{EventSpec, EventUpdate},
|
||||
};
|
||||
use log::debug;
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
event::{Edit, EditProps},
|
||||
Block, Button,
|
||||
};
|
||||
use crate::components::{Block, Button};
|
||||
|
||||
#[component]
|
||||
pub fn EventsPage<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||
let event_spec = create_signal(cx, EventSpec::default());
|
||||
let event_update = create_signal(cx, EventUpdate::default());
|
||||
|
||||
view! { cx,
|
||||
Block(title="Create new event".into()) {
|
||||
(event_spec.edit(cx))
|
||||
(event_update.edit(cx))
|
||||
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_spec.get()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue