From 58178fd48ebf7b265b29375ec57abbcef1bf61c4 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 18 Jul 2026 12:50:56 +0000 Subject: [PATCH] Add typed attribute keys: attrs! macro, Attr trait, and typed accessors --- node-graph/libraries/core-types/src/attr.rs | 70 ++++++ node-graph/libraries/core-types/src/lib.rs | 2 + node-graph/libraries/core-types/src/list.rs | 219 +++++++++++++++++- .../libraries/graphic-types/src/attr.rs | 31 +++ node-graph/libraries/graphic-types/src/lib.rs | 1 + node-graph/libraries/vector-types/src/attr.rs | 31 +++ node-graph/libraries/vector-types/src/lib.rs | 1 + node-graph/node-macro/src/attrs.rs | 113 +++++++++ node-graph/node-macro/src/lib.rs | 8 + node-graph/nodes/text/src/attr.rs | 23 ++ node-graph/nodes/text/src/lib.rs | 1 + 11 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 node-graph/libraries/core-types/src/attr.rs create mode 100644 node-graph/libraries/graphic-types/src/attr.rs create mode 100644 node-graph/libraries/vector-types/src/attr.rs create mode 100644 node-graph/node-macro/src/attrs.rs create mode 100644 node-graph/nodes/text/src/attr.rs diff --git a/node-graph/libraries/core-types/src/attr.rs b/node-graph/libraries/core-types/src/attr.rs new file mode 100644 index 0000000000..e083c16cc7 --- /dev/null +++ b/node-graph/libraries/core-types/src/attr.rs @@ -0,0 +1,70 @@ +//! Typed attribute keys. +//! +//! Each key is a zero-sized marker implementing [`Attr`], which ties the name (the string +//! stored in the attribute store) to the Rust value type. +//! +//! 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). + +use crate::Color; +use crate::list::NodeIdPath; +use glam::{DAffine2, DVec2}; +use graphene_hash::CacheHash; +use std::fmt::Debug; + +pub trait Attr { + type Value: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static; + fn name() -> &'static str; +} + +node_macro::attrs! { + /// Item's `DAffine2` transformation, composed multiplicatively through nested groups. + Transform: DAffine2, + /// 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, + /// Item's fill opacity multiplier. Like opacity but does not affect content clipped to the item. + OpacityFill: f64, + /// 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). + Start: u64, + /// Byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes). + End: u64, + /// A regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node). + Name: String, + /// A JSON value's type (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'. + Type: String, + /// Artboard's top-left corner in document coordinates. + Location: DVec2, + /// Artboard's width and height. + Dimensions: DVec2, + /// Artboard's background fill. + Background: Color, + /// Whether an artboard clips content to its bounds. + Clip: bool, + /// Text item's font size in document-space units. + FontSize: f64, + /// Text item's line height as a ratio of the font size. + LineHeight: f64, + /// Text item's extra spacing between letters in document-space units. + LetterSpacing: f64, + /// Text item's maximum line-wrap width in document-space units. + MaxWidth: Option, + /// Text item's maximum block height in document-space units, past which lines are not drawn. + MaxHeight: Option, + /// Text item's faux-italic letter tilt angle in degrees. + LetterTilt: f64, + editor { + /// Path from the root network to the layer node owning this item. + /// Used by editor tools to route clicks/selection back to the originating layer. + LayerPath: NodeIdPath, + /// Affine mapping the unit square `[(0, 0), (1, 1)]` (top-left convention) onto the 'Text' + /// node's text frame in this item's local space. Each item carries the frame relative to its own + /// glyph origin so it survives `Index Elements` filtering. The Text tool reads this to position + /// its drag cage. Stored as an affine to allow non-axis-aligned frames in the future. + TextFrame: DAffine2, + }, +} diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 878e7f5360..32b35976ff 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -1,5 +1,6 @@ extern crate log; +pub mod attr; pub mod bounds; pub mod consts; pub mod context; @@ -16,6 +17,7 @@ pub mod uuid; pub mod value; pub use crate as core_types; +pub use attr::Attr; pub use blending::*; pub use color::Color; pub use context::*; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 4684721872..b4ca9e255a 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -1,3 +1,4 @@ +use crate::attr::{self, Attr}; use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::math::quad::Quad; use crate::transform::ApplyTransform; @@ -132,14 +133,24 @@ 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> { - match key { - ATTR_OPACITY | ATTR_OPACITY_FILL => Some(Box::new(1_f64)), - _ => None, + 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`]). fn pad_with_implicit_default(key: &str, attribute: &mut Box, count: usize) { match implicit_default_value(key) { @@ -500,6 +511,11 @@ impl ListDyn { .iter() .find_map(|(k, attribute)| if k == key { attribute.get_any(index)?.downcast_ref::() } else { None }) } + + /// Returns a reference to the value of the typed attribute at the given item index, if present. + pub fn attr(&self, index: usize) -> Option<&A::Value> { + self.attribute(A::name(), index) + } } impl From> for ListDyn { @@ -622,6 +638,11 @@ impl ItemAttributeValues { /// Gets a mutable reference to the value, inserting a default if it doesn't exist or has the wrong type. pub fn get_or_insert_default_mut(&mut self, key: &str) -> &mut T { + self.get_or_insert_with_mut(key, T::default) + } + + /// Gets a mutable reference to the value, inserting the provided default if it doesn't exist or has the wrong type. + pub fn get_or_insert_with_mut(&mut self, key: &str, default: impl FnOnce() -> T) -> &mut T { let needs_insert = match self.0.iter().position(|(existing_key, _)| existing_key == key) { Some(index) => { if (*self.0[index].1).as_any().downcast_ref::().is_some() { @@ -635,7 +656,7 @@ impl ItemAttributeValues { }; if needs_insert { - self.0.push((key.to_string(), Box::new(T::default()))); + self.0.push((key.to_string(), Box::new(default()))); } self.get_mut::(key).expect("Attribute was just ensured to exist with correct type") @@ -700,6 +721,35 @@ impl ItemAttributeValues { self.0.push((key.to_string(), value)); } } + + // ================== + // Typed key variants + // ================== + + /// Gets a reference to the value of the typed attribute, if present. + pub fn attr(&self) -> Option<&A::Value> { + self.get(A::name()) + } + + /// Gets a mutable reference to the value of the typed attribute, if present. + pub fn attr_mut(&mut self) -> Option<&mut A::Value> { + self.get_mut(A::name()) + } + + /// 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::) + } + + /// Inserts the typed attribute's value, replacing any existing entry. + pub fn set_attr(&mut self, value: A::Value) { + self.insert(A::name(), value); + } + + /// Removes and returns the value of the typed attribute, if present. + pub fn remove_attr(&mut self) -> Option { + self.remove(A::name()) + } } // ========== @@ -1125,6 +1175,68 @@ impl List { (element.as_mut_slice(), &mut attribute.0) } + // ================== + // Typed key variants + // ================== + + /// Returns a shared reference to the value of the typed attribute at the given item index, if present. + pub fn attr(&self, index: usize) -> Option<&A::Value> { + self.attribute(A::name(), index) + } + + /// 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::) + } + + /// Returns a clone of the value of the typed attribute at the given item index, or the provided default if absent. + pub fn attr_cloned_or(&self, index: usize, default: A::Value) -> A::Value { + self.attr::(index).cloned().unwrap_or(default) + } + + /// Sets the value of the typed attribute at the given item index, creating the attribute with defaults if it doesn't exist. + pub fn set_attr(&mut self, index: usize, value: A::Value) { + self.set_attribute(A::name(), index, value); + } + + /// Removes the entire typed attribute, if present. + pub fn remove_attr(&mut self) { + self.remove_attribute(A::name()); + } + + /// Runs the given closure on a mutable reference to the value of the typed attribute at the given item index, + /// creating the attribute with defaults if it doesn't exist, and returns the closure's result. + pub fn with_attr_mut_or_default R>(&mut self, index: usize, f: F) -> R { + self.with_attribute_mut_or_default(A::name(), index, f) + } + + /// Returns an iterator over shared references to the values of the typed attribute, or `None` if it doesn't exist. + pub fn iter_attr_values(&self) -> Option> { + self.iter_attribute_values(A::name()) + } + + /// Returns an iterator over mutable references to the values of the typed attribute, or `None` if it doesn't exist. + pub fn iter_attr_values_mut(&mut self) -> Option> { + self.iter_attribute_values_mut(A::name()) + } + + /// Returns an iterator that yields cloned values of the typed attribute, falling back to the key's default value for each item if the attribute is missing. + 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())) + } + + /// Returns a mutable iterator over the typed attribute, creating the attribute with defaults if it doesn't exist. + pub fn iter_attr_values_mut_or_default(&mut self) -> std::slice::IterMut<'_, A::Value> { + self.iter_attribute_values_mut_or_default(A::name()) + } + + /// Returns disjoint mutable references to the element slice and the typed attribute's slice, creating the attribute with defaults if it doesn't exist. + pub fn element_and_attr_slices_mut(&mut self) -> (&mut [T], &mut [A::Value]) { + self.element_and_attribute_slices_mut(A::name()) + } + // ================== // Item-level cloning // ================== @@ -1394,6 +1506,56 @@ impl Item { pub fn remove_attribute(&mut self, key: &str) -> Option { self.attributes.remove(key) } + + // ================== + // Typed key variants + // ================== + + /// Returns a reference to the value of the typed attribute, if present. + pub fn attr(&self) -> Option<&A::Value> { + self.attributes.attr::() + } + + /// Returns a reference to the value of the typed attribute, or the provided default if absent. + pub fn attr_or<'a, A: Attr>(&'a self, default: &'a A::Value) -> &'a A::Value { + self.attr::().unwrap_or(default) + } + + /// Returns a clone of the value of the typed attribute, or the provided default if absent. + pub fn attr_cloned_or(&self, default: A::Value) -> A::Value { + self.attr::().cloned().unwrap_or(default) + } + + /// 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::) + } + + /// Returns a mutable reference to the value of the typed attribute, if present. + pub fn attr_mut(&mut self) -> Option<&mut A::Value> { + self.attributes.attr_mut::() + } + + /// Returns 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.attributes.attr_mut_or_insert_default::() + } + + /// Sets the value of the typed attribute, replacing any existing entry. + pub fn set_attr(&mut self, value: A::Value) { + self.attributes.set_attr::(value); + } + + /// Sets the value of the typed attribute and returns the item, enabling builder-style chaining. + pub fn with_attr(mut self, value: A::Value) -> Self { + self.set_attr::(value); + self + } + + /// Removes and returns the value of the typed attribute, if present. + pub fn remove_attr(&mut self) -> Option { + self.attributes.remove_attr::() + } } impl From for Item { @@ -1501,4 +1663,53 @@ mod tests { other.push(Item::new_from_element(()).with_attribute(ATTR_START, 5_u64)); assert_eq!(other.attribute_cloned_or_default::(ATTR_START, 0), 0); } + + // The typed keys must resolve to the same names as the string constants, and the typed + // and string-keyed accessors must hit the same storage. + #[test] + fn typed_attribute_keys() { + use crate::attr; + + assert_eq!(attr::Transform::name(), ATTR_TRANSFORM); + assert_eq!(attr::BlendMode::name(), ATTR_BLEND_MODE); + assert_eq!(attr::Opacity::name(), ATTR_OPACITY); + assert_eq!(attr::OpacityFill::name(), ATTR_OPACITY_FILL); + assert_eq!(attr::ClippingMask::name(), ATTR_CLIPPING_MASK); + assert_eq!(attr::editor::LayerPath::name(), ATTR_EDITOR_LAYER_PATH); + assert_eq!(attr::editor::TextFrame::name(), ATTR_EDITOR_TEXT_FRAME); + assert_eq!(attr::Start::name(), ATTR_START); + assert_eq!(attr::End::name(), ATTR_END); + assert_eq!(attr::Name::name(), ATTR_NAME); + assert_eq!(attr::Type::name(), ATTR_TYPE); + assert_eq!(attr::Location::name(), ATTR_LOCATION); + assert_eq!(attr::Dimensions::name(), ATTR_DIMENSIONS); + assert_eq!(attr::Background::name(), ATTR_BACKGROUND); + assert_eq!(attr::Clip::name(), ATTR_CLIP); + assert_eq!(attr::FontSize::name(), ATTR_FONT_SIZE); + assert_eq!(attr::LineHeight::name(), ATTR_LINE_HEIGHT); + assert_eq!(attr::LetterSpacing::name(), ATTR_LETTER_SPACING); + assert_eq!(attr::MaxWidth::name(), ATTR_MAX_WIDTH); + assert_eq!(attr::MaxHeight::name(), ATTR_MAX_HEIGHT); + assert_eq!(attr::LetterTilt::name(), ATTR_LETTER_TILT); + + // Typed writes are visible through string reads and vice versa + let mut item = Item::new_from_element(()); + item.set_attr::(0.5); + assert_eq!(item.attribute::(ATTR_OPACITY), Some(&0.5)); + item.set_attribute(ATTR_START, 5_u64); + assert_eq!(item.attr::(), Some(&5)); + + // A missing attribute reads as the key's declared default + let empty = Item::new_from_element(()); + assert_eq!(empty.attr_cloned_or_default::(), 1.); + assert_eq!(empty.attr_cloned_or_default::(), 0); + + // The generated implicit-default lookup drives dense-store padding + let mut list = List::<()>::new(); + list.push(Item::new_from_element(())); + list.push(Item::new_from_element(())); + list.set_attr::(1, 0.5); + assert_eq!(list.attr_cloned_or_default::(0), 1.); + assert_eq!(list.attr_cloned_or_default::(1), 0.5); + } } diff --git a/node-graph/libraries/graphic-types/src/attr.rs b/node-graph/libraries/graphic-types/src/attr.rs new file mode 100644 index 0000000000..a34edbb05c --- /dev/null +++ b/node-graph/libraries/graphic-types/src/attr.rs @@ -0,0 +1,31 @@ +//! Typed attribute keys whose value types live in this crate. See `core_types::attr` for the trait and macro. + +use crate::graphic::Graphic; +use core_types::list::List; + +node_macro::attrs! { + /// Vector graphics object's filled area paint. + Fill: List, + /// Vector graphics object's stroke paint. + Stroke: List, + editor { + /// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation, + /// Rasterize, etc.), so the editor can still surface click targets for the original child + /// layers after their content has been collapsed. + MergedLayers: List, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attr::Attr; + + // Key names are the stored document format — pinned as literals so a key rename shows up as a breaking change. + #[test] + fn key_names_are_pinned() { + assert_eq!(Fill::name(), "fill"); + assert_eq!(Stroke::name(), "stroke"); + assert_eq!(editor::MergedLayers::name(), "editor:merged_layers"); + } +} diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index d0c940d772..df19bd9492 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -1,4 +1,5 @@ pub mod artboard; +pub mod attr; pub mod graphic; // Re-export all transitive dependencies so downstream crates only need to depend on graphic-types diff --git a/node-graph/libraries/vector-types/src/attr.rs b/node-graph/libraries/vector-types/src/attr.rs new file mode 100644 index 0000000000..9e12f5e998 --- /dev/null +++ b/node-graph/libraries/vector-types/src/attr.rs @@ -0,0 +1,31 @@ +//! Typed attribute keys whose value types live in this crate. See `core_types::attr` for the trait and macro. + +use crate::gradient::GradientSpreadMethod; +use crate::vector::Vector; + +node_macro::attrs! { + /// Gradient's spread method (`Pad`, `Reflect`, or `Repeat`). + SpreadMethod: GradientSpreadMethod, + /// Gradient's type (`Linear` or `Radial`). + GradientType: crate::gradient::GradientType, + editor { + /// Vector that overrides the item's own geometry for click-target generation. + /// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable + /// by clicking anywhere within their bounds, not just the filled letterform. + ClickTarget: Vector, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attr::Attr; + + // Key names are the stored document format — pinned as literals so a key rename shows up as a breaking change. + #[test] + fn key_names_are_pinned() { + assert_eq!(SpreadMethod::name(), "spread_method"); + assert_eq!(GradientType::name(), "gradient_type"); + assert_eq!(editor::ClickTarget::name(), "editor:click_target"); + } +} diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index d66703a690..8da929e954 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate log; +pub mod attr; pub mod gradient; pub mod math; pub mod subpath; diff --git a/node-graph/node-macro/src/attrs.rs b/node-graph/node-macro/src/attrs.rs new file mode 100644 index 0000000000..5b97e429dd --- /dev/null +++ b/node-graph/node-macro/src/attrs.rs @@ -0,0 +1,113 @@ +use crate::crate_ident::CrateIdent; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::ParseStream; +use syn::{Attribute, 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). +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::>>()?; + + Ok(quote! { + #(#items)* + }) +} + +struct Entries(Vec); + +enum Entry { + Key { docs: Vec, ident: Ident, ty: Type }, + Namespace { docs: Vec, ident: Ident, entries: Vec }, +} + +impl syn::parse::Parse for Entries { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self(parse_entries(input)?)) + } +} + +fn parse_entries(input: ParseStream) -> syn::Result> { + let mut entries = Vec::new(); + while !input.is_empty() { + let docs = input.call(Attribute::parse_outer)?; + let ident: Ident = input.parse()?; + if input.peek(token::Brace) { + let content; + braced!(content in input); + entries.push(Entry::Namespace { + docs, + ident, + entries: parse_entries(&content)?, + }); + } else { + input.parse::()?; + let ty: Type = input.parse()?; + entries.push(Entry::Key { docs, ident, ty }); + } + if !input.is_empty() { + input.parse::()?; + } + } + Ok(entries) +} + +fn generate_entry(entry: &Entry, core: &TokenStream, prefix: &str) -> syn::Result { + match entry { + Entry::Key { docs, ident, ty } => { + let name = key_name(ident, prefix); + Ok(quote! { + #(#docs)* + pub struct #ident; + impl #core::attr::Attr for #ident { + type Value = #ty; + fn name() -> &'static str { + #name + } + } + }) + } + Entry::Namespace { docs, ident, entries } => { + let child_prefix = child_prefix(ident, prefix); + let items = entries.iter().map(|entry| generate_entry(entry, core, &child_prefix)).collect::>>()?; + Ok(quote! { + #(#docs)* + pub mod #ident { + use super::*; + #(#items)* + } + }) + } + } +} + +fn key_name(ident: &Ident, prefix: &str) -> String { + let snake = snake_case(&ident.to_string()); + if prefix.is_empty() { snake } else { format!("{prefix}:{snake}") } +} + +fn child_prefix(ident: &Ident, prefix: &str) -> String { + if prefix.is_empty() { ident.to_string() } else { format!("{prefix}:{ident}") } +} + +fn snake_case(name: &str) -> String { + let mut result = String::with_capacity(name.len() + 4); + for (i, c) in name.chars().enumerate() { + if c.is_uppercase() { + if i > 0 { + result.push('_'); + } + result.extend(c.to_lowercase()); + } else { + result.push(c); + } + } + result +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 35fe604a01..33ed7ec5e5 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -3,6 +3,7 @@ use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; use syn::GenericParam; +mod attrs; mod buffer_struct; mod codegen; mod crate_ident; @@ -33,6 +34,13 @@ pub fn derive_choice_type(input_item: TokenStream) -> TokenStream { derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()).into() } +/// Declares typed attribute keys implementing the `Attr` trait, plus a wire-name lookup for their +/// implicit defaults. See `core_types::attr` for the trait and syntax. +#[proc_macro] +pub fn attrs(input: TokenStream) -> TokenStream { + attrs::attrs_impl(input.into()).unwrap_or_else(|err| err.to_compile_error()).into() +} + /// Derive a struct to implement `ShaderStruct`, see that for docs. #[proc_macro_derive(BufferStruct)] pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream { diff --git a/node-graph/nodes/text/src/attr.rs b/node-graph/nodes/text/src/attr.rs new file mode 100644 index 0000000000..7d8164d58e --- /dev/null +++ b/node-graph/nodes/text/src/attr.rs @@ -0,0 +1,23 @@ +//! Typed attribute keys whose value types live in this crate. See `core_types::attr` for the trait and macro. + +use graphene_resource::Resource; + +node_macro::attrs! { + /// Text item's font, as a resource of the loaded font file. + Font: Resource, + /// Text item's horizontal alignment of lines within the block. + TextAlign: crate::TextAlign, +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attr::Attr; + + // Key names are the stored document format — pinned as literals so a key rename shows up as a breaking change. + #[test] + fn key_names_are_pinned() { + assert_eq!(Font::name(), "font"); + assert_eq!(TextAlign::name(), "text_align"); + } +} diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 6dd4bd6e57..953fec73b2 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -1,3 +1,4 @@ +pub mod attr; pub mod fallback; mod font; pub mod json;