mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add typed attribute keys: attrs! macro, Attr trait, and typed accessors
This commit is contained in:
70
node-graph/libraries/core-types/src/attr.rs
Normal file
70
node-graph/libraries/core-types/src/attr.rs
Normal file
@@ -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<f64>,
|
||||
/// Text item's maximum block height in document-space units, past which lines are not drawn.
|
||||
MaxHeight: Option<f64>,
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<T: StaticTypeSized> StaticType for Bundle<T> {
|
||||
// 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<Box<dyn AnyAttributeValue>> {
|
||||
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: Attr>() -> A::Value {
|
||||
implicit_default_value(A::name())
|
||||
.and_then(|value| value.into_any().downcast::<A::Value>().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<dyn AnyAttribute>, 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::<U>() } else { None })
|
||||
}
|
||||
|
||||
/// Returns a reference to the value of the typed attribute at the given item index, if present.
|
||||
pub fn attr<A: Attr>(&self, index: usize) -> Option<&A::Value> {
|
||||
self.attribute(A::name(), index)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<List<T>> 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<T: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>(&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<T: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>(&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::<T>().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::<T>(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<A: 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<A: Attr>(&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<A: Attr>(&mut self) -> &mut A::Value {
|
||||
self.get_or_insert_with_mut(A::name(), implicit_default::<A>)
|
||||
}
|
||||
|
||||
/// Inserts the typed attribute's value, replacing any existing entry.
|
||||
pub fn set_attr<A: 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<A: Attr>(&mut self) -> Option<A::Value> {
|
||||
self.remove(A::name())
|
||||
}
|
||||
}
|
||||
|
||||
// ==========
|
||||
@@ -1125,6 +1175,68 @@ impl<T> List<T> {
|
||||
(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<A: 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<A: Attr>(&self, index: usize) -> A::Value {
|
||||
self.attr::<A>(index).cloned().unwrap_or_else(implicit_default::<A>)
|
||||
}
|
||||
|
||||
/// 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<A: Attr>(&self, index: usize, default: A::Value) -> A::Value {
|
||||
self.attr::<A>(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<A: 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<A: 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<A: Attr, R, F: FnOnce(&mut A::Value) -> 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<A: Attr>(&self) -> Option<std::slice::Iter<'_, A::Value>> {
|
||||
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<A: Attr>(&mut self) -> Option<std::slice::IterMut<'_, A::Value>> {
|
||||
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<A: Attr>(&self) -> impl Iterator<Item = A::Value> + '_ {
|
||||
let slice = self.attributes.get_attribute_slice::<A::Value>(A::name());
|
||||
let len = self.element.len();
|
||||
(0..len).map(move |i| slice.map_or_else(implicit_default::<A>, |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<A: Attr>(&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<A: Attr>(&mut self) -> (&mut [T], &mut [A::Value]) {
|
||||
self.element_and_attribute_slices_mut(A::name())
|
||||
}
|
||||
|
||||
// ==================
|
||||
// Item-level cloning
|
||||
// ==================
|
||||
@@ -1394,6 +1506,56 @@ impl<T> Item<T> {
|
||||
pub fn remove_attribute<U: 'static>(&mut self, key: &str) -> Option<U> {
|
||||
self.attributes.remove(key)
|
||||
}
|
||||
|
||||
// ==================
|
||||
// Typed key variants
|
||||
// ==================
|
||||
|
||||
/// Returns a reference to the value of the typed attribute, if present.
|
||||
pub fn attr<A: Attr>(&self) -> Option<&A::Value> {
|
||||
self.attributes.attr::<A>()
|
||||
}
|
||||
|
||||
/// 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::<A>().unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Returns a clone of the value of the typed attribute, or the provided default if absent.
|
||||
pub fn attr_cloned_or<A: Attr>(&self, default: A::Value) -> A::Value {
|
||||
self.attr::<A>().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<A: Attr>(&self) -> A::Value {
|
||||
self.attr::<A>().cloned().unwrap_or_else(implicit_default::<A>)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the value of the typed attribute, if present.
|
||||
pub fn attr_mut<A: Attr>(&mut self) -> Option<&mut A::Value> {
|
||||
self.attributes.attr_mut::<A>()
|
||||
}
|
||||
|
||||
/// 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<A: Attr>(&mut self) -> &mut A::Value {
|
||||
self.attributes.attr_mut_or_insert_default::<A>()
|
||||
}
|
||||
|
||||
/// Sets the value of the typed attribute, replacing any existing entry.
|
||||
pub fn set_attr<A: Attr>(&mut self, value: A::Value) {
|
||||
self.attributes.set_attr::<A>(value);
|
||||
}
|
||||
|
||||
/// Sets the value of the typed attribute and returns the item, enabling builder-style chaining.
|
||||
pub fn with_attr<A: Attr>(mut self, value: A::Value) -> Self {
|
||||
self.set_attr::<A>(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes and returns the value of the typed attribute, if present.
|
||||
pub fn remove_attr<A: Attr>(&mut self) -> Option<A::Value> {
|
||||
self.attributes.remove_attr::<A>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Item<T> {
|
||||
@@ -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::<u64>(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::<attr::Opacity>(0.5);
|
||||
assert_eq!(item.attribute::<f64>(ATTR_OPACITY), Some(&0.5));
|
||||
item.set_attribute(ATTR_START, 5_u64);
|
||||
assert_eq!(item.attr::<attr::Start>(), 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::<attr::Opacity>(), 1.);
|
||||
assert_eq!(empty.attr_cloned_or_default::<attr::Start>(), 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::<attr::Opacity>(1, 0.5);
|
||||
assert_eq!(list.attr_cloned_or_default::<attr::Opacity>(0), 1.);
|
||||
assert_eq!(list.attr_cloned_or_default::<attr::Opacity>(1), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
31
node-graph/libraries/graphic-types/src/attr.rs
Normal file
31
node-graph/libraries/graphic-types/src/attr.rs
Normal file
@@ -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<Graphic>,
|
||||
/// Vector graphics object's stroke paint.
|
||||
Stroke: List<Graphic>,
|
||||
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<Graphic>,
|
||||
},
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
31
node-graph/libraries/vector-types/src/attr.rs
Normal file
31
node-graph/libraries/vector-types/src/attr.rs
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod attr;
|
||||
pub mod gradient;
|
||||
pub mod math;
|
||||
pub mod subpath;
|
||||
|
||||
113
node-graph/node-macro/src/attrs.rs
Normal file
113
node-graph/node-macro/src/attrs.rs
Normal file
@@ -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<TokenStream> {
|
||||
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::<syn::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#(#items)*
|
||||
})
|
||||
}
|
||||
|
||||
struct Entries(Vec<Entry>);
|
||||
|
||||
enum Entry {
|
||||
Key { docs: Vec<Attribute>, ident: Ident, ty: Type },
|
||||
Namespace { docs: Vec<Attribute>, ident: Ident, entries: Vec<Entry> },
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for Entries {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self(parse_entries(input)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_entries(input: ParseStream) -> syn::Result<Vec<Entry>> {
|
||||
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::<Token![:]>()?;
|
||||
let ty: Type = input.parse()?;
|
||||
entries.push(Entry::Key { docs, ident, ty });
|
||||
}
|
||||
if !input.is_empty() {
|
||||
input.parse::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn generate_entry(entry: &Entry, core: &TokenStream, prefix: &str) -> syn::Result<TokenStream> {
|
||||
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::<syn::Result<Vec<_>>>()?;
|
||||
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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
23
node-graph/nodes/text/src/attr.rs
Normal file
23
node-graph/nodes/text/src/attr.rs
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod attr;
|
||||
pub mod fallback;
|
||||
mod font;
|
||||
pub mod json;
|
||||
|
||||
Reference in New Issue
Block a user