Auto-generate enum type widget boilerplate for radio buttons and dropdown menus (#2589)

* First draft of factoring out the dropdown boilerplate

* Add proc macro for enum boilerplate

* Detect whether to say `crate` or the name

* Clean up the input and naming of the enum macro

* Rename a file

* Do the rename of code too

* Use the attribute-driven selection of radio vs dropdown

* Add a metadata struct and tooltips

* Move the new traits to a better place.

* Use ChoiceType, part 1

* Use ChoiceType, part 2

* Introduce a builder API for choice widgets

* Start using the new new API

* DomainWarpType should be a dropdown still

* Handle the case where a node property can never have a socket

* Rustfmt

* Code review

* Update stable node IDs in test

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
kythyria 2025-05-01 12:14:26 +01:00 committed by GitHub
parent 9303953cf8
commit 9ef9b205d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 713 additions and 990 deletions

1
Cargo.lock generated
View file

@ -2403,6 +2403,7 @@ dependencies = [
name = "graphite-proc-macros" name = "graphite-proc-macros"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"convert_case 0.7.1",
"graphite-editor", "graphite-editor",
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,3 +1,4 @@
use super::node_properties::choice::enum_choice;
use super::node_properties::{self, ParameterWidgetsInfo}; use super::node_properties::{self, ParameterWidgetsInfo};
use super::utility_types::FrontendNodeType; use super::utility_types::FrontendNodeType;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
@ -3212,7 +3213,9 @@ fn static_input_properties() -> InputProperties {
"noise_properties_noise_type".to_string(), "noise_properties_noise_type".to_string(),
Box::new(|node_id, index, context| { Box::new(|node_id, index, context| {
let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?;
let noise_type_row = node_properties::noise_type_widget(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true)); let noise_type_row = enum_choice::<NoiseType>()
.for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true))
.property_row();
Ok(vec![noise_type_row, LayoutGroup::Row { widgets: Vec::new() }]) Ok(vec![noise_type_row, LayoutGroup::Row { widgets: Vec::new() }])
}), }),
); );
@ -3221,7 +3224,10 @@ fn static_input_properties() -> InputProperties {
Box::new(|node_id, index, context| { Box::new(|node_id, index, context| {
let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?;
let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?;
let domain_warp_type = node_properties::domain_warp_type_widget(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), !coherent_noise_active); let domain_warp_type = enum_choice::<DomainWarpType>()
.for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true))
.disabled(!coherent_noise_active)
.property_row();
Ok(vec![domain_warp_type]) Ok(vec![domain_warp_type])
}), }),
); );
@ -3242,7 +3248,10 @@ fn static_input_properties() -> InputProperties {
Box::new(|node_id, index, context| { Box::new(|node_id, index, context| {
let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?;
let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; let (_, coherent_noise_active, _, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?;
let fractal_type_row = node_properties::fractal_type_widget(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), !coherent_noise_active); let fractal_type_row = enum_choice::<FractalType>()
.for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true))
.disabled(!coherent_noise_active)
.property_row();
Ok(vec![fractal_type_row]) Ok(vec![fractal_type_row])
}), }),
); );
@ -3333,10 +3342,10 @@ fn static_input_properties() -> InputProperties {
Box::new(|node_id, index, context| { Box::new(|node_id, index, context| {
let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?;
let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?;
let cellular_distance_function_row = node_properties::cellular_distance_function_widget( let cellular_distance_function_row = enum_choice::<CellularDistanceFunction>()
ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true))
!coherent_noise_active || !cellular_noise_active, .disabled(!coherent_noise_active || !cellular_noise_active)
); .property_row();
Ok(vec![cellular_distance_function_row]) Ok(vec![cellular_distance_function_row])
}), }),
); );
@ -3345,10 +3354,10 @@ fn static_input_properties() -> InputProperties {
Box::new(|node_id, index, context| { Box::new(|node_id, index, context| {
let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?; let (document_node, input_name, input_description) = node_properties::query_node_and_input_info(node_id, index, context)?;
let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?; let (_, coherent_noise_active, cellular_noise_active, _, _, _) = node_properties::query_noise_pattern_state(node_id, context)?;
let cellular_return_type = node_properties::cellular_return_type_widget( let cellular_return_type = enum_choice::<CellularReturnType>()
ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true), .for_socket(ParameterWidgetsInfo::new(document_node, node_id, index, input_name, input_description, true))
!coherent_noise_active || !cellular_noise_active, .disabled(!coherent_noise_active || !cellular_noise_active)
); .property_row();
Ok(vec![cellular_return_type]) Ok(vec![cellular_return_type])
}), }),
); );

View file

@ -414,16 +414,15 @@ impl LayoutHolder for MenuBarMessageHandler {
action: MenuBarEntry::no_action(), action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers, disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![{ children: MenuBarEntryChildren(vec![{
let operations = BooleanOperation::list(); let list = <BooleanOperation as graphene_core::registry::ChoiceTypeStatic>::list();
let icons = BooleanOperation::icons(); list.into_iter()
operations .map(|i| i.into_iter())
.into_iter() .flatten()
.zip(icons) .map(move |(operation, info)| MenuBarEntry {
.map(move |(operation, icon)| MenuBarEntry { label: info.label.to_string(),
label: operation.to_string(), icon: info.icon.as_ref().map(|i| i.to_string()),
icon: Some(icon.into()),
action: MenuBarEntry::create_action(move |_| { action: MenuBarEntry::create_action(move |_| {
let group_folder_type = GroupFolderType::BooleanOperation(operation); let group_folder_type = GroupFolderType::BooleanOperation(*operation);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into() DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
}), }),
disabled: no_active_document || !has_selected_layers, disabled: no_active_document || !has_selected_layers,

View file

@ -90,11 +90,11 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Gradien
impl LayoutHolder for GradientTool { impl LayoutHolder for GradientTool {
fn layout(&self) -> Layout { fn layout(&self) -> Layout {
let gradient_type = RadioInput::new(vec![ let gradient_type = RadioInput::new(vec![
RadioEntryData::new("linear") RadioEntryData::new("Linear")
.label("Linear") .label("Linear")
.tooltip("Linear gradient") .tooltip("Linear gradient")
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()), .on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()),
RadioEntryData::new("radial") RadioEntryData::new("Radial")
.label("Radial") .label("Radial")
.tooltip("Radial gradient") .tooltip("Radial gradient")
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()), .on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()),
@ -611,7 +611,7 @@ mod test_gradient {
let (gradient, transform) = get_gradient(&mut editor).await; let (gradient, transform) = get_gradient(&mut editor).await;
// Gradient goes from secondary colour to primary colour // Gradient goes from secondary color to primary color
let stops = gradient.stops.iter().map(|stop| (stop.0, stop.1.to_rgba8_srgb())).collect::<Vec<_>>(); let stops = gradient.stops.iter().map(|stop| (stop.0, stop.1.to_rgba8_srgb())).collect::<Vec<_>>();
assert_eq!(stops, vec![(0., Color::BLUE.to_rgba8_srgb()), (1., Color::GREEN.to_rgba8_srgb())]); assert_eq!(stops, vec![(0., Color::BLUE.to_rgba8_srgb()), (1., Color::GREEN.to_rgba8_srgb())]);
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10)); assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));

View file

@ -181,14 +181,18 @@ impl SelectTool {
} }
fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetHolder> + use<> { fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetHolder> + use<> {
let operations = BooleanOperation::list(); let list = <BooleanOperation as graphene_core::registry::ChoiceTypeStatic>::list();
let icons = BooleanOperation::icons(); list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, info)| {
operations.into_iter().zip(icons).map(move |(operation, icon)| { let mut tooltip = info.label.to_string();
IconButton::new(icon, 24) if let Some(doc) = info.docstring.as_deref() {
.tooltip(operation.to_string()) tooltip.push_str("\n\n");
tooltip.push_str(doc);
}
IconButton::new(info.icon.as_deref().unwrap(), 24)
.tooltip(tooltip)
.disabled(selected_count == 0) .disabled(selected_count == 0)
.on_update(move |_| { .on_update(move |_| {
let group_folder_type = GroupFolderType::BooleanOperation(operation); let group_folder_type = GroupFolderType::BooleanOperation(*operation);
DocumentMessage::GroupSelectedLayers { group_folder_type }.into() DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
}) })
.widget_holder() .widget_holder()

View file

@ -2,9 +2,10 @@ use crate::{Ctx, ExtractAnimationTime, ExtractTime};
const DAY: f64 = 1000. * 3600. * 24.; const DAY: f64 = 1000. * 3600. * 24.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RealTimeMode { pub enum RealTimeMode {
#[label("UTC")]
Utc, Utc,
Year, Year,
Hour, Hour,
@ -13,18 +14,6 @@ pub enum RealTimeMode {
Second, Second,
Millisecond, Millisecond,
} }
impl core::fmt::Display for RealTimeMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RealTimeMode::Utc => write!(f, "UTC"),
RealTimeMode::Year => write!(f, "Year"),
RealTimeMode::Hour => write!(f, "Hour"),
RealTimeMode::Minute => write!(f, "Minute"),
RealTimeMode::Second => write!(f, "Second"),
RealTimeMode::Millisecond => write!(f, "Millisecond"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimationTimeMode { pub enum AnimationTimeMode {

View file

@ -533,22 +533,16 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
} }
} }
/// The X or Y component of a vector2.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum XY { pub enum XY {
#[default] #[default]
X, X,
Y, Y,
} }
impl core::fmt::Display for XY {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
XY::X => write!(f, "X"),
XY::Y => write!(f, "Y"),
}
}
}
// TODO: Rename to "Passthrough" // TODO: Rename to "Passthrough"
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes. /// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.

View file

@ -34,9 +34,11 @@ use spirv_std::num_traits::float::Float;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum LuminanceCalculation { pub enum LuminanceCalculation {
#[default] #[default]
#[label("sRGB")]
SRGB, SRGB,
Perceptual, Perceptual,
AverageChannels, AverageChannels,
@ -44,30 +46,6 @@ pub enum LuminanceCalculation {
MaximumChannels, MaximumChannels,
} }
impl LuminanceCalculation {
pub fn list() -> [LuminanceCalculation; 5] {
[
LuminanceCalculation::SRGB,
LuminanceCalculation::Perceptual,
LuminanceCalculation::AverageChannels,
LuminanceCalculation::MinimumChannels,
LuminanceCalculation::MaximumChannels,
]
}
}
impl core::fmt::Display for LuminanceCalculation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
LuminanceCalculation::SRGB => write!(f, "sRGB"),
LuminanceCalculation::Perceptual => write!(f, "Perceptual"),
LuminanceCalculation::AverageChannels => write!(f, "Average Channels"),
LuminanceCalculation::MinimumChannels => write!(f, "Minimum Channels"),
LuminanceCalculation::MaximumChannels => write!(f, "Maximum Channels"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)]
@ -844,9 +822,11 @@ async fn vibrance<T: Adjust<Color>>(
image image
} }
/// Color Channel
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum RedGreenBlue { pub enum RedGreenBlue {
#[default] #[default]
Red, Red,
@ -854,19 +834,11 @@ pub enum RedGreenBlue {
Blue, Blue,
} }
impl core::fmt::Display for RedGreenBlue { /// Color Channel
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RedGreenBlue::Red => write!(f, "Red"),
RedGreenBlue::Green => write!(f, "Green"),
RedGreenBlue::Blue => write!(f, "Blue"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum RedGreenBlueAlpha { pub enum RedGreenBlueAlpha {
#[default] #[default]
Red, Red,
@ -875,24 +847,17 @@ pub enum RedGreenBlueAlpha {
Alpha, Alpha,
} }
impl core::fmt::Display for RedGreenBlueAlpha { /// Style of noise pattern
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RedGreenBlueAlpha::Red => write!(f, "Red"),
RedGreenBlueAlpha::Green => write!(f, "Green"),
RedGreenBlueAlpha::Blue => write!(f, "Blue"),
RedGreenBlueAlpha::Alpha => write!(f, "Alpha"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum NoiseType { pub enum NoiseType {
#[default] #[default]
Perlin, Perlin,
#[label("OpenSimplex2")]
OpenSimplex2, OpenSimplex2,
#[label("OpenSimplex2S")]
OpenSimplex2S, OpenSimplex2S,
Cellular, Cellular,
ValueCubic, ValueCubic,
@ -900,176 +865,71 @@ pub enum NoiseType {
WhiteNoise, WhiteNoise,
} }
impl core::fmt::Display for NoiseType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
NoiseType::Perlin => write!(f, "Perlin"),
NoiseType::OpenSimplex2 => write!(f, "OpenSimplex2"),
NoiseType::OpenSimplex2S => write!(f, "OpenSimplex2S"),
NoiseType::Cellular => write!(f, "Cellular"),
NoiseType::ValueCubic => write!(f, "Value Cubic"),
NoiseType::Value => write!(f, "Value"),
NoiseType::WhiteNoise => write!(f, "White Noise"),
}
}
}
impl NoiseType {
pub fn list() -> &'static [NoiseType; 7] {
&[
NoiseType::Perlin,
NoiseType::OpenSimplex2,
NoiseType::OpenSimplex2S,
NoiseType::Cellular,
NoiseType::ValueCubic,
NoiseType::Value,
NoiseType::WhiteNoise,
]
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
/// Style of layered levels of the noise pattern
pub enum FractalType { pub enum FractalType {
#[default] #[default]
None, None,
#[label("Fractional Brownian Motion")]
FBm, FBm,
Ridged, Ridged,
PingPong, PingPong,
#[label("Progressive (Domain Warp Only)")]
DomainWarpProgressive, DomainWarpProgressive,
#[label("Independent (Domain Warp Only)")]
DomainWarpIndependent, DomainWarpIndependent,
} }
impl core::fmt::Display for FractalType { /// Distance function used by the cellular noise
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FractalType::None => write!(f, "None"),
FractalType::FBm => write!(f, "Fractional Brownian Motion"),
FractalType::Ridged => write!(f, "Ridged"),
FractalType::PingPong => write!(f, "Ping Pong"),
FractalType::DomainWarpProgressive => write!(f, "Progressive (Domain Warp Only)"),
FractalType::DomainWarpIndependent => write!(f, "Independent (Domain Warp Only)"),
}
}
}
impl FractalType {
pub fn list() -> &'static [FractalType; 6] {
&[
FractalType::None,
FractalType::FBm,
FractalType::Ridged,
FractalType::PingPong,
FractalType::DomainWarpProgressive,
FractalType::DomainWarpIndependent,
]
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
pub enum CellularDistanceFunction { pub enum CellularDistanceFunction {
#[default] #[default]
Euclidean, Euclidean,
#[label("Euclidean Squared (Faster)")]
EuclideanSq, EuclideanSq,
Manhattan, Manhattan,
Hybrid, Hybrid,
} }
impl core::fmt::Display for CellularDistanceFunction {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CellularDistanceFunction::Euclidean => write!(f, "Euclidean"),
CellularDistanceFunction::EuclideanSq => write!(f, "Euclidean Squared (Faster)"),
CellularDistanceFunction::Manhattan => write!(f, "Manhattan"),
CellularDistanceFunction::Hybrid => write!(f, "Hybrid"),
}
}
}
impl CellularDistanceFunction {
pub fn list() -> &'static [CellularDistanceFunction; 4] {
&[
CellularDistanceFunction::Euclidean,
CellularDistanceFunction::EuclideanSq,
CellularDistanceFunction::Manhattan,
CellularDistanceFunction::Hybrid,
]
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
pub enum CellularReturnType { pub enum CellularReturnType {
CellValue, CellValue,
#[default] #[default]
#[label("Nearest (F1)")]
Nearest, Nearest,
#[label("Next Nearest (F2)")]
NextNearest, NextNearest,
#[label("Average (F1 / 2 + F2 / 2)")]
Average, Average,
#[label("Difference (F2 - F1)")]
Difference, Difference,
#[label("Product (F2 * F1 / 2)")]
Product, Product,
#[label("Division (F1 / F2)")]
Division, Division,
} }
impl core::fmt::Display for CellularReturnType { /// Type of domain warp
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CellularReturnType::CellValue => write!(f, "Cell Value"),
CellularReturnType::Nearest => write!(f, "Nearest (F1)"),
CellularReturnType::NextNearest => write!(f, "Next Nearest (F2)"),
CellularReturnType::Average => write!(f, "Average (F1 / 2 + F2 / 2)"),
CellularReturnType::Difference => write!(f, "Difference (F2 - F1)"),
CellularReturnType::Product => write!(f, "Product (F2 * F1 / 2)"),
CellularReturnType::Division => write!(f, "Division (F1 / F2)"),
}
}
}
impl CellularReturnType {
pub fn list() -> &'static [CellularReturnType; 7] {
&[
CellularReturnType::CellValue,
CellularReturnType::Nearest,
CellularReturnType::NextNearest,
CellularReturnType::Average,
CellularReturnType::Difference,
CellularReturnType::Product,
CellularReturnType::Division,
]
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum DomainWarpType { pub enum DomainWarpType {
#[default] #[default]
None, None,
#[label("OpenSimplex2")]
OpenSimplex2, OpenSimplex2,
#[label("OpenSimplex2 Reduced")]
OpenSimplex2Reduced, OpenSimplex2Reduced,
BasicGrid, BasicGrid,
} }
impl core::fmt::Display for DomainWarpType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DomainWarpType::None => write!(f, "None"),
DomainWarpType::OpenSimplex2 => write!(f, "OpenSimplex2"),
DomainWarpType::OpenSimplex2Reduced => write!(f, "OpenSimplex2 Reduced"),
DomainWarpType::BasicGrid => write!(f, "Basic Grid"),
}
}
}
impl DomainWarpType {
pub fn list() -> &'static [DomainWarpType; 4] {
&[DomainWarpType::None, DomainWarpType::OpenSimplex2, DomainWarpType::OpenSimplex2Reduced, DomainWarpType::BasicGrid]
}
}
// Aims for interoperable compatibility with: // Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr
@ -1169,26 +1029,18 @@ async fn channel_mixer<T: Adjust<Color>>(
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum RelativeAbsolute { pub enum RelativeAbsolute {
#[default] #[default]
Relative, Relative,
Absolute, Absolute,
} }
impl core::fmt::Display for RelativeAbsolute {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RelativeAbsolute::Relative => write!(f, "Relative"),
RelativeAbsolute::Absolute => write!(f, "Absolute"),
}
}
}
#[repr(C)] #[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "std", derive(specta::Type))] #[cfg_attr(feature = "std", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)]
pub enum SelectiveColorChoice { pub enum SelectiveColorChoice {
#[default] #[default]
Reds, Reds,
@ -1197,27 +1049,13 @@ pub enum SelectiveColorChoice {
Cyans, Cyans,
Blues, Blues,
Magentas, Magentas,
#[menu_separator]
Whites, Whites,
Neutrals, Neutrals,
Blacks, Blacks,
} }
impl core::fmt::Display for SelectiveColorChoice {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SelectiveColorChoice::Reds => write!(f, "Reds"),
SelectiveColorChoice::Yellows => write!(f, "Yellows"),
SelectiveColorChoice::Greens => write!(f, "Greens"),
SelectiveColorChoice::Cyans => write!(f, "Cyans"),
SelectiveColorChoice::Blues => write!(f, "Blues"),
SelectiveColorChoice::Magentas => write!(f, "Magentas"),
SelectiveColorChoice::Whites => write!(f, "Whites"),
SelectiveColorChoice::Neutrals => write!(f, "Neutrals"),
SelectiveColorChoice::Blacks => write!(f, "Blacks"),
}
}
}
// Aims for interoperable compatibility with: // Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27selc%27%20%3D%20Selective%20color // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27selc%27%20%3D%20Selective%20color
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=from%20%2D100...100.%20.-,Selective%20Color,-Selective%20Color%20settings // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=from%20%2D100...100.%20.-,Selective%20Color,-Selective%20Color%20settings

View file

@ -1,5 +1,6 @@
use crate::{Node, NodeIO, NodeIOTypes, Type, WasmNotSend}; use crate::{Node, NodeIO, NodeIOTypes, Type, WasmNotSend};
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
@ -53,6 +54,33 @@ pub struct FieldMetadata {
pub number_mode_range: Option<(f64, f64)>, pub number_mode_range: Option<(f64, f64)>,
} }
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
}
pub enum ChoiceWidgetHint {
Dropdown,
RadioButtons,
}
/// Translation struct between macro and definition.
#[derive(Clone, Debug)]
pub struct VariantMetadata {
/// Name as declared in source code.
pub name: Cow<'static, str>,
/// Name to be displayed in UI.
pub label: Cow<'static, str>,
/// User-facing documentation text.
pub docstring: Option<Cow<'static, str>>,
/// Name of icon to display in radio buttons and such.
pub icon: Option<Cow<'static, str>>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum RegistryWidgetOverride { pub enum RegistryWidgetOverride {
None, None,

View file

@ -3,7 +3,8 @@ use glam::DVec2;
use kurbo::Point; use kurbo::Point;
/// Represents different ways of calculating the centroid. /// Represents different ways of calculating the centroid.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum CentroidType { pub enum CentroidType {
/// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material. /// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material.
#[default] #[default]
@ -12,41 +13,28 @@ pub enum CentroidType {
Length, Length,
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum BooleanOperation { pub enum BooleanOperation {
#[default] #[default]
#[icon("BooleanUnion")]
Union, Union,
#[icon("BooleanSubtractFront")]
SubtractFront, SubtractFront,
#[icon("BooleanSubtractBack")]
SubtractBack, SubtractBack,
#[icon("BooleanIntersect")]
Intersect, Intersect,
#[icon("BooleanDifference")]
Difference, Difference,
} }
impl BooleanOperation { pub trait AsU32 {
pub fn list() -> [BooleanOperation; 5] { fn as_u32(&self) -> u32;
[
BooleanOperation::Union,
BooleanOperation::SubtractFront,
BooleanOperation::SubtractBack,
BooleanOperation::Intersect,
BooleanOperation::Difference,
]
}
pub fn icons() -> [&'static str; 5] {
["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference"]
}
} }
impl AsU32 for u32 {
impl core::fmt::Display for BooleanOperation { fn as_u32(&self) -> u32 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { *self
match self {
BooleanOperation::Union => write!(f, "Union"),
BooleanOperation::SubtractFront => write!(f, "Subtract Front"),
BooleanOperation::SubtractBack => write!(f, "Subtract Back"),
BooleanOperation::Intersect => write!(f, "Intersect"),
BooleanOperation::Difference => write!(f, "Difference"),
}
} }
} }
@ -88,7 +76,8 @@ impl AsI64 for f64 {
} }
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GridType { pub enum GridType {
#[default] #[default]
Rectangular, Rectangular,
@ -96,7 +85,8 @@ pub enum GridType {
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum ArcType { pub enum ArcType {
#[default] #[default]
Open, Open,

View file

@ -5,9 +5,10 @@ use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::renderer::format_transform_matrix; use crate::renderer::format_transform_matrix;
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use std::fmt::{self, Display, Write}; use std::fmt::Write;
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientType { pub enum GradientType {
#[default] #[default]
Linear, Linear,
@ -462,7 +463,8 @@ impl From<Fill> for FillChoice {
/// Enum describing the type of [Fill]. /// Enum describing the type of [Fill].
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum FillType { pub enum FillType {
#[default] #[default]
Solid, Solid,
@ -471,7 +473,8 @@ pub enum FillType {
/// The stroke (outline) style of an SVG element. /// The stroke (outline) style of an SVG element.
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum LineCap { pub enum LineCap {
#[default] #[default]
Butt, Butt,
@ -479,18 +482,19 @@ pub enum LineCap {
Square, Square,
} }
impl Display for LineCap { impl LineCap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn svg_name(&self) -> &'static str {
match self { match self {
LineCap::Butt => write!(f, "butt"), LineCap::Butt => "butt",
LineCap::Round => write!(f, "round"), LineCap::Round => "round",
LineCap::Square => write!(f, "square"), LineCap::Square => "square",
} }
} }
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum LineJoin { pub enum LineJoin {
#[default] #[default]
Miter, Miter,
@ -498,12 +502,12 @@ pub enum LineJoin {
Round, Round,
} }
impl Display for LineJoin { impl LineJoin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn svg_name(&self) -> &'static str {
match self { match self {
LineJoin::Bevel => write!(f, "bevel"), LineJoin::Bevel => "bevel",
LineJoin::Miter => write!(f, "miter"), LineJoin::Miter => "miter",
LineJoin::Round => write!(f, "round"), LineJoin::Round => "round",
} }
} }
} }
@ -652,10 +656,10 @@ impl Stroke {
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset); let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
} }
if let Some(line_cap) = line_cap { if let Some(line_cap) = line_cap {
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap); let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap.svg_name());
} }
if let Some(line_join) = line_join { if let Some(line_join) = line_join {
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join); let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join.svg_name());
} }
if let Some(line_join_miter_limit) = line_join_miter_limit { if let Some(line_join_miter_limit) = line_join_miter_limit {
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit); let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);

View file

@ -157,56 +157,75 @@ macro_rules! tagged_value {
} }
tagged_value! { tagged_value! {
// TODO: Eventually remove this migration document upgrade code // ===============
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // PRIMITIVE TYPES
ImageFrame(graphene_core::raster::image::ImageFrameTable<Color>), // ===============
// TODO: Eventually remove this migration document upgrade code #[cfg_attr(feature = "serde", serde(alias = "F32"))] // TODO: Eventually remove this alias document upgrade code
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::vector::migrate_vector_data"))] F64(f64),
VectorData(graphene_core::vector::VectorDataTable),
// TODO: Eventually remove this migration document upgrade code
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_graphic_group"))]
GraphicGroup(graphene_core::GraphicGroupTable),
// TODO: Eventually remove this migration document upgrade code
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_artboard_group"))]
ArtboardGroup(graphene_core::ArtboardGroupTable),
GraphicElement(graphene_core::GraphicElement),
Artboard(graphene_core::Artboard),
String(String),
U32(u32), U32(u32),
U64(u64), U64(u64),
// TODO: Eventually remove this alias document upgrade code
#[cfg_attr(feature = "serde", serde(alias = "F32"))]
F64(f64),
OptionalF64(Option<f64>),
Bool(bool), Bool(bool),
String(String),
UVec2(UVec2), UVec2(UVec2),
IVec2(IVec2), IVec2(IVec2),
DVec2(DVec2), DVec2(DVec2),
OptionalDVec2(Option<DVec2>),
DAffine2(DAffine2), DAffine2(DAffine2),
OptionalF64(Option<f64>),
OptionalDVec2(Option<DVec2>),
// ==========================
// PRIMITIVE COLLECTION TYPES
// ==========================
#[cfg_attr(feature = "serde", serde(alias = "VecF32"))] // TODO: Eventually remove this alias document upgrade code
VecF64(Vec<f64>),
VecU64(Vec<u64>),
VecDVec2(Vec<DVec2>),
F64Array4([f64; 4]),
NodePath(Vec<NodeId>),
#[cfg_attr(feature = "serde", serde(alias = "ManipulatorGroupIds"))] // TODO: Eventually remove this alias document upgrade code
PointIds(Vec<graphene_core::vector::PointId>),
// ====================
// GRAPHICAL DATA TYPES
// ====================
GraphicElement(graphene_core::GraphicElement),
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::vector::migrate_vector_data"))] // TODO: Eventually remove this migration document upgrade code
VectorData(graphene_core::vector::VectorDataTable),
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // TODO: Eventually remove this migration document upgrade code
ImageFrame(graphene_core::raster::image::ImageFrameTable<Color>),
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_graphic_group"))] // TODO: Eventually remove this migration document upgrade code
GraphicGroup(graphene_core::GraphicGroupTable),
#[cfg_attr(all(feature = "serde", target_arch = "wasm32"), serde(deserialize_with = "graphene_core::migrate_artboard_group"))] // TODO: Eventually remove this migration document upgrade code
ArtboardGroup(graphene_core::ArtboardGroupTable),
// ============
// STRUCT TYPES
// ============
Artboard(graphene_core::Artboard),
Image(graphene_core::raster::Image<Color>), Image(graphene_core::raster::Image<Color>),
Color(graphene_core::raster::color::Color), Color(graphene_core::raster::color::Color),
OptionalColor(Option<graphene_core::raster::color::Color>), OptionalColor(Option<graphene_core::raster::color::Color>),
Palette(Vec<Color>),
Subpaths(Vec<bezier_rs::Subpath<graphene_core::vector::PointId>>), Subpaths(Vec<bezier_rs::Subpath<graphene_core::vector::PointId>>),
BlendMode(BlendMode),
LuminanceCalculation(LuminanceCalculation),
// ImaginateCache(ImaginateCache),
// ImaginateSamplingMethod(ImaginateSamplingMethod),
// ImaginateMaskStartingFill(ImaginateMaskStartingFill),
// ImaginateController(ImaginateController),
Fill(graphene_core::vector::style::Fill), Fill(graphene_core::vector::style::Fill),
Stroke(graphene_core::vector::style::Stroke), Stroke(graphene_core::vector::style::Stroke),
F64Array4([f64; 4]), Gradient(graphene_core::vector::style::Gradient),
// TODO: Eventually remove this alias document upgrade code #[cfg_attr(feature = "serde", serde(alias = "GradientPositions"))] // TODO: Eventually remove this alias document upgrade code
#[cfg_attr(feature = "serde", serde(alias = "VecF32"))] GradientStops(graphene_core::vector::style::GradientStops),
VecF64(Vec<f64>), Font(graphene_core::text::Font),
VecU64(Vec<u64>), BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
NodePath(Vec<NodeId>), BrushCache(BrushCache),
VecDVec2(Vec<DVec2>), DocumentNode(DocumentNode),
Curve(graphene_core::raster::curve::Curve),
Footprint(graphene_core::transform::Footprint),
VectorModification(Box<graphene_core::vector::VectorModification>),
FontCache(Arc<graphene_core::text::FontCache>),
// ==========
// ENUM TYPES
// ==========
BlendMode(BlendMode),
LuminanceCalculation(LuminanceCalculation),
XY(graphene_core::ops::XY), XY(graphene_core::ops::XY),
RedGreenBlue(graphene_core::raster::RedGreenBlue), RedGreenBlue(graphene_core::raster::RedGreenBlue),
RealTimeMode(graphene_core::animation::RealTimeMode),
RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha), RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha),
RealTimeMode(graphene_core::animation::RealTimeMode),
NoiseType(graphene_core::raster::NoiseType), NoiseType(graphene_core::raster::NoiseType),
FractalType(graphene_core::raster::FractalType), FractalType(graphene_core::raster::FractalType),
CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction), CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction),
@ -220,26 +239,15 @@ tagged_value! {
LineJoin(graphene_core::vector::style::LineJoin), LineJoin(graphene_core::vector::style::LineJoin),
FillType(graphene_core::vector::style::FillType), FillType(graphene_core::vector::style::FillType),
FillChoice(graphene_core::vector::style::FillChoice), FillChoice(graphene_core::vector::style::FillChoice),
Gradient(graphene_core::vector::style::Gradient),
GradientType(graphene_core::vector::style::GradientType), GradientType(graphene_core::vector::style::GradientType),
// TODO: Eventually remove this alias document upgrade code
#[cfg_attr(feature = "serde", serde(alias = "GradientPositions"))]
GradientStops(graphene_core::vector::style::GradientStops),
// TODO: Eventually remove this alias document upgrade code
#[cfg_attr(feature = "serde", serde(alias = "ManipulatorGroupIds"))]
PointIds(Vec<graphene_core::vector::PointId>),
Font(graphene_core::text::Font),
BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
BrushCache(BrushCache),
DocumentNode(DocumentNode),
Curve(graphene_core::raster::curve::Curve),
Footprint(graphene_core::transform::Footprint),
ReferencePoint(graphene_core::transform::ReferencePoint), ReferencePoint(graphene_core::transform::ReferencePoint),
Palette(Vec<Color>),
VectorModification(Box<graphene_core::vector::VectorModification>),
CentroidType(graphene_core::vector::misc::CentroidType), CentroidType(graphene_core::vector::misc::CentroidType),
BooleanOperation(graphene_core::vector::misc::BooleanOperation), BooleanOperation(graphene_core::vector::misc::BooleanOperation),
FontCache(Arc<graphene_core::text::FontCache>),
// ImaginateCache(ImaginateCache),
// ImaginateSamplingMethod(ImaginateSamplingMethod),
// ImaginateMaskStartingFill(ImaginateMaskStartingFill),
// ImaginateController(ImaginateController),
} }
impl TaggedValue { impl TaggedValue {

View file

@ -938,12 +938,12 @@ mod test {
assert_eq!( assert_eq!(
ids, ids,
vec![ vec![
NodeId(8409339180888025381), NodeId(16997244687192517417),
NodeId(210279231591542793), NodeId(12226224850522777131),
NodeId(11043024792989571946), NodeId(9162113827627229771),
NodeId(16261870568621497283), NodeId(12793582657066318419),
NodeId(6520148642810552409), NodeId(16945623684036608820),
NodeId(8779776256867305756) NodeId(2640415155091892458)
] ]
); );
} }

View file

@ -147,6 +147,7 @@ where
image image
} }
#[node_macro::node] #[node_macro::node]
fn combine_channels< fn combine_channels<
// _P is the color of the input image. // _P is the color of the input image.

View file

@ -0,0 +1,202 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::parse::Parse;
use syn::{Attribute, DeriveInput, Expr, LitStr, Meta};
pub fn derive_choice_type_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
match input.data {
syn::Data::Enum(data_enum) => derive_enum(&input.attrs, input.ident, data_enum),
_ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")),
}
}
struct Type {
basic_item: BasicItem,
widget_hint: WidgetHint,
}
enum WidgetHint {
Radio,
Dropdown,
}
impl Parse for WidgetHint {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let tokens: Ident = input.parse()?;
if tokens == "Radio" {
Ok(Self::Radio)
} else if tokens == "Dropdown" {
Ok(Self::Dropdown)
} else {
Err(syn::Error::new_spanned(tokens, "Widget must be either Radio or Dropdown"))
}
}
}
#[derive(Default)]
struct BasicItem {
label: String,
description: Option<String>,
icon: Option<String>,
}
impl BasicItem {
fn read_attribute(&mut self, attribute: &Attribute) -> syn::Result<()> {
if attribute.path().is_ident("label") {
let token: LitStr = attribute.parse_args()?;
self.label = token.value();
}
if attribute.path().is_ident("icon") {
let token: LitStr = attribute.parse_args()?;
self.icon = Some(token.value());
}
if attribute.path().is_ident("doc") {
if let Meta::NameValue(meta_name_value) = &attribute.meta {
if let Expr::Lit(el) = &meta_name_value.value {
if let syn::Lit::Str(token) = &el.lit {
self.description = Some(token.value());
}
}
}
}
Ok(())
}
}
struct Variant {
name: Ident,
basic_item: BasicItem,
}
fn derive_enum(enum_attributes: &[Attribute], name: Ident, input: syn::DataEnum) -> syn::Result<TokenStream> {
let mut enum_info = Type {
basic_item: BasicItem::default(),
widget_hint: WidgetHint::Dropdown,
};
for attribute in enum_attributes {
enum_info.basic_item.read_attribute(attribute)?;
if attribute.path().is_ident("widget") {
enum_info.widget_hint = attribute.parse_args()?;
}
}
let mut variants = vec![Vec::new()];
for variant in &input.variants {
let mut basic_item = BasicItem::default();
for attribute in &variant.attrs {
if attribute.path().is_ident("menu_separator") {
attribute.meta.require_path_only()?;
variants.push(Vec::new());
}
basic_item.read_attribute(attribute)?;
}
if basic_item.label.is_empty() {
basic_item.label = ident_to_label(&variant.ident);
}
variants.last_mut().unwrap().push(Variant {
name: variant.ident.clone(),
basic_item,
})
}
let display_arm: Vec<_> = variants
.iter()
.flat_map(|variants| variants.iter())
.map(|variant| {
let variant_name = &variant.name;
let variant_label = &variant.basic_item.label;
quote! { #name::#variant_name => write!(f, #variant_label), }
})
.collect();
let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e),
)
})?;
let crate_name = match crate_name {
proc_macro_crate::FoundCrate::Itself => quote!(crate),
proc_macro_crate::FoundCrate::Name(name) => {
let identifier = Ident::new(&name, Span::call_site());
quote! { #identifier }
}
};
let enum_description = match &enum_info.basic_item.description {
Some(s) => {
let s = s.trim();
quote! { Some(#s) }
}
None => quote! { None },
};
let group: Vec<_> = variants
.iter()
.map(|variants| {
let items = variants
.iter()
.map(|variant| {
let vname = &variant.name;
let vname_str = variant.name.to_string();
let label = &variant.basic_item.label;
let docstring = match &variant.basic_item.description {
Some(s) => {
let s = s.trim();
quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) }
}
None => quote! { None },
};
let icon = match &variant.basic_item.icon {
Some(s) => quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) },
None => quote! { None },
};
quote! {
(
#name::#vname, #crate_name::registry::VariantMetadata {
name: ::alloc::borrow::Cow::Borrowed(#vname_str),
label: ::alloc::borrow::Cow::Borrowed(#label),
docstring: #docstring,
icon: #icon,
}
),
}
})
.collect::<Vec<_>>();
quote! { &[ #(#items)* ], }
})
.collect();
let widget_hint = match enum_info.widget_hint {
WidgetHint::Radio => quote! { RadioButtons },
WidgetHint::Dropdown => quote! { Dropdown },
};
Ok(quote! {
impl #crate_name::vector::misc::AsU32 for #name {
fn as_u32(&self) -> u32 {
*self as u32
}
}
impl #crate_name::registry::ChoiceTypeStatic for #name {
const WIDGET_HINT: #crate_name::registry::ChoiceWidgetHint = #crate_name::registry::ChoiceWidgetHint::#widget_hint;
const DESCRIPTION: Option<&'static str> = #enum_description;
fn list() -> &'static [&'static [(Self, #crate_name::registry::VariantMetadata)]] {
&[ #(#group)* ]
}
}
impl core::fmt::Display for #name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
#( #display_arm )*
}
}
}
})
}
fn ident_to_label(id: &Ident) -> String {
use convert_case::{Case, Casing};
id.to_string().from_case(Case::Pascal).to_case(Case::Title)
}

View file

@ -10,9 +10,24 @@ use syn::{
}; };
mod codegen; mod codegen;
mod derive_choice_type;
mod parsing; mod parsing;
mod validation; mod validation;
/// Generate meta-information for an enum.
///
/// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported.
///
/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). All labels are collected into a [`core::fmt::Display`] impl.
///
/// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button.
///
/// Doc comments on a variant become tooltip text.
#[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))]
pub fn derive_choice_type(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// A macro used to construct a proto node implementation from the given struct and the decorated function. /// A macro used to construct a proto node implementation from the given struct and the decorated function.
/// ///
/// This works by generating two `impl` blocks for the given struct: /// This works by generating two `impl` blocks for the given struct:

View file

@ -23,6 +23,7 @@ serde-discriminant = []
proc-macro2 = { workspace = true } proc-macro2 = { workspace = true }
syn = { workspace = true } syn = { workspace = true }
quote = { workspace = true } quote = { workspace = true }
convert_case = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Local dependencies # Local dependencies