diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message.rs index 20b0f8486a..0f9df27a14 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message.rs @@ -4,10 +4,12 @@ use crate::messages::prelude::*; #[impl_message(Message, DialogMessage, ExportDialog)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ExportDialogMessage { + #[child] FileType(FileType), + #[child] + ExportBounds(ExportBounds), ScaleFactor(f64), TransparentBackground(bool), - ExportBounds(ExportBounds), Submit, } diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index cb55047daa..692595e25d 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -29,6 +29,7 @@ pub enum MouseCursorIcon { Rotate, } +#[impl_message(Message, ExportDialogMessage, FileType)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub enum FileType { #[default] @@ -47,6 +48,7 @@ impl FileType { } } +#[impl_message(Message, ExportDialogMessage, ExportBounds)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub enum ExportBounds { #[default] diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index ee441f6bfd..eefaa0976b 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -37,10 +37,10 @@ pub enum Message { Workspace(WorkspaceMessage), // Messages - NoOp, Batched { messages: Box<[Message]>, }, + NoOp, } /// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`. @@ -96,6 +96,17 @@ mod test { } } + // Print message field if any + if let Some(fields) = tree.fields() { + let len = fields.len(); + for (i, field) in fields.iter().enumerate() { + let is_last_field = i == len - 1; + let branch = if is_last_field { "└── " } else { "├── " }; + + file.write_all(format!("{}{}{}\n", child_prefix, branch, field).as_bytes()).unwrap(); + } + } + // Print handler field if any if let Some(data) = tree.message_handler_fields() { let len = data.fields().len(); @@ -104,16 +115,19 @@ mod test { } else { ("└── ", format!("{} ", prefix)) }; - if data.path().is_empty() { - file.write_all(format!("{}{}{}\n", prefix, branch, data.name()).as_bytes()).unwrap(); - } else { + + const FRONTEND_MESSAGE_STR: &str = "FrontendMessage"; + if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR { + panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name()); + } else if tree.name() != FRONTEND_MESSAGE_STR { file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap(); - } - for (i, field) in data.fields().iter().enumerate() { - let is_last_field = i == len - 1; - let branch = if is_last_field { "└── " } else { "├── " }; - file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap(); + for (i, field) in data.fields().iter().enumerate() { + let is_last_field = i == len - 1; + let branch = if is_last_field { "└── " } else { "├── " }; + + file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap(); + } } } diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 1f12aad6b1..a657f7b403 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -1,5 +1,5 @@ // Root -pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild}; +pub use crate::utility_traits::{ActionList, AsMessage, ExtractField, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild}; pub use crate::utility_types::{DebugMessageTree, MessageData}; // Message, MessageData, MessageDiscriminant, MessageHandler pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler}; diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 55bb34fb35..aba9b7dabc 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -27,7 +27,7 @@ use graphene_std::renderer::Quad; use graphene_std::vector::misc::ArcType; use std::vec; -#[derive(Default)] +#[derive(Default, ExtractField)] pub struct ShapeTool { fsm_state: ShapeToolFsmState, tool_data: ShapeToolData, @@ -191,6 +191,7 @@ impl LayoutHolder for ShapeTool { } } +#[message_handler_data] impl<'a> MessageHandler> for ShapeTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message else { diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index bcad0b24d4..d25ff8a384 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -67,6 +67,7 @@ pub struct TransformLayerMessageHandler { ghost_outline: Vec<(Vec, DAffine2)>, } +#[message_handler_data] impl MessageHandler> for TransformLayerMessageHandler { fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque, context: TransformLayerMessageContext) { let TransformLayerMessageContext { diff --git a/editor/src/utility_traits.rs b/editor/src/utility_traits.rs index aaf977d1dd..386d6fda65 100644 --- a/editor/src/utility_traits.rs +++ b/editor/src/utility_traits.rs @@ -60,3 +60,9 @@ pub trait HierarchicalTree { "" } } + +pub trait ExtractField { + fn field_types() -> Vec<(String, usize)>; + fn path() -> &'static str; + fn print_field_types(); +} diff --git a/editor/src/utility_types.rs b/editor/src/utility_types.rs index 6b5dc6de6b..2a31b2b634 100644 --- a/editor/src/utility_types.rs +++ b/editor/src/utility_types.rs @@ -26,6 +26,7 @@ impl MessageData { #[derive(Debug)] pub struct DebugMessageTree { name: String, + fields: Option>, variants: Option>, message_handler: Option, message_handler_data: Option, @@ -36,6 +37,7 @@ impl DebugMessageTree { pub fn new(name: &str) -> DebugMessageTree { DebugMessageTree { name: name.to_string(), + fields: None, variants: None, message_handler: None, message_handler_data: None, @@ -43,6 +45,10 @@ impl DebugMessageTree { } } + pub fn add_fields(&mut self, fields: Vec) { + self.fields = Some(fields); + } + pub fn set_path(&mut self, path: &'static str) { self.path = path; } @@ -67,6 +73,10 @@ impl DebugMessageTree { &self.name } + pub fn fields(&self) -> Option<&Vec> { + self.fields.as_ref() + } + pub fn path(&self) -> &'static str { self.path } @@ -84,16 +94,10 @@ impl DebugMessageTree { } pub fn has_message_handler_data_fields(&self) -> bool { - match self.message_handler_data_fields() { - Some(_) => true, - None => false, - } + self.message_handler_data_fields().is_some() } pub fn has_message_handler_fields(&self) -> bool { - match self.message_handler_fields() { - Some(_) => true, - None => false, - } + self.message_handler_fields().is_some() } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 4b8d17fc89..29eaf0e955 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -4,7 +4,9 @@ use graph_craft::document::value::RenderOutput; use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_core::raster::color::Color; use graphene_core::raster::*; -use graphene_core::raster_types::{CPU, GPU, Raster}; +#[cfg(feature = "gpu")] +use graphene_core::raster_types::GPU; +use graphene_core::raster_types::{CPU, Raster}; use graphene_core::{Artboard, concrete, generic}; use graphene_core::{Cow, ProtoNodeIdentifier, Type}; use graphene_core::{NodeIO, NodeIOTypes}; diff --git a/proc-macros/src/extract_fields.rs b/proc-macros/src/extract_fields.rs index 606b3b8a37..949ff3f50d 100644 --- a/proc-macros/src/extract_fields.rs +++ b/proc-macros/src/extract_fields.rs @@ -31,23 +31,23 @@ pub fn derive_extract_field_impl(input: TokenStream) -> syn::Result }) .collect::>(); - let field_str = field_info.into_iter().map(|(name, ty)| (format!("{}: {}", name, ty))); + let field_str = field_info.into_iter().map(|(name, ty)| (format!("{name}: {ty}"))); let res = quote! { - impl #impl_generics #struct_name #ty_generics #where_clause { - pub fn field_types() -> Vec<(String, usize)> { + impl #impl_generics ExtractField for #struct_name #ty_generics #where_clause { + fn field_types() -> Vec<(String, usize)> { vec![ #((String::from(#field_str), #field_line)),* ] } - pub fn print_field_types() { + fn print_field_types() { for (field, line) in Self::field_types() { println!("{} at line {}", field, line); } } - pub fn path() -> &'static str { + fn path() -> &'static str { file!() } } diff --git a/proc-macros/src/hierarchical_tree.rs b/proc-macros/src/hierarchical_tree.rs index e0d6fb6a71..50df803502 100644 --- a/proc-macros/src/hierarchical_tree.rs +++ b/proc-macros/src/hierarchical_tree.rs @@ -1,3 +1,4 @@ +use crate::helpers::clean_rust_type_syntax; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, quote}; use syn::{Data, DeriveInput, Fields, Type, parse2}; @@ -11,51 +12,89 @@ pub fn generate_hierarchical_tree(input: TokenStream) -> syn::Result return Err(syn::Error::new(Span::call_site(), "Tried to derive HierarchicalTree for non-enum")), }; - let build_message_tree = data.variants.iter().map(|variant| { - let variant_type = &variant.ident; + let build_message_tree: Result, syn::Error> = data + .variants + .iter() + .map(|variant| { + let variant_type = &variant.ident; - let has_child = variant - .attrs - .iter() - .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "sub_discriminant" || ident == "child")); + let has_child = variant + .attrs + .iter() + .any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "sub_discriminant" || ident == "child")); - if has_child { - if let Fields::Unnamed(fields) = &variant.fields { - let field_type = &fields.unnamed.first().unwrap().ty; - quote! { - { - let mut variant_tree = DebugMessageTree::new(stringify!(#variant_type)); - let field_name = stringify!(#field_type); - const message_string: &str = "Message"; - if message_string == &field_name[field_name.len().saturating_sub(message_string.len())..] { - // The field is a Message type, recursively build its tree - let sub_tree = #field_type::build_message_tree(); - variant_tree.add_variant(sub_tree); - } - message_tree.add_variant(variant_tree); + match &variant.fields { + Fields::Unit => Ok(quote! { + message_tree.add_variant(DebugMessageTree::new(stringify!(#variant_type))); + }), + Fields::Unnamed(fields) => { + if has_child { + let field_type = &fields.unnamed.first().unwrap().ty; + Ok(quote! { + { + let mut variant_tree = DebugMessageTree::new(stringify!(#variant_type)); + let field_name = stringify!(#field_type); + const MESSAGE_SUFFIX: &str = "Message"; + if MESSAGE_SUFFIX == &field_name[field_name.len().saturating_sub(MESSAGE_SUFFIX.len())..] { + // The field is a Message type, recursively build its tree + let sub_tree = #field_type::build_message_tree(); + variant_tree.add_variant(sub_tree); + } else { + variant_tree.add_fields(vec![format!("{field_name}")]); + } + message_tree.add_variant(variant_tree); + } + }) + } else { + let error_msg = match fields.unnamed.len() { + 0 => format!("Remove the unnecessary `()` from the `{}` message enum variant.", variant_type), + 1 => { + let field_type = &fields.unnamed.first().unwrap().ty; + format!( + "The `{}` message should be defined as a struct-style (not tuple-style) enum variant to maintain consistent formatting across all editor messages.\n\ + Replace `{}` with a named field using {{curly braces}} instead of a positional field using (parentheses).", + variant_type, + field_type.to_token_stream() + ) + } + _ => { + let field_types = fields.unnamed.iter().map(|f| f.ty.to_token_stream().to_string()).collect::>().join(", "); + format!( + "The `{}` message should be defined as a struct-style (not tuple-style) enum variant to maintain consistent formatting across all editor messages.\n\ + Replace `{}` with named fields using {{curly braces}} instead of positional fields using (parentheses).", + variant_type, field_types + ) + } + }; + return Err(syn::Error::new(Span::call_site(), error_msg)); } } - } else { - quote! { - message_tree.add_variant(DebugMessageTree::new(stringify!(#variant_type))); + Fields::Named(fields) => { + let names = fields.named.iter().map(|f| f.ident.as_ref().unwrap()); + let ty = fields.named.iter().map(|f| clean_rust_type_syntax(f.ty.to_token_stream().to_string())); + Ok(quote! { + { + let mut field_names = Vec::new(); + #(field_names.push(format!("{}: {}",stringify!(#names), #ty));)* + let mut variant_tree = DebugMessageTree::new(stringify!(#variant_type)); + variant_tree.add_fields(field_names); + message_tree.add_variant(variant_tree); + } + }) } } - } else { - quote! { - message_tree.add_variant(DebugMessageTree::new(stringify!(#variant_type))); - } - } - }); + }) + .collect(); + let build_message_tree = build_message_tree?; let res = quote! { impl HierarchicalTree for #input_type { fn build_message_tree() -> DebugMessageTree { let mut message_tree = DebugMessageTree::new(stringify!(#input_type)); #(#build_message_tree)* + let message_handler_str = #input_type::message_handler_str(); - if message_handler_str.fields().len() > 0 { - message_tree.add_message_handler_field(message_handler_str); - } + message_tree.add_message_handler_field(message_handler_str); let message_handler_data_str = #input_type::message_handler_data_str(); if message_handler_data_str.fields().len() > 0 { diff --git a/proc-macros/src/message_handler_data_attr.rs b/proc-macros/src/message_handler_data_attr.rs index 6c1c88b886..2459ac5a64 100644 --- a/proc-macros/src/message_handler_data_attr.rs +++ b/proc-macros/src/message_handler_data_attr.rs @@ -43,10 +43,8 @@ pub fn message_handler_data_attr_impl(attr: TokenStream, input_item: TokenStream quote! { #input_item impl #message_type { - pub fn message_handler_data_str() -> MessageData - { + pub fn message_handler_data_str() -> MessageData { MessageData::new(format!("{}", stringify!(#type_name)), #type_name::field_types(), #type_name::path()) - } pub fn message_handler_str() -> MessageData { MessageData::new(format!("{}", stringify!(#input_type)), #input_type::field_types(), #input_type::path()) diff --git a/website/other/editor-structure/generate.js b/website/other/editor-structure/generate.js index b9ce6d9969..1ae98156eb 100644 --- a/website/other/editor-structure/generate.js +++ b/website/other/editor-structure/generate.js @@ -57,31 +57,37 @@ function buildHtmlList(nodes, currentIndex, currentLevel) { continue; } - const hasChildren = (i + 1 < nodes.length) && (nodes[i + 1].level > node.level); + const hasDirectChildren = i + 1 < nodes.length && nodes[i + 1].level > node.level; + const hasDeeperChildren = hasDirectChildren && i + 2 < nodes.length && nodes[i + 2].level > nodes[i + 1].level; + const linkHtml = node.link ? `${path.basename(node.link)}` : ""; const fieldPieces = node.text.match(/([^:]*):(.*)/); - const partOfMessageFromNamingConvention = ["Message", "MessageHandler", "MessageContext"].some((suffix) => node.text.replace(/(.*)<.*>/g, "$1").endsWith(suffix)); - const partOfMessageViolatesNamingConvention = node.link && !partOfMessageFromNamingConvention; - const partOfMessage = node.link ? "subsystem" : ""; - const messageParent = (hasChildren && !node.link) ? " submessage": ""; - const violatesNamingConvention = partOfMessageViolatesNamingConvention ? "(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')" : ""; let escapedText; if (fieldPieces && fieldPieces.length === 3) { escapedText = [escapeHtml(fieldPieces[1].trim()), escapeHtml(fieldPieces[2].trim())]; } else { escapedText = [escapeHtml(node.text)]; } + + let role = "message"; + if (node.link) role = "subsystem"; + else if (hasDeeperChildren) role = "submessage"; + else if (escapedText.length === 2) role = "field"; - if (hasChildren) { - html += `
  • ${escapedText}${linkHtml}${violatesNamingConvention}`; + const partOfMessageFromNamingConvention = ["Message", "MessageHandler", "MessageContext"].some((suffix) => node.text.replace(/(.*)<.*>/g, "$1").endsWith(suffix)); + const partOfMessageViolatesNamingConvention = node.link && !partOfMessageFromNamingConvention; + const violatesNamingConvention = partOfMessageViolatesNamingConvention ? "(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')" : ""; + + if (hasDirectChildren) { + html += `
  • ${escapedText}${linkHtml}${violatesNamingConvention}`; const childResult = buildHtmlList(nodes, i + 1, node.level + 1); html += `
    ${childResult.html}
  • \n`; i = childResult.nextIndex; - } else if (escapedText.length === 2) { - html += `
  • ${escapedText[0]}: ${escapedText[1]}${linkHtml}
  • \n`; + } else if (role === "field") { + html += `
  • ${escapedText[0]}: ${escapedText[1]}${linkHtml}
  • \n`; i++; } else { - html += `
  • ${escapedText[0]}${linkHtml}${violatesNamingConvention}
  • \n`; + html += `
  • ${escapedText[0]}${linkHtml}${violatesNamingConvention}
  • \n`; i++; } } diff --git a/website/sass/page/developer-guide-editor-structure.scss b/website/sass/page/developer-guide-editor-structure.scss index 3e649aa69a..7d145f207b 100644 --- a/website/sass/page/developer-guide-editor-structure.scss +++ b/website/sass/page/developer-guide-editor-structure.scss @@ -30,10 +30,8 @@ \ '); position: absolute; - margin: auto; - top: 0; - bottom: 0; left: 0; + margin: calc((1.5em - 10px) / 2) auto; width: 10px; height: 10px; } @@ -41,34 +39,10 @@ &.expanded::before { transform: rotate(90deg); } - - a { - margin-left: 12px; - color: var(--color-crimson); - font-size: 12px; - font-family: Arial, sans-serif; - position: relative; - - &:hover::after { - content: "↗"; - margin-left: 4px; - position: absolute; - } - } } .tree-leaf { margin-left: calc(10px + 8px); - - &.field { - padding-left: 4px; - color: var(--color-storm); - } - - &:not(.field) { - padding: 0 4px; - background: var(--color-fog); - } } .nested { @@ -80,6 +54,7 @@ } .warn { + display: inline; margin-left: 12px; color: var(--color-flamingo); font-family: Arial, sans-serif; @@ -87,6 +62,20 @@ text-decoration: none; font-style: italic; } + + a { + margin-left: 12px; + color: var(--color-crimson); + font-size: 12px; + font-family: Arial, sans-serif; + position: relative; + + &:hover::after { + content: "↗"; + margin-left: 4px; + position: absolute; + } + } } .subsystem, @@ -94,13 +83,27 @@ font-family: monospace; line-height: 1.5; padding: 0 4px; + + &.subsystem { + color: #ffffff; + background: var(--color-crimson); + } + + &.submessage { + background: var(--color-mustard); + } } -.subsystem { - color: #ffffff; - background: var(--color-storm); +.message { + padding: 0 4px; + background: var(--color-fog); } -.submessage { - background: var(--color-lilac); +.field { + padding-left: 4px; + color: #8887c0; + + + span { + color: #457297; + } }