register.rs
raw
//! `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<String> {
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<dyn ::core::any::Any>| {
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 },
}
}