Props, reconciler fixes, demo fixes

This commit is contained in:
Ryan McGrath 2019-06-05 21:15:46 -07:00
parent 47163c64f9
commit 290b57d336
No known key found for this signature in database
GPG key ID: 811674B62B666830
17 changed files with 284 additions and 190 deletions

View file

@ -0,0 +1,31 @@
//! This is a generic view used for avoiding a circular dependency issue.
//! You should never need to touch this.
use std::any::Any;
use crate::ComponentKey;
use crate::traits::{Component, Props};
#[derive(Default)]
pub struct GenericRootViewProps;
/// This is never actually created, and is here primarily to avoid a circular
/// depedency issue (we can't import the View from alchemy's core crate, since the core crate
/// depends on this crate).
pub struct GenericRootView;
impl GenericRootView {
fn get_default_props() -> GenericRootViewProps {
GenericRootViewProps {}
}
}
impl Props for GenericRootView {
fn set_props(&mut self, _: &mut Any) {}
}
impl Component for GenericRootView {
fn new(_: ComponentKey) -> GenericRootView {
GenericRootView {}
}
}

View file

@ -1,33 +1,15 @@
//! Internal struct used for tracking component instances and their
//! associated metadata (layout, appearance, etc).
use alchemy_styles::Appearance;
use alchemy_styles::{Appearance, StylesList};
use alchemy_styles::stretch::node::{Node as LayoutNode};
use crate::rsx::Props;
use crate::traits::Component;
pub(crate) struct Instance {
pub(crate) tag: &'static str,
pub(crate) component: Box<Component>,
pub(crate) props: Props,
pub(crate) style_keys: StylesList,
pub(crate) component: Box<Component + 'static>,
pub(crate) appearance: Appearance,
pub(crate) layout: Option<LayoutNode>
}
impl Instance {
pub(crate) fn new(
tag: &'static str,
component: Box<Component>,
props: Props,
layout: Option<LayoutNode>
) -> Instance {
Instance {
tag: tag,
component: component,
props: props,
appearance: Appearance::default(),
layout: layout
}
}
}

View file

@ -8,12 +8,11 @@ use std::error::Error;
use alchemy_styles::THEME_ENGINE;
use alchemy_styles::styles::{Appearance, Dimension, Number, Size, Style};
use crate::traits::Component;
use crate::rsx::{Props, RSX, VirtualNode};
use alchemy_styles::stretch::node::{Node as LayoutNode, Stretch as LayoutStore};
use crate::rsx::{RSX, VirtualNode};
use crate::traits::Component;
pub mod key;
use key::ComponentKey;
@ -26,16 +25,10 @@ use error::RenderEngineError;
mod instance;
use instance::Instance;
/// This is never actually created, and is here primarily to avoid a circular
/// depedency issue (we can't import the View from alchemy's core crate, since the core crate
/// depends on this crate).
mod generic_root_view_stub;
use generic_root_view_stub::{GenericRootView, GenericRootViewProps};
pub struct GenericRootView;
impl Component for GenericRootView {
fn constructor(key: ComponentKey) -> GenericRootView {
GenericRootView {}
}
}
struct GenericRootProps;
pub struct RenderEngine {
queued_state_updates: Mutex<Vec<i32>>,
@ -60,25 +53,24 @@ impl RenderEngine {
/// they get a key back. When they want to instruct the global `RenderEngine`
/// to re-render or update their tree, they pass that key and whatever the new tree
/// should be.
pub fn register_root_component<C: Component + 'static>(&self, instance: C) -> Result<ComponentKey, Box<Error>> {
pub fn register_root_component<C: Component + 'static>(&self, component: C) -> Result<ComponentKey, Box<Error>> {
// Conceivably, this doesn't NEED to be a thing... but for now it is. If you've stumbled
// upon here, wayward traveler, in need of a non-native-root-component, please open an
// issue to discuss. :)
if !instance.has_native_backing_node() {
if !component.has_native_backing_node() {
return Err(Box::new(RenderEngineError::InvalidRootComponent {}));
}
let mut component_store = self.components.lock().unwrap();
let mut layouts_store = self.layouts.lock().unwrap();
let component_key = component_store.new_key();
component_store.insert(component_key, Instance::new("root", Box::new(instance), {
let mut props = Props::default();
props.styles = "root".into();
props
}, {
let mut layouts_store = self.layouts.lock().unwrap();
let style = Style::default();
Some(layouts_store.new_node(style, vec![])?)
}))?;
component_store.insert(component_key, Instance {
tag: "root",
style_keys: "root".into(),
component: Box::new(component),
appearance: Appearance::default(),
layout: Some(layouts_store.new_node(Style::default(), vec![])?)
})?;
Ok(component_key)
}
@ -97,19 +89,19 @@ impl RenderEngine {
let mut component_store = self.components.lock().unwrap();
let mut layout_store = self.layouts.lock().unwrap();
let new_root_node = RSX::node("root", |_| {
let new_root_node = RSX::node("root", "root".into(), |_| {
Box::new(GenericRootView {})
}, Props::root(match child {
}, Box::new(GenericRootViewProps {}), match child {
RSX::VirtualNode(node) => {
if node.tag == "Fragment" {
node.props.children
node.children
} else {
vec![RSX::VirtualNode(node)]
}
},
_ => vec![]
}));
});
recursively_diff_tree(key, new_root_node, &mut component_store, &mut layout_store)?;
@ -117,7 +109,7 @@ impl RenderEngine {
let mut root_instance = component_store.get_mut(key)?;
let layout = root_instance.layout.unwrap();
let mut style = Style::default();
THEME_ENGINE.configure_styles_for_keys(&root_instance.props.styles, &mut style, &mut root_instance.appearance);
THEME_ENGINE.configure_styles_for_keys(&root_instance.style_keys, &mut style, &mut root_instance.appearance);
style.size = Size {
width: Dimension::Points(dimensions.0 as f32),
height: Dimension::Points(dimensions.1 as f32)
@ -179,7 +171,7 @@ fn recursively_diff_tree(
old_children.reverse();
if let RSX::VirtualNode(mut child) = new_tree {
for new_child_tree in child.props.children {
for new_child_tree in child.children {
match old_children.pop() {
// If there's a key in the old children for this position, it's
// something we need to update, so let's recurse right back into it.
@ -238,14 +230,21 @@ fn mount_component_tree(
let is_native_backed = component.has_native_backing_node();
// let state = get_derived_state_from_props()
let mut instance = Instance::new(tree.tag, component, tree.props, None);
let mut instance = Instance {
tag: tree.tag,
style_keys: tree.styles,
component: component,
appearance: Appearance::default(),
layout: None
};
if is_native_backed {
let mut style = Style::default();
THEME_ENGINE.configure_styles_for_keys(&instance.props.styles, &mut style, &mut instance.appearance);
THEME_ENGINE.configure_styles_for_keys(&instance.style_keys, &mut style, &mut instance.appearance);
instance.layout = Some(layout_store.new_node(style, vec![])?);
}
let rendered = instance.component.render(&instance.props);
let rendered = instance.component.render(tree.children);
// instance.get_snapshot_before_update()
component_store.insert(key, instance)?;
@ -256,7 +255,7 @@ fn mount_component_tree(
// tag similar to what React does, which just hoists the children out of it and
// discards the rest.
if child.tag == "Fragment" {
for child_tree in child.props.children {
for child_tree in child.children {
if let RSX::VirtualNode(child_tree) = child_tree {
let child_key = mount_component_tree(child_tree, component_store, layout_store)?;
@ -286,7 +285,7 @@ fn mount_component_tree(
}
let instance_lol = component_store.get_mut(key)?;
instance_lol.component.component_did_mount(&instance_lol.props);
instance_lol.component.component_did_mount();
Ok(key)
}
@ -301,7 +300,7 @@ fn unmount_component_tree(
layout_store: &mut LayoutStore
) -> Result<Vec<LayoutNode>, Box<Error>> {
let mut instance = component_store.remove(key)?;
instance.component.component_will_unmount(&instance.props);
instance.component.component_will_unmount();
let mut layout_nodes = vec![];
@ -342,7 +341,11 @@ fn link_layout_nodess(
if let (Ok(parent_instance), Ok(child_instance)) = (components.get(parent), components.get(child)) {
if let (Some(parent_layout), Some(child_layout)) = (parent_instance.layout, child_instance.layout) {
layouts.add_child(parent_layout, child_layout)?;
parent_instance.component.append_child_component(&*child_instance.component);
if let Some(platform_node) = child_instance.component.borrow_native_backing_node() {
parent_instance.component.append_child_node(platform_node);
}
return Ok(());
}
}

View file

@ -3,23 +3,22 @@
//! uses these to build and alter UI; they're typically returned from `render()`
//! methods.
use std::any::Any;
use std::fmt::{Debug, Display};
use alchemy_styles::StylesList;
mod virtual_node;
pub use virtual_node::VirtualNode;
mod virtual_text;
pub use virtual_text::VirtualText;
mod props;
pub use props::Props;
use crate::reconciler::key::ComponentKey;
use crate::traits::Component;
/// An enum representing the types of nodes that the
/// system can work with. `None`, `VirtualText`, or `VirtualNode`.
#[derive(Clone)]
pub enum RSX {
None,
VirtualText(VirtualText),
@ -29,15 +28,19 @@ pub enum RSX {
impl RSX {
/// Shorthand method for creating a new `RSX::VirtualNode` instance. Rarely should you call
/// this yourself; the `rsx! {}` macro handles this for you.
pub fn node(
pub fn node<P: Any + 'static>(
tag: &'static str,
styles: StylesList,
create_fn: fn(key: ComponentKey) -> Box<Component>,
props: Props
props: P,
children: Vec<RSX>
) -> RSX {
RSX::VirtualNode(VirtualNode {
tag: tag,
create_component_fn: create_fn,
props: props
styles: styles,
props: Box::new(props),
children: children
})
}

View file

@ -31,7 +31,7 @@ impl<'a> From<&'a str> for AttributeType {
#[derive(Clone, Debug, Default)]
pub struct Props {
pub attributes: HashMap<&'static str, AttributeType>,
pub children: Vec<RSX>,
//pub children: Vec<RSX>,
pub key: String,
pub styles: StylesList
}
@ -42,17 +42,17 @@ impl Props {
key: String,
styles: StylesList,
attributes: HashMap<&'static str, AttributeType>,
children: Vec<RSX>
//children: Vec<RSX>
) -> Props {
Props {
attributes: attributes,
children: children,
//children: children,
key: key,
styles: styles
}
}
/// A helper method used for constructing root-level Properties.
/*/// A helper method used for constructing root-level Properties.
pub(crate) fn root(children: Vec<RSX>) -> Props {
Props {
attributes: HashMap::new(),
@ -65,7 +65,7 @@ impl Props {
/// Returns a Vec of RSX nodes, which are really just cloned pointers for the most part.
pub fn children(&self) -> Vec<RSX> {
self.children.clone()
}
}*/
/// Returns a Option<&AttributeType> from the `attributes` inner HashMap.
pub fn get(&self, key: &str) -> Option<&AttributeType> {

View file

@ -1,30 +1,36 @@
//! Implements the `RSX::VirtualNode` struct, which is a bit of a recursive
//! structure.
use std::any::Any;
use std::fmt::{Display, Debug};
use alchemy_styles::StylesList;
use crate::reconciler::key::ComponentKey;
use crate::rsx::{RSX, Props};
use crate::rsx::RSX;
use crate::traits::Component;
/// A VirtualNode is akin to an `Element` in React terms. Here, we provide a way
/// for lazy `Component` instantiation, properties, children and so on.
#[derive(Clone)]
pub struct VirtualNode {
/// Used in debugging/printing/etc.
pub tag: &'static str,
/// Used for determining which CSS styles should be applied to this node.
/// This property is accessed often enough that it's separated out here.
pub styles: StylesList,
/// `Component` instances are created on-demand, if the reconciler deems it be so. This
/// is a closure that should return an instance of the correct type.
pub create_component_fn: fn(key: ComponentKey) -> Box<Component>,
/// `Props`, which are to be passed to this `Component` at various lifecycle methods. Once
/// the reconciler takes ownership of this VirtualNode, these props are moved to a different
/// location - thus, you shouldn't rely on them for anything unless you specifically keep
/// ownership of a VirtualNode.
///
/// This aspect of functionality may be pulled in a later release if it causes too many issues.
pub props: Props
/// When some RSX is returned, we scoop up the props inside a special block, and then shove
/// them in here as an `Any` object. When you `derive(Props)` on a `Component` struct, it
/// creates a setter that specifically handles downcasting and persisting props for you.
pub props: Box<Any>,
/// Child components for this node.
pub children: Vec<RSX>
}
impl Display for VirtualNode {
@ -32,7 +38,7 @@ impl Display for VirtualNode {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(f, "<{}>", self.tag)?;
for child in &self.props.children {
for child in &self.children {
write!(f, "{:?}", child)?;
}

View file

@ -1,12 +1,14 @@
//! Traits that are used in Alchemy. Alchemy implements a React-based Component
//! lifecycle, coupled with a delegate pattern inspired by those found in AppKit/UIKit.
use std::any::Any;
use alchemy_styles::styles::{Appearance, Layout};
//use crate::RENDER_ENGINE;
use crate::error::Error;
use crate::reconciler::key::ComponentKey;
use crate::rsx::{RSX, Props};
use crate::rsx::RSX;
/// A per-platform wrapped Pointer type, used for attaching views/widgets.
#[cfg(feature = "cocoa")]
@ -70,7 +72,9 @@ pub trait WindowDelegate: Send + Sync {
fn render(&self) -> Result<RSX, Error> { Ok(RSX::None) }
}
pub trait State {}
pub trait Props {
fn set_props(&mut self, new_props: &mut Any);
}
/// The `Component` lifecycle, mostly inspired from React, with a few extra methods for views that
/// need to have a backing native layer. A good breakdown of the React Component lifecycle can be
@ -79,8 +83,8 @@ pub trait State {}
/// Alchemy does not currently implement Hooks, and at the moment has no plans to do so (the API
/// doesn't feel comfortable in Rust, in any way I tried). If you think you have an interesting
/// proposal for this, feel free to open an issue!
pub trait Component: Send + Sync {
fn constructor(key: ComponentKey) -> Self where Self: Sized;
pub trait Component: Props + Send + Sync {
fn new(key: ComponentKey) -> Self where Self: Sized;
/// Indicates whether a Component instance carries a native backing node. If you return `true`
/// from this, the reconciler will opt-in to the native backing layer. Returns `false` by
@ -91,16 +95,16 @@ pub trait Component: Send + Sync {
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> { None }
/// If you implement a Native-backed component, you'll need to implement this. Given a
/// `component`, you need to instruct the system how to append it to the tree at your point.
fn append_child_component(&self, _component: &Component) {}
/// `node`, you need to instruct the system how to append it to the tree at your point.
fn append_child_node(&self, _component: PlatformSpecificNodeType) {}
/// If you implement a Native-backed component, you'll need to implement this. Given a
/// `component`, you need to instruct the system how to replace it in the tree at your point.
fn replace_child_component(&self, _component: &Component) {}
/// `node`, you need to instruct the system how to replace it in the tree at your point.
fn replace_child_node(&self, _component: PlatformSpecificNodeType) {}
/// If you implement a Native-backed component, you'll need to implement this. Given a
/// `component`, you need to instruct the system how to remove it from the tree at your point.
fn remove_child_component(&self, _component: &Component) {}
/// `node`, you need to instruct the system how to remove it from the tree at your point.
fn remove_child_node(&self, _component: PlatformSpecificNodeType) {}
/// Given a configured 'appearance' and computed `layout`, this method should transform them
/// into appropriate calls to the backing native node.
@ -109,7 +113,7 @@ pub trait Component: Send + Sync {
/// Invoked right before calling the render method, both on the initial mount and on subsequent updates.
/// It should return an object to update the state, or null to update nothing.
/// This method exists for rare use cases where the state depends on changes in props over time.
fn get_derived_state_from_props(&self, _props: Props) {}
fn get_derived_state_from_props(&self) {}
/// Invoked right before the most recently rendered output is committed to the backing layer tree.
/// It enables your component to capture some information from the tree (e.g. scroll position) before it's
@ -118,18 +122,18 @@ pub trait Component: Send + Sync {
///
/// This use case is not common, but it may occur in UIs like a chat thread that need to handle scroll
/// position in a special way. A snapshot value (or None) should be returned.
fn get_snapshot_before_update(&self, _props: Props) {}
fn get_snapshot_before_update(&self) {}
/// Invoked immediately after a component is mounted (inserted into the tree).
/// If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
/// This method is also a good place to set up any subscriptions. If you do that, dont forget to unsubscribe
/// in component_will_unmount().
fn component_did_mount(&mut self, _props: &Props) {}
fn component_did_mount(&mut self) {}
/// Invoked immediately after updating occurs. This method is not called for the initial render.
/// This is also a good place to do network requests as long as you compare the current props to previous props
/// (e.g. a network request may not be necessary if the props have not changed).
fn component_did_update(&mut self, _props: &Props) {}
fn component_did_update(&mut self) {}
/// Invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this
/// method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that
@ -137,12 +141,12 @@ pub trait Component: Send + Sync {
///
/// You should not call set state in this method because the component will never be re-rendered. Once a
/// component instance is unmounted, it will never be mounted again.
fn component_will_unmount(&mut self, _props: &Props) {}
fn component_will_unmount(&mut self) {}
/// Invoked after an error has been thrown by a descendant component. Called during the "commit" phase,
/// so side-effects are permitted. It should be used for things like logging errors (e.g,
/// Sentry).
fn component_did_catch(&mut self, _props: &Props/* error: */) {}
fn component_did_catch(&mut self /* error: */) {}
/// Use this to let Alchemy know if a components output is not affected by the current change in state
/// or props. The default behavior is to re-render on every state change, and in the vast majority of
@ -161,11 +165,11 @@ pub trait Component: Send + Sync {
/// returns the same result each time its invoked, and it does not directly interact with the
/// backing rendering framework.
///
/// If you need to interact with the browser, perform your work in component_did_mount() or the other
/// If you need to interact with the native layer, perform your work in component_did_mount() or the other
/// lifecycle methods instead. Keeping `render()` pure makes components easier to think about.
///
/// This method is not called if should_component_update() returns `false`.
fn render(&self, _props: &Props) -> Result<RSX, Error> { Ok(RSX::None) }
fn render(&self, children: Vec<RSX>) -> Result<RSX, Error> { Ok(RSX::None) }
/// This lifecycle is invoked after an error has been thrown by a descendant component. It receives
/// the error that was thrown as a parameter and should return a value to update state.