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 },
    }
}