diff --git a/node-graph/libraries/core-types/src/attr.rs b/node-graph/libraries/core-types/src/attr.rs index e083c16cc7..fe69601730 100644 --- a/node-graph/libraries/core-types/src/attr.rs +++ b/node-graph/libraries/core-types/src/attr.rs @@ -5,7 +5,8 @@ //! //! Keys are declared with the [`node_macro::attrs!`] macro: `Name: Type` entries, where //! `namespace { ... }` blocks contribute a `namespace:` name prefix. The key name is -//! derived mechanically from the ident (UpperCamel -> snake_case). +//! derived mechanically from the ident (UpperCamel -> snake_case). An optional `= value` +//! after the type declares the key's implicit default (see [`Attr::implicit_default`]). use crate::Color; use crate::list::NodeIdPath; @@ -16,6 +17,12 @@ use std::fmt::Debug; pub trait Attr { type Value: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static; fn name() -> &'static str; + + /// The value an item without this attribute is considered to have: the value type's `Default`, + /// unless the key's `attrs!` declaration overrides it with `= value`. + fn implicit_default() -> Self::Value { + Default::default() + } } node_macro::attrs! { @@ -24,9 +31,9 @@ node_macro::attrs! { /// Item's `BlendMode`, controlling how it composites with content beneath it. BlendMode: crate::blending::BlendMode, /// Item's opacity multiplier, composed multiplicatively through nested groups. Affects content clipped to the item. - Opacity: f64, + Opacity: f64 = 1., /// Item's fill opacity multiplier. Like opacity but does not affect content clipped to the item. - OpacityFill: f64, + OpacityFill: f64 = 1., /// Whether an item inherits the alpha of the content beneath it (clipping mask). ClippingMask: bool, /// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes). diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 7bee28d79d..60c95dde91 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -60,27 +60,9 @@ unsafe impl StaticType for Bundle { // Implicit attribute defaults // =========================== -// TODO: Remove this is not maintainable -/// Overrides the type's default value for certain attributes. -fn implicit_default_value(key: &str) -> Option> { - if key == attr::Opacity::name() || key == attr::OpacityFill::name() { - Some(Box::new(1_f64)) - } else { - None - } -} - -/// The value an item without attribute `A` is considered to have: the key's implicit default if it -/// has one, otherwise the value type's `Default`. -fn implicit_default() -> A::Value { - implicit_default_value(A::name()) - .and_then(|value| value.into_any().downcast::().ok()) - .map_or_else(Default::default, |value| *value) -} - -/// Appends `count` copies of `key`'s implicit default to `attribute` (see [`implicit_default_value`]). +/// Appends `count` copies of `key`'s implicit default to `attribute` (see [`attr::implicit_default_value`]). fn pad_with_implicit_default(key: &str, attribute: &mut Box, count: usize) { - match implicit_default_value(key) { + match attr::implicit_default_value(key) { Some(default) => attribute.push_repeated(&*default, count), None => { for _ in 0..count { @@ -661,7 +643,7 @@ impl ItemAttributeValues { /// Gets a mutable reference to the value of the typed attribute, inserting the key's default value if absent. pub fn attr_mut_or_insert_default(&mut self) -> &mut A::Value { - self.get_or_insert_with_mut(A::name(), implicit_default::) + self.get_or_insert_with_mut(A::name(), A::implicit_default) } /// Inserts the typed attribute's value, replacing any existing entry. @@ -1049,7 +1031,7 @@ impl List { /// Returns a clone of the value of the typed attribute at the given item index, or the key's default value if absent. pub fn attr_cloned_or_default(&self, index: usize) -> A::Value { - self.attr::(index).cloned().unwrap_or_else(implicit_default::) + self.attr::(index).cloned().unwrap_or_else(A::implicit_default) } /// Returns a clone of the value of the typed attribute at the given item index, or the provided default if absent. @@ -1087,7 +1069,7 @@ impl List { pub fn iter_attr_values_or_default(&self) -> impl Iterator + '_ { let slice = self.attributes.get_attribute_slice::(A::name()); let len = self.element.len(); - (0..len).map(move |i| slice.map_or_else(implicit_default::, |s| s[i].clone())) + (0..len).map(move |i| slice.map_or_else(A::implicit_default, |s| s[i].clone())) } /// Returns a mutable iterator over the typed attribute, creating the attribute with defaults if it doesn't exist. @@ -1349,7 +1331,7 @@ impl Item { /// Returns a clone of the value of the typed attribute, or the key's default value if absent. pub fn attr_cloned_or_default(&self) -> A::Value { - self.attr::().cloned().unwrap_or_else(implicit_default::) + self.attr::().cloned().unwrap_or_else(A::implicit_default) } /// Returns a mutable reference to the value of the typed attribute, if present. diff --git a/node-graph/node-macro/src/attrs.rs b/node-graph/node-macro/src/attrs.rs index 5b97e429dd..170c82803e 100644 --- a/node-graph/node-macro/src/attrs.rs +++ b/node-graph/node-macro/src/attrs.rs @@ -2,30 +2,42 @@ use crate::crate_ident::CrateIdent; use proc_macro2::TokenStream; use quote::quote; use syn::parse::ParseStream; -use syn::{Attribute, Ident, Token, Type, braced, token}; +use syn::{Attribute, Expr, Ident, Token, Type, braced, token}; /// Implementation of the `attrs!` macro declaring typed attribute keys. /// /// Grammar: `Name: Type`, comma-separated; `namespace { ... }` blocks nest and contribute a /// `namespace:` prefix to the key name, which is otherwise derived mechanically from the key -/// ident (UpperCamel → snake_case). +/// ident (UpperCamel → snake_case). An optional `= value` after the type declares the key's +/// implicit default, overriding the value type's `Default` for items lacking the attribute. pub fn attrs_impl(input: TokenStream) -> syn::Result { let entries: Entries = syn::parse2(input)?; let crate_ident = CrateIdent::default(); let core = crate_ident.gcore()?; let items = entries.0.iter().map(|entry| generate_entry(entry, core, "")).collect::>>()?; + let lookup = generate_implicit_default_lookup(&entries.0, core); Ok(quote! { #(#items)* + #lookup }) } struct Entries(Vec); enum Entry { - Key { docs: Vec, ident: Ident, ty: Type }, - Namespace { docs: Vec, ident: Ident, entries: Vec }, + Key { + docs: Vec, + ident: Ident, + ty: Box, + default: Option>, + }, + Namespace { + docs: Vec, + ident: Ident, + entries: Vec, + }, } impl syn::parse::Parse for Entries { @@ -49,8 +61,14 @@ fn parse_entries(input: ParseStream) -> syn::Result> { }); } else { input.parse::()?; - let ty: Type = input.parse()?; - entries.push(Entry::Key { docs, ident, ty }); + let ty = Box::new(input.parse::()?); + let default = if input.peek(Token![=]) { + input.parse::()?; + Some(Box::new(input.parse::()?)) + } else { + None + }; + entries.push(Entry::Key { docs, ident, ty, default }); } if !input.is_empty() { input.parse::()?; @@ -61,8 +79,15 @@ fn parse_entries(input: ParseStream) -> syn::Result> { fn generate_entry(entry: &Entry, core: &TokenStream, prefix: &str) -> syn::Result { match entry { - Entry::Key { docs, ident, ty } => { + Entry::Key { docs, ident, ty, default } => { let name = key_name(ident, prefix); + let implicit_default = default.as_ref().map(|value| { + quote! { + fn implicit_default() -> Self::Value { + #value + } + } + }); Ok(quote! { #(#docs)* pub struct #ident; @@ -71,6 +96,7 @@ fn generate_entry(entry: &Entry, core: &TokenStream, prefix: &str) -> syn::Resul fn name() -> &'static str { #name } + #implicit_default } }) } @@ -88,6 +114,40 @@ fn generate_entry(entry: &Entry, core: &TokenStream, prefix: &str) -> syn::Resul } } +/// Generates a string-keyed lookup of the boxed implicit defaults for erased attribute code paths, +/// or nothing if no key in this invocation declares a `= value` default. +fn generate_implicit_default_lookup(entries: &[Entry], core: &TokenStream) -> Option { + let mut defaulted_keys = Vec::new(); + collect_defaulted_key_paths(entries, &TokenStream::new(), &mut defaulted_keys); + + if defaulted_keys.is_empty() { + return None; + } + + Some(quote! { + /// The boxed implicit default for the key named `key`, if that key declares one with `= value` in `attrs!`. + pub fn implicit_default_value(key: &str) -> ::std::option::Option<::std::boxed::Box> { + #( + if key == <#defaulted_keys as #core::attr::Attr>::name() { + return ::std::option::Option::Some(::std::boxed::Box::new(<#defaulted_keys as #core::attr::Attr>::implicit_default())); + } + )* + ::std::option::Option::None + } + }) +} + +/// Walks the entry tree collecting module-qualified paths (like `namespace::Key`) of keys that declare a default. +fn collect_defaulted_key_paths(entries: &[Entry], module_path: &TokenStream, paths: &mut Vec) { + for entry in entries { + match entry { + Entry::Key { ident, default: Some(_), .. } => paths.push(quote!(#module_path #ident)), + Entry::Key { .. } => {} + Entry::Namespace { ident, entries, .. } => collect_defaulted_key_paths(entries, "e!(#module_path #ident::), paths), + } + } +} + fn key_name(ident: &Ident, prefix: &str) -> String { let snake = snake_case(&ident.to_string()); if prefix.is_empty() { snake } else { format!("{prefix}:{snake}") }