Temp
This commit is contained in:
parent
59dfb89ee6
commit
49d473e213
|
@ -2,7 +2,9 @@
|
||||||
use crate::edit::prelude::*;
|
use crate::edit::prelude::*;
|
||||||
use crate::util::PartyError;
|
use crate::util::PartyError;
|
||||||
#[cfg(feature = "sycamore")]
|
#[cfg(feature = "sycamore")]
|
||||||
use lan_party_macros::WebEdit;
|
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;
|
||||||
|
@ -23,6 +25,7 @@ pub struct EventOutcome {
|
||||||
#[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(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))]
|
||||||
|
@ -55,7 +58,7 @@ 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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct EventSpec {
|
pub struct EventSpec {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
@ -71,7 +74,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum EventTypeSpec {
|
pub enum EventTypeSpec {
|
||||||
$($name($module::[<$name Spec>]),)*
|
$($name($module::[<$name Spec>]),)*
|
||||||
}
|
}
|
||||||
|
@ -82,7 +85,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum EventUpdate {
|
pub enum EventUpdate {
|
||||||
$($name($module::[<$name Update>]),)*
|
$($name($module::[<$name Update>]),)*
|
||||||
}
|
}
|
||||||
|
@ -93,7 +96,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub enum EventType {
|
pub enum EventType {
|
||||||
$($name($module::$name),)*
|
$($name($module::$name),)*
|
||||||
}
|
}
|
||||||
|
@ -160,7 +163,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct Test {
|
pub struct Test {
|
||||||
pub num_players: i64,
|
pub num_players: i64,
|
||||||
}
|
}
|
||||||
|
@ -168,7 +171,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct TestSpec {
|
pub struct TestSpec {
|
||||||
pub num_players: i64,
|
pub num_players: i64,
|
||||||
}
|
}
|
||||||
|
@ -176,7 +179,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct TestUpdate {
|
pub struct TestUpdate {
|
||||||
pub win_game: bool,
|
pub win_game: bool,
|
||||||
}
|
}
|
||||||
|
@ -219,7 +222,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))]
|
#[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>>,
|
||||||
|
@ -231,7 +234,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))]
|
#[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>>,
|
||||||
|
@ -255,7 +258,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))]
|
#[cfg_attr(feature = "sycamore", derive(WebEdit, WebView))]
|
||||||
pub struct TeamGameUpdateSetTeam {
|
pub struct TeamGameUpdateSetTeam {
|
||||||
pub team: String,
|
pub team: String,
|
||||||
pub members: Vec<String>,
|
pub members: Vec<String>,
|
||||||
|
@ -264,7 +267,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))]
|
#[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(TeamGameUpdateSetTeam),
|
||||||
|
@ -283,7 +286,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))]
|
#[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),
|
||||||
|
@ -301,10 +304,14 @@ 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))]
|
#[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),
|
||||||
}
|
}
|
||||||
|
@ -390,7 +397,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))]
|
#[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.)
|
||||||
|
@ -417,7 +424,7 @@ pub mod free_for_all_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))]
|
#[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.)
|
||||||
|
@ -439,7 +446,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))]
|
#[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>,
|
||||||
|
@ -457,7 +464,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))]
|
#[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),
|
||||||
|
@ -475,7 +482,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))]
|
#[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>),
|
||||||
|
@ -493,7 +500,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))]
|
#[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>),
|
||||||
|
@ -515,7 +522,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))]
|
#[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),
|
||||||
|
|
|
@ -5,6 +5,9 @@ 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;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
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 Signal<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> From<&'a Signal<T>> for ViewProps<'a, T> {
|
||||||
|
fn from(state: &'a Signal<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 Signal<T>
|
||||||
|
where
|
||||||
|
ViewProps<'a, T>: From<&'a Signal<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.get().clone().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> {
|
||||||
|
let signal = create_signal(cx, props.state.get());
|
||||||
|
view! { cx,
|
||||||
|
input(type="checkbox", checked=*signal.get().as_ref().clone(), 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.get().as_ref().clone().into_iter().map(|x| {
|
||||||
|
let x = create_signal(cx, 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> {
|
||||||
|
let a = create_signal(cx, props.state.get().as_ref().clone().0);
|
||||||
|
let b = create_signal(cx, props.state.get().as_ref().clone().1);
|
||||||
|
view! { cx,
|
||||||
|
Block(title="Tuple".into()) {
|
||||||
|
(a.view(cx))
|
||||||
|
(b.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.get().as_ref().clone() {
|
||||||
|
Some(x) => view! { cx, (create_signal(cx, x.clone()).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;
|
||||||
|
}
|
|
@ -2,8 +2,425 @@ mod edit;
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{format_ident, quote};
|
use quote::{__private::TokenStream as TokenStream2, format_ident, quote};
|
||||||
use syn::Fields;
|
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 {
|
||||||
|
#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 {
|
||||||
|
#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 signals = fields.clone().map(|f| {
|
||||||
|
let name = f.name;
|
||||||
|
quote! {
|
||||||
|
let #name = create_signal(cx, state.get().#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;
|
||||||
|
|
||||||
|
#(#signals)*
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
Block(title=#title.to_string()) {
|
||||||
|
p {
|
||||||
|
#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) => create_signal(cx, 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 {
|
||||||
|
#description
|
||||||
|
}
|
||||||
|
(match state.get().as_ref().clone() {
|
||||||
|
#(#view_description,)*
|
||||||
|
_ => view! { cx, }
|
||||||
|
})
|
||||||
|
(match state.get().as_ref().clone() {
|
||||||
|
#(#view_match,)*
|
||||||
|
_ => view! { cx, }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(WebEdit)]
|
#[proc_macro_derive(WebEdit)]
|
||||||
pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
|
@ -12,161 +429,77 @@ pub fn web_edit(tokens: TokenStream) -> TokenStream {
|
||||||
let name = input.ident;
|
let name = input.ident;
|
||||||
let edit_ident = format_ident!("{}Edit", name);
|
let edit_ident = format_ident!("{}Edit", name);
|
||||||
|
|
||||||
let editable = quote! {
|
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;
|
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 {
|
impl<'a, G: Html> Editable<'a, G> for #name {
|
||||||
type Editor = #edit_ident;
|
type Editor = #edit_ident;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let derived = match input.data {
|
TokenStream::from(res)
|
||||||
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)| {
|
#[proc_macro_derive(WebView)]
|
||||||
let title = name_str.to_case(Case::Title);
|
pub fn web_view(tokens: TokenStream) -> TokenStream {
|
||||||
quote! {
|
let input: syn::DeriveInput = syn::parse(tokens).unwrap();
|
||||||
p {
|
|
||||||
label { (#title) }
|
|
||||||
(#name.edit(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let signals = fields.clone().map(|(_name_str, name)| {
|
let name = input.ident;
|
||||||
quote! {
|
let view_ident = format_ident!("{}View", name);
|
||||||
let #name = create_signal(cx, state.get_untracked().#name.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let effect_fields = fields.clone().map(|(_name_str, name)| {
|
let (t, d) = get_title_description(&input.attrs);
|
||||||
quote! {
|
|
||||||
#name: #name.get().as_ref().clone()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
quote! {
|
let title = t.unwrap_or(name.to_string());
|
||||||
impl<'a, G: Html> Editor<'a, G, #name> for #edit_ident {
|
let description = d;
|
||||||
fn edit(cx: Scope<'a>, props: EditProps<'a, #name>) -> View<G> {
|
|
||||||
let state = props.state;
|
|
||||||
|
|
||||||
#(#signals)*
|
let props = ItemProps {
|
||||||
|
name: name.clone(),
|
||||||
|
title: title.clone(),
|
||||||
|
description: description.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
create_effect(cx, || {
|
let inner = match input.data {
|
||||||
state.set(#name {
|
syn::Data::Struct(s) => struct_view(&props, s),
|
||||||
#(#effect_fields,)*
|
syn::Data::Enum(e) => enum_view(&props, e),
|
||||||
..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!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", &derived.to_string());
|
let res = quote! {
|
||||||
TokenStream::from(quote! {
|
pub struct #view_ident;
|
||||||
#derived
|
|
||||||
|
|
||||||
#editable
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-13bbcf1f99e2f75c.css">
|
<link rel="stylesheet" href="/style-6e929d2286e0d817.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-7e6e148776e9cebb_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
<link rel="preload" href="/index-16cdb8c780ec2870_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
<link rel="modulepreload" href="/index-7e6e148776e9cebb.js"></head>
|
<link rel="modulepreload" href="/index-16cdb8c780ec2870.js"></head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module">import init from '/index-7e6e148776e9cebb.js';init('/index-7e6e148776e9cebb_bg.wasm');</script></body></html>
|
<script type="module">import init from '/index-16cdb8c780ec2870.js';init('/index-16cdb8c780ec2870_bg.wasm');</script></body></html>
|
|
@ -1,22 +1,65 @@
|
||||||
use lan_party_core::{
|
use lan_party_core::{
|
||||||
edit::IntoEdit,
|
edit::IntoEdit,
|
||||||
event::{EventSpec, EventUpdate},
|
event::{Event, EventSpec, EventUpdate},
|
||||||
|
view::IntoView,
|
||||||
};
|
};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use sycamore::prelude::*;
|
use reqwasm::http::Method;
|
||||||
|
use sycamore::{futures::spawn_local_scoped, prelude::*};
|
||||||
|
|
||||||
use crate::components::{Block, Button};
|
use crate::{
|
||||||
|
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 = create_signal(cx, None);
|
||||||
|
let test_event = create_signal(cx, None);
|
||||||
|
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
events.set(Some(
|
||||||
|
api_request::<_, Vec<Event>>(Method::GET, "/event", Option::<()>::None)
|
||||||
|
.await
|
||||||
|
.map(|inner| inner.unwrap())
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
test_event.set(Some(events.get().unwrap().get(0).unwrap().clone()));
|
||||||
|
debug!("{:#?}", test_event);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
let mut cloned = (*events).get().as_ref().clone();
|
||||||
|
cloned.unwrap().push(new_event.unwrap());
|
||||||
|
events.set(cloned);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
view! { cx,
|
view! { cx,
|
||||||
|
Block(title="Events".into()) {
|
||||||
|
(test_event.view(cx))
|
||||||
|
(events.view(cx))
|
||||||
|
}
|
||||||
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_spec.get()))
|
Button(icon="mdi-check".into(), onclick=move |_| debug!("{:#?}", event_update.get()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,3 +58,7 @@ textarea:focus, input:focus{
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue