Move towards a new reconciler model, undo a lot of the weird stretch integrations I had done, separate out Layout and Appearance concepts, introduce ComponentKey and constructor lifecycle for components

This commit is contained in:
Ryan McGrath 2019-05-27 00:22:33 -07:00
parent 6fd3f79099
commit 91266cc841
No known key found for this signature in database
GPG key ID: 811674B62B666830
31 changed files with 820 additions and 941 deletions

View file

@ -17,4 +17,3 @@ alchemy-styles = { version = "0.1", path = "../styles" }
objc = { version = "0.2.6", optional = true }
objc_id = { version = "0.1.1", optional = true }
serde_json = "1"
uuid = { version = "0.7", features = ["v4"] }

View file

@ -11,7 +11,7 @@
//! This crate also includes the diffing and patching system for the widget tree -
//! it needs to live with the `Component` lifecycle to enable state updating.
pub use uuid::Uuid;
pub use std::sync::Arc;
use alchemy_styles::lazy_static;
@ -21,6 +21,7 @@ pub mod traits;
mod reconciler;
use reconciler::RenderEngine;
pub use reconciler::key::ComponentKey;
lazy_static! {
pub static ref RENDER_ENGINE: RenderEngine = RenderEngine::new();

View file

@ -2,29 +2,33 @@
//! run. These are mostly internal to the rendering engine itself, but could potentially
//! show up elsewhere.
use std::fmt;
use std::error::Error;
use core::any::Any;
use crate::reconciler::key::ComponentKey;
#[derive(Debug)]
pub enum RenderEngineError {
InvalidKeyError
InvalidKey,
InvalidRootComponent,
InvalidComponentKey(ComponentKey)
}
impl fmt::Display for RenderEngineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RenderEngineError::InvalidKeyError => write!(f, "An invalid key was passed to the render engine.")
impl std::fmt::Display for RenderEngineError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
RenderEngineError::InvalidComponentKey(ref node) => write!(f, "Invalid component key {:?}", node),
RenderEngineError::InvalidRootComponent => write!(f, "Invalid component type! Root nodes must be a natively backed node."),
RenderEngineError::InvalidKey => write!(f, "An invalid key was passed to the render engine.")
}
}
}
impl fmt::Debug for RenderEngineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RenderEngineError::InvalidKeyError => write!(f, "An invalid key was passed to the render engine: {{ file: {}, line: {} }}", file!(), line!())
impl std::error::Error for RenderEngineError {
fn description(&self) -> &str {
match *self {
RenderEngineError::InvalidComponentKey(_) => "The key is not part of the component storage instance",
RenderEngineError::InvalidRootComponent => "The root component must be a natively backed Component instance.",
RenderEngineError::InvalidKey => "An invalid key was passed to the render engine."
}
}
}
impl Error for RenderEngineError {
}

View file

@ -0,0 +1,51 @@
//! Implements an auto-incrementing ID for Component instances.
use std::sync::Mutex;
use alchemy_styles::lazy_static;
lazy_static! {
/// Global stretch instance id allocator.
pub(crate) static ref INSTANCE_ALLOCATOR: Mutex<Allocator> = Mutex::new(Allocator::new());
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Id {
id: u32
}
pub(crate) struct Allocator {
new_id: u32
}
impl Allocator {
pub fn new() -> Self {
Allocator { new_id: 1 }
}
pub fn allocate(&mut self) -> Id {
let id = self.new_id;
self.new_id += 1;
Id { id: id }
}
}
/// Used as a key for Component storage. Component instances receive these
/// in their constructor methods, and should retain them as a tool to update their
/// state.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ComponentKey {
pub(crate) instance: Id,
pub(crate) local: Id,
}
impl ComponentKey {
/// A placeholder value, used purely for ensuring the diffing algorithm remains
/// readable by reducing some unwrapping hell.
pub fn placeholder() -> ComponentKey {
ComponentKey {
instance: Id { id: 0 },
local: Id { id: 0 }
}
}
}

View file

@ -6,71 +6,73 @@ use std::collections::HashMap;
use std::error::Error;
use std::mem::{discriminant, swap};
use uuid::Uuid;
use alchemy_styles::{Stretch, THEME_ENGINE};
use alchemy_styles::styles::{Style, Dimension};
use alchemy_styles::number::Number;
use alchemy_styles::geometry::Size;
use alchemy_styles::THEME_ENGINE;
use alchemy_styles::styles::{Appearance,Dimension, Number, Size, Style};
use crate::traits::Component;
use crate::rsx::{Props, RSX, VirtualNode};
mod error;
use alchemy_styles::stretch::node::{Node as StyleNode, Stretch as LayoutStore};
pub mod key;
use key::ComponentKey;
pub mod storage;
use storage::ComponentStore;
pub mod error;
use error::RenderEngineError;
// This is never actually created, it's just to satisfy the fact that View
// is defined in the core crate, which we can't import here without creating a
// circular dependency.
struct StubView;
impl Component for StubView {}
impl Component for StubView {
fn constructor(key: ComponentKey) -> StubView {
StubView {}
}
}
pub struct RenderEngine {
pending_state_updates: Mutex<Vec<i32>>,
trees: Mutex<HashMap<Uuid, (RSX, Stretch)>>
queued_state_updates: Mutex<Vec<i32>>,
components: Mutex<ComponentStore>,
layouts: Mutex<LayoutStore>
}
impl RenderEngine {
pub(crate) fn new() -> RenderEngine {
RenderEngine {
pending_state_updates: Mutex::new(vec![]),
trees: Mutex::new(HashMap::new())
queued_state_updates: Mutex::new(vec![]),
components: Mutex::new(ComponentStore::new()),
layouts: Mutex::new(LayoutStore::new())
}
}
// pub fn queue_update_for(&self, component_ptr: usize, updater: Box<Fn() -> Component + Send + Sync + 'static>) {
// }
/// `Window`'s (or anything "root" in nature) need to register with the
/// reconciler for things like setState to work properly. When they do so,
/// 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) -> Uuid {
let mut root_node = RSX::node("root", || {
Arc::new(RwLock::new(StubView {}))
}, {
let mut props = Props::default();
props.styles = "root".into();
props
});
let mut stretch = Stretch::new();
if let RSX::VirtualNode(root) = &mut root_node {
let mut style = Style::default();
style.size = Size {
width: Dimension::Points(600.),
height: Dimension::Points(600.)
};
root.instance = Some(Arc::new(RwLock::new(instance)));
root.layout_node = match stretch.new_node(style, vec![]) {
Ok(node) => Some(node),
Err(e) => { None }
}
pub fn register_root_component<C: Component + 'static>(&self, instance: 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() {
return Err(Box::new(RenderEngineError::InvalidRootComponent {}));
}
let key = Uuid::new_v4();
let mut trees = self.trees.lock().unwrap();
trees.insert(key, (root_node, stretch));
key
let layout_key = {
let style = Style::default();
let mut layouts = self.layouts.lock().unwrap();
Some(layouts.new_node(style, vec![])?)
};
let mut components = self.components.lock().unwrap();
let component_key = components.new_node(instance, layout_key, vec![])?;
Ok(component_key)
}
/// Given a key, and a new root tree, will diff the tree structure (position, components,
@ -80,36 +82,37 @@ impl RenderEngine {
/// the new tree before discarding the old tree.
///
/// This calls the necessary component lifecycles per-component.
pub fn diff_and_render_root(&self, key: &Uuid, child: RSX) -> Result<(), Box<Error>> {
pub fn diff_and_render_root(&self, key: &ComponentKey, child: RSX) -> Result<(), Box<Error>> {
/*
let mut new_root = RSX::node("root", || {
Arc::new(RwLock::new(StubView {}))
Box::new(StubView {})
}, {
let mut props = Props::default();
props.styles = "root".into();
props
}, match child {
RSX::VirtualNode(mut child) => {
let mut children = vec![];
if child.tag == "Fragment" {
children.append(&mut child.children);
} else {
children.push(RSX::VirtualNode(child));
}
children
},
// If it's an RSX::None or RSX::VirtualText, we'll just do nothing, as... one
// requires nothing, and one isn't supported unless it's inside a <Text> tag, and
// we know the root element isn't a <Text> if we're here.
_ => vec![]
});
// If it's an RSX::None, or a RSX::VirtualText, we do nothing, as... one
// requires nothing, and one isn't supported unless it's inside a <Text> tag.
if let RSX::VirtualNode(mut child) = child {
if let RSX::VirtualNode(new_root_node) = &mut new_root {
if child.tag == "Fragment" {
new_root_node.children.append(&mut child.children);
} else {
new_root_node.children.push(RSX::VirtualNode(child));
}
}
}
let mut trees = self.trees.lock().unwrap();
let (old_root, mut stretch) = trees.remove(key).ok_or_else(|| RenderEngineError::InvalidKeyError {})?;
let (old_root, mut stretch) = trees.remove(key).ok_or_else(|| RenderEngineError::InvalidKey {})?;
let patched_new_root = diff_and_patch_trees(old_root, new_root, &mut stretch, 0)?;
/*let window_size = Size {
width: Number::Defined(600.),
height: Number::Defined(600.)
};*/
if let RSX::VirtualNode(node) = &patched_new_root {
if let Some(layout_node) = &node.layout_node {
stretch.compute_layout(*layout_node, Size {
@ -120,375 +123,7 @@ impl RenderEngine {
}
}
trees.insert(*key, (patched_new_root, stretch));
trees.insert(*key, (patched_new_root, stretch));*/
Ok(())
}
}
/// Given two node trees, will compare, diff, and apply changes in a recursive fashion.
pub fn diff_and_patch_trees(old: RSX, new: RSX, stretch: &mut Stretch, depth: usize) -> Result<RSX, Box<Error>> {
// Whether we replace or not depends on a few things. If we're working on two different node
// types (text vs node), if the node tags are different, or if the key (in some cases) is
// different.
let is_replace = match discriminant(&old) != discriminant(&new) {
true => true,
false => {
if let (RSX::VirtualNode(old_element), RSX::VirtualNode(new_element)) = (&old, &new) {
old_element.tag != new_element.tag
} else {
false
}
}
};
match (old, new) {
(RSX::VirtualNode(mut old_element), RSX::VirtualNode(mut new_element)) => {
if is_replace {
// Do something different in here...
//let mut mounted = mount_component_tree(new_tree);
// unmount_component_tree(old_tree);
// Swap them in memory, copy any layout + etc as necessary
// append, link layout nodes, etc
return Ok(RSX::VirtualNode(new_element));
}
// If we get here, it's an update to an existing element. This means a cached Component
// instance might exist, and we want to keep it around and reuse it if possible. Let's check
// and do some swapping action to handle it.
//
// These need to move to the new tree, since we always keep 'em. We also wanna cache a
// reference to our content view.
swap(&mut old_element.instance, &mut new_element.instance);
swap(&mut old_element.layout_node, &mut new_element.layout_node);
// For the root tag, which is usually the content view of the Window, we don't want to
// perform the whole render/component lifecycle routine. It's a special case element,
// where the Window (or other root element) patches in the output of a render method
// specific to that object. An easy way to handle this is the depth parameter - in
// fact, it's why it exists. Depth 0 should be considered special and skip the
// rendering phase.
if depth > 0 {
// diff props, set new props
// instance.get_derived_state_from_props()
if let Some(instance) = &mut new_element.instance {
// diff props, set new props
// instance.get_derived_state_from_props()
//if instance.should_component_update() {
// instance.render() { }
// instance.get_snapshot_before_update()
// apply changes
//instance.component_did_update();
//} else {
// If should_component_update() returns false, then we want to take the
// children from the old node, move them to the new node, and recurse into
// that tree instead.
//}
}
}
// This None path should never be hit, we just need to use a rather verbose pattern
// here. It's unsightly, I know.
let is_native_backed = match &new_element.instance {
Some(instance) => {
let lock = instance.read().unwrap();
lock.has_native_backing_node()
},
None => false
};
// There is probably a nicer way to do this that doesn't allocate as much, and I'm open
// to revisiting it. Platforms outside of Rust allocate far more than this, though, and
// in general the whole "avoid allocations" thing is fear mongering IMO. Revisit later.
//
// tl;dr we allocate a new Vec<RSX> that's equal to the length of our new children, and
// then swap it on our (owned) node... it's safe, as we own it. This allows us to
// iterate and dodge the borrow checker.
let mut children: Vec<RSX> = Vec::with_capacity(new_element.children.len());
std::mem::swap(&mut children, &mut new_element.children);
old_element.children.reverse();
for new_child_tree in children {
match old_element.children.pop() {
// A matching child in the old tree means we can recurse right back into the
// update phase.
Some(old_child_tree) => {
let updated = diff_and_patch_trees(old_child_tree, new_child_tree, stretch, depth + 1)?;
new_element.children.push(updated);
},
// If there's no matching child in the old tree, this is a new Component and we
// can feel free to mount/connect it.
None => {
if let RSX::VirtualNode(new_el) = new_child_tree {
let mut mounted = mount_component_tree(new_el, stretch)?;
// Link the layout nodes, handle the appending, etc.
// This happens inside mount_component_tree, but that only handles that
// specific tree. Think of this step as joining two trees in the graph.
if is_native_backed {
find_and_link_layout_nodes(&mut new_element, &mut mounted, stretch)?;
}
new_element.children.push(RSX::VirtualNode(mounted));
}
}
}
}
// Trim the fat - more children in the old tree than the new one means we gonna be
// droppin'. We need to send unmount lifecycle calls to these, and break any links we
// have (e.g, layout, backing view tree, etc).
loop {
match old_element.children.pop() {
Some(child) => {
if let RSX::VirtualNode(mut old_child) = child {
unmount_component_tree(&mut old_child, stretch)?;
}
},
None => { break; }
}
}
Ok(RSX::VirtualNode(new_element))
}
// We're comparing two text nodes. Realistically... this requires nothing from us, because
// the <Text> tag (or any other component instance, if it desires) should handle it.
(RSX::VirtualText(_), RSX::VirtualText(text)) => {
Ok(RSX::VirtualText(text))
}
// These are all edge cases that shouldn't get hit. In particular:
//
// - VirtualText being replaced by VirtualNode should be caught by the discriminant check
// in the beginning of this function, which registers as a replace/mount.
// - VirtualNode being replaced with VirtualText is the same scenario as above.
// - The (RSX::None, ...) checks are to shut the compiler up; we never store the RSX::None
// return value, as it's mostly a value in place for return signature usability. Thus,
// these should quite literally never register.
//
// This goes without saying, but: never ever store RSX::None lol
(RSX::VirtualText(_), RSX::VirtualNode(_)) | (RSX::VirtualNode(_), RSX::VirtualText(_)) |
(RSX::None, RSX::VirtualText(_)) | (RSX::None, RSX::VirtualNode(_)) | (RSX::None, RSX::None) |
(RSX::VirtualNode(_), RSX::None) | (RSX::VirtualText(_), RSX::None) => {
unreachable!("Unequal variant discriminants should already have been handled.");
}
}
}
/// Walks the tree and applies styles. This happens after a layout computation, typically.
pub(crate) fn walk_and_apply_styles(node: &VirtualNode, layout_manager: &mut Stretch) -> Result<(), Box<Error>> {
if let (Some(layout_node), Some(instance)) = (node.layout_node, &node.instance) {
let component = instance.write().unwrap();
component.apply_styles(
layout_manager.layout(layout_node)?,
layout_manager.style(layout_node)?
);
}
for child in &node.children {
if let RSX::VirtualNode(child_node) = child {
walk_and_apply_styles(child_node, layout_manager)?;
}
}
Ok(())
}
/// Given a tree, will walk the branches until it finds the next root nodes to connect.
/// While this sounds slow, in practice it rarely has to go far in any direction.
fn find_and_link_layout_nodes(parent_node: &mut VirtualNode, child_tree: &mut VirtualNode, stretch: &mut Stretch) -> Result<(), Box<Error>> {
if let (Some(parent_instance), Some(child_instance)) = (&mut parent_node.instance, &mut child_tree.instance) {
if let (Some(parent_layout_node), Some(child_layout_node)) = (&parent_node.layout_node, &child_tree.layout_node) {
stretch.add_child(*parent_layout_node, *child_layout_node)?;
let parent_component = parent_instance.write().unwrap();
let child_component = child_instance.read().unwrap();
parent_component.append_child_component(&*child_component);
return Ok(());
}
}
for child in child_tree.children.iter_mut() {
if let RSX::VirtualNode(child_tree) = child {
find_and_link_layout_nodes(parent_node, child_tree, stretch)?;
}
}
Ok(())
}
/// Recursively constructs a Component tree. This entails adding it to the backing
/// view tree, firing various lifecycle methods, and ensuring that nodes for layout
/// passes are configured.
///
/// In the future, this would ideally return patch-sets for the backing layer or something.
fn mount_component_tree(mut new_element: VirtualNode, stretch: &mut Stretch) -> Result<VirtualNode, Box<Error>> {
let instance = (new_element.create_component_fn)();
let mut is_native_backed = false;
let rendered = {
let component = instance.read().unwrap();
// instance.get_derived_state_from_props(props)
is_native_backed = component.has_native_backing_node();
if is_native_backed {
let mut style = Style::default();
THEME_ENGINE.configure_style_for_keys(&new_element.props.styles, &mut style);
let layout_node = stretch.new_node(style, vec![])?;
new_element.layout_node = Some(layout_node);
}
component.render(&new_element.props)
};
// instance.get_snapshot_before_update()
new_element.instance = Some(instance);
let mut children = match rendered {
Ok(opt) => match opt {
RSX::VirtualNode(child) => {
let mut children = vec![];
// We want to support Components being able to return arbitrary iteratable
// elements, but... well, it's not quite that simple. Thus we'll offer a <Fragment>
// tag similar to what React does, which just hoists the children out of it and
// discards the rest.
if child.tag == "Fragment" {
for child_node in child.props.children {
if let RSX::VirtualNode(node) = child_node {
let mut mounted = mount_component_tree(node, stretch)?;
if is_native_backed {
find_and_link_layout_nodes(&mut new_element, &mut mounted, stretch)?;
}
children.push(RSX::VirtualNode(mounted));
}
}
} else {
let mut mounted = mount_component_tree(child, stretch)?;
if is_native_backed {
find_and_link_layout_nodes(&mut new_element, &mut mounted, stretch)?;
}
children.push(RSX::VirtualNode(mounted));
}
children
},
// If a Component renders nothing (or this is a Text string, which we do nothing with)
// that's totally fine.
_ => vec![]
},
Err(e) => {
// return an RSX::VirtualNode(ErrorComponentView) or something?
/* instance.get_derived_state_from_error(e) */
// render error state or something I guess?
/* instance.component_did_catch(e, info) */
eprintln!("Error rendering: {}", e);
vec![]
}
};
new_element.children.append(&mut children);
if let Some(instance) = &mut new_element.instance {
let mut component = instance.write().unwrap();
component.component_did_mount(&new_element.props);
}
Ok(new_element)
}
/// Walk the tree and unmount Component instances. This means we fire the
/// `component_will_unmount` hook and remove the node(s) from their respective trees.
///
/// This fires the hooks from a recursive inward-out pattern; that is, the deepest nodes in the tree
/// are the first to go, ensuring that everything is properly cleaned up.
fn unmount_component_tree(old_element: &mut VirtualNode, stretch: &mut Stretch) -> Result<(), Box<Error>> {
// We only need to recurse on VirtualNodes. Text and so on will automagically drop
// because we don't support freeform text, it has to be inside a <Text> at all times.
for child in old_element.children.iter_mut() {
if let RSX::VirtualNode(child_element) = child {
unmount_component_tree(child_element, stretch)?;
}
}
// Fire the appropriate lifecycle method and then remove the node from the underlying
// graph. Remember that a Component can actually not necessarily have a native backing
// node, hence our necessary check.
if let Some(old_component) = &mut old_element.instance {
let mut component = old_component.write().unwrap();
component.component_will_unmount(&old_element.props);
/*if let Some(view) = old_component.get_native_backing_node() {
if let Some(native_view) = replace_native_view {
//replace_view(&view, &native_view);
} else {
//remove_view(&view);
}
}*/
}
// Rather than try to keep track of parent/child stuff for removal... just obliterate it,
// the underlying library does a good job of killing the links anyway.
if let Some(layout_node) = &mut old_element.layout_node {
stretch.set_children(*layout_node, vec![])?;
}
Ok(())
}
/*let mut add_attributes: HashMap<&str, &str> = HashMap::new();
let mut remove_attributes: Vec<&str> = vec![];
// TODO: -> split out into func
for (new_attr_name, new_attr_val) in new_element.attrs.iter() {
match old_element.attrs.get(new_attr_name) {
Some(ref old_attr_val) => {
if old_attr_val != &new_attr_val {
add_attributes.insert(new_attr_name, new_attr_val);
}
}
None => {
add_attributes.insert(new_attr_name, new_attr_val);
}
};
}
// TODO: -> split out into func
for (old_attr_name, old_attr_val) in old_element.attrs.iter() {
if add_attributes.get(&old_attr_name[..]).is_some() {
continue;
};
match new_element.attrs.get(old_attr_name) {
Some(ref new_attr_val) => {
if new_attr_val != &old_attr_val {
remove_attributes.push(old_attr_name);
}
}
None => {
remove_attributes.push(old_attr_name);
}
};
}
if add_attributes.len() > 0 {
patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes));
}
if remove_attributes.len() > 0 {
patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes));
}*/

View file

@ -0,0 +1,163 @@
//! Implements storage for Component instances, in a way that allows us to
//! short-circuit the rendering process so we don't have to re-scan entire
//! tree structures when updating state.
use std::collections::HashMap;
pub use alchemy_styles::Appearance;
use alchemy_styles::stretch::node::{Node as LayoutNode};
use crate::reconciler::error::{RenderEngineError as Error};
use crate::reconciler::key::{Allocator, Id, INSTANCE_ALLOCATOR, ComponentKey};
use crate::traits::Component;
/// This is a clone of a structure you'll also find over in stretch. We do this separately
/// here for two reasons.
///
/// - First, a Component may have children that don't require styles or layout passes. These nodes
/// should not have `Style` or `Appearance` nodes created, but we do need the correct parent/child
/// relationships in place.
/// - The `Storage` pieces of stretch are realistically an implementation detail that we shouldn't
/// rely on.
struct Storage<T>(HashMap<ComponentKey, T>);
impl<T> Storage<T> {
pub fn new() -> Self {
Storage(HashMap::new())
}
pub fn get(&self, key: ComponentKey) -> Result<&T, Error> {
match self.0.get(&key) {
Some(v) => Ok(v),
None => Err(Error::InvalidComponentKey(key)),
}
}
pub fn get_mut(&mut self, key: ComponentKey) -> Result<&mut T, Error> {
match self.0.get_mut(&key) {
Some(v) => Ok(v),
None => Err(Error::InvalidComponentKey(key)),
}
}
pub fn insert(&mut self, key: ComponentKey, value: T) -> Option<T> {
self.0.insert(key, value)
}
}
impl<T> std::ops::Index<&ComponentKey> for Storage<T> {
type Output = T;
fn index(&self, idx: &ComponentKey) -> &T {
&(self.0)[idx]
}
}
pub struct Instance {
component: Box<Component>,
appearance: Appearance,
layout: Option<LayoutNode>
}
pub(crate) struct ComponentStore {
id: Id,
nodes: Allocator,
components: Storage<Instance>,
parents: Storage<Vec<ComponentKey>>,
children: Storage<Vec<ComponentKey>>
}
impl ComponentStore {
pub fn new() -> Self {
ComponentStore {
id: INSTANCE_ALLOCATOR.lock().unwrap().allocate(),
nodes: Allocator::new(),
components: Storage::new(),
parents: Storage::new(),
children: Storage::new()
}
}
fn allocate_node(&mut self) -> ComponentKey {
let local = self.nodes.allocate();
ComponentKey { instance: self.id, local }
}
pub fn new_node<C: Component + 'static>(&mut self, component: C, layout_key: Option<LayoutNode>, children: Vec<ComponentKey>) -> Result<ComponentKey, Error> {
let key = self.allocate_node();
for child in &children {
self.parents.get_mut(*child)?.push(key);
}
self.components.insert(key, Instance {
component: Box::new(component),
appearance: Appearance::default(),
layout: layout_key
});
self.parents.insert(key, Vec::with_capacity(1));
self.children.insert(key, children);
Ok(key)
}
pub fn add_child(&mut self, key: ComponentKey, child: ComponentKey) -> Result<(), Error> {
self.parents.get_mut(child)?.push(key);
self.children.get_mut(key)?.push(child);
Ok(())
}
pub fn set_children(&mut self, key: ComponentKey, children: Vec<ComponentKey>) -> Result<(), Error> {
// Remove node as parent from all its current children.
for child in self.children.get(key)? {
self.parents.get_mut(*child)?.retain(|p| *p != key);
}
*self.children.get_mut(key)? = Vec::with_capacity(children.len());
// Build up relation node <-> child
for child in children {
self.parents.get_mut(child)?.push(key);
self.children.get_mut(key)?.push(child);
}
Ok(())
}
pub fn remove_child(&mut self, key: ComponentKey, child: ComponentKey) -> Result<ComponentKey, Error> {
match self.children(key)?.iter().position(|n| *n == child) {
Some(index) => self.remove_child_at_index(key, index),
None => Err(Error::InvalidComponentKey(child)),
}
}
pub fn remove_child_at_index(&mut self, key: ComponentKey, index: usize) -> Result<ComponentKey, Error> {
let child = self.children.get_mut(key)?.remove(index);
self.parents.get_mut(child)?.retain(|p| *p != key);
Ok(child)
}
pub fn replace_child_at_index(&mut self, key: ComponentKey, index: usize, child: ComponentKey) -> Result<ComponentKey, Error> {
self.parents.get_mut(child)?.push(key);
let old_child = std::mem::replace(&mut self.children.get_mut(key)?[index], child);
self.parents.get_mut(old_child)?.retain(|p| *p != key);
Ok(old_child)
}
pub fn children(&self, key: ComponentKey) -> Result<Vec<ComponentKey>, Error> {
self.children.get(key).map(Clone::clone)
}
pub fn child_count(&self, key: ComponentKey) -> Result<usize, Error> {
self.children.get(key).map(Vec::len)
}
pub fn get(&self, key: ComponentKey) -> Result<&Instance, Error> {
self.components.get(key)
}
pub fn get_mut(&mut self, key: ComponentKey) -> Result<&mut Instance, Error> {
self.components.get_mut(key)
}
}

View file

@ -3,7 +3,6 @@
//! uses these to build and alter UI; they're typically returned from `render()`
//! methods.
use std::sync::{Arc, RwLock};
use std::fmt::{Debug, Display};
mod virtual_node;
@ -15,6 +14,7 @@ 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
@ -31,16 +31,15 @@ impl RSX {
/// this yourself; the `rsx! {}` macro handles this for you.
pub fn node(
tag: &'static str,
create_fn: fn() -> Arc<RwLock<Component>>,
props: Props
create_fn: fn(key: ComponentKey) -> Box<Component>,
props: Props,
children: Vec<RSX>
) -> RSX {
RSX::VirtualNode(VirtualNode {
tag: tag,
create_component_fn: create_fn,
instance: None,
layout_node: None,
props: props,
children: vec![]
props: Some(props),
children: children
})
}

View file

@ -39,6 +39,15 @@ pub struct Props {
}
impl Props {
pub fn new(key: String, styles: StylesList, attributes: HashMap<&'static str, AttributeType>) -> Props {
Props {
attributes: attributes,
children: vec![],
key: key,
styles: styles
}
}
/// 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()

View file

@ -1,17 +1,14 @@
//! Implements the `RSX::VirtualNode` struct, which is a bit of a recursive
//! structure.
use std::sync::{Arc, RwLock};
use std::fmt::{Display, Debug};
use alchemy_styles::node::Node;
use crate::traits::Component;
use crate::reconciler::key::ComponentKey;
use crate::rsx::{RSX, Props};
use crate::traits::Component;
/// A VirtualNode is akin to an `Element` in React terms. Here, we provide a way
/// for lazy `Component` instantiation, along with storage for things like layout nodes,
/// properties, children and so on.
/// for lazy `Component` instantiation, properties, children and so on.
#[derive(Clone)]
pub struct VirtualNode {
/// Used in debugging/printing/etc.
@ -19,22 +16,17 @@ pub struct VirtualNode {
/// `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() -> Arc<RwLock<Component>>,
pub create_component_fn: fn(key: ComponentKey) -> Box<Component>,
/// A cached component instance, which is transferred between trees. Since `Component`
/// instances are lazily created, this is an `Option`, and defaults to `None`.
pub instance: Option<Arc<RwLock<Component>>>,
/// A cached `Node` for computing `Layout` with `Stretch`. Some components may not have
/// a need for layout (e.g, if they don't have a backing node), and thus this is optional.
/// `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.
///
/// The reconciler will handle bridging tree structures as necessary.
pub layout_node: Option<Node>,
/// This aspect of functionality may be pulled in a later release if it causes too many issues.
pub props: Option<Props>,
/// `Props`, which are to be passed to this `Component` at various lifecycle methods.
pub props: Props,
/// Computed children get stored here.
///
pub children: Vec<RSX>
}

View file

@ -1,11 +1,11 @@
//! 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::sync::Arc;
use alchemy_styles::styles::{Layout, Style};
use alchemy_styles::styles::{Appearance, Layout};
//use crate::RENDER_ENGINE;
use crate::error::Error;
use crate::reconciler::key::ComponentKey;
use crate::rsx::{RSX, Props};
/// A per-platform wrapped Pointer type, used for attaching views/widgets.
@ -16,6 +16,11 @@ pub type PlatformSpecificNodeType = objc_id::ShareId<objc::runtime::Object>;
#[cfg(not(feature = "cocoa"))]
pub type PlatformSpecificNodeType = ();
/*fn update<C: Component, F: Fn() -> Box<C> + Send + Sync + 'static>(component: &Component, updater: F) {
let component_ptr = component as *const C as usize;
RENDER_ENGINE.queue_update_for(component_ptr, Box::new(updater));
}*/
/// Each platform tends to have their own startup routine, their own runloop, and so on.
/// Alchemy recognizes this and provides an `AppDelegate` that receives events at a system
/// level and allows the user to operate within the established framework per-system.
@ -75,6 +80,8 @@ pub trait State {}
/// 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;
/// 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
/// default.
@ -89,16 +96,15 @@ pub trait Component: Send + Sync {
/// 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: Arc<Component>) {}
fn replace_child_component(&self, _component: &Component) {}
/// 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: Arc<Component>) {}
fn remove_child_component(&self, _component: &Component) {}
/// Given a computed `layout`, and an accompanying `Style` (which holds appearance-based
/// styles, like colors), this method should transform them into appropriate calls to the
/// backing native node.
fn apply_styles(&self, _layout: &Layout, _style: &Style) {}
/// Given a configured 'appearance' and computed `layout`, this method should transform them
/// into appropriate calls to the backing native node.
fn apply_styles(&self, _appearance: &Appearance, _layout: &Layout) {}
/// 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.