diff --git a/Cargo.toml b/Cargo.toml index 4158f4c78..749472725 100644 --- a/Cargo.toml +++ b/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", diff --git a/docs/6-subscription-api.md b/docs/6-subscription-api.md index e69de29bb..790266bbc 100644 --- a/docs/6-subscription-api.md +++ b/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(ctx: Context) -> I { + +} + + + + + + + +``` + + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f40a5712a..cd3bf37af 100644 --- a/examples/Cargo.toml +++ b/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" diff --git a/examples/doc_generator.rs b/examples/doc_generator.rs new file mode 100644 index 000000000..e6b7846f4 --- /dev/null +++ b/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! { + + + + + + + + + + } + }; +} + +static Homepage: FC<()> = |_| { + html! {
} +}; + +static DocPage: FC<()> = |_| { + html! {
} +}; + +// struct StaticSiteCfg { +// gen_dir: &'static str, +// homepage_template: fn() -> VNode, +// page_template: fn(page: &'static str) -> VNode, +// } + +// impl StaticSiteCfg { +// fn render(self) -> anyhow::Result { +// 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! {
}) +// } +// } diff --git a/examples/router.rs b/examples/router.rs new file mode 100644 index 000000000..8f7a4fa8a --- /dev/null +++ b/examples/router.rs @@ -0,0 +1,9 @@ +//! Dioxus Router + +use dioxus::prelude::*; + +fn main() { + let mut app = App::new(); + + // app.at(c) +} diff --git a/examples/tide_ssr.rs b/examples/tide_ssr.rs index 9147ea441..66ac42498 100644 --- a/examples/tide_ssr.rs +++ b/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] diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml new file mode 100644 index 000000000..c67da6442 --- /dev/null +++ b/packages/core-macro/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dioxus-core-macro" +version = "0.0.0" +authors = ["Jonathan Kelley "] +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" diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs new file mode 100644 index 000000000..eeb335574 --- /dev/null +++ b/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, + props_type: Box, + arg: FnArg, + vis: Visibility, + attrs: Vec, + name: Ident, + return_type: Box, +} + +impl Parse for FunctionComponent { + fn parse(input: ParseStream) -> syn::Result { + 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 { + 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 { + 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) +} diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 6ad22c9d9..7e9685a5f 100644 --- a/packages/core/Cargo.toml +++ b/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" diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 4f9dc9919..0304a80a8 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,6 +1,47 @@ -//! Dioxus: a concurrent, functional, virtual dom for any renderer in Rust +//!
+//!

🌗🚀 📦 Dioxus

+//!

+//! A concurrent, functional, virtual DOM for Rust +//!

+//!
+//! 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! {
"Hello world!"
} +//! } +//! ``` +//! 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, + /// 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, + + /// Components generate lifecycle events + event_queue: Vec, } 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(root: FC) -> 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 = fn(&mut Context) -> 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() -> 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::*; diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml new file mode 100644 index 000000000..b50cef952 --- /dev/null +++ b/packages/dioxus/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dioxus" +version = "0.1.0" +authors = ["Jonathan Kelley "] +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" } diff --git a/packages/dioxus/README.md b/packages/dioxus/README.md new file mode 100644 index 000000000..62ec74482 --- /dev/null +++ b/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 diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs new file mode 100644 index 000000000..89297af93 --- /dev/null +++ b/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 {}