فهرست منبع

Feat: dioxus frontend crate

Jonathan Kelley 4 سال پیش
والد
کامیت
4d7ac5b

+ 2 - 0
Cargo.toml

@@ -1,10 +1,12 @@
 [workspace]
 members = [
     # Built-in
+    "packages/dioxus",
     "packages/core",
     "packages/hooks",
     "packages/recoil",
     "packages/redux",
+    "packages/core-macro",
     # TODO @Jon, share the validation code
     # "packages/web",
     "packages/cli",

+ 37 - 0
docs/6-subscription-api.md

@@ -0,0 +1,37 @@
+# Subscriptions 
+
+Yew subscriptions are used to schedule update for components into the future. The `Context` object can create subscriptions:
+
+```rust
+fn Component(ctx: Component<()>) -> VNode {
+    let update = ctx.schedule();
+    
+    // Now, when the subscription is called, the component will be re-evaluted
+    update.consume();
+}
+```
+
+Whenever a component's subscription is called, the component will then re-evaluated. You can consider the input properties of
+a component to be just another form of subscription. By default, the Dioxus component system automatically diffs a component's props
+when the parent function is called, and if the props are different, the child component's subscription is called. 
+
+The subscription API exposes this functionality allowing hooks and state management solutions the ability to update components whenever
+some state or event occurs outside of the component. For instance, the `use_context` hook uses this to subscribe components that use a
+particular context.
+
+
+```rust
+fn use_context<I>(ctx: Context<T>) -> I {
+  
+}
+
+
+
+
+
+
+
+```
+
+
+

+ 16 - 1
examples/Cargo.toml

@@ -9,14 +9,21 @@ edition = "2018"
 [dependencies]
 fern = { version = "0.6.0", features = ["colored"] }
 log = "0.4.1"
-dioxus-core = { path = "../packages/core" }
+dioxus = { path = "../packages/dioxus" }
 rand = "0.8.2"
 
 
 [dev-dependencies]
+# For the tide ssr examples
 async-std = { version = "1.9.0", features = ["attributes"] }
 tide = { version = "0.15.0" }
 
+# For the doc generator
+pulldown-cmark = { version = "0.8.0", default-features = false }
+
+
+anyhow = "*"
+
 
 [lib]
 path = "common.rs"
@@ -28,3 +35,11 @@ name = "hello"
 [[example]]
 path = "tide_ssr.rs"
 name = "tide_ssr"
+
+[[example]]
+path = "doc_generator.rs"
+name = "doc_generator"
+
+[[example]]
+path = "router.rs"
+name = "router"

+ 57 - 0
examples/doc_generator.rs

@@ -0,0 +1,57 @@
+//! The docs generator takes in the `docs` folder and creates a neat, statically-renderer webpage.
+//! These docs are used to generate the public-facing doc content, showcasing Dioxus' abiltity to
+//! be used in custom static rendering pipelines.
+//!
+//! We use pulldown_cmark as the markdown parser, but instead of outputting html directly, we output
+//! VNodes to be used in conjuction with our custom templates.
+
+use dioxus::core::prelude::*;
+use pulldown_cmark::{Options, Parser};
+
+fn main() {
+    let gen_dir = "../docs/";
+
+    let site: FC<()> = |_| {
+        html! {
+            <html>
+
+            <head>
+            </head>
+
+            <body>
+            </body>
+
+            </html>
+        }
+    };
+}
+
+static Homepage: FC<()> = |_| {
+    html! {<div> </div>}
+};
+
+static DocPage: FC<()> = |_| {
+    html! {<div> </div>}
+};
+
+// struct StaticSiteCfg {
+//     gen_dir: &'static str,
+//     homepage_template: fn() -> VNode,
+//     page_template: fn(page: &'static str) -> VNode,
+// }
+
+// impl StaticSiteCfg {
+//     fn render(self) -> anyhow::Result<VNode> {
+//         let StaticSiteCfg { .. } = self;
+
+//         // Set up options and parser. Strikethroughs are not part of the CommonMark standard
+//         // and we therefore must enable it explicitly.
+//         let mut options = Options::empty();
+//         options.insert(Options::ENABLE_STRIKETHROUGH);
+//         let parser = Parser::new_ext(markdown_input, options);
+
+//         //
+
+//         Ok(html! {<div> </div>})
+//     }
+// }

+ 9 - 0
examples/router.rs

@@ -0,0 +1,9 @@
+//! Dioxus Router
+
+use dioxus::prelude::*;
+
+fn main() {
+    let mut app = App::new();
+
+    // app.at(c)
+}

+ 1 - 1
examples/tide_ssr.rs

@@ -4,7 +4,7 @@
 //! Server-side-renderered webpages are a great use of Rust's async story, where servers can handle
 //! thousands of simultaneous clients on minimal hardware.
 
-use dioxus_core::prelude::*;
+use dioxus::prelude::*;
 use tide::{Request, Response};
 
 #[async_std::main]

+ 20 - 0
packages/core-macro/Cargo.toml

@@ -0,0 +1,20 @@
+[package]
+name = "dioxus-core-macro"
+version = "0.0.0"
+authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1.0.6"
+quote = "1.0"
+syn = { version = "1.0.11", features = ["full"] }
+
+# testing
+[dev-dependencies]
+rustversion = "1.0"
+trybuild = "1.0"

+ 211 - 0
packages/core-macro/src/lib.rs

@@ -0,0 +1,211 @@
+use proc_macro2::TokenStream;
+use quote::{quote, quote_spanned, ToTokens};
+use syn::parse::{Parse, ParseStream};
+use syn::spanned::Spanned;
+use syn::{
+    parse_macro_input, Attribute, Block, FnArg, Ident, Item, ItemFn, ReturnType, Type, Visibility,
+};
+
+struct FunctionComponent {
+    block: Box<Block>,
+    props_type: Box<Type>,
+    arg: FnArg,
+    vis: Visibility,
+    attrs: Vec<Attribute>,
+    name: Ident,
+    return_type: Box<Type>,
+}
+
+impl Parse for FunctionComponent {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let parsed: Item = input.parse()?;
+
+        match parsed {
+            Item::Fn(func) => {
+                let ItemFn {
+                    attrs,
+                    vis,
+                    sig,
+                    block,
+                } = func;
+
+                if !sig.generics.params.is_empty() {
+                    return Err(syn::Error::new_spanned(
+                        sig.generics,
+                        "function components can't contain generics",
+                    ));
+                }
+
+                if sig.asyncness.is_some() {
+                    return Err(syn::Error::new_spanned(
+                        sig.asyncness,
+                        "function components can't be async",
+                    ));
+                }
+
+                if sig.constness.is_some() {
+                    return Err(syn::Error::new_spanned(
+                        sig.constness,
+                        "const functions can't be function components",
+                    ));
+                }
+
+                if sig.abi.is_some() {
+                    return Err(syn::Error::new_spanned(
+                        sig.abi,
+                        "extern functions can't be function components",
+                    ));
+                }
+
+                let return_type = match sig.output {
+                    ReturnType::Default => {
+                        return Err(syn::Error::new_spanned(
+                            sig,
+                            "function components must return `yew::Html`",
+                        ))
+                    }
+                    ReturnType::Type(_, ty) => ty,
+                };
+
+                let mut inputs = sig.inputs.into_iter();
+                let arg: FnArg = inputs
+                    .next()
+                    .unwrap_or_else(|| syn::parse_quote! { _: &() });
+
+                let ty = match &arg {
+                    FnArg::Typed(arg) => match &*arg.ty {
+                        Type::Reference(ty) => {
+                            if ty.lifetime.is_some() {
+                                return Err(syn::Error::new_spanned(
+                                    &ty.lifetime,
+                                    "reference must not have a lifetime",
+                                ));
+                            }
+
+                            if ty.mutability.is_some() {
+                                return Err(syn::Error::new_spanned(
+                                    &ty.mutability,
+                                    "reference must not be mutable",
+                                ));
+                            }
+
+                            ty.elem.clone()
+                        }
+                        ty => {
+                            let msg = format!(
+                                "expected a reference to a `Properties` type (try: `&{}`)",
+                                ty.to_token_stream()
+                            );
+                            return Err(syn::Error::new_spanned(ty, msg));
+                        }
+                    },
+
+                    FnArg::Receiver(_) => {
+                        return Err(syn::Error::new_spanned(
+                            arg,
+                            "function components can't accept a receiver",
+                        ));
+                    }
+                };
+
+                // Checking after param parsing may make it a little inefficient
+                // but that's a requirement for better error messages in case of receivers
+                // `>0` because first one is already consumed.
+                if inputs.len() > 0 {
+                    let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect();
+                    return Err(syn::Error::new_spanned(
+                        params,
+                        "function components can accept at most one parameter for the props",
+                    ));
+                }
+
+                Ok(Self {
+                    props_type: ty,
+                    block,
+                    arg,
+                    vis,
+                    attrs,
+                    name: sig.ident,
+                    return_type,
+                })
+            }
+            item => Err(syn::Error::new_spanned(
+                item,
+                "`function_component` attribute can only be applied to functions",
+            )),
+        }
+    }
+}
+
+struct FunctionComponentName {
+    component_name: Ident,
+}
+
+impl Parse for FunctionComponentName {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        if input.is_empty() {
+            return Err(input.error("expected identifier for the component"));
+        }
+
+        let component_name = input.parse()?;
+
+        Ok(Self { component_name })
+    }
+}
+
+#[proc_macro_attribute]
+pub fn function_component(
+    attr: proc_macro::TokenStream,
+    item: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+    let item = parse_macro_input!(item as FunctionComponent);
+    let attr = parse_macro_input!(attr as FunctionComponentName);
+
+    function_component_impl(attr, item)
+        .unwrap_or_else(|err| err.to_compile_error())
+        .into()
+}
+
+fn function_component_impl(
+    name: FunctionComponentName,
+    component: FunctionComponent,
+) -> syn::Result<TokenStream> {
+    let FunctionComponentName { component_name } = name;
+
+    let FunctionComponent {
+        block,
+        props_type,
+        arg,
+        vis,
+        attrs,
+        name: function_name,
+        return_type,
+    } = component;
+
+    if function_name == component_name {
+        return Err(syn::Error::new_spanned(
+            component_name,
+            "the component must not have the same name as the function",
+        ));
+    }
+
+    let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html);
+
+    let quoted = quote! {
+        #[doc(hidden)]
+        #[allow(non_camel_case_types)]
+        #vis struct #function_name;
+
+        impl ::yew_functional::FunctionProvider for #function_name {
+            type TProps = #props_type;
+
+            fn run(#arg) -> #ret_type {
+                #block
+            }
+        }
+
+        #(#attrs)*
+        #vis type #component_name = ::yew_functional::FunctionComponent<#function_name>;
+    };
+    Ok(quoted)
+}

+ 1 - 1
packages/core/Cargo.toml

@@ -11,8 +11,8 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
 
 [dependencies]
 generational-arena = "0.2.8"
-dioxus-html-macro = { path = "../html-macro", version = "0.1.0" }
 once_cell = "1.5.2"
+dioxus-html-macro = { path = "../html-macro", version = "0.1.0" }
 
 
 # web-sys = "0.3.46"

+ 77 - 12
packages/core/src/lib.rs

@@ -1,6 +1,47 @@
-//! Dioxus: a concurrent, functional, virtual dom for any renderer in Rust
+//! <div align="center">
+//!   <h1>🌗🚀 📦 Dioxus</h1>
+//!   <p>
+//!     <strong>A concurrent, functional, virtual DOM for Rust</strong>
+//!   </p>
+//! </div>
+//! Dioxus: a concurrent, functional, reactive virtual dom for any renderer in Rust.
 //!
 //!
+//! Dioxus is an efficient virtual dom implementation for building interactive user interfaces in Rust.
+//! This crate aims to maintain a uniform hook-based, renderer-agnostic UI framework for cross-platform development.
+//!
+//! ## Components
+//! The base unit of Dioxus is the `component`. Components can be easily created from just a function - no traits required:
+//! ```
+//! use dioxus_core::prelude::*;
+//!
+//! fn Example(ctx: Context<()>) -> VNode {
+//!     html! { <div> "Hello world!" </div> }
+//! }
+//! ```
+//! Components need to take a "Context" parameter which is generic over some properties. This defines how the component can be used
+//! and what properties can be used to specify it in the VNode output. All components in Dioxus are hook-based, which might be more
+//! complex than other approaches that use traits + lifecycle events. Alternatively, we provide a "lifecycle hook" if you want more
+//! granualar control with behavior similar to other UI frameworks.
+//!
+//! ## Hooks
+//! Dioxus uses hooks for state management. Hooks are a form of state persisted between calls of the function component. Instead of
+//! using a single struct to store data, hooks use the "use_hook" building block which allows the persistence of data between
+//! function component renders.
+//!
+//! This allows functions to reuse stateful logic between components, simplify large complex components, and adopt more clear context
+//! subscription patterns to make components easier to read.
+//!
+//! ## Supported Renderers
+//! Instead of being tightly coupled to a platform, browser, or toolkit, Dioxus implements a VirtualDOM object which
+//! can be consumed to draw the UI. The Dioxus VDOM is reactive and easily consumable by 3rd-party renderers via
+//! the `Patch` object. See [Implementing a Renderer](docs/8-custom-renderer.md) and the `StringRenderer` classes for information
+//! on how to implement your own custom renderer. We provide 1st-class support for these renderers:
+//! - dioxus-desktop (via WebView)
+//! - dioxus-web (via WebSys)
+//! - dioxus-ssr (via StringRenderer)
+//! - dioxus-liveview (SSR + StringRenderer)
+//!
 
 /// Re-export common types for ease of development use.
 /// Essential when working with the html! macro
@@ -9,12 +50,12 @@
 ///
 pub mod prelude {
     use crate::nodes;
-    pub use crate::virtual_dom::Context;
-    pub use crate::virtual_dom::VirtualDom;
+    pub use crate::virtual_dom::{Context, VirtualDom, FC};
     pub use nodes::iterables::IterableNodes;
     pub use nodes::*;
 
-    // hack "VNode"
+    // TODO @Jon, fix this
+    // hack the VNode type until VirtualNode is fixed in the macro crate
     pub type VirtualNode = VNode;
 
     // Re-export from the macro crate
@@ -24,7 +65,6 @@ pub mod prelude {
 /// The Dioxus Virtual Dom integrates an event system and virtual nodes to create reactive user interfaces.
 ///
 /// This module includes all life-cycle related mechanics, including the virtual dom, scopes, properties, and lifecycles.
-///
 pub mod virtual_dom {
     use super::*;
     use crate::nodes::VNode;
@@ -33,7 +73,12 @@ pub mod virtual_dom {
     /// An integrated virtual node system that progresses events and diffs UI trees.
     /// Differences are converted into patches which a renderer can use to draw the UI.
     pub struct VirtualDom {
-        arena: Arena<Scope>,
+        /// All mounted components are arena allocated to make additions, removals, and references easy to work with
+        /// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
+        components: Arena<Scope>,
+
+        /// Components generate lifecycle events
+        event_queue: Vec<LifecycleEvent>,
     }
 
     impl VirtualDom {
@@ -51,25 +96,41 @@ pub mod virtual_dom {
         /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
         /// to toss out the entire tree.
         pub fn new_with_props<T>(root: FC<T>) -> Self {
+            // Set a first lifecycle event to add the component
+            let first_event = LifecycleEvent::Add;
+
             Self {
-                arena: Arena::new(),
+                components: Arena::new(),
+                event_queue: vec![first_event],
             }
         }
     }
 
+    enum LifecycleEvent {
+        Add,
+    }
+
     /// Functional Components leverage the type FC to
     pub type FC<T> = fn(&mut Context<T>) -> VNode;
 
     /// The Scope that wraps a functional component
     /// Scope's hold subscription, context, and hook information, however, it is allocated on the heap.
-    pub struct Scope {}
+    pub struct Scope {
+        hook_idx: i32,
+        hooks: Vec<()>,
+    }
 
     impl Scope {
         fn new<T>() -> Self {
-            Self {}
+            Self {
+                hook_idx: 0,
+                hooks: vec![],
+            }
         }
     }
 
+    pub struct HookState {}
+
     /// Components in Dioxus use the "Context" object to interact with their lifecycle.
     /// This lets components schedule updates, integrate hooks, and expose their context via the context api.
     ///
@@ -92,7 +153,12 @@ pub mod virtual_dom {
     }
 
     pub trait Properties {}
+
+    // Auto derive for pure components
     impl Properties for () {}
+
+    // Set up a derive macro
+    // #[derive(Macro)]
 }
 
 /// Virtual Node Support
@@ -600,9 +666,8 @@ pub mod nodes {
     }
 }
 
-///
-///
-///
+/// The diffing algorithm to compare two VNode trees and generate a list of patches to update the VDom.
+/// Currently, using an index-based patching algorithm
 ///
 pub mod diff {
     use super::*;

+ 11 - 0
packages/dioxus/Cargo.toml

@@ -0,0 +1,11 @@
+[package]
+name = "dioxus"
+version = "0.1.0"
+authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
+edition = "2018"
+description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
+"license" = "MIT/Apache-2.0"
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+dioxus-core = { path = "../core", version = "0.1.0" }

+ 7 - 0
packages/dioxus/README.md

@@ -0,0 +1,7 @@
+# Dioxus
+
+This crate serves as the thin frontend over dioxus-core and dioxus-app. Here, we support docs, feature flags, and incorporate some extra tools and utilities into a consistent API.
+
+Ideally, we can prevent cfg creep into the core crate by moving it all into this frontend crate.
+
+For now, see dioxus-core for all docs and details

+ 16 - 0
packages/dioxus/src/lib.rs

@@ -0,0 +1,16 @@
+pub mod prelude {
+    pub use dioxus_core::prelude::*;
+}
+
+// Re-export core completely
+pub use dioxus_core as core;
+
+struct App {}
+
+fn new() -> App {
+    todo!()
+}
+
+struct Router {}
+
+struct Route {}