registry.rs raw

//! The property registry: which widgets exist, which properties each
//! accepts, and what each property *is* — static layout style, a settable
//! widget property, an event wire, or a query.
//!
//! # One descriptor, keyed by name
//!
//! Every widget — a framework builtin or one an application wrote in Rust and
//! declared in a `.gdw` manifest ([`crate::manifest`]) — is one
//! [`WidgetDescriptor`], resolved by name. There is no privileged description:
//! the builtins are parsed from an embedded `builtins.gdw` in the very same
//! grammar an application uses, so a `container` and a `markdown` reach the
//! compiler as the same kind of thing. The [`Cow`] fields let a parsed
//! descriptor's owned strings and the universal tables' borrowed ones share a
//! type, and every view — [`WidgetDescriptor::lookup`],
//! [`WidgetDescriptor::theme_value_type`], [`WidgetDescriptor::known_props`] —
//! reads a `&WidgetDescriptor` without caring where it came from. A widget is
//! *just a name* to the rest of the compiler;
//! the framework's own compile-time rules (where a widget may appear) are
//! descriptor data, applicable to a builtin and an application's widget alike.
//! Whether a widget defers its content is not among them — that is a runtime
//! property of the widget, decided by its `Widget::defers_content`.
//!
//! This is the compiler's half of the property-kind table; the widget setters
//! in guiduck-core are the runtime half (each setter marks its own dirt class).
//! What every widget shares — the universal layout properties, the pointer
//! events, `:class`/`:tooltip`/`:enabled` — lives in the global tables below and
//! is unioned in by [`WidgetDescriptor::lookup`] for every widget.

use std::borrow::Cow;
use std::sync::LazyLock;

use crate::ast::TypeKind;
use crate::diagnostics::Diagnostic;
use crate::sexpr::Span;

/// Layout (taffy) properties. Static: reactive layout comes from the style
/// engine, not from bindings.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LayoutProp {
    Width,
    Height,
    Padding,
    Gap,
    Direction,
    AlignItems,
    JustifyContent,
    AlignSelf,
    Grow,
    Shrink,
    Basis,
}

/// Event wires the framework defines. A builtin's own event and every widget's
/// universal pointer events name one of these; both back ends map it to the
/// runtime `EventKind`, so this closed set is genuinely load-bearing. A
/// manifest's event is not one of these — it dispatches as `EventKind::User`
/// (see [`EventRef`]).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum EventProp {
    Click,
    /// `:on-counted-click` — the settled outcome of a click burst, one
    /// dispatch once the ≤200ms window closes, carrying the final count in
    /// `event.click-count`. Unlike `:on-click` it never fires on the first
    /// press of a double, so a handler can act on a confirmed single or a
    /// double: `(edit event.click-count)`.
    CountedClick,
    PointerEnter,
    PointerLeave,
    PointerDown,
    PointerUp,
    /// `:on-change` — the widget's value changed through user input. The
    /// wired handler receives the new value, so it must be declared with a
    /// payload: `(handler edited String)`.
    Changed,
    /// `:on-close` — the user asked to dismiss (Escape). Carries no payload;
    /// it is a *request*, and the app answers it by clearing `:open`, which
    /// is what keeps the binding the single source of truth.
    Close,
    /// `:on-toggle` — a toggle control flipped through user input. The wired
    /// handler receives the new checked state: `(handler set-flag bool)`.
    Toggled,
    /// `:on-select` — a menu item was chosen, by pointer, by Enter on the
    /// highlighted row, or by its accelerator. Carries no payload: what was
    /// chosen is the wire's own arguments (`:on-select (open f.id)`).
    Select,
    /// `:on-link` — a `(link …)` span inside a paragraph was clicked. The
    /// wired handler receives the span's target: `(handler navigate String)`.
    ///
    /// A span rather than a widget, because wrapping a link across a line
    /// break needs the link and the prose around it to be *one* parley layout;
    /// a link that were its own widget would break the line at its own edges
    /// instead of between words.
    Link,
    /// `:on-file-drop` — a file was dropped on the window. The wired handler
    /// receives its path: `(handler import String)`. One dispatch per file;
    /// the framework carries the path and the app interprets it.
    FileDrop,
    /// `:on-focus-change` — this widget gained or lost focus. The wired
    /// handler receives the new state: `(handler editing-changed bool)`.
    ///
    /// The report half of `:focused`. It fires for every transition whatever
    /// caused it — a click, Tab, the focus invariant, a disabled widget being
    /// evicted — which is what lets an application drive `:focused` from its
    /// own state and still keep that state true when the user moves focus
    /// some other way.
    FocusChange,
    /// `:on-key` — a key the focused widget did not consume, offered to its
    /// own wire and then to its ancestors'. Carries no payload: which key it
    /// was is `event.key`, so the wire reads
    /// `:on-key (maybe-commit event.key)`.
    ///
    /// It sits *after* the focused widget's own handling, so a text input's
    /// Ctrl+C is still the text input's, and *before* overlay dismissal and
    /// the accelerator table, because a wire written on the focused widget is
    /// more specific than either. A handler claims the key with
    /// `cx.consume()`.
    Key,
    /// `:on-value-change` — a control whose value is a number moved. The
    /// wired handler receives the new value: `(handler set-volume f64)`.
    ///
    /// Distinct from `:on-change`, which carries a string: a slider and a text
    /// field both "change", and conflating them would make one wire mean two
    /// payload types.
    ValueChanged,
    /// `:on-scroll` — a scroll container's position moved because the reader
    /// moved it. It carries no payload: the new position is two numbers, and
    /// the wire reads them by name (`event.offset-x`, `event.offset-y`) rather
    /// than pretending a scroll is one value.
    Scrolled,
}

impl EventProp {
    /// The payload type the event delivers to its handler, if any. This is
    /// the compiler's half of the contract; the runtime half is the
    /// `EventData` variant the widget emits.
    ///
    /// Both back ends key their payload plumbing on this rather than on which
    /// event it is, so an event that delivers a `String` needs no arm of its
    /// own in either.
    pub fn payload(self) -> Option<PayloadKind> {
        match self {
            Self::Changed | Self::Link | Self::FileDrop => Some(PayloadKind::String),
            Self::Toggled | Self::FocusChange => Some(PayloadKind::Bool),
            Self::ValueChanged => Some(PayloadKind::Number),
            _ => None,
        }
    }

    /// The framework per-widget event a `.gdw`'s `(event NAME :builtin)` names,
    /// or `None` if the name is not one. The universal pointer events
    /// (`on-click`, `on-pointer-…`, `on-file-drop`) are not here: every widget
    /// has them already through [`COMMON_EVENT_DECLS`], so a widget's own
    /// `:builtin` events are the semantic ones only it emits.
    pub fn from_widget_event(name: &str) -> Option<Self> {
        Some(match name {
            "on-change" => Self::Changed,
            "on-close" => Self::Close,
            "on-toggle" => Self::Toggled,
            "on-select" => Self::Select,
            "on-link" => Self::Link,
            "on-value-change" => Self::ValueChanged,
            "on-scroll" => Self::Scrolled,
            _ => return None,
        })
    }
}

/// Payload types events can deliver (a subset of the `.gdc` type set).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PayloadKind {
    String,
    Bool,
    /// An `f64`. A handler may declare `f32` or `f64`; the narrowing happens
    /// where the argument is re-typed, as for every other numeric.
    Number,
}

/// The value type a themable visual property expects. Themes carry a widget's
/// *appearance tokens* — brushes, metrics, vector marks — which the widget's
/// paint code reads from computed style and places. This is a superset of the
/// settable widget-property set: it also covers paint-only tokens that only a
/// theme sets (focus rings, the checkbox mark), which is why theming resolves
/// against this table rather than the property registry.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ThemeValueType {
    /// A color/brush.
    Brush,
    /// A scalar metric (width, radius, size).
    Number,
    /// A string (a font family).
    Str,
    /// Something to draw into a rectangle — `(path …)` / `(svg-path …)`
    /// geometry, or an `(asset "…")` file's pixels, interchangeably. Marks
    /// like the checkbox check take one.
    Graphic,
}

/// The type of a value a widget property carries — which is to say, what the
/// Rust setter for it takes.
///
/// This is the vocabulary a `.gdc` can put into a widget, and it is one
/// vocabulary: a `.gdw` manifest declares from it exactly as a builtin
/// descriptor does. The non-scalar members are the framework's own value
/// types, which is why an application's widget can take one — a `markdown`
/// with a `(prop icon Graphic)` is not a special case, it is the same case.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PropTy {
    /// One of the `.gdc` scalar types: `i32`, `i64`, `f32`, `f64`, `bool`,
    /// `String`.
    Scalar(TypeKind),
    /// A color/brush, from a `"#rrggbb"` literal or an `if` choosing between
    /// them.
    Brush,
    /// Something to draw: an `(asset "…")` file's pixels or `(path …)`
    /// geometry, interchangeably.
    Graphic,
    /// A paragraph's content: a string, or a `(rich …)` with styled runs.
    /// Plain text is the case with no runs, so one setter takes both.
    RichText,
    /// Which axes a scroll area scrolls.
    ScrollAxes,
}

impl PropTy {
    /// The type name a `.gdw` writes for this, if a manifest may declare it.
    pub fn manifest_name(self) -> Option<&'static str> {
        Some(match self {
            Self::Scalar(kind) => kind.rust_name(),
            Self::Brush => "Brush",
            Self::Graphic => "Graphic",
            Self::RichText => "RichText",
            Self::ScrollAxes => "ScrollAxes",
        })
    }

    /// Whether the property accepts a reactive expression.
    ///
    /// A graphic does not: its bytes are found and embedded at build time, so
    /// there is no path a runtime value could name. An axis does not: it is
    /// read at insert, to shape the layout node itself.
    pub fn bindable(self) -> bool {
        !matches!(self, Self::Graphic | Self::ScrollAxes)
    }
}

/// One event wire a widget offers.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EventRef {
    /// A framework event, mapped to the runtime `EventKind` by both back ends.
    Builtin(EventProp),
    /// A manifest event: dispatched as `EventKind::User`, carrying the payload
    /// the manifest declared (if any).
    User { payload: Option<TypeKind> },
}

/// What one of a widget's own properties *is*: how a `.gdc` value written for
/// it is checked, lowered, and routed to the Rust type.
///
/// Builtins and manifests both declare from this one vocabulary — the whole
/// point of a single descriptor. The scroll axis is an ordinary
/// [`Setter`](Self::Setter) of [`PropTy::ScrollAxes`], so `(prop axis
/// ScrollAxes)` declares it like any other; only [`Self::Accel`] is special (it
/// registers a keystroke as well as setting a string).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PropDeclKind {
    /// A universal layout property. Only the global [`COMMON_LAYOUT_DECLS`]
    /// table produces this; a widget's own props never carry it.
    Layout(LayoutProp),

    /// A settable property: call `method` with a value of `ty`. A static value
    /// and a binding both flow through it, dispatched on `ty` — one lowering
    /// for a builtin's `:background` and a manifest's `(prop icon Graphic)`
    /// alike.
    Setter {
        method: Cow<'static, str>,
        ty: PropTy,
    },

    /// An event wire.
    Event(EventRef),

    /// A query wire: the handler *returns* a value the widget installs as a
    /// resolver, through `setter`. `:get-image` on the markdown widget is what
    /// this exists for.
    Query {
        setter: Cow<'static, str>,
        payload: TypeKind,
        returns: Cow<'static, str>,
    },

    /// `:autofocus true` — the widget takes initial focus at mount. At most
    /// one per component; only on focusable widgets.
    Autofocus,

    /// `:accel "Ctrl+S"` — a keyboard accelerator for a menu item. Its string
    /// half is set through `method`; its keystroke half registers in the
    /// shortcut table at mount. Static.
    Accel { method: Cow<'static, str> },
}

/// One property a widget accepts, beyond the universal ones.
///
/// The [`Self::name`] is the keyword a `.gdc` writes (`corner-radius` →
/// `:corner-radius`), and the key both [`WidgetDescriptor::lookup`] and the
/// interpreter's factory table use.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PropDecl {
    pub name: Cow<'static, str>,
    pub kind: PropDeclKind,
}

impl PropDecl {
    /// The Rust setter this property drives and the type it takes, if it has
    /// one. Events, queries, and the autofocus marker do not: they are wired,
    /// not set.
    fn setter(&self) -> Option<(&str, &str, PropTy)> {
        match &self.kind {
            PropDeclKind::Setter { method, ty } => Some((&self.name, method, *ty)),
            PropDeclKind::Accel { method } => {
                Some((&self.name, method, PropTy::Scalar(TypeKind::String)))
            }
            PropDeclKind::Layout(_)
            | PropDeclKind::Event(_)
            | PropDeclKind::Query { .. }
            | PropDeclKind::Autofocus => None,
        }
    }
}

/// A property a widget takes that no file writes, because its value is a fact
/// about the file's *structure* that only the compiler knows. It is an ordinary
/// setter — that is the point of naming it here rather than special-casing a
/// method name in each back end — and it never appears in the `.gdc` property
/// vocabulary.
///
/// A `menu` is a bar title or a submenu row depending on what encloses it.
pub const SUBMENU_PROP: &str = "submenu";
pub const SUBMENU_METHOD: &str = "set_submenu";

/// The widget whose title/submenu duality is a framework rule: a `menu`.
const MENU: &str = "menu";

/// A named group of themable tokens. A descriptor holds a list of groups so a
/// builtin's shared appearance bundle could compose with its own; a parsed
/// manifest is one group, the tokens it lists.
type ThemeToken = (Cow<'static, str>, ThemeValueType);
type TokenGroup = Cow<'static, [ThemeToken]>;

/// Everything the registry knows about one widget — a builtin parsed from the
/// embedded manifest, or an application's, parsed from its own. Both own their
/// strings; the [`Cow`] fields share a type with the borrowed universal tables.
/// The descriptor holds only what is *its own*: the properties and tokens
/// common to every widget live in the global tables below and are never copied
/// in here.
///
/// A widget is identified by [`Self::name`] alone — there is no builtin handle.
/// The framework's own rules ([`Self::required_parents`], [`Self::is_writable`])
/// are fields here, so an application's widget answers them the same way a
/// builtin does (with the defaults an app widget has: writable, no placement
/// rule). Whether a widget defers its content is *not* here — it is the
/// widget's own runtime answer (the `Widget::defers_content` trait method),
/// invisible to a manifest.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WidgetDescriptor {
    /// The name a `.gdc` or a theme selector writes.
    pub name: Cow<'static, str>,

    /// Where a manifest declared the name, for the collision diagnostics a
    /// [`Registry`] raises. `None` for a builtin — it has no source span.
    pub name_span: Option<Span>,

    /// The Rust path codegen constructs. **Never resolved by this compiler** —
    /// a proc-macro cannot see another crate's items — so it is carried
    /// verbatim and rustc reports a bad path against the generated
    /// construction.
    pub type_path: Cow<'static, str>,

    /// The widget names this one may appear directly inside; empty for
    /// anywhere. A builtin's placement rule and a `.gdw`'s `(parents …)` produce
    /// this identically — the [`Cow`] borrows for a builtin and owns for a
    /// manifest. The names are checked against the real vocabulary by
    /// [`Registry::default`]'s construction test, which recovers the typo-safety
    /// a closed enum would have given.
    pub required_parents: Cow<'static, [Cow<'static, str>]>,

    /// Whether a `.gdc` may write this widget. A `menu-panel` exists so a
    /// theme can name the popup a menu builds; nobody writes one.
    pub is_writable: bool,

    /// Properties this widget accepts beyond the universal ones — the `.gdc`
    /// vocabulary, and (for its settable members) how to drive the Rust type.
    pub props: Cow<'static, [PropDecl]>,

    /// The token groups a theme may set on this widget, searched in order.
    pub theme_tokens: Cow<'static, [TokenGroup]>,
}

impl WidgetDescriptor {
    /// Resolve a property name against this widget: the universal layout
    /// properties, the universal pointer events, then its own props — the same
    /// search for a builtin and a manifest widget alike.
    ///
    /// `:class`/`:tooltip`/`:enabled` are not here: `validate` lowers them
    /// before it consults the registry, because they are structural rather
    /// than per-widget. `:autofocus` *is* here, in [`COMMON_UNIVERSAL_DECLS`]:
    /// any widget accepts it, and it no-ops on one that is not focusable (the
    /// tree's `set_focus` filters a non-focusable target), so it is a universal
    /// capability rather than a per-control property.
    pub fn lookup(&self, prop: &str) -> Option<&PropDecl> {
        COMMON_LAYOUT_DECLS
            .iter()
            .chain(COMMON_EVENT_DECLS.iter())
            .chain(COMMON_UNIVERSAL_DECLS.iter())
            .chain(self.props.iter())
            .find(|decl| decl.name == prop)
    }

    /// The type of a themable visual property on this widget, or `None` if it
    /// has no such token.
    pub fn theme_value_type(&self, name: &str) -> Option<ThemeValueType> {
        self.theme_tokens()
            .find(|(token, _)| *token == name)
            .map(|(_, ty)| ty)
    }

    /// Every themable token, flattened across the groups.
    pub fn theme_tokens(&self) -> impl Iterator<Item = (&str, ThemeValueType)> {
        self.theme_tokens
            .iter()
            .flat_map(|group| group.iter())
            .map(|(name, ty)| (name.as_ref(), *ty))
    }

    /// Property names accepted by this widget, for "did you mean" diagnostics —
    /// derived from the same tables [`Self::lookup`] consults, so the list is
    /// exactly what the widget takes.
    pub fn known_props(&self) -> Vec<&str> {
        COMMON_LAYOUT_DECLS
            .iter()
            .map(|decl| decl.name.as_ref())
            .chain(self.props.iter().map(|decl| decl.name.as_ref()))
            .chain(COMMON_EVENT_DECLS.iter().map(|decl| decl.name.as_ref()))
            .chain(COMMON_UNIVERSAL_DECLS.iter().map(|decl| decl.name.as_ref()))
            .chain(UNIVERSAL_PROPS.iter().copied())
            .collect()
    }

    /// How to build this widget and set each of its properties: the Rust type
    /// path, then a `(prop, method, ty)` for every settable property — its own
    /// setters plus a menu's framework-supplied submenu flag, which no file
    /// writes but the tree drives all the same. (Deferred content is not a
    /// setter here: the tree hands every widget its content builder through
    /// [`Widget::set_content`], and the widget decides at runtime.)
    ///
    /// This is the whole of what the interpreter's factory table needs, and it
    /// is one description for a builtin and a `markdown` alike — so there is no
    /// second account of how a `container` is built to drift from this one.
    pub fn setters(&self) -> Vec<(&str, &str, PropTy)> {
        let mut setters: Vec<(&str, &str, PropTy)> =
            self.props.iter().filter_map(PropDecl::setter).collect();
        if self.name == MENU {
            setters.push((SUBMENU_PROP, SUBMENU_METHOD, PropTy::Scalar(TypeKind::Bool)));
        }
        setters
    }
}

/// A builtin event declaration.
const fn event(name: &'static str, event: EventProp) -> PropDecl {
    PropDecl {
        name: Cow::Borrowed(name),
        kind: PropDeclKind::Event(EventRef::Builtin(event)),
    }
}

/// The `:autofocus` marker.
const AUTOFOCUS: PropDecl = PropDecl {
    name: Cow::Borrowed("autofocus"),
    kind: PropDeclKind::Autofocus,
};

/// Properties every widget accepts through [`WidgetDescriptor::lookup`] but that
/// are not layout or events. `:autofocus` is the one: a widget takes initial
/// focus if it is focusable, and the request is silently ignored if it is not,
/// so there is no reason to gate it per-widget — a control declares nothing to
/// be autofocusable, and an application widget that is focusable gets it for
/// free.
const COMMON_UNIVERSAL_DECLS: &[PropDecl] = &[AUTOFOCUS];

/// Layout properties every widget accepts.
const COMMON_LAYOUT_DECLS: &[PropDecl] = &[
    PropDecl {
        name: Cow::Borrowed("width"),
        kind: PropDeclKind::Layout(LayoutProp::Width),
    },
    PropDecl {
        name: Cow::Borrowed("height"),
        kind: PropDeclKind::Layout(LayoutProp::Height),
    },
    PropDecl {
        name: Cow::Borrowed("padding"),
        kind: PropDeclKind::Layout(LayoutProp::Padding),
    },
    PropDecl {
        name: Cow::Borrowed("gap"),
        kind: PropDeclKind::Layout(LayoutProp::Gap),
    },
    PropDecl {
        name: Cow::Borrowed("direction"),
        kind: PropDeclKind::Layout(LayoutProp::Direction),
    },
    PropDecl {
        name: Cow::Borrowed("align-items"),
        kind: PropDeclKind::Layout(LayoutProp::AlignItems),
    },
    PropDecl {
        name: Cow::Borrowed("justify-content"),
        kind: PropDeclKind::Layout(LayoutProp::JustifyContent),
    },
    PropDecl {
        name: Cow::Borrowed("align-self"),
        kind: PropDeclKind::Layout(LayoutProp::AlignSelf),
    },
    PropDecl {
        name: Cow::Borrowed("grow"),
        kind: PropDeclKind::Layout(LayoutProp::Grow),
    },
    PropDecl {
        name: Cow::Borrowed("shrink"),
        kind: PropDeclKind::Layout(LayoutProp::Shrink),
    },
    PropDecl {
        name: Cow::Borrowed("basis"),
        kind: PropDeclKind::Layout(LayoutProp::Basis),
    },
];

/// Events every widget accepts: the pointer family, plus the two the *tree*
/// reports rather than any widget — focus and unconsumed keys, which are
/// facts about a widget's place in the tree rather than about what it is.
const COMMON_EVENT_DECLS: &[PropDecl] = &[
    event("on-click", EventProp::Click),
    event("on-counted-click", EventProp::CountedClick),
    event("on-file-drop", EventProp::FileDrop),
    event("on-focus-change", EventProp::FocusChange),
    event("on-key", EventProp::Key),
    event("on-pointer-enter", EventProp::PointerEnter),
    event("on-pointer-leave", EventProp::PointerLeave),
    event("on-pointer-down", EventProp::PointerDown),
    event("on-pointer-up", EventProp::PointerUp),
];

/// Properties every widget accepts that are not a [`PropDecl`] at all:
/// `validate` lowers them before it consults the tables, because they are
/// structural rather than per-widget — `:class` keys theme matching,
/// `:tooltip` is the tree's to show, `:enabled` gates interaction, `:focused`
/// drives which widget has focus. They appear here so
/// [`WidgetDescriptor::known_props`] tells the truth.
const UNIVERSAL_PROPS: &[&str] = &["class", "tooltip", "enabled", "focused"];

/// Whether a name belongs to the vocabulary *every* widget has: the layout
/// properties, the pointer events, and the structural universals.
///
/// A `.gdw` manifest cannot redeclare one — [`WidgetDescriptor::lookup`]
/// consults these tables first, so the declaration would be silently dead — and
/// this reads the same tables the lookup does.
pub fn is_universal_prop(name: &str) -> bool {
    COMMON_LAYOUT_DECLS.iter().any(|decl| decl.name == name)
        || COMMON_EVENT_DECLS.iter().any(|decl| decl.name == name)
        || COMMON_UNIVERSAL_DECLS.iter().any(|decl| decl.name == name)
        || UNIVERSAL_PROPS.contains(&name)
}

/// The Rust setter a property name implies by convention: `link-color` →
/// `set_link_color`. A manifest's `:setter` overrides it, exactly as a builtin
/// descriptor names a method the convention would miss — `:text` is
/// `set_content` on a `text-input` but `set_text` on a `text`.
pub fn setter_name(prop: &str) -> String {
    format!("set_{}", prop.replace('-', "_"))
}

/// The widget vocabulary of one compilation: the builtins, plus whatever
/// `.gdw` manifests an application put on its widget search path.
///
/// The builtins are not copied in — they live once in the process-wide
/// [`BUILTINS`], parsed from the embedded manifest, and [`Self::resolve`]
/// borrows from there for a builtin and from the owned [`Self::widgets`] for a
/// declared one. A registry therefore owns only what is genuinely owned, and
/// [`Registry::default`] costs nothing.
#[derive(Clone, Debug, Default)]
pub struct Registry {
    widgets: Vec<WidgetDescriptor>,
}

impl Registry {
    /// Add a declared widget, rejecting a name the vocabulary already has.
    ///
    /// Shadowing is never allowed, in either direction: a `.gdw` cannot
    /// redefine `button` (the framework's rules key on the builtin names), and
    /// two manifests cannot both claim `markdown`.
    pub fn insert(&mut self, widget: WidgetDescriptor) -> Result<(), Diagnostic> {
        let clash = if is_builtin(&widget.name) {
            Some("a builtin widget")
        } else if self.widgets.iter().any(|w| w.name == widget.name) {
            Some("another widget manifest")
        } else {
            None
        };
        if let Some(what) = clash {
            return Err(Diagnostic::new(
                format!("`{}` is already the name of {what}", widget.name),
                widget
                    .name_span
                    .expect("a declared widget carries its name span"),
            ));
        }
        self.widgets.push(widget);
        Ok(())
    }

    /// The declared widget of this name, if any.
    pub fn user(&self, name: &str) -> Option<&WidgetDescriptor> {
        self.widgets.iter().find(|w| w.name == name)
    }

    /// Every declared widget, in declaration order.
    pub fn user_widgets(&self) -> &[WidgetDescriptor] {
        &self.widgets
    }

    /// Every descriptor in the vocabulary: the builtins, then the declared
    /// widgets. One list, which is what lets the runtime factory table be
    /// built by one loop that cannot tell a `container` from a `markdown`.
    pub fn descriptors(&self) -> impl Iterator<Item = &WidgetDescriptor> {
        BUILTINS.iter().chain(self.widgets.iter())
    }

    /// Resolve a name in widget position (or in a theme selector) to its
    /// descriptor — a builtin's `&'static` row, or a declared widget's owned
    /// entry. Builtins first, since a manifest can never take one of their
    /// names.
    pub fn resolve(&self, name: &str) -> Option<&WidgetDescriptor> {
        BUILTINS
            .iter()
            .find(|d| d.name == name)
            .or_else(|| self.user(name))
    }

    /// Every widget a `.gdc` may write, comma-separated — for the "unknown
    /// widget" diagnostic. Generated from the vocabulary in force, so it names
    /// the application's own widgets too.
    pub fn writable_names(&self) -> String {
        self.descriptors()
            .filter(|d| d.is_writable)
            .map(|d| d.name.as_ref())
            .collect::<Vec<_>>()
            .join(", ")
    }
}

/// The framework's builtins, parsed once from the embedded manifest.
///
/// This is the whole point of the exercise: a builtin is described in the same
/// `.gdw` grammar an application writes, so there is no privileged path. The
/// manifest is `include_str!`d into the compiler — no cross-crate boundary, so
/// no cycle — and parsed like any other. A static so a reference into it is
/// `'static`, matching what the const table gave. The parse cannot fail on a
/// shipped binary: [`builtins.gdw`](builtins.gdw) is compiled into it and a test
/// parses it, so the only failure it can have is one the test catches first.
static BUILTINS: LazyLock<Vec<WidgetDescriptor>> = LazyLock::new(|| {
    crate::manifest::compile_manifest(include_str!("builtins.gdw"))
        .expect("the builtin widget manifest parses")
});

/// Every builtin descriptor, for callers that want the framework's own set
/// without an application's declarations — the theme-coverage tests, chiefly.
pub fn builtins() -> impl Iterator<Item = &'static WidgetDescriptor> {
    BUILTINS.iter()
}

/// A builtin descriptor by name.
pub fn builtin(name: &str) -> Option<&'static WidgetDescriptor> {
    BUILTINS.iter().find(|d| d.name == name)
}

/// Whether a name is one of the framework's builtins.
pub fn is_builtin(name: &str) -> bool {
    builtin(name).is_some()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The builtins are the parsed `.gdw`, not a hand-written table — this is
    /// the one thing that proves the manifest is what the compiler runs on.
    /// (The migration equality against the retired const table proved the
    /// translation faithful; from here the standing suite — goldens, the m13
    /// EXACT differentials, every theme test — is the gate, exercising every
    /// one of them through the parsed descriptors.)
    #[test]
    fn the_builtins_are_the_parsed_manifest() {
        // Forces the parse; a malformed `builtins.gdw` panics here (and in
        // every other test, which all reach the registry).
        let names: Vec<&str> = builtins().map(|d| d.name.as_ref()).collect();
        assert_eq!(names.len(), 28, "every builtin declared: {names:?}");
        assert_eq!(names[0], "container", "order is the diagnostic vocabulary");

        // The capabilities that used to be the const table's alone, now read
        // back off the manifest: placement, non-writability, the accelerator.
        let menu_item = builtin("menu-item").expect("declared");
        assert_eq!(
            menu_item.required_parents.as_ref(),
            &[
                Cow::Borrowed("menu"),
                Cow::Borrowed("context-menu"),
                Cow::Borrowed("dropdown"),
                Cow::Borrowed("combo-box")
            ],
        );
        assert!(
            matches!(
                menu_item.lookup("accel").map(|d| &d.kind),
                Some(PropDeclKind::Accel { .. })
            ),
            "the accelerator survived the round trip through the grammar",
        );
        assert!(
            !builtin("menu-panel").expect("declared").is_writable,
            "`(internal)` kept the panel out of the writable vocabulary",
        );
    }
}