Compare commits

..

No commits in common. "2ddbfc2fdb1099d6e61d8558fe00a12ce1603d18" and "59dfb89ee655b564c5823b477870c413c40d38c5" have entirely different histories.

11 changed files with 218 additions and 804 deletions

1
Cargo.lock generated
View File

@ -1158,7 +1158,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"lan_party_macros", "lan_party_macros",
"log",
"paste", "paste",
"rocket", "rocket",
"schemars", "schemars",

View File

@ -21,4 +21,3 @@ lan_party_macros = { path = "../macros", optional = true }
paste = "1" paste = "1"
thiserror = "1.0" thiserror = "1.0"
displaydoc = "0.2" displaydoc = "0.2"
log = "0.4"

View File

@ -6,7 +6,6 @@ use std::{
}; };
use crate::components::Block; use crate::components::Block;
use log::debug;
use paste::paste; use paste::paste;
use sycamore::prelude::*; use sycamore::prelude::*;
@ -263,7 +262,7 @@ pub struct VecEdit;
impl<'a, G, T, I> Editor<'a, G, I> for VecEdit impl<'a, G, T, I> Editor<'a, G, I> for VecEdit
where where
G: Html, G: Html,
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + std::fmt::Debug + 'a, T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a,
I: IntoIterator<Item = T> + FromIterator<T> + Clone, I: IntoIterator<Item = T> + FromIterator<T> + Clone,
{ {
fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View<G> { fn edit(cx: Scope<'a>, props: EditProps<'a, I>) -> View<G> {
@ -291,17 +290,6 @@ where
}); });
let onadd = move |_| vec.modify().push(create_signal(cx, T::default())); 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( Block(
cx, cx,
@ -309,30 +297,62 @@ where
title: "List".into(), title: "List".into(),
children: Children::new(cx, move |_| { children: Children::new(cx, move |_| {
view! { cx, view! { cx,
//Block(title="List".into()) {
div { div {
Indexed( Indexed(
iterable=vec, iterable=vec,
view=move |cx: BoundedScope<'_, 'a>, x: &'a Signal<T>| { view=|cx: BoundedScope<'_, 'a>, x: &'a Signal<T>| {
view! { cx, view! { cx,
(x.edit(cx)) (x.edit(cx))
Button(onclick=onremove(x), icon="mdi-delete".into())
br() br()
} }
}, },
) )
Button(onclick=onadd, text="Add new".into(), icon="mdi-plus".into()) 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_scoped(move |cx| {
View::new_fragment(
vec.get()
.as_ref()
.clone()
.into_iter()
.map(|x| div().c(x.edit(cx)).c(br()).view(cx))
.collect(),
)
})
.c(Button(
cx,
ButtonProps {
onclick: onadd,
text: "Add new".into(),
icon: "mdi-plus".into(),
},
))
.view(cx)
}),
},
)
*/
} }
} }
impl<'a, G, T> Editable<'a, G> for Vec<T> impl<'a, G, T> Editable<'a, G> for Vec<T>
where where
G: Html, G: Html,
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + std::fmt::Debug + 'a, T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + 'a,
{ {
type Editor = VecEdit; type Editor = VecEdit;
} }
@ -340,7 +360,7 @@ where
impl<'a, G, T> Editable<'a, G> for HashSet<T> impl<'a, G, T> Editable<'a, G> for HashSet<T>
where where
G: Html, G: Html,
T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a, T: for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
{ {
type Editor = VecEdit; type Editor = VecEdit;
} }
@ -350,8 +370,7 @@ where
G: Html, G: Html,
K: Clone + Hash + Eq, K: Clone + Hash + Eq,
V: Clone, V: Clone,
(K, V): (K, V): for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + 'a,
for<'b> Editable<'b, G> + Clone + PartialEq + Default + Hash + Eq + std::fmt::Debug + 'a,
{ {
type Editor = VecEdit; type Editor = VecEdit;
} }

View File

@ -2,9 +2,7 @@
use crate::edit::prelude::*; use crate::edit::prelude::*;
use crate::util::PartyError; use crate::util::PartyError;
#[cfg(feature = "sycamore")] #[cfg(feature = "sycamore")]
use crate::view::prelude::*; use lan_party_macros::WebEdit;
#[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 +20,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 +33,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 +55,9 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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 +71,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub enum EventTypeSpec { pub enum EventTypeSpec {
$($name($module::[<$name Spec>]),)* $($name($module::[<$name Spec>]),)*
} }
@ -86,7 +82,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub enum EventUpdate { pub enum EventUpdate {
$($name($module::[<$name Update>]),)* $($name($module::[<$name Update>]),)*
} }
@ -94,10 +90,10 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub enum EventType { pub enum EventType {
$($name($module::$name),)* $($name($module::$name),)*
} }
@ -161,10 +157,10 @@ pub trait EventTrait {
pub mod test { pub mod test {
use super::*; use super::*;
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub struct Test { pub struct Test {
pub num_players: i64, pub num_players: i64,
} }
@ -172,7 +168,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub struct TestSpec { pub struct TestSpec {
pub num_players: i64, pub num_players: i64,
} }
@ -180,7 +176,7 @@ pub mod test {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub struct TestUpdate { pub struct TestUpdate {
pub win_game: bool, pub win_game: bool,
} }
@ -220,10 +216,10 @@ pub mod team_game {
*, *,
}; };
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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 +231,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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>>,
@ -259,7 +255,7 @@ pub mod team_game {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub struct TeamGameUpdateSetTeam { pub struct TeamGameUpdateSetTeam {
pub team: String, pub team: String,
pub members: Vec<String>, pub members: Vec<String>,
@ -268,7 +264,7 @@ pub mod team_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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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(TeamGameUpdateSetTeam),
@ -287,7 +283,7 @@ pub mod team_game {
#[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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub enum TeamGameFfaInheritedUpdate { pub enum TeamGameFfaInheritedUpdate {
/// Change the ranking and scores /// Change the ranking and scores
Ranking(FreeForAllGameUpdateRanking), Ranking(FreeForAllGameUpdateRanking),
@ -305,14 +301,10 @@ pub mod team_game {
#[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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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),
} }
@ -398,7 +390,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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.)
@ -422,10 +414,10 @@ pub mod free_for_all_game {
} }
} }
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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 +439,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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 +457,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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),
@ -483,7 +475,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub enum FreeForAllGameUpdateRewards { pub enum FreeForAllGameUpdateRewards {
/// Set rewards for winning the game /// Set rewards for winning the game
SetWinRewards(Vec<i64>), SetWinRewards(Vec<i64>),
@ -501,7 +493,7 @@ 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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
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>),
@ -523,7 +515,7 @@ pub mod free_for_all_game {
#[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))] #[cfg_attr(feature = "sycamore", derive(WebEdit))]
pub enum FreeForAllGameUpdate { pub enum FreeForAllGameUpdate {
/// Change the ranking and scores /// Change the ranking and scores
Ranking(FreeForAllGameUpdateRanking), Ranking(FreeForAllGameUpdateRanking),

View File

@ -5,9 +5,6 @@ pub mod util;
#[cfg(feature = "sycamore")] #[cfg(feature = "sycamore")]
pub mod edit; pub mod edit;
#[cfg(feature = "sycamore")]
pub mod view;
#[cfg(feature = "sycamore")] #[cfg(feature = "sycamore")]
pub mod components; pub mod components;

View File

@ -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;
}

View File

@ -2,86 +2,26 @@ mod edit;
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{__private::TokenStream as TokenStream2, format_ident, quote}; use quote::{format_ident, quote};
use syn::{token::Do, Attribute, DataEnum, DataStruct, Fields, Ident, Path, Type}; use syn::Fields;
#[derive(Debug)] #[proc_macro_derive(WebEdit)]
enum Documentation { pub fn web_edit(tokens: TokenStream) -> TokenStream {
Title(String), let input: syn::DeriveInput = syn::parse(tokens).unwrap();
Description(String),
None, 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;
} }
};
impl Documentation { let derived = match input.data {
fn parse(attr: &Attribute) -> Documentation { syn::Data::Struct(s) => {
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 fields = s.fields.iter().map(|f| {
let name = f let name = f
.ident .ident
@ -89,51 +29,34 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
.expect("each struct field must be named") .expect("each struct field must be named")
.clone(); .clone();
let name_str = name.to_string(); let name_str = name.to_string();
let (title, description) = get_title_description(&f.attrs); (name_str, name)
StructField {
name_str,
name,
title,
description,
}
}); });
let fields_view = fields.clone().map(|f| { let fields_view = fields.clone().map(|(name_str, name)| {
let title = f.title.unwrap_or(f.name_str.to_case(Case::Title)); let title = 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! { quote! {
p { p {
label { (#title) br() #description } label { (#title) }
(#name.edit(cx)) (#name.edit(cx))
} }
} }
}); });
let signals = fields.clone().map(|f| { let signals = fields.clone().map(|(_name_str, name)| {
let name = f.name;
quote! { quote! {
let #name = create_signal(cx, state.get_untracked().#name.clone()); let #name = create_signal(cx, state.get_untracked().#name.clone());
} }
}); });
let effect_fields = fields.clone().map(|f| { let effect_fields = fields.clone().map(|(_name_str, name)| {
let name = f.name;
quote! { quote! {
#name: #name.get().as_ref().clone() #name: #name.get().as_ref().clone()
} }
}); });
quote! { 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 state = props.state;
#(#signals)* #(#signals)*
@ -146,56 +69,39 @@ fn struct_edit(props: &ItemProps, s: DataStruct) -> TokenStream2 {
}); });
view! { cx, view! { cx,
Block(title=#title.to_string()) { Block(title=stringify!(#name).to_string()) {
p(class="description") {
#description
}
#(#fields_view)* #(#fields_view)*
} }
} }
} }
} }
}
fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 { }
let ItemProps { syn::Data::Enum(e) => {
name, //dbg!(&e);
title,
description,
} = props;
let variants = e.variants.iter().map(|v| { let variants = e.variants.iter().map(|v| {
let variant = v.ident.clone(); let variant = v.ident.clone();
let inner = match &v.fields { let inner = match &v.fields {
Fields::Unnamed(u) => u.unnamed.first().expect("the should be a field").ty.clone(), Fields::Unnamed(u) => {
u.unnamed.first().expect("the should be a field").ty.clone()
}
_ => unimplemented!(), _ => unimplemented!(),
}; };
let (title, description) = get_title_description(&v.attrs);
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake));
EnumVariant { (variant_lower, variant, inner)
variant_lower,
variant,
inner,
title,
description,
}
}); });
let first = variants.clone().next().unwrap().variant_lower; let first = variants.clone().next().unwrap().0;
let first_str = first.to_string();
let options = variants.clone().map(|v| { let options = variants.clone().map(|(lower, variant, _inner)| {
let lower = v.variant_lower;
let title = v
.title
.unwrap_or(v.variant.to_string().to_case(Case::Title));
let selected = first == lower; let selected = first == lower;
quote! { option(value={stringify!(#lower)}, selected=#selected) { (#title) } } quote! { option(value={stringify!(#lower)}, selected=#selected) { (stringify!(#variant)) } }
}); });
let view_match = variants.clone().map(|v| { let view_match = variants.clone().map(|(lower, _variant, _inner)| {
let lower = v.variant_lower;
let lower_str = format!("{}", lower); let lower_str = format!("{}", lower);
quote! { quote! {
@ -203,44 +109,17 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
} }
}); });
let view_description = variants.clone().map(|v| { let signals = variants.clone().map(|(lower, variant, inner)| {
let lower = v.variant_lower;
let lower_str = format!("{}", lower);
let description = if let Some(d) = v.description {
quote! { quote! {
span(class="description") { let #lower = if let #name::#variant(v) = state.get_untracked().as_ref().clone() {
" " #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()) create_signal(cx, v.clone())
} else { } else {
create_signal(cx, <#inner>::default()) create_signal(cx, <#inner>::default())
}; };
} }
}, });
);
let effect_match = variants.clone().map(|v| { let effect_match = variants.clone().map(|(lower, variant, _inner)| {
let lower = v.variant_lower;
let variant = v.variant;
let lower_str = format!("{}", lower); let lower_str = format!("{}", lower);
quote! { quote! {
@ -249,9 +128,11 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
}); });
quote! { 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 state = props.state;
let selected = create_signal(cx, String::from(#first_str)); let selected = create_signal(cx, String::from("0"));
#(#signals)* #(#signals)*
@ -259,247 +140,33 @@ fn enum_edit(props: &ItemProps, e: DataEnum) -> TokenStream2 {
match selected.get().as_str() { match selected.get().as_str() {
#(#effect_match,)* #(#effect_match,)*
_ => {} _ => {}
//_ => unreachable!()
} }
}); });
view! { cx, view! { cx,
Block(title=#title.to_string()) { Block(title=stringify!(#name).to_string()) {
p(class="description") {
#description
}
select(bind:value=selected) { select(bind:value=selected) {
#(#options)* #(#options)*
} }
(match selected.get().as_str() {
#(#view_description,)*
_ => view! { cx, }
})
(match selected.get().as_str() { (match selected.get().as_str() {
#(#view_match,)* #(#view_match,)*
_ => view! { cx, } _ => view! { cx, }
//_ => unreachable!()
}) })
} }
} }
} }
} }
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!(), _ => unimplemented!(),
}; };
let (title, description) = get_title_description(&v.attrs); println!("{}", &derived.to_string());
let variant_lower = format_ident!("{}", variant.to_string().to_case(Case::Snake)); TokenStream::from(quote! {
EnumVariant { #derived
variant_lower,
variant,
inner,
title,
description,
}
});
let view_match = variants.clone().map(|v| { #editable
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)
}

8
web/dist/index.html vendored
View File

@ -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-7e6e148776e9cebb_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/index-61979c95126900f4.js"></head> <link rel="modulepreload" href="/index-7e6e148776e9cebb.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-7e6e148776e9cebb.js';init('/index-7e6e148776e9cebb_bg.wasm');</script></body></html>

View File

@ -1,70 +1,22 @@
use lan_party_core::{ use lan_party_core::{
edit::IntoEdit, edit::IntoEdit,
event::{Event, EventSpec, EventUpdate}, 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::{Block, Button};
components::{Block, Button},
util::api_request,
};
#[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 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)
}
br()
Block(title="Update an event".into()) {
(event_update.edit(cx)) (event_update.edit(cx))
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get())) Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_spec.get()))
} }
} }
} }

View File

@ -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);
}); });
}; };

View File

@ -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;
}