diff --git a/alchemy/src/lib.rs b/alchemy/src/lib.rs index d9b2376..3152c39 100644 --- a/alchemy/src/lib.rs +++ b/alchemy/src/lib.rs @@ -32,8 +32,6 @@ use app::App; pub mod components; pub use components::{Fragment, Text, View}; -pub(crate) mod reconciler; - pub mod window; pub use window::Window; diff --git a/alchemy/src/reconciler.rs b/alchemy/src/reconciler.rs deleted file mode 100644 index fa9ef7d..0000000 --- a/alchemy/src/reconciler.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Implements tree diffing, and attempts to cache Component instances where -//! possible. - -use std::error::Error; -use std::mem::{discriminant, swap}; - -use alchemy_styles::Stretch; -use alchemy_styles::styles::Style; - -use alchemy_lifecycle::rsx::{RSX, VirtualNode}; - -/// Given two node trees, will compare, diff, and apply changes in a recursive fashion. -pub fn diff_and_patch_tree(old: RSX, new: RSX, stretch: &mut Stretch, depth: usize) -> Result> { - // 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 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 = 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_tree(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 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> { - 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> { - 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> { - 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(); - - 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 - // 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> { - // 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 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)); -}*/ diff --git a/alchemy/src/window/window.rs b/alchemy/src/window/window.rs index 5b2fa65..80bad3d 100644 --- a/alchemy/src/window/window.rs +++ b/alchemy/src/window/window.rs @@ -3,52 +3,20 @@ use std::sync::{Arc, Mutex, RwLock}; -use alchemy_lifecycle::traits::{Component, WindowDelegate}; +use alchemy_lifecycle::{Uuid, RENDER_ENGINE}; use alchemy_lifecycle::rsx::{Props, RSX}; +use alchemy_lifecycle::traits::{Component, WindowDelegate}; -use alchemy_styles::Stretch; use alchemy_styles::number::Number; use alchemy_styles::geometry::Size; use alchemy_styles::styles::{Style, Dimension}; use crate::{App, SHARED_APP}; use crate::components::View; -use crate::reconciler::{diff_and_patch_tree, walk_and_apply_styles}; #[cfg(feature = "cocoa")] use alchemy_cocoa::window::{Window as PlatformWindowBridge}; -/// Utility function for creating a root_node. -fn create_root_node(instance: Option>>, layout_manager: &mut Stretch) -> RSX { - let mut props = Props::default(); - props.styles = "root".into(); - - let mut root_node = RSX::node("root", || Arc::new(RwLock::new(View::default())), props); - - if let RSX::VirtualNode(root) = &mut root_node { - root.layout_node = match instance.is_some() { - true => { - let mut style = Style::default(); - style.size = Size { - width: Dimension::Points(600.), - height: Dimension::Points(600.) - }; - - match layout_manager.new_node(style, vec![]) { - Ok(node) => Some(node), - Err(e) => { None } - } - }, - - false => None - }; - - root.instance = instance; - } - - root_node -} - /// AppWindow contains the inner details of a Window. It's guarded by a Mutex on `Window`, /// and you shouldn't create this yourself, but it's documented here so you can understand what /// it holds. @@ -57,8 +25,7 @@ pub struct AppWindow { pub title: String, pub bridge: PlatformWindowBridge, pub delegate: Box, - pub root_node: RSX, - pub layout: Stretch + pub render_key: Uuid } impl AppWindow { @@ -70,6 +37,7 @@ impl AppWindow { /// This method is called on the `show` event, and in rare cases can be useful to call /// directly. pub fn render(&mut self) { + /* let mut new_root_node = create_root_node(None, &mut self.layout); // For API reasons, we'll call the render for this Window, and then patch it into a new @@ -106,6 +74,7 @@ impl AppWindow { }; self.configure_and_apply_styles(); + */ } /// Walks the tree again, purely concerning itself with calculating layout and applying styles. @@ -119,12 +88,12 @@ impl AppWindow { height: Number::Defined(600.) }; - if let RSX::VirtualNode(root_node) = &mut self.root_node { + /*if let RSX::VirtualNode(root_node) = &mut self.root_node { if let Some(layout_node) = &root_node.layout_node { self.layout.compute_layout(*layout_node, window_size)?; walk_and_apply_styles(&root_node, &mut self.layout)?; } - } + }*/ Ok(()) } @@ -149,18 +118,17 @@ impl Window { /// Creates a new window. pub fn new(title: &str, dimensions: (f64, f64, f64, f64), delegate: S) -> Window { let window_id = SHARED_APP.windows.allocate_new_window_id(); - let mut layout = Stretch::new(); let view = View::default(); let shared_app_ptr: *const App = &**SHARED_APP; let bridge = PlatformWindowBridge::new(window_id, title, dimensions, &view, shared_app_ptr); + let key = RENDER_ENGINE.register_root_component(view); Window(Arc::new(Mutex::new(AppWindow { id: window_id, title: title.into(), bridge: bridge, delegate: Box::new(delegate), - root_node: create_root_node(Some(Arc::new(RwLock::new(view))), &mut layout), - layout: layout + render_key: key }))) } diff --git a/lifecycle/src/lib.rs b/lifecycle/src/lib.rs index c666053..f2d3a60 100644 --- a/lifecycle/src/lib.rs +++ b/lifecycle/src/lib.rs @@ -11,6 +11,8 @@ //! 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; + use alchemy_styles::lazy_static; pub mod error; diff --git a/lifecycle/src/reconciler.rs b/lifecycle/src/reconciler.rs index d9ee33f..5ea3fdf 100644 --- a/lifecycle/src/reconciler.rs +++ b/lifecycle/src/reconciler.rs @@ -1,7 +1,7 @@ //! Implements tree diffing, and attempts to cache Component instances where //! possible. -use std::sync::Mutex; +use std::sync::{Arc, Mutex, RwLock}; use std::collections::HashMap; use std::error::Error; use std::mem::{discriminant, swap}; @@ -9,9 +9,18 @@ use std::mem::{discriminant, swap}; use uuid::Uuid; use alchemy_styles::{Stretch, THEME_ENGINE}; -use alchemy_styles::styles::Style; +use alchemy_styles::styles::{Style, Dimension}; +use alchemy_styles::number::Number; +use alchemy_styles::geometry::Size; -use crate::rsx::{RSX, VirtualNode}; +use crate::traits::Component; +use crate::rsx::{Props, RSX, VirtualNode}; + +// 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 {} pub struct RenderEngine { pending_state_updates: Mutex>, @@ -31,11 +40,33 @@ 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(&self, root: RSX) -> Uuid { + pub fn register_root_component(&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 } + } + } + let key = Uuid::new_v4(); - let stretch = Stretch::new(); let mut trees = self.trees.lock().unwrap(); - trees.insert(key, (root, stretch)); + trees.insert(key, (root_node, stretch)); key }