diff --git a/lifecycle/src/reconciler.rs b/lifecycle/src/reconciler.rs new file mode 100644 index 0000000..d9ee33f --- /dev/null +++ b/lifecycle/src/reconciler.rs @@ -0,0 +1,425 @@ +//! Implements tree diffing, and attempts to cache Component instances where +//! possible. + +use std::sync::Mutex; +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; + +use crate::rsx::{RSX, VirtualNode}; + +pub struct RenderEngine { + pending_state_updates: Mutex>, + trees: Mutex> +} + +impl RenderEngine { + pub(crate) fn new() -> RenderEngine { + RenderEngine { + pending_state_updates: Mutex::new(vec![]), + trees: Mutex::new(HashMap::new()) + } + } + + /// `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(&self, root: RSX) -> Uuid { + let key = Uuid::new_v4(); + let stretch = Stretch::new(); + let mut trees = self.trees.lock().unwrap(); + trees.insert(key, (root, stretch)); + 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_apply_root(&self, key: &Uuid, new_root: RSX) -> Result<(), Box> { + /*let trees = self.trees.lock().unwrap(); + let (old_root, stretch) = trees.remove(key)?; + diff_and_patch_trees(old_root, new_root, &mut stretch, 0)?; + trees.insert(*key, (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> { + // 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_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 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(); + 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 + // 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/styles/src/engine.rs b/styles/src/engine.rs new file mode 100644 index 0000000..4b00481 --- /dev/null +++ b/styles/src/engine.rs @@ -0,0 +1,123 @@ +//! Implements a Theme Engine. This behaves a bit differently depending on +//! the mode your application is compiled in. +//! +//! - In `debug`, it scans a few places and loads any CSS files that are +//! necessary. It will also hot-reload CSS files as they change. +//! - In `release`, it scans those same places, and compiles your CSS into +//! your resulting binary. The hot-reloading functionality is not in release, +//! however it can be enabled if desired. +//! + +use std::fs; +use std::env; +use std::sync::RwLock; +use std::path::PathBuf; +use std::collections::HashMap; + +use toml; +use serde::Deserialize; + +use crate::StylesList; +use crate::styles::Style; +use crate::stylesheet::StyleSheet; + +static CONFIG_FILE_NAME: &str = "alchemy.toml"; + +#[derive(Debug, Deserialize)] +struct RawConfig<'d> { + #[serde(borrow)] + general: Option>, +} + +#[derive(Debug, Deserialize)] +struct General<'a> { + #[serde(borrow)] + dirs: Option> +} + +/// The `ThemeEngine` controls loading themes and registering associated +/// styles. +#[derive(Debug)] +pub struct ThemeEngine { + pub dirs: Vec, + pub themes: RwLock> +} + +impl ThemeEngine { + /// Creates a new 'ThemeEngine` instance. + pub fn new() -> ThemeEngine { + // This env var is set by Cargo... so if this code breaks, there's + // bigger concerns, lol + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let root = PathBuf::from(manifest_dir); + let default_dirs = vec![root.join("themes")]; + + let toml_contents = read_config_file(); + let raw: RawConfig<'_> = toml::from_str(&toml_contents).expect(&format!("Invalid TOML in {}!", CONFIG_FILE_NAME)); + + let dirs = match raw.general { + Some(General { dirs }) => ( + dirs.map_or(default_dirs, |v| { + v.into_iter().map(|dir| root.join(dir)).collect() + }) + ), + + None => default_dirs + }; + + ThemeEngine { dirs, themes: RwLock::new(HashMap::new()) } + } + + /// Registers a stylesheet (typically created by the `styles! {}` macro) for a given + /// theme. + pub fn register_styles(&self, key: &str, stylesheet: StyleSheet) { + let mut themes = self.themes.write().unwrap(); + if !themes.contains_key(key) { + themes.insert(key.to_string(), stylesheet); + return; + } + + // if let Some(existing_stylesheet) = self.themes.get_mut(key) { + // *existing_stylesheet.merge(stylesheet); + //} + } + + /// Given a theme key, style keys, and a style, configures the style for layout + /// and appearance. + pub fn configure_style_for_keys_in_theme(&self, theme: &str, keys: &StylesList, style: &mut Style) { + let themes = self.themes.read().unwrap(); + + match themes.get(theme) { + Some(theme) => { + for key in &keys.0 { + theme.apply_styles(key, style); + } + }, + + None => { + eprintln!("No styles for theme!"); + } + } + } + + /// The same logic as `configure_style_for_keys_in_theme`, but defaults to the default theme. + pub fn configure_style_for_keys(&self, keys: &StylesList, style: &mut Style) { + self.configure_style_for_keys_in_theme("default", keys, style) + } +} + +/// Utility method for reading a config file from the `CARGO_MANIFEST_DIR`. Hat tip to +/// [askama](https://github.com/djc/askama) for this! +pub fn read_config_file() -> String { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let root = PathBuf::from(manifest_dir); + let filename = root.join(CONFIG_FILE_NAME); + + if filename.exists() { + fs::read_to_string(&filename) + .expect(&format!("Unable to read {}", filename.to_str().unwrap())) + } else { + "".to_string() + } +} diff --git a/styles/src/spacedlist.rs b/styles/src/spacedlist.rs new file mode 100644 index 0000000..0deefeb --- /dev/null +++ b/styles/src/spacedlist.rs @@ -0,0 +1,262 @@ +//! A space separated list of values. +//! +//! This type represents a list of non-unique values represented as a string of +//! values separated by spaces in HTML attributes. This is rarely used; a +//! SpacedSet of unique values is much more common. + + +use std::fmt::{Debug, Display, Error, Formatter}; +use std::iter::FromIterator; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +/// A space separated list of values. +/// +/// This type represents a list of non-unique values represented as a string of +/// values separated by spaces in HTML attributes. This is rarely used; a +/// SpacedSet of unique values is much more common. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SpacedList(Vec); + +impl SpacedList { + /// Construct an empty `SpacedList`. + pub fn new() -> Self { + SpacedList(Vec::new()) + } +} + +impl Default for SpacedList { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator for SpacedList { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + SpacedList(iter.into_iter().collect()) + } +} + +impl<'a, A: 'a + Clone> FromIterator<&'a A> for SpacedList { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + SpacedList(iter.into_iter().cloned().collect()) + } +} + +impl<'a, A: FromStr> From<&'a str> for SpacedList +where + ::Err: Debug, +{ + fn from(s: &'a str) -> Self { + Self::from_iter(s.split_whitespace().map(|s| FromStr::from_str(s).unwrap())) + } +} + +impl Deref for SpacedList { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SpacedList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for SpacedList { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + let mut it = self.0.iter().peekable(); + while let Some(class) = it.next() { + Display::fmt(class, f)?; + if it.peek().is_some() { + Display::fmt(" ", f)?; + } + } + Ok(()) + } +} + +impl Debug for SpacedList { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + f.debug_list().entries(self.0.iter()).finish() + } +} + +impl<'a, 'b, A: FromStr> From<(&'a str, &'b str)> for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list + } +} + +impl<'a, 'b, 'c, A: FromStr> From<(&'a str, &'b str, &'c str)> for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list.push(FromStr::from_str(s.2).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, A: FromStr> From<(&'a str, &'b str, &'c str, &'d str)> for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list.push(FromStr::from_str(s.2).unwrap()); + list.push(FromStr::from_str(s.3).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, A: FromStr> From<(&'a str, &'b str, &'c str, &'d str, &'e str)> + for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list.push(FromStr::from_str(s.2).unwrap()); + list.push(FromStr::from_str(s.3).unwrap()); + list.push(FromStr::from_str(s.4).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, 'f, A: FromStr> + From<(&'a str, &'b str, &'c str, &'d str, &'e str, &'f str)> for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list.push(FromStr::from_str(s.2).unwrap()); + list.push(FromStr::from_str(s.3).unwrap()); + list.push(FromStr::from_str(s.4).unwrap()); + list.push(FromStr::from_str(s.5).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, A: FromStr> + From<( + &'a str, + &'b str, + &'c str, + &'d str, + &'e str, + &'f str, + &'g str, + )> for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list.push(FromStr::from_str(s.2).unwrap()); + list.push(FromStr::from_str(s.3).unwrap()); + list.push(FromStr::from_str(s.4).unwrap()); + list.push(FromStr::from_str(s.5).unwrap()); + list.push(FromStr::from_str(s.6).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, A: FromStr> + From<( + &'a str, + &'b str, + &'c str, + &'d str, + &'e str, + &'f str, + &'g str, + &'h str, + )> for SpacedList +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.push(FromStr::from_str(s.0).unwrap()); + list.push(FromStr::from_str(s.1).unwrap()); + list.push(FromStr::from_str(s.2).unwrap()); + list.push(FromStr::from_str(s.3).unwrap()); + list.push(FromStr::from_str(s.4).unwrap()); + list.push(FromStr::from_str(s.5).unwrap()); + list.push(FromStr::from_str(s.6).unwrap()); + list.push(FromStr::from_str(s.7).unwrap()); + list + } +} + +macro_rules! spacedlist_from_array { + ($num:tt) => { + impl<'a, A: FromStr> From<[&'a str; $num]> for SpacedList + where + ::Err: Debug, + { + fn from(s: [&str; $num]) -> Self { + Self::from_iter(s.into_iter().map(|s| FromStr::from_str(*s).unwrap())) + } + } + }; +} +spacedlist_from_array!(1); +spacedlist_from_array!(2); +spacedlist_from_array!(3); +spacedlist_from_array!(4); +spacedlist_from_array!(5); +spacedlist_from_array!(6); +spacedlist_from_array!(7); +spacedlist_from_array!(8); +spacedlist_from_array!(9); +spacedlist_from_array!(10); +spacedlist_from_array!(11); +spacedlist_from_array!(12); +spacedlist_from_array!(13); +spacedlist_from_array!(14); +spacedlist_from_array!(15); +spacedlist_from_array!(16); +spacedlist_from_array!(17); +spacedlist_from_array!(18); +spacedlist_from_array!(19); +spacedlist_from_array!(20); +spacedlist_from_array!(21); +spacedlist_from_array!(22); +spacedlist_from_array!(23); +spacedlist_from_array!(24); +spacedlist_from_array!(25); +spacedlist_from_array!(26); +spacedlist_from_array!(27); +spacedlist_from_array!(28); +spacedlist_from_array!(29); +spacedlist_from_array!(30); +spacedlist_from_array!(31); +spacedlist_from_array!(32); diff --git a/styles/src/spacedset.rs b/styles/src/spacedset.rs new file mode 100644 index 0000000..8096b56 --- /dev/null +++ b/styles/src/spacedset.rs @@ -0,0 +1,293 @@ +//! A space separated set of unique values. +//! +//! This type represents a set of unique values represented as a string of +//! values separated by spaces in HTML attributes. + +use std::collections::BTreeSet; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::iter::FromIterator; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +/// A space separated set of unique values. +/// +/// This type represents a set of unique values represented as a string of +/// values separated by spaces in HTML attributes. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SpacedSet(pub BTreeSet); + +impl SpacedSet { + /// Construct an empty `SpacedSet`. + pub fn new() -> Self { + SpacedSet(BTreeSet::new()) + } + + /// Add a value to the `SpacedSet`. + pub fn add>(&mut self, value: T) -> bool { + self.0.insert(value.into()) + } +} + +impl Default for SpacedSet { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator for SpacedSet { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + SpacedSet(iter.into_iter().collect()) + } +} + +impl<'a, A: 'a + Ord + Clone> FromIterator<&'a A> for SpacedSet { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + SpacedSet(iter.into_iter().cloned().collect()) + } +} + +impl<'a, A: Ord + FromStr> FromStr for SpacedSet +where + ::Err: Debug, +{ + type Err = ::Err; + + fn from_str(s: &str) -> Result { + let result: Result, Self::Err> = + s.split_whitespace().map(|s| FromStr::from_str(s)).collect(); + result.map(Self::from_iter) + } +} + +impl<'a, A: Ord + FromStr> From<&'a str> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: &'a str) -> Self { + Self::from_iter(s.split_whitespace().map(|s| FromStr::from_str(s).unwrap())) + } +} + +impl Deref for SpacedSet { + type Target = BTreeSet; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SpacedSet { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for SpacedSet { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + let mut it = self.0.iter().peekable(); + while let Some(class) = it.next() { + Display::fmt(class, f)?; + if it.peek().is_some() { + Display::fmt(" ", f)?; + } + } + Ok(()) + } +} + +impl Debug for SpacedSet { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + f.debug_list().entries(self.0.iter()).finish() + } +} + +impl<'a, A: Ord + FromStr> From> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: Vec<&'a str>) -> Self { + let mut list = Self::new(); + + for key in s { + list.insert(FromStr::from_str(key).unwrap()); + } + + list + } +} + +impl<'a, 'b, A: Ord + FromStr> From<(&'a str, &'b str)> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list + } +} + +impl<'a, 'b, 'c, A: Ord + FromStr> From<(&'a str, &'b str, &'c str)> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list.insert(FromStr::from_str(s.2).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, A: Ord + FromStr> From<(&'a str, &'b str, &'c str, &'d str)> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list.insert(FromStr::from_str(s.2).unwrap()); + list.insert(FromStr::from_str(s.3).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, A: Ord + FromStr> From<(&'a str, &'b str, &'c str, &'d str, &'e str)> + for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list.insert(FromStr::from_str(s.2).unwrap()); + list.insert(FromStr::from_str(s.3).unwrap()); + list.insert(FromStr::from_str(s.4).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, 'f, A: Ord + FromStr> + From<(&'a str, &'b str, &'c str, &'d str, &'e str, &'f str)> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list.insert(FromStr::from_str(s.2).unwrap()); + list.insert(FromStr::from_str(s.3).unwrap()); + list.insert(FromStr::from_str(s.4).unwrap()); + list.insert(FromStr::from_str(s.5).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, A: Ord + FromStr> + From<( + &'a str, + &'b str, + &'c str, + &'d str, + &'e str, + &'f str, + &'g str, + )> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list.insert(FromStr::from_str(s.2).unwrap()); + list.insert(FromStr::from_str(s.3).unwrap()); + list.insert(FromStr::from_str(s.4).unwrap()); + list.insert(FromStr::from_str(s.5).unwrap()); + list.insert(FromStr::from_str(s.6).unwrap()); + list + } +} + +impl<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, A: Ord + FromStr> + From<( + &'a str, + &'b str, + &'c str, + &'d str, + &'e str, + &'f str, + &'g str, + &'h str, + )> for SpacedSet +where + ::Err: Debug, +{ + fn from(s: (&str, &str, &str, &str, &str, &str, &str, &str)) -> Self { + let mut list = Self::new(); + list.insert(FromStr::from_str(s.0).unwrap()); + list.insert(FromStr::from_str(s.1).unwrap()); + list.insert(FromStr::from_str(s.2).unwrap()); + list.insert(FromStr::from_str(s.3).unwrap()); + list.insert(FromStr::from_str(s.4).unwrap()); + list.insert(FromStr::from_str(s.5).unwrap()); + list.insert(FromStr::from_str(s.6).unwrap()); + list.insert(FromStr::from_str(s.7).unwrap()); + list + } +} + +macro_rules! spacedlist_from_array { + ($num:tt) => { + impl<'a, A: Ord + FromStr> From<[&'a str; $num]> for SpacedSet + where + ::Err: Debug, + { + fn from(s: [&str; $num]) -> Self { + Self::from_iter(s.into_iter().map(|s| FromStr::from_str(*s).unwrap())) + } + } + }; +} +spacedlist_from_array!(1); +spacedlist_from_array!(2); +spacedlist_from_array!(3); +spacedlist_from_array!(4); +spacedlist_from_array!(5); +spacedlist_from_array!(6); +spacedlist_from_array!(7); +spacedlist_from_array!(8); +spacedlist_from_array!(9); +spacedlist_from_array!(10); +spacedlist_from_array!(11); +spacedlist_from_array!(12); +spacedlist_from_array!(13); +spacedlist_from_array!(14); +spacedlist_from_array!(15); +spacedlist_from_array!(16); +spacedlist_from_array!(17); +spacedlist_from_array!(18); +spacedlist_from_array!(19); +spacedlist_from_array!(20); +spacedlist_from_array!(21); +spacedlist_from_array!(22); +spacedlist_from_array!(23); +spacedlist_from_array!(24); +spacedlist_from_array!(25); +spacedlist_from_array!(26); +spacedlist_from_array!(27); +spacedlist_from_array!(28); +spacedlist_from_array!(29); +spacedlist_from_array!(30); +spacedlist_from_array!(31); +spacedlist_from_array!(32); diff --git a/styles/src/style_keys.rs b/styles/src/style_keys.rs new file mode 100644 index 0000000..75ca840 --- /dev/null +++ b/styles/src/style_keys.rs @@ -0,0 +1,83 @@ +//! A valid CSS class. +//! +//! A CSS class is a non-empty string that starts with an alphanumeric character +//! and is followed by any number of alphanumeric characters and the +//! `_`, `-` and `.` characters. + +use std::fmt::{Display, Error, Formatter}; +use std::ops::Deref; +use std::str::FromStr; + +/// A valid CSS class. +/// +/// A CSS class is a non-empty string that starts with an alphanumeric character +/// and is followed by any number of alphanumeric characters and the +/// `_`, `-` and `.` characters. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct StyleKey(String); + +impl StyleKey { + /// Construct a new styles list from a string. + /// + /// Returns `Err` if the provided string is invalid. + pub fn try_new>(id: S) -> Result { + let id = id.into(); + { + let mut chars = id.chars(); + match chars.next() { + None => return Err("style keys cannot be empty"), + Some(c) if !c.is_alphabetic() => { + return Err("style keys must start with an alphabetic character") + } + _ => (), + } + for c in chars { + if !c.is_alphanumeric() && c != '-' { + return Err( + "style keys can only contain alphanumerics (dash included)", + ); + } + } + } + Ok(StyleKey(id)) + } + + /// Construct a new class name from a string. + /// + /// Panics if the provided string is invalid. + pub fn new>(id: S) -> Self { + let id = id.into(); + Self::try_new(id.clone()).unwrap_or_else(|err| { + panic!( + "alchemy::dom::types::StyleKey: {:?} is not a valid class name: {}", + id, err + ) + }) + } +} + +impl FromStr for StyleKey { + type Err = &'static str; + fn from_str(s: &str) -> Result { + StyleKey::try_new(s) + } +} + +impl<'a> From<&'a str> for StyleKey { + fn from(str: &'a str) -> Self { + StyleKey::from_str(str).unwrap() + } +} + +impl Display for StyleKey { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + Display::fmt(&self.0, f) + } +} + +impl Deref for StyleKey { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/styles/src/stylesheet.rs b/styles/src/stylesheet.rs new file mode 100644 index 0000000..6a563e7 --- /dev/null +++ b/styles/src/stylesheet.rs @@ -0,0 +1,308 @@ +//! Implements a `StyleSheet`, which contains inner logic for +//! determining what styles should be applied to a given widget. +//! +//! You can think of this as a compiled form of your CSS. You generally +//! don't need to create these structs yourself, but feel free to if +//! you have some creative use. + +use std::collections::HashMap; + +use crate::styles::{Dimension, Rect, Size, Style, Styles}; + +/// A `StyleSheet` contains selectors and parsed `Styles` attributes. +/// It also has some logic to apply styles for n keys to a given `Style` node. +#[derive(Debug)] +pub struct StyleSheet(HashMap<&'static str, Vec>); + +impl StyleSheet { + /// Creates a new `Stylesheet`. + pub fn new(styles: HashMap<&'static str, Vec>) -> Self { + StyleSheet(styles) + } + + pub fn apply_styles(&self, key: &str, style: &mut Style) { + match self.0.get(key) { + Some(styles) => { reduce_styles_into_style(styles, style); }, + None => {} + } + } +} + +/// This takes a list of styles, and a mutable style object, and attempts to configure the +/// style object in a way that makes sense given n styles. +fn reduce_styles_into_style(styles: &Vec, layout: &mut Style) { + for style in styles { match style { + Styles::AlignContent(val) => { layout.align_content = *val; }, + Styles::AlignItems(val) => { layout.align_items = *val; }, + Styles::AlignSelf(val) => { layout.align_self = *val; }, + Styles::AspectRatio(val) => { layout.aspect_ratio = *val; }, + Styles::BackfaceVisibility(_val) => { }, + Styles::BackgroundColor(val) => { layout.background_color = *val; }, + + Styles::BorderColor(_val) => { }, + Styles::BorderEndColor(_val) => { }, + Styles::BorderBottomColor(_val) => { }, + Styles::BorderLeftColor(_val) => { }, + Styles::BorderRightColor(_val) => { }, + Styles::BorderTopColor(_val) => { }, + Styles::BorderStartColor(_val) => { }, + + Styles::BorderStyle(_val) => { }, + Styles::BorderEndStyle(_val) => { }, + Styles::BorderBottomStyle(_val) => { }, + Styles::BorderLeftStyle(_val) => { }, + Styles::BorderRightStyle(_val) => { }, + Styles::BorderTopStyle(_val) => { }, + Styles::BorderStartStyle(_val) => { }, + + Styles::BorderWidth(_val) => { }, + Styles::BorderEndWidth(_val) => { }, + Styles::BorderBottomWidth(_val) => { }, + Styles::BorderLeftWidth(_val) => { }, + Styles::BorderRightWidth(_val) => { }, + Styles::BorderTopWidth(_val) => { }, + Styles::BorderStartWidth(_val) => { }, + + Styles::BorderRadius(_val) => { }, + Styles::BorderBottomEndRadius(_val) => { }, + Styles::BorderBottomLeftRadius(_val) => { }, + Styles::BorderBottomRightRadius(_val) => { }, + Styles::BorderBottomStartRadius(_val) => { }, + Styles::BorderTopLeftRadius(_val) => { }, + Styles::BorderTopRightRadius(_val) => { }, + Styles::BorderTopEndRadius(_val) => { }, + Styles::BorderTopStartRadius(_val) => { }, + + Styles::Bottom(val) => { + layout.position = Rect { + start: layout.position.start, + end: layout.position.end, + top: layout.position.top, + bottom: Dimension::Points(*val) + }; + }, + + Styles::Direction(val) => { layout.direction = *val; }, + Styles::Display(val) => { layout.display = *val; }, + + Styles::End(val) => { + layout.position = Rect { + start: layout.position.start, + end: Dimension::Points(*val), + top: layout.position.top, + bottom: layout.position.bottom + }; + }, + + Styles::FlexBasis(val) => { layout.flex_basis = Dimension::Points(*val); }, + Styles::FlexDirection(val) => { layout.flex_direction = *val; }, + Styles::FlexGrow(val) => { layout.flex_grow = *val; }, + Styles::FlexShrink(val) => { layout.flex_shrink = *val; }, + Styles::FlexWrap(val) => { layout.flex_wrap = *val; }, + + Styles::FontFamily(_val) => { }, + Styles::FontLineHeight(_val) => { }, + Styles::FontSize(val) => { layout.font_size = *val; }, + Styles::FontStyle(val) => { layout.font_style = *val; }, + Styles::FontWeight(val) => { layout.font_weight = *val; }, + + Styles::Height(val) => { + layout.size = Size { + width: layout.size.width, + height: Dimension::Points(*val) + }; + }, + + Styles::JustifyContent(val) => { layout.justify_content = *val; }, + + Styles::Left(val) => { + layout.position = Rect { + start: Dimension::Points(*val), + end: layout.position.end, + top: layout.position.top, + bottom: layout.position.bottom + }; + }, + + Styles::MarginBottom(val) => { + layout.margin = Rect { + start: layout.margin.start, + end: layout.margin.end, + top: layout.margin.top, + bottom: Dimension::Points(*val) + }; + }, + + Styles::MarginEnd(val) => { + layout.margin = Rect { + start: layout.margin.start, + end: Dimension::Points(*val), + top: layout.margin.top, + bottom: layout.margin.bottom + }; + }, + + Styles::MarginLeft(val) => { + layout.margin = Rect { + start: Dimension::Points(*val), + end: layout.margin.end, + top: layout.margin.top, + bottom: layout.margin.bottom + }; + }, + + Styles::MarginRight(val) => { + layout.margin = Rect { + start: layout.margin.start, + end: Dimension::Points(*val), + top: layout.margin.top, + bottom: layout.margin.bottom + }; + }, + + Styles::MarginStart(val) => { + layout.margin = Rect { + start: Dimension::Points(*val), + end: layout.margin.end, + top: layout.margin.top, + bottom: layout.margin.bottom + }; + }, + + Styles::MarginTop(val) => { + layout.margin = Rect { + start: layout.margin.start, + end: layout.margin.end, + top: Dimension::Points(*val), + bottom: layout.margin.bottom + }; + }, + + Styles::MaxHeight(val) => { + layout.max_size = Size { + width: layout.max_size.width, + height: Dimension::Points(*val) + }; + }, + + Styles::MaxWidth(val) => { + layout.max_size = Size { + width: Dimension::Points(*val), + height: layout.max_size.height + }; + }, + + Styles::MinHeight(val) => { + layout.min_size = Size { + width: layout.min_size.width, + height: Dimension::Points(*val) + }; + }, + + Styles::MinWidth(val) => { + layout.min_size = Size { + width: Dimension::Points(*val), + height: layout.min_size.height + }; + }, + + Styles::Opacity(val) => { layout.opacity = *val; }, + Styles::Overflow(val) => { layout.overflow = *val; }, + + Styles::PaddingBottom(val) => { + layout.padding = Rect { + start: layout.padding.start, + end: layout.padding.end, + top: layout.padding.top, + bottom: Dimension::Points(*val) + }; + }, + + Styles::PaddingEnd(val) => { + layout.padding = Rect { + start: layout.padding.start, + end: Dimension::Points(*val), + top: layout.padding.top, + bottom: layout.padding.bottom + }; + }, + + Styles::PaddingLeft(val) => { + layout.padding = Rect { + start: Dimension::Points(*val), + end: layout.padding.end, + top: layout.padding.top, + bottom: layout.padding.bottom + }; + }, + + Styles::PaddingRight(val) => { + layout.padding = Rect { + start: layout.padding.start, + end: Dimension::Points(*val), + top: layout.padding.top, + bottom: layout.padding.bottom + }; + }, + + Styles::PaddingStart(val) => { + layout.padding = Rect { + start: Dimension::Points(*val), + end: layout.padding.end, + top: layout.padding.top, + bottom: layout.padding.bottom + }; + }, + + Styles::PaddingTop(val) => { + layout.padding = Rect { + start: layout.padding.start, + end: layout.padding.end, + top: Dimension::Points(*val), + bottom: layout.padding.bottom + }; + }, + + Styles::PositionType(val) => { layout.position_type = *val; }, + + Styles::Right(val) => { + layout.position = Rect { + start: layout.position.start, + end: Dimension::Points(*val), + top: layout.position.top, + bottom: layout.position.bottom + }; + }, + + Styles::Start(val) => { + layout.position = Rect { + start: Dimension::Points(*val), + end: layout.position.end, + top: layout.position.top, + bottom: layout.position.bottom + }; + }, + + Styles::TextAlignment(val) => { layout.text_alignment = *val; }, + Styles::TextColor(val) => { layout.text_color = *val; }, + Styles::TextDecorationColor(val) => { layout.text_decoration_color = *val; }, + Styles::TextShadowColor(val) => { layout.text_shadow_color = *val; }, + Styles::TintColor(val) => { layout.tint_color = *val; }, + + Styles::Top(val) => { + layout.position = Rect { + start: layout.position.start, + end: layout.position.end, + top: Dimension::Points(*val), + bottom: layout.position.bottom + }; + }, + + Styles::Width(val) => { + layout.size = Size { + width: Dimension::Points(*val), + height: layout.size.height + }; + } + }} +}