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",
);
}
}