diff --git a/alchemy/src/components/text.rs b/alchemy/src/components/text.rs index 1c20ed8..6bb292f 100644 --- a/alchemy/src/components/text.rs +++ b/alchemy/src/components/text.rs @@ -24,7 +24,6 @@ use alchemy_cocoa::text::{Text as PlatformTextBridge}; /// /// ``` pub struct Text { - key: ComponentKey, text: String, bridge: Mutex } @@ -48,9 +47,8 @@ impl Text { } impl Component for Text { - fn constructor(key: ComponentKey) -> Text { + fn constructor(_: ComponentKey) -> Text { Text { - key: key, text: "".into(), bridge: Mutex::new(PlatformTextBridge::new()) } diff --git a/alchemy/src/window/window.rs b/alchemy/src/window/window.rs index 4412e59..d4d383d 100644 --- a/alchemy/src/window/window.rs +++ b/alchemy/src/window/window.rs @@ -41,8 +41,8 @@ impl AppWindow { } }; - match RENDER_ENGINE.diff_and_render_root(&self.render_key, children) { - Ok(_) => {} + match RENDER_ENGINE.diff_and_render_root(self.render_key, children) { + Ok(_) => { println!("RENDERED!!!!"); } Err(e) => { eprintln!("Error rendering window! {}", e); } } } diff --git a/cocoa/src/window.rs b/cocoa/src/window.rs index d1c8586..fc8e43a 100644 --- a/cocoa/src/window.rs +++ b/cocoa/src/window.rs @@ -4,7 +4,7 @@ use std::sync::{Once, ONCE_INIT}; -use cocoa::base::{id, nil, YES, NO}; +use cocoa::base::{id, nil, /*YES,*/ NO}; use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSBackingStoreType}; use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSAutoreleasePool}; diff --git a/lifecycle/src/reconciler/error.rs b/lifecycle/src/reconciler/error.rs index ea72825..c3cb923 100644 --- a/lifecycle/src/reconciler/error.rs +++ b/lifecycle/src/reconciler/error.rs @@ -2,8 +2,6 @@ //! run. These are mostly internal to the rendering engine itself, but could potentially //! show up elsewhere. -use core::any::Any; - use crate::reconciler::key::ComponentKey; #[derive(Debug)] diff --git a/lifecycle/src/reconciler/instance.rs b/lifecycle/src/reconciler/instance.rs new file mode 100644 index 0000000..07cfc03 --- /dev/null +++ b/lifecycle/src/reconciler/instance.rs @@ -0,0 +1,33 @@ +//! Internal struct used for tracking component instances and their +//! associated metadata (layout, appearance, etc). + +use alchemy_styles::Appearance; +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, + pub(crate) props: Props, + pub(crate) appearance: Appearance, + pub(crate) layout: Option +} + +impl Instance { + pub(crate) fn new( + tag: &'static str, + component: Box, + props: Props, + layout: Option + ) -> Instance { + Instance { + tag: tag, + component: component, + props: props, + appearance: Appearance::default(), + layout: layout + } + } +} diff --git a/lifecycle/src/reconciler/mod.rs b/lifecycle/src/reconciler/mod.rs index 0fd48b1..3e6ae6e 100644 --- a/lifecycle/src/reconciler/mod.rs +++ b/lifecycle/src/reconciler/mod.rs @@ -1,18 +1,18 @@ -//! Implements tree diffing, and attempts to cache Component instances where -//! possible. +//! Implements tree diffing, updating, and so on. Unlike a lot of the VDom implementations +//! you find littered around the web, this is a bit more ECS-ish, and expects Components to retain +//! their `ComponentKey` passed in their constructor if they want to update. Doing this +//! enables us to avoid re-scanning or diffing an entire tree. -use std::sync::{Arc, Mutex, RwLock}; -use std::collections::HashMap; +use std::sync::Mutex; use std::error::Error; -use std::mem::{discriminant, swap}; use alchemy_styles::THEME_ENGINE; -use alchemy_styles::styles::{Appearance,Dimension, Number, Size, Style}; +use alchemy_styles::styles::{Appearance, Number, Size, Style}; use crate::traits::Component; use crate::rsx::{Props, RSX, VirtualNode}; -use alchemy_styles::stretch::node::{Node as StyleNode, Stretch as LayoutStore}; +use alchemy_styles::stretch::node::{Node as LayoutNode, Stretch as LayoutStore}; pub mod key; use key::ComponentKey; @@ -23,13 +23,17 @@ 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 { - fn constructor(key: ComponentKey) -> StubView { - StubView {} +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). + +pub struct GenericRootView; +impl Component for GenericRootView { + fn constructor(key: ComponentKey) -> GenericRootView { + GenericRootView {} } } @@ -64,66 +68,307 @@ impl RenderEngine { return Err(Box::new(RenderEngineError::InvalidRootComponent {})); } - let layout_key = { + let mut component_store = self.components.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(); - let mut layouts = self.layouts.lock().unwrap(); - Some(layouts.new_node(style, vec![])?) - }; + Some(layouts_store.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, - /// attributes and so on), and then queue the changes for application to the backing - /// framework tree. As it goes through the tree, if a `Component` at a given position - /// in the two trees is deemed to be the same, it will move instances from the old tree to - /// the new tree before discarding the old tree. - /// - /// This calls the necessary component lifecycles per-component. - pub fn diff_and_render_root(&self, key: &ComponentKey, child: RSX) -> Result<(), Box> { - /* - let mut new_root = RSX::node("root", || { - Box::new(StubView {}) - }, { + /// Rendering the root node is a bit different than rendering or updating other nodes, as we + /// never want to unmount it, and the results come from a non-`Component` entity (e.g, a + /// `Window`). Thus, for this one, we do some manual mucking with what we know is the + /// root view (a `Window` or such root component would call this with it's registered + /// `ComponentKey`), and then recurse based on the children. + pub fn diff_and_render_root(&self, key: ComponentKey, child: RSX) -> Result<(), Box> { + let mut component_store = self.components.lock().unwrap(); + let mut layout_store = self.layouts.lock().unwrap(); + + println!("Child: {:?}", child); + let new_root_node = RSX::node("root", |_| Box::new(GenericRootView {}), { 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); + RSX::VirtualNode(node) => { + if node.tag == "Fragment" { + node.children } else { - children.push(RSX::VirtualNode(child)); + println!("Def here..."); + vec![RSX::VirtualNode(node)] } - - 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 tag, and - // we know the root element isn't a if we're here. _ => vec![] }); - let mut trees = self.trees.lock().unwrap(); - 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)?; + recursively_diff_tree(key, new_root_node, &mut component_store, &mut layout_store)?; - if let RSX::VirtualNode(node) = &patched_new_root { - if let Some(layout_node) = &node.layout_node { - stretch.compute_layout(*layout_node, Size { - width: Number::Defined(600.), - height: Number::Defined(600.), - })?; - walk_and_apply_styles(node, &mut stretch)?; - } - } + let layout_node = { + let root_instance = component_store.get(key)?; + root_instance.layout.unwrap() + }; + + layout_store.compute_layout(layout_node, Size { + width: Number::Defined(600.), + height: Number::Defined(600.) + })?; + + println!("Applying layout..."); + walk_and_apply_styles(key, &mut component_store, &mut layout_store)?; - trees.insert(*key, (patched_new_root, stretch));*/ Ok(()) } } + +/// Given two trees, will diff them to see if we need to replace or update. Depending on the +/// result, we'll either recurse down a level, or tear down and build up a new tree. The final +/// parameter on this method, `is_root_entity_view`, should only be passed for `Window` or other +/// such instances, as it instructs us to skip the first level since these ones act different. +fn recursively_diff_tree( + key: ComponentKey, + new_tree: RSX, + component_store: &mut ComponentStore, + layout_store: &mut LayoutStore +) -> Result<(), Box> { + // First we need to determine if this node is being replaced or updated. A replace happens if + // two nodes are different types - in this case, we check their tag values. This is also a case + // where, for instance, if the RSX tag is `::None` or `::VirtualText`, we'll treat it as + // replacing with nothing. + let is_replace = match &new_tree { + RSX::VirtualNode(new_tree) => { + let old_tree = component_store.get(key)?; + old_tree.tag != new_tree.tag + }, + + // The algorithm will know below not to recurse if we're trying to diff text or empty + // values. We return false here to avoid entering the `is_replace` phase; `Component` + // instances (like ) handle taking the child VirtualText instances and working with + // them to pass to a native widget. + _ => false + }; + + if is_replace { + println!("here, what?!"); + unmount_component_tree(key, component_store, layout_store)?; + //mount_component_tree( + return Ok(()); + } + + // At this point, we know it's an update pass. Now we need to do a few things: + // + // - Diff our `props` and figure out what actions we can take or shortcut. + // - Let the `Component` instance determine what it should render. + // - Recurse into the child trees if necessary. + let mut old_children = component_store.children(key)?; + old_children.reverse(); + + if let RSX::VirtualNode(mut child) = new_tree { + 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. + Some(old_child_key) => { + recursively_diff_tree( + old_child_key, + new_child_tree, + component_store, + layout_store + )?; + }, + + // If there's no matching old key in this position, then we've got a + // new component instance to mount. This part now diverts into the Mount + // phase. + None => { + if let RSX::VirtualNode(tr33amimustfeelohlol) = new_child_tree { + let new_child_key = mount_component_tree( + tr33amimustfeelohlol, + component_store, + layout_store + )?; + + link_nodes(key, new_child_key, component_store, layout_store)?; + } + } + } + } + } + + // Trim the fat. If we still have child nodes after diffing in the new child trees, + // then they're ones that simply need to be unmounted and dropped. + if old_children.len() > 0 { + for child in old_children { + unmount_component_tree(child, component_store, layout_store)?; + } + } + + Ok(()) +} + +/// Given a new `RSX` tree, a `ComponentStore`, and a `LayoutStore`, will recursively construct the +/// tree, emitting required lifecycle events and persisting values. This happens in an inward-out +/// fashion, which helps avoid unnecessary reflow in environments where it can get tricky. +/// +/// This method returns a Result, the `Ok` variant containing a tuple of Vecs. These are the child +/// Component instances and Layout instances that need to be set in the stores. +fn mount_component_tree( + tree: VirtualNode, + component_store: &mut ComponentStore, + layout_store: &mut LayoutStore +) -> Result> { + println!(" Mounting Component"); + let key = component_store.new_key(); + let component = (tree.create_component_fn)(key); + 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); + if is_native_backed { + let mut style = Style::default(); + THEME_ENGINE.configure_styles_for_keys(&instance.props.styles, &mut style, &mut instance.appearance); + instance.layout = Some(layout_store.new_node(style, vec![])?); + } + + let rendered = instance.component.render(&instance.props); + // instance.get_snapshot_before_update() + println!("Rendered... {}", instance.tag); + component_store.insert(key, instance)?; + + match rendered { + Ok(child) => if let RSX::VirtualNode(child) = child { + // 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 + // tag similar to what React does, which just hoists the children out of it and + // discards the rest. + if child.tag == "Fragment" { + println!(" In Fragment {}", child.children.len()); + for child_tree in child.children { + println!(" > WHAT"); + if let RSX::VirtualNode(child_tree) = child_tree { + let child_key = mount_component_tree(child_tree, component_store, layout_store)?; + + if is_native_backed { + link_nodes(key, child_key, component_store, layout_store)?; + } + } + } + } else { + println!(" In regular"); + let child_key = mount_component_tree(child, component_store, layout_store)?; + if is_native_backed { + link_nodes(key, child_key, component_store, layout_store)?; + } + } + } else { println!("WTF"); }, + + 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); + } + } + + let instance_lol = component_store.get_mut(key)?; + instance_lol.component.component_did_mount(&instance_lol.props); + + Ok(key) +} + +/// Given a `ComponentKey`, a `ComponentStore`, and a `LayoutStore`, will recursively walk the tree found at +/// said key, emitting required lifecycle events and dropping values. This happens in an inward-out +/// fashion, so deepest nodes/components get destroyed first to ensure that the backing widget tree +/// doesn't get some weird dangling issue. +fn unmount_component_tree( + key: ComponentKey, + component_store: &mut ComponentStore, + layout_store: &mut LayoutStore +) -> Result, Box> { + let mut instance = component_store.remove(key)?; + instance.component.component_will_unmount(&instance.props); + + let mut layout_nodes = vec![]; + + let children = component_store.children(key)?; + for child in children { + match unmount_component_tree(child, component_store, layout_store) { + Ok(mut child_layout_nodes) => { + if let Some(parent_layout_node) = instance.layout { + for node in child_layout_nodes { + layout_store.remove_child(parent_layout_node, node)?; + } + } else { + layout_nodes.append(&mut child_layout_nodes); + } + }, + + Err(e) => { eprintln!("Error unmounting a component tree: {}", e); } + } + } + + // remove node from backing tree + + Ok(layout_nodes) +} + +/// 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. This could +/// potentially be done away with some hoisting magic in the `mount()` recursion, but I couldn't +/// find a pattern that didn't feel like some utter magic in Rust. +/// +/// It might be because I'm writing this at 3AM. Feel free to improve it. +fn link_nodes( + parent: ComponentKey, + child: ComponentKey, + components: &mut ComponentStore, + layouts: &mut LayoutStore +) -> Result<(), Box> { + 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); + println!("APPENDED NODE!"); + return Ok(()); + } + } + + let children = components.children(child)?; + for child_key in children { + link_nodes(parent, child_key, components, layouts)?; + } + + Ok(()) +} + +/// Walks the tree and passes necessary Layout and Appearance-based styles to Components so they can +/// update their backing widgets accordingly. This happens after a layout computation, typically. +fn walk_and_apply_styles( + key: ComponentKey, + components: &mut ComponentStore, + layouts: &mut LayoutStore +) -> Result<(), Box> { + let instance = components.get_mut(key)?; + + if let Some(layout_key) = instance.layout { + instance.component.apply_styles( + &instance.appearance, + layouts.layout(layout_key)? + ); + } + + for child in components.children(key)? { + walk_and_apply_styles(child, components, layouts)?; + } + + Ok(()) +} diff --git a/lifecycle/src/reconciler/storage.rs b/lifecycle/src/reconciler/storage.rs index 3138f13..63f087b 100644 --- a/lifecycle/src/reconciler/storage.rs +++ b/lifecycle/src/reconciler/storage.rs @@ -5,11 +5,10 @@ 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::instance::Instance; 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. @@ -40,6 +39,13 @@ impl Storage { } } + pub fn remove(&mut self, key: ComponentKey) -> Result { + match self.0.remove(&key) { + Some(v) => Ok(v), + None => Err(Error::InvalidComponentKey(key)) + } + } + pub fn insert(&mut self, key: ComponentKey, value: T) -> Option { self.0.insert(key, value) } @@ -53,12 +59,6 @@ impl std::ops::Index<&ComponentKey> for Storage { } } -pub struct Instance { - component: Box, - appearance: Appearance, - layout: Option -} - pub(crate) struct ComponentStore { id: Id, nodes: Allocator, @@ -78,28 +78,31 @@ impl ComponentStore { } } - fn allocate_node(&mut self) -> ComponentKey { + pub fn new_key(&mut self) -> ComponentKey { let local = self.nodes.allocate(); ComponentKey { instance: self.id, local } } - pub fn new_node(&mut self, component: C, layout_key: Option, children: Vec) -> Result { - let key = self.allocate_node(); - - for child in &children { + pub fn insert( + &mut self, + key: ComponentKey, + instance: Instance + ) -> Result<(), Error> { + /*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.components.insert(key, instance); self.parents.insert(key, Vec::with_capacity(1)); - self.children.insert(key, children); + self.children.insert(key, vec![]); //children); - Ok(key) + Ok(()) + } + + pub fn remove(&mut self, key: ComponentKey) -> Result { + self.parents.remove(key)?; + self.children.remove(key)?; + self.components.remove(key) } pub fn add_child(&mut self, key: ComponentKey, child: ComponentKey) -> Result<(), Error> { diff --git a/lifecycle/src/rsx/mod.rs b/lifecycle/src/rsx/mod.rs index ad3bcf0..6a5b6f1 100644 --- a/lifecycle/src/rsx/mod.rs +++ b/lifecycle/src/rsx/mod.rs @@ -38,7 +38,7 @@ impl RSX { RSX::VirtualNode(VirtualNode { tag: tag, create_component_fn: create_fn, - props: Some(props), + props: props, children: children }) } diff --git a/lifecycle/src/rsx/props.rs b/lifecycle/src/rsx/props.rs index 8d297c9..04247e1 100644 --- a/lifecycle/src/rsx/props.rs +++ b/lifecycle/src/rsx/props.rs @@ -1,14 +1,12 @@ //! Implements a Props struct that mostly acts as expected. For arbitrary primitive values, //! it shadows a `serde_json::Value`. -use std::sync::{Arc, RwLock}; use serde_json::Value; use std::collections::HashMap; use alchemy_styles::StylesList; use crate::rsx::RSX; -use crate::traits::{Component}; /// A value stored inside the `attributes` field on a `Props` instance. /// It shadows `serde_json::Value`, but also allows for some other value diff --git a/lifecycle/src/rsx/virtual_node.rs b/lifecycle/src/rsx/virtual_node.rs index df3ec57..ff65b0c 100644 --- a/lifecycle/src/rsx/virtual_node.rs +++ b/lifecycle/src/rsx/virtual_node.rs @@ -24,7 +24,7 @@ pub struct VirtualNode { /// ownership of a VirtualNode. /// /// This aspect of functionality may be pulled in a later release if it causes too many issues. - pub props: Option, + pub props: Props, /// pub children: Vec