//! `register_widget!(NoteView, "widgets/note.gdw")` — the widget-crate half of //! the open registry. //! //! A widget crate authors its interface in a `.gdw` manifest and calls this to //! register the Rust side *from* it. The macro reads and parses the manifest at //! expansion, then emits an [`inventory::submit!`] of a `WidgetRegistration`: //! a constructor and one type-checked setter thunk per declared property. The //! thunks name the concrete widget and value types, so a manifest that names a //! setter the widget lacks — or the wrong value type — is a rustc error at the //! generated call, which is the whole correctness backstop of the split. //! //! The same manifest is what the application's `include_component!` reads to //! type-check a `.gdc` that writes the widget: one authored file, two readers. use std::path::PathBuf; use guiduck_component_core::ast::TypeKind; use guiduck_component_core::registry::{PropTy, WidgetDescriptor}; use proc_macro2::{TokenStream, TokenTree}; use quote::{format_ident, quote}; /// Expand `register_widget!(Type, "path/to.gdw")`. pub fn register_widget(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let (ty, rel_path) = match parse_args(input.into()) { Ok(parsed) => parsed, Err(message) => return crate::compile_error(&message), }; let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") { Ok(dir) => PathBuf::from(dir), Err(_) => return crate::compile_error("CARGO_MANIFEST_DIR is not set"), }; let absolute = manifest_dir.join(&rel_path); let source = match std::fs::read_to_string(&absolute) { Ok(source) => source, Err(e) => { return crate::compile_error(&format!( "cannot read widget manifest {}: {e}", absolute.display() )); } }; let widgets = match guiduck_component_core::manifest::compile_manifest(&source) { Ok(widgets) => widgets, Err(diagnostics) => { let rendered = guiduck_component_core::diagnostics::render(&diagnostics, &source, &rel_path); return crate::compile_error(&format!("invalid widget manifest\n{rendered}")); } }; // The manifest may declare several widgets; the one this call registers is // the one whose `(type "…")` ends in the type named here. let Some(last) = type_last_segment(&ty) else { return crate::compile_error( "expected a widget type, e.g. `register_widget!(NoteView, …)`", ); }; let descriptor = match widgets .iter() .find(|w| w.type_path.rsplit("::").next() == Some(last.as_str())) { Some(descriptor) => descriptor, None => { return crate::compile_error(&format!( "no `(widget …)` in `{rel_path}` declares a type ending in `{last}`" )); } }; tokens(&ty, descriptor, &rel_path).into() } /// Parse `Type , "path"` — the type as a token stream (an ident or a path), and /// the manifest path as a string literal. fn parse_args(input: TokenStream) -> Result<(TokenStream, String), String> { let usage = "expected `register_widget!(Type, \"path/to.gdw\")`"; let mut ty = TokenStream::new(); let mut trees = input.into_iter(); let mut path = None; for tree in trees.by_ref() { if let TokenTree::Punct(punct) = &tree && punct.as_char() == ',' { let literal = trees.next().ok_or_else(|| usage.to_owned())?; path = Some( litrs::StringLit::parse(literal.to_string()) .map_err(|_| "the second argument is the manifest path, a string".to_owned())? .value() .to_owned(), ); break; } ty.extend(std::iter::once(tree)); } if trees.next().is_some() { return Err(usage.to_owned()); } let path = path.ok_or_else(|| usage.to_owned())?; if ty.is_empty() { return Err("expected a widget type before `,`".to_owned()); } Ok((ty, path)) } /// The final path segment of the type tokens: the last identifier, which is /// what a `(type "…")` path's last segment must match. fn type_last_segment(ty: &TokenStream) -> Option { ty.clone() .into_iter() .filter_map(|tree| match tree { TokenTree::Ident(ident) => Some(ident.to_string()), _ => None, }) .last() } fn tokens(ty: &TokenStream, descriptor: &WidgetDescriptor, rel_path: &str) -> TokenStream { let submission = submission_tokens(ty, descriptor); quote! { #submission // Editing the manifest rebuilds this expansion, exactly as editing a // `.gdc` rebuilds the component that includes it. const _: &[u8] = ::core::include_bytes!(::core::concat!( ::core::env!("CARGO_MANIFEST_DIR"), "/", #rel_path, )); } } /// The `inventory::submit!` of one widget's registration — a constructor and a /// type-checked setter thunk per property. /// /// `widget_type` is the concrete Rust type (a manifest widget's, or a builtin's /// from its descriptor), and naming it is what makes every setter call checked /// against a real method. Shared by [`register_widget`] and /// [`crate::register_builtins`], so a `container` is registered by the code that /// registers a `markdown`, from the same [`WidgetDescriptor::setters`]. pub(crate) fn submission_tokens( widget_type: &TokenStream, descriptor: &WidgetDescriptor, ) -> TokenStream { let name = descriptor.name.as_ref(); let setters = descriptor .setters() .into_iter() .map(|(prop, method, prop_ty)| { let method = format_ident!("{method}"); let value_ty = value_type(prop_ty); quote! { ::guiduck_core::SetterEntry { prop: #prop, // The concrete widget and value types are named here, so the // setter call below is type-checked: a wrong or missing setter // is a compile error at this line. apply: |__widget: &mut dyn ::guiduck_core::Widget, __value: ::std::boxed::Box| { let ( ::core::option::Option::Some(__widget), ::core::result::Result::Ok(__value), ) = ( __widget.downcast_mut::<#widget_type>(), __value.downcast::<#value_ty>(), ) else { return; }; __widget.#method(*__value); }, } } }); quote! { ::guiduck_core::inventory::submit! { ::guiduck_core::WidgetRegistration { name: #name, construct: || ::std::boxed::Box::new( <#widget_type as ::core::default::Default>::default(), ), setters: &[ #(#setters),* ], } } } } /// The `inventory::submit!`s for every framework builtin, from the descriptor /// table — the runtime counterpart of `register_widget!` for the widgets guiduck /// ships. Emitted once, inside `guiduck-core`, so the interpreter builds a /// builtin from the link-time registry exactly as it builds an application's /// widget. pub fn register_builtins(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { let submissions = guiduck_component_core::registry::builtins().map(|descriptor| { // The descriptor's `(type "…")` is the facade path (`::guiduck::core::X`), // for an app's generated code; inside guiduck-core the type is reached as // `::guiduck_core::X` (its own re-export, via `extern crate self`). let segment = descriptor.type_path.rsplit("::").next().unwrap_or_default(); let widget_type: TokenStream = format!("::guiduck_core::{segment}") .parse() .expect("a builtin type path is a valid Rust path"); submission_tokens(&widget_type, descriptor) }); quote! { #(#submissions)* }.into() } /// The Rust value type a property's setter takes, by the widget crate's own /// paths (it depends on `guiduck-core`, not the facade). fn value_type(ty: PropTy) -> TokenStream { match ty { PropTy::Scalar(TypeKind::I32) => quote! { i32 }, PropTy::Scalar(TypeKind::I64) => quote! { i64 }, PropTy::Scalar(TypeKind::F32) => quote! { f32 }, PropTy::Scalar(TypeKind::F64) => quote! { f64 }, PropTy::Scalar(TypeKind::Bool) => quote! { bool }, PropTy::Scalar(TypeKind::String) => quote! { ::std::string::String }, PropTy::Brush => quote! { ::guiduck_scene::paint::Brush }, PropTy::Graphic => quote! { ::guiduck_core::graphic::Graphic }, PropTy::RichText => quote! { ::guiduck_core::RichText }, PropTy::ScrollAxes => quote! { ::guiduck_core::ScrollAxes }, } }