Feat: dioxus frontend crate

This commit is contained in:
Jonathan Kelley 2021-01-20 12:04:27 -05:00
parent 6c1205e91b
commit 4d7ac5bb5d
13 changed files with 465 additions and 15 deletions

View file

@ -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",

View file

@ -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 {
}
```

View file

@ -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
examples/doc_generator.rs Normal file
View file

@ -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
examples/router.rs Normal file
View file

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

View file

@ -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]

View file

@ -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"

View file

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

View file

@ -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"

View file

@ -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::*;

View file

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

View file

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

View file

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