diff --git a/alchemy/src/components/fragment.rs b/alchemy/src/components/fragment.rs index 80e3e12..d447440 100644 --- a/alchemy/src/components/fragment.rs +++ b/alchemy/src/components/fragment.rs @@ -5,7 +5,9 @@ //! just allow returning arbitrary iterators. use alchemy_lifecycle::ComponentKey; -use alchemy_lifecycle::traits::Component; +use alchemy_lifecycle::traits::{Component, Props}; + +pub struct FragmentProps; /// Fragments are special - you can do something like the following in cases where you /// want to render some views without requiring an intermediate view. @@ -20,8 +22,18 @@ use alchemy_lifecycle::traits::Component; #[derive(Default, Debug)] pub struct Fragment; -impl Component for Fragment { - fn constructor(_key: ComponentKey) -> Fragment { - Fragment { } +impl Fragment { + fn default_props() -> FragmentProps { + FragmentProps {} + } +} + +impl Props for Fragment { + fn set_props(&mut self, _: &mut std::any::Any) {} +} + +impl Component for Fragment { + fn new(_: ComponentKey) -> Fragment { + Fragment {} } } diff --git a/alchemy/src/components/text.rs b/alchemy/src/components/text.rs index 6bb292f..105a537 100644 --- a/alchemy/src/components/text.rs +++ b/alchemy/src/components/text.rs @@ -9,12 +9,14 @@ use alchemy_styles::styles::{Appearance, Layout}; use alchemy_lifecycle::ComponentKey; use alchemy_lifecycle::error::Error; -use alchemy_lifecycle::rsx::{Props, RSX}; -use alchemy_lifecycle::traits::{Component, PlatformSpecificNodeType}; +use alchemy_lifecycle::rsx::RSX; +use alchemy_lifecycle::traits::{Component, Props, PlatformSpecificNodeType}; #[cfg(feature = "cocoa")] use alchemy_cocoa::text::{Text as PlatformTextBridge}; +pub struct TextProps; + /// Text rendering is a complicated mess, and being able to defer to the /// backing platform for this is amazing. This is a very common Component. /// @@ -23,59 +25,60 @@ use alchemy_cocoa::text::{Text as PlatformTextBridge}; /// ``` /// /// ``` -pub struct Text { - text: String, - bridge: Mutex -} +pub struct Text(Mutex); impl Text { + pub fn default_props() -> TextProps { TextProps {} } // This is very naive for now, but it's fine - we probably // want to do some fun stuff here later with stylized text // rendering anyway. - fn compare_and_update_text(&mut self, props: &Props) { - let text = props.children.iter().map(|child| match child { - RSX::VirtualText(s) => s.0.clone(), - _ => String::new() - }).collect::(); - - if self.text != text { - let mut bridge = self.bridge.lock().unwrap(); - bridge.set_text(&text); - self.text = text; - } - } + //fn compare_and_update_text(&mut self, props: &Props) { + /*let text = props.*/ + //} +} + +impl Props for Text { + fn set_props(&mut self, _: &mut std::any::Any) {} } impl Component for Text { - fn constructor(_: ComponentKey) -> Text { - Text { - text: "".into(), - bridge: Mutex::new(PlatformTextBridge::new()) - } + fn new(_: ComponentKey) -> Text { + Text(Mutex::new(PlatformTextBridge::new())) } fn has_native_backing_node(&self) -> bool { true } fn borrow_native_backing_node(&self) -> Option { - let bridge = self.bridge.lock().unwrap(); + let bridge = self.0.lock().unwrap(); Some(bridge.borrow_native_backing_node()) } // Shouldn't be allowed to have child elements... or, should it? // Panic might not be right here, but eh, should probably do something. - fn append_child_component(&self, _component: &Component) {} + //fn append_child_component(&self, _component: &Component) {} fn apply_styles(&self, appearance: &Appearance, layout: &Layout) { - let mut bridge = self.bridge.lock().unwrap(); + let mut bridge = self.0.lock().unwrap(); bridge.apply_styles(appearance, layout); } - fn component_did_mount(&mut self, props: &Props) { - self.compare_and_update_text(props); + fn component_did_mount(&mut self) { + let mut bridge = self.0.lock().unwrap(); + bridge.render(); } - fn render(&self, _props: &Props) -> Result { + // This one is a bit tricky, due to the way we have to do props + children in Rust. + // Here, we set it as the new text on render(), and then ensure it gets rendered on + // `component_did_update()` and `component_did_mount()`. + fn render(&self, children: Vec) -> Result { + let text = children.iter().map(|child| match child { + RSX::VirtualText(s) => s.0.to_owned(), + _ => String::new() + }).collect::(); + + let mut bridge = self.0.lock().unwrap(); + bridge.set_text(text); + Ok(RSX::None) } } - diff --git a/alchemy/src/components/view.rs b/alchemy/src/components/view.rs index 9ddc064..b9213e1 100644 --- a/alchemy/src/components/view.rs +++ b/alchemy/src/components/view.rs @@ -9,14 +9,16 @@ use alchemy_styles::{Appearance, Layout, StylesList}; use alchemy_lifecycle::ComponentKey; use alchemy_lifecycle::error::Error; -use alchemy_lifecycle::rsx::{Props, RSX}; -use alchemy_lifecycle::traits::{Component, PlatformSpecificNodeType}; +use alchemy_lifecycle::rsx::RSX; +use alchemy_lifecycle::traits::{Component, Props, PlatformSpecificNodeType}; use crate::components::Fragment; #[cfg(feature = "cocoa")] use alchemy_cocoa::view::{View as PlatformViewBridge}; +pub struct ViewProps; + /// Views are the most basic piece of the API. If you want to display something, you'll /// probably be reaching for a View first and foremost. /// @@ -25,44 +27,53 @@ use alchemy_cocoa::view::{View as PlatformViewBridge}; /// ``` /// /// ``` -pub struct View(Mutex); +pub struct View { + bridge: Mutex +} impl Default for View { fn default() -> View { - View(Mutex::new(PlatformViewBridge::new())) + View { + bridge: Mutex::new(PlatformViewBridge::new()) + } } } +impl View { + pub fn default_props() -> ViewProps { + ViewProps {} + } +} + +impl Props for View { + fn set_props(&mut self, _: &mut std::any::Any) {} +} + impl Component for View { - fn constructor(_key: ComponentKey) -> View { - View(Mutex::new(PlatformViewBridge::new())) + fn new(_: ComponentKey) -> View { + View::default() } fn has_native_backing_node(&self) -> bool { true } fn borrow_native_backing_node(&self) -> Option { - let bridge = self.0.lock().unwrap(); + let bridge = self.bridge.lock().unwrap(); Some(bridge.borrow_native_backing_node()) } - fn append_child_component(&self, component: &Component) { - if let Some(child) = component.borrow_native_backing_node() { - let mut bridge = self.0.lock().unwrap(); - bridge.append_child(child); - } + fn append_child_node(&self, node: PlatformSpecificNodeType) { + let mut bridge = self.bridge.lock().unwrap(); + bridge.append_child(node); } fn apply_styles(&self, appearance: &Appearance, layout: &Layout) { - let mut bridge = self.0.lock().unwrap(); + let mut bridge = self.bridge.lock().unwrap(); bridge.apply_styles(appearance, layout); } - fn render(&self, props: &Props) -> Result { - Ok(RSX::node("Fragment", |key| Box::new(Fragment::constructor(key)), Props { - attributes: std::collections::HashMap::new(), - key: "".into(), - styles: StylesList::new(), - children: props.children.clone() - })) + fn render(&self, children: Vec) -> Result { + Ok(RSX::node("Fragment", "".into(), |key| { + Box::new(::new(key)) + }, Box::new(ViewProps {}), children)) } } diff --git a/alchemy/src/lib.rs b/alchemy/src/lib.rs index 1591c17..ce9b625 100644 --- a/alchemy/src/lib.rs +++ b/alchemy/src/lib.rs @@ -11,12 +11,12 @@ use proc_macro_hack::proc_macro_hack; pub use alchemy_lifecycle::ComponentKey; pub use alchemy_lifecycle::traits::{ - AppDelegate, Component, WindowDelegate + AppDelegate, Component, Props, WindowDelegate }; pub use alchemy_lifecycle::error::Error; pub use alchemy_lifecycle::rsx::{ - Props, RSX, VirtualNode, VirtualText + RSX, VirtualNode, VirtualText }; #[proc_macro_hack(support_nested)] diff --git a/alchemy/src/window/window.rs b/alchemy/src/window/window.rs index 1bc95a7..9c4a84b 100644 --- a/alchemy/src/window/window.rs +++ b/alchemy/src/window/window.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex}; use alchemy_lifecycle::{ComponentKey, RENDER_ENGINE}; use alchemy_lifecycle::rsx::RSX; -use alchemy_lifecycle::traits::WindowDelegate; +use alchemy_lifecycle::traits::{Component, WindowDelegate}; use alchemy_styles::{Appearance, Style, StylesList, THEME_ENGINE}; @@ -92,7 +92,11 @@ impl Window { let window_id = SHARED_APP.windows.allocate_new_window_id(); let view = View::default(); let shared_app_ptr: *const App = &**SHARED_APP; - let bridge = PlatformWindowBridge::new(window_id, &view, shared_app_ptr); + + // This unwrap() is fine, since we implement View ourselves in Alchemy + let backing_node = view.borrow_native_backing_node().unwrap(); + let bridge = PlatformWindowBridge::new(window_id, backing_node, shared_app_ptr); + let key = match RENDER_ENGINE.register_root_component(view) { Ok(key) => key, Err(_e) => { panic!("Uhhhh this really messed up"); } diff --git a/cocoa/src/text.rs b/cocoa/src/text.rs index 30261f4..2845955 100644 --- a/cocoa/src/text.rs +++ b/cocoa/src/text.rs @@ -24,6 +24,7 @@ static ALCHEMY_DELEGATE: &str = "alchemyDelegate"; /// colors and so forth. #[derive(Debug)] pub struct Text { + text: String, inner_mut: Id, inner_share: ShareId, background_color: Id, @@ -49,6 +50,7 @@ impl Text { }; Text { + text: "".into(), inner_mut: inner_mut, inner_share: inner_share, background_color: Color::transparent().into_nscolor(), @@ -88,9 +90,13 @@ impl Text { } } - pub fn set_text(&mut self, text: &str) { + pub fn set_text(&mut self, text: String) { + self.text = text; + } + + pub fn render(&mut self) { unsafe { - let string_value = NSString::alloc(nil).init_str(text); + let string_value = NSString::alloc(nil).init_str(&self.text); msg_send![&*self.inner_mut, setStringValue:string_value]; } } diff --git a/cocoa/src/window.rs b/cocoa/src/window.rs index 6f57320..64b4dd9 100644 --- a/cocoa/src/window.rs +++ b/cocoa/src/window.rs @@ -30,7 +30,7 @@ impl Window { /// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance), /// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime /// pointers. - pub fn new(window_id: usize, content_view: &Component, app_ptr: *const T) -> Window { + pub fn new(window_id: usize, content_view: ShareId, app_ptr: *const T) -> Window { let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)); let style = NSWindowStyleMask::NSResizableWindowMask | @@ -53,9 +53,9 @@ impl Window { // Objective-C runtime gets out of sync. msg_send![window, setReleasedWhenClosed:NO]; - if let Some(view_ptr) = content_view.borrow_native_backing_node() { - msg_send![window, setContentView:view_ptr]; - } + //if let Some(view_ptr) = content_view.borrow_native_backing_node() { + msg_send![window, setContentView:content_view]; + //} ShareId::from_ptr(window) }; diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 3f35c07..f1a5e50 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -24,23 +24,26 @@ impl AppDelegate for AppState { } } -#[derive(Default)] +/*#[derive(Default)] pub struct Banner; impl Component for Banner { + type Props = Box<()>; + type State = Box<()>; + fn constructor(_key: ComponentKey) -> Banner { Banner {} } - fn render(&self, props: &Props) -> Result { + fn render(&self, props: &Self::Props) -> Result { Ok(rsx! { - {props.children.clone()} + //{props.children.clone()} }) } -} +}*/ pub struct WindowState; @@ -51,7 +54,6 @@ impl WindowDelegate for WindowState { fn render(&self) -> Result { let messages = vec!["LOL"]; //, "wut", "BERT"]; - Ok(rsx! { "Hello there, my name is Bert" @@ -61,9 +63,9 @@ impl WindowDelegate for WindowState { })}*/ // - - - + // + // + // }) diff --git a/lifecycle/src/reconciler/generic_root_view_stub.rs b/lifecycle/src/reconciler/generic_root_view_stub.rs new file mode 100644 index 0000000..4658c5f --- /dev/null +++ b/lifecycle/src/reconciler/generic_root_view_stub.rs @@ -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 {} + } +} diff --git a/lifecycle/src/reconciler/instance.rs b/lifecycle/src/reconciler/instance.rs index 07cfc03..eca423c 100644 --- a/lifecycle/src/reconciler/instance.rs +++ b/lifecycle/src/reconciler/instance.rs @@ -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, - pub(crate) props: Props, + pub(crate) style_keys: StylesList, + pub(crate) component: Box, 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 e1bae24..093b9dc 100644 --- a/lifecycle/src/reconciler/mod.rs +++ b/lifecycle/src/reconciler/mod.rs @@ -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>, @@ -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(&self, instance: C) -> Result> { + pub fn register_root_component(&self, component: C) -> Result> { // 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, Box> { 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(()); } } diff --git a/lifecycle/src/rsx/mod.rs b/lifecycle/src/rsx/mod.rs index 34aeb46..6278f36 100644 --- a/lifecycle/src/rsx/mod.rs +++ b/lifecycle/src/rsx/mod.rs @@ -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( tag: &'static str, + styles: StylesList, create_fn: fn(key: ComponentKey) -> Box, - props: Props + props: P, + children: Vec ) -> RSX { RSX::VirtualNode(VirtualNode { tag: tag, create_component_fn: create_fn, - props: props + styles: styles, + props: Box::new(props), + children: children }) } diff --git a/lifecycle/src/rsx/props.rs b/lifecycle/src/rsx/props.rs index fbbe399..b3a3ea0 100644 --- a/lifecycle/src/rsx/props.rs +++ b/lifecycle/src/rsx/props.rs @@ -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, + //pub children: Vec, pub key: String, pub styles: StylesList } @@ -42,17 +42,17 @@ impl Props { key: String, styles: StylesList, attributes: HashMap<&'static str, AttributeType>, - children: Vec + //children: Vec ) -> 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) -> 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 { self.children.clone() - } + }*/ /// Returns a Option<&AttributeType> from the `attributes` inner HashMap. pub fn get(&self, key: &str) -> Option<&AttributeType> { diff --git a/lifecycle/src/rsx/virtual_node.rs b/lifecycle/src/rsx/virtual_node.rs index 64a57c6..aa15630 100644 --- a/lifecycle/src/rsx/virtual_node.rs +++ b/lifecycle/src/rsx/virtual_node.rs @@ -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, - /// `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, + + /// Child components for this node. + pub children: Vec } 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)?; } diff --git a/lifecycle/src/traits.rs b/lifecycle/src/traits.rs index 0831560..1a1d6d6 100644 --- a/lifecycle/src/traits.rs +++ b/lifecycle/src/traits.rs @@ -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 { 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 { 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, don’t 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 component’s 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 it’s 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 { Ok(RSX::None) } + fn render(&self, children: Vec) -> Result { 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. diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 802ce4e..c738691 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -22,9 +22,10 @@ mod parser; mod span; use proc_macro::TokenStream; -use proc_macro2::{TokenStream as TokenStream2, Literal}; +use proc_macro2::{Ident, TokenStream as TokenStream2, Literal, Span}; use proc_macro_hack::proc_macro_hack; use quote::quote; +use syn::{DeriveInput, parse_macro_input}; use alchemy_styles::cssparser::{Parser, ParserInput, RuleListParser}; use alchemy_styles::styles_parser::{Rule, RuleParser}; @@ -77,3 +78,30 @@ pub fn styles(input: TokenStream) -> TokenStream { styles })).into() } + +/// Implements a derive macro for automating props setting and conversion. +#[proc_macro_derive(Props)] +pub fn writable_props_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let name_props = Ident::new(&format!("{}Props", name), Span::call_site()); + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #name #ty_generics #where_clause { + fn default_props() -> #name_props { + #name_props::default() + } + } + + impl #impl_generics alchemy::ComponentProps for #name #ty_generics #where_clause { + fn set_props(&mut self, new_props: &mut Any) { + match new_props.downcast_ref::<#name_props>() { + Some(props) => { }, + None => { panic!("Woah there, somehow the wrong props were being passed!"); } + } + } + } + }) +} diff --git a/macros/src/rsx.rs b/macros/src/rsx.rs index afce3c6..9243b63 100644 --- a/macros/src/rsx.rs +++ b/macros/src/rsx.rs @@ -125,8 +125,7 @@ impl Element { let name = key.to_string(); let token = TokenTree::Ident(ident::new_raw(&name, key.span())); (name, token, value) - }); - + }); let mut attributes = TokenStream::new(); let mut styles = TokenStream::new(); @@ -159,29 +158,29 @@ impl Element { eprintln_msg += "\nERROR: rebuild with nightly to print source location"; } - //body.extend(quote!( - /*element.attrs.#key = Some(#lit.parse().unwrap_or_else(|err| { + attributes.extend(quote!( + props.#key = #lit.parse().unwrap_or_else(|err| { eprintln!(#eprintln_msg, err); - panic!("failed to parse string literal"); - }));*/ - //)); + panic!("Failed to parse string literal"); + }); + )); }, value => { - let key = key.to_string(); + let prop = key.to_string(); let value = process_value(value); - if key == "r#styles" { + if prop == "r#styles" { styles = quote!(std::convert::Into::into(#value)); continue; } - if key == "r#key" { + if prop == "r#key" { continue; } attributes.extend(quote!( - attributes.insert(#key, std::convert::Into::into(#value)); + props.#key = std::convert::Into::into(#value); )); } } @@ -219,19 +218,19 @@ impl Element { let component_name = Literal::string(&typename.to_string()); - Ok(quote!( - alchemy::RSX::node(#component_name, |key| { - Box::new(#typename::constructor(key)) - }, alchemy::Props::new("".into(), #styles, { - let mut attributes = std::collections::HashMap::new(); + Ok(quote! { + alchemy::RSX::node(#component_name, #styles, |key| { + Box::new(<#typename as alchemy::Component>::new(key)) + }, { + let props = #typename::default_props(); #attributes - attributes + Box::new(props) }, { let mut children = vec![]; #children children - })) - )) + }) + }) } }