//! The compiled component IR: everything both consumers (the proc-macro //! codegen and the runtime interpreter) need, as plain data with no //! dependency on taffy or the widget crates. use std::collections::BTreeMap; use crate::ast::TypeKind; use crate::expr::Expr; use crate::registry::{EventProp, PropTy}; /// A compiled component together with every component it transitively /// instantiates, keyed by declared name (`CounterButton`). #[derive(Clone, Debug, PartialEq)] pub struct Compiled { pub root: Ir, pub components: BTreeMap, } #[derive(Clone, Debug, PartialEq)] pub struct Ir { pub name: String, pub records: Vec, pub props: Vec, pub outputs: Vec, pub states: Vec, pub handlers: Vec, /// Flattened widget tree; index 0 is the root, children by index. pub nodes: Vec, } #[derive(Clone, Debug, PartialEq)] pub struct IrHandler { pub name: String, /// The payloads the handler receives, in order: one for widget /// payload-carrying events (`:on-change`), any number for invocation /// wires (`:on-click (moved dx dy)`); empty for plain handlers. pub payloads: Vec, /// The Rust path this handler returns, for a handler wired to a widget /// [`query`](IrQuery); `None` for an ordinary event handler, which returns /// nothing. Set during validation from the query the handler answers. pub returns: Option, } /// A resolved element type: a scalar, or a record by index into the /// file's record table (records are file-local in v1, so an index keeps /// this Copy). #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ElemKind { Scalar(TypeKind), Record(u32), } /// A value type: an element, or a list of them. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ValueTy { pub kind: ElemKind, pub list: bool, } impl ValueTy { pub fn scalar(kind: TypeKind) -> Self { Self { kind: ElemKind::Scalar(kind), list: false, } } /// The Rust spelling, for diagnostics (`i32`, `Vec`). pub fn rust_name(self, records: &[IrRecord]) -> String { let element = match self.kind { ElemKind::Scalar(kind) => kind.rust_name().to_owned(), ElemKind::Record(index) => records .get(index as usize) .map(|r| r.name.clone()) .unwrap_or_else(|| "?".to_owned()), }; if self.list { format!("Vec<{element}>") } else { element } } } /// One `(record …)` declaration: flat, scalar fields (nested records and /// list fields are planned extensions). #[derive(Clone, Debug, PartialEq)] pub struct IrRecord { pub name: String, pub fields: Vec<(String, TypeKind)>, } #[derive(Clone, Debug, PartialEq)] pub struct IrProp { pub name: String, pub ty: ValueTy, pub default: Option, } #[derive(Clone, Debug, PartialEq)] pub struct IrOutput { pub name: String, pub ty: TypeKind, } #[derive(Clone, Debug, PartialEq)] pub struct IrState { pub name: String, pub ty: ValueTy, pub init: Literal, } /// What a node instantiates: a widget, another `.gdc` component (a /// capitalized name in widget position), or the component's slot — the point /// where an instance's children project in. #[derive(Clone, Debug, PartialEq)] pub enum IrWidget { /// A widget: the Rust type to build and everything this node gives it. /// One variant, because a builtin and a `.gdw`-declared widget are /// registered on the same terms — see [`IrWidgetNode`]. Widget(IrWidgetNode), Component(String), Slot, /// A structural `(if …)`: children are the branches, the condition is /// in [`IrNode::control`]. If, /// A keyed `(for …)`: the single child is the row template, the loop /// data is in [`IrNode::control`]. For, } impl IrWidget { /// The widget this node instantiates, if it instantiates one. pub fn widget(&self) -> Option<&IrWidgetNode> { match self { Self::Widget(widget) => Some(widget), Self::Component(_) | Self::Slot | Self::If | Self::For => None, } } } /// A widget as one node instantiates it: the Rust type to construct, and /// everything this node gives it. /// /// **This is the whole of how a widget is built, and there is one of it.** A /// `container` and an application's `markdown` arrive here identically — /// `Default + Widget` plus a setter per property — because that is the /// contract both are registered under. /// /// The registry's facts are resolved *in* here at lowering rather than left in /// a side table the way [`IrWidget::Component`] leaves a component's body in /// [`Compiled::components`]. The asymmetry is real and deliberate: a component /// is a recursive tree that must be keyed and shared, while a widget is flat /// leaf data. Copying it makes a node self-describing — a consumer needs no /// registry to emit one — and there is exactly one writer (validation), so the /// copies cannot drift. #[derive(Clone, Debug, PartialEq)] pub struct IrWidgetNode { /// The name written in the `.gdc` (`container`, `markdown`) — the key a /// theme rule matches on, and what diagnostics call it. pub name: String, /// The Rust path to construct, verbatim from the descriptor or the /// manifest's `(type "…")`. The compiler never resolves it; rustc does, /// against the generated construction. pub type_path: String, /// The properties this node gives the widget, in written order, followed /// by the one the framework supplies (a menu's submenu-ness). This node's /// children are *not* here — they live in [`IrNode::children`], and the tree /// hands every widget its content builder over them. pub props: Vec, /// The declared event wires this node makes. Builtin event wires are /// [`IrNode::events`]: those are keyed by the closed [`EventProp`] set, /// whose payload contract the registry knows. pub events: Vec, /// The declared query wires this node makes — `:get-image (load payload)`. /// A query's handler returns a value the widget consumes, so codegen /// installs it as a resolver rather than dispatching it as an event. pub queries: Vec, } impl IrWidgetNode { /// The value this node gives one property, if it gives it one. pub fn prop(&self, name: &str) -> Option<&IrWidgetProp> { self.props.iter().find(|prop| prop.name == name) } } /// One property given to a widget. #[derive(Clone, Debug, PartialEq)] pub struct IrWidgetProp { /// The name the registry declares and the `.gdc` writes (`corner-radius`, /// `source`). pub name: String, /// The Rust setter to call — a descriptor's method, a manifest's /// `:setter`, or the `set_` convention both default to. pub setter: String, /// The declared type, so a consumer can emit the value at the width the /// setter takes. **The one place a value's width is decided**, which is /// what stops two emitters from disagreeing about a cast. pub ty: PropTy, pub value: IrPropValue, } /// What a property is set from. #[derive(Clone, Debug, PartialEq)] pub enum IrPropValue { /// A compile-time constant: set once, at construction. Static(Literal), /// A reactive expression: re-applied through the setter when a /// referenced signal changes. Binding(Expr), } /// One event wire on a `.gdw`-declared widget — `:on-link navigate` or /// `:on-link (navigate payload page.id)`. /// /// Shaped like [`IrEvent`] but not it: that one is keyed by the closed /// [`EventProp`] set, whose payload contract the registry knows, while this /// one carries the name and payload its manifest declared. #[derive(Clone, Debug, PartialEq)] pub struct IrUserEvent { /// The event name the manifest declares and the `.gdc` writes (`on-link`). pub name: String, /// The value the widget delivers to the handler, if any. pub payload: Option, pub handler: String, /// Invocation arguments; empty for the bare-name form. pub args: Vec, } /// One query wire on a `.gdw`-declared widget — `:get-image (load payload)`. /// /// Unlike [`IrUserEvent`], the handler *returns* a value; codegen installs it /// as a resolver closure through [`setter`](Self::setter), so the widget calls /// it synchronously. #[derive(Clone, Debug, PartialEq)] pub struct IrQuery { /// The query name the manifest declares and the `.gdc` writes (`get-image`). pub name: String, /// The widget method that installs the resolver. pub setter: String, /// The value handed to the handler — the thing being resolved. pub payload: TypeKind, /// The Rust path the handler returns, verbatim from the manifest. pub returns: String, /// The handler to call. It receives exactly the value being resolved (the /// query's `payload`); anything else it needs it reads from `cx`, which /// keeps the compiled and dev-interpreter paths identical. pub handler: String, } /// Whether a component declares a slot (instances may then pass children). pub fn has_slot(ir: &Ir) -> bool { ir.nodes.iter().any(|node| node.widget == IrWidget::Slot) } #[derive(Clone, Debug, PartialEq)] pub struct IrNode { pub widget: IrWidget, /// Style classes for theme rule matching, from `:class "a b"`. pub classes: Vec, pub style: StyleIr, /// Event wires to declared handlers. pub events: Vec, /// Whether this widget takes initial focus at mount (`:autofocus true`). pub autofocus: bool, /// `:enabled expr` — a boolean expression driving the widget's enabled /// flag (disabling is inherited by the subtree). `None` means always /// enabled. Universal: valid on every builtin and on instances (where /// it applies to the instance root). pub enabled: Option, /// `:focused expr` — a boolean expression driving whether this widget /// holds keyboard focus. `None` leaves focus to the tree (clicks, Tab, /// and the focus invariant). Universal. /// /// Controlled, like `:checked` and `:open`: the application owns the /// state, the widget never flips it, and `:on-focus-change` reports every /// transition so the state can stay true. pub focused: Option, /// `:tooltip "text"` — shown after the pointer rests on this widget. /// Universal. pub tooltip: Option, /// A menu item's keyboard accelerator (`:accel "Ctrl+S"`), parsed. /// /// The *keystroke* half: an accelerator fires while its menu is closed, so /// the tree registers it at mount, beside the menu, where the row does not /// exist. The text half is an ordinary property of the row, and is in /// [`IrWidgetNode::props`] like any other. Meaningless on other widgets. pub accel: Option, /// Props given to a component instance, as expressions evaluated in the /// *parent's* scope; omitted defaulted props come from the child's own /// declarations. Meaningless on builtin widgets. pub component_props: Vec<(String, Expr)>, /// Child-output → parent-handler wires on a component instance — /// `:on-changed apply`, or an invocation `(apply payload todo.id)` /// whose arguments evaluate at dispatch. Meaningless on builtin /// widgets. pub component_outputs: Vec, /// Explicit instance identity (`:key "left"`) for state matching across /// hot reloads; unkeyed instances match by occurrence order among their /// unkeyed same-type siblings. Meaningless on builtin widgets. pub component_key: Option, /// The structural data of an [`IrWidget::If`] or [`IrWidget::For`] /// node; `None` for everything else. pub control: Option, pub children: Vec, } /// The data of a structural control node. #[derive(Clone, Debug, PartialEq)] pub enum ControlIr { /// `(if cond then else?)` — `children[0]` is the then-branch, /// `children[1]` (when present) the else-branch. If { cond: Expr }, /// `(for var list :index i :key expr body)` — `children[0]` is the row /// template. For { var: String, index: Option, /// The list to iterate: the name of a `(List T)` prop or state. list: String, /// The element type of that list. element: ElemKind, /// Row identity; `None` keys by position. key: Option, }, } /// One instance output wire. #[derive(Clone, Debug, PartialEq)] pub struct IrOutputWire { pub output: String, pub handler: String, /// Invocation arguments; empty for the bare-name form (which passes /// the output's payload as the single argument). pub args: Vec, } /// One event wire: `:on-click increment` (no args) or /// `:on-click (remove todo.id)` (arguments evaluated at dispatch time, in /// the node's scope). #[derive(Clone, Debug, PartialEq)] pub struct IrEvent { pub event: EventProp, pub handler: String, pub args: Vec, } /// Which axes a scroll area scrolls — the value of an `:axis` property. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ScrollAxesIr { Vertical, Horizontal, Both, } /// A resolved compile-time constant. #[derive(Clone, Debug, PartialEq)] pub enum Literal { Int(i64), Float(f64), Bool(bool), Str(String), /// sRGB 8-bit color, from `"#rrggbb"` / `"#rrggbbaa"`. Color(u8, u8, u8, u8), /// `(list …)` of literal elements, for `(List T)` defaults and inits. List(Vec), /// A record literal, fields in declaration order after validation. Record { name: String, fields: Vec<(String, Literal)>, }, /// A vector path — `(path …)` or `(svg-path "…")` — as a sequence of /// drawing commands in a normalized coordinate space (the consumer /// scales them to the element it decorates). Path(Vec), /// Rich text — `(rich "Hi " (bold "there"))` — flattened to runs. The /// nesting is gone by here: a consumer gets a string and the ranges that /// differ from it, and nothing walks a tree at runtime. Rich(Vec), /// An asset reference — `(asset "path")` — carrying the path exactly as /// written. Nothing here says what kind of asset it is: the slot's type /// decides what the bytes mean, and resolving them against the asset /// search path belongs to the consumer, which keeps this compiler /// filesystem-free. Asset(String), /// Which axes a scroll area scrolls, from `:axis vertical|horizontal|both`. ScrollAxes(ScrollAxesIr), } /// One command of a vector [`Literal::Path`]. Coordinates are `f64` in the /// path's own space; there is no implicit unit — a theme conventionally /// draws marks in the unit square `[0,1]×[0,1]` and lets the widget scale. /// One styled run of rich text: its characters, and what differs from the /// paragraph's own style. Styles are already resolved through the nesting. #[derive(Clone, Debug, PartialEq)] pub struct RichRun { pub text: String, pub bold: bool, pub italic: bool, pub underline: bool, pub strikethrough: bool, /// sRGB 8-bit. pub color: Option<(u8, u8, u8, u8)>, pub size: Option, /// What `(link "target" …)` this run is inside, if any — the string the /// paragraph hands back on `:on-link`. Its meaning is the application's: /// nothing here parses it, because nothing here knows whether it is a /// URL, a page name, or a record id. pub link: Option, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum PathCmd { /// Start a new subpath at `(x, y)`. Move(f64, f64), /// Straight line to `(x, y)`. Line(f64, f64), /// Quadratic Bézier through control `(cx, cy)` to `(x, y)`. Quad(f64, f64, f64, f64), /// Cubic Bézier through controls `(c1x, c1y)`, `(c2x, c2y)` to `(x, y)`. Cubic(f64, f64, f64, f64, f64, f64), /// Close the current subpath. Close, } /// The static layout style of one node — a dependency-free mirror of the /// taffy properties the surface grammar exposes. #[derive(Clone, Debug, Default, PartialEq)] pub struct StyleIr { pub width: Option, pub height: Option, /// Uniform padding, logical px. pub padding: Option, /// Uniform gap, logical px. pub gap: Option, pub direction: Option, pub align_items: Option, pub justify_content: Option, pub align_self: Option, pub grow: Option, pub shrink: Option, pub basis: Option, } #[derive(Copy, Clone, Debug, PartialEq)] pub enum DimIr { Px(f32), Percent(f32), Auto, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DirectionIr { Row, Column, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum AlignIr { Start, End, Center, Stretch, } /// Parse `#rgb`, `#rrggbb`, or `#rrggbbaa`. pub fn parse_color(text: &str) -> Option { let hex = text.strip_prefix('#')?; let component = |s: &str| u8::from_str_radix(s, 16).ok(); match hex.len() { 3 => { let mut it = hex.chars().map(|c| component(&format!("{c}{c}"))); Some(Literal::Color(it.next()??, it.next()??, it.next()??, 255)) } 6 | 8 => { let r = component(&hex[0..2])?; let g = component(&hex[2..4])?; let b = component(&hex[4..6])?; let a = if hex.len() == 8 { component(&hex[6..8])? } else { 255 }; Some(Literal::Color(r, g, b, a)) } _ => None, } }