Component Type System

Overview

WIP. The component type system is the unit of azul's GUI builder: a typed bundle of (data model, render function, compile function, scoped CSS, source kind), described by ComponentDef and friends in core/src/xml.rs. Every type is #[repr(C)], so libraries can be authored, exported, and re-imported across the FFI boundary. The runtime types are stable and used today by the live preview, the compile-to-source pipeline, and the multi-language code generator. The debug-server JSON serialiser and the render-function call signature still flatten field types to strings and pass &ComponentDataModel (with default values doubling as current values) instead of the eventual ComponentFieldNamedValueVec. Both are mechanical changes pending consolidation.

The type system does three jobs at once. It describes the class of input a component accepts (what ComponentFieldType does), the literal default for each field (ComponentDefaultValue), and the runtime instance the component is rendered with (ComponentFieldValue). Together they let the same ComponentDef drive the in-app preview, the JSON / XML round-trip, the codegen emitter that produces standalone Rust / C / C++ / Python source, and the debug server's introspection UI.

This page describes the shape of those types and how they interact, then lists the procedures for adding a new built-in HTML element or a new field-type variant.

ComponentDef

ComponentDef is the component itself:

#[repr(C)]
pub struct ComponentDef {
    pub id: ComponentId,
    pub display_name: AzString,
    pub description: AzString,
    pub css: AzString,
    pub source: ComponentSource,
    pub data_model: ComponentDataModel,
    pub render_fn: ComponentRenderFn,
    pub compile_fn: ComponentCompileFn,
    pub render_fn_source: OptionString,
    pub compile_fn_source: OptionString,
}

id = ComponentId { collection, name } qualifies the component as "collection:name": "builtin:div", "shadcn:avatar", "myproject:user_card". data_model is the typed input struct; render_fn and compile_fn are the only behavioural attachments. render_fn_source / compile_fn_source carry the originating source code for UserDefined components so the debugger can show and re-export them.

The earlier proposal had separate parameters, callback_slots, accepts_text, child_policy, and template fields; all were folded into data_model. A field of type StyledDom is a child slot, a field of type Callback(...) is a callback slot, and child acceptance is derived from the model's shape. The template was dropped in favour of source-edit-recompile.

ComponentFieldType

ComponentFieldType is the per-field type descriptor:

#[repr(C, u8)]
pub enum ComponentFieldType {
    String, Bool,
    I32, I64, U32, U64, Usize,
    F32, F64,
    ColorU,
    CssProperty,
    ImageRef, FontRef,
    /// StyledDom slot — field name = slot name
    StyledDom,
    /// Callback with typed signature
    Callback(ComponentCallbackSignature),
    /// RefAny data binding with type hint
    RefAny(AzString),
    /// Optional value (recursive via Box)
    OptionType(ComponentFieldTypeBox),
    /// Vec of values (recursive via Box)
    VecType(ComponentFieldTypeBox),
    /// Reference to a struct defined in the same library
    StructRef(AzString),
    /// Reference to an enum defined in the same library
    EnumRef(AzString),
}

The variants split into five groups:

  • Primitives. String, Bool, the eight numeric widths, and ColorU. They map straight to a Rust / C / Python primitive in code generation.
  • Azul-specific. CssProperty, ImageRef, and FontRef. Pre-declared azul types the editor can offer pickers for.
  • Slot. StyledDom. The field name is the slot name; there is no separate string. The debugger renders this as a drop-zone, and render_fn reads the embedded child subtree.
  • Callback. Callback(ComponentCallbackSignature) carries a return_type: AzString and args: ComponentCallbackArgVec. The &mut RefAny and &mut CallbackInfo arguments are implicit and not stored in args.
  • References. RefAny(type_hint), StructRef(name), and EnumRef(name) resolve names against ComponentLibrary::data_models and ComponentLibrary::enum_models.
  • Containers. OptionType(Box) and VecType(Box) recurse via ComponentFieldTypeBox. The names are OptionType / VecType in code (not bare Option / Vec) to avoid clashing with core::option::Option and alloc::vec::Vec.

A Box<ComponentFieldType> doesn't survive the C ABI, so the recursion is broken with a raw pointer plus a hand-rolled Drop / Clone / Hash:

#[repr(C)]
pub struct ComponentFieldTypeBox {
    pub ptr: *mut ComponentFieldType,
}

ComponentFieldType::parse and the serde implementation are the only producers; both go through ComponentFieldTypeBox::new, which Box::into_raws the inner value. ComponentFieldValueBox uses the same pattern for the runtime-value enum.

Data model and fields

#[repr(C)]
pub struct ComponentDataModel {
    pub name: AzString,
    pub description: AzString,
    pub fields: ComponentDataFieldVec,
}

#[repr(C)]
pub struct ComponentDataField {
    pub name: AzString,
    pub field_type: ComponentFieldType,
    pub default_value: OptionComponentDefaultValue,
    pub required: bool,
    pub description: AzString,
}

The model's name is what the code generator emits as the input struct name (e.g. "AvatarData"). Two convenience helpers exist on ComponentDataModel: get_field(name) -> Option<&ComponentDataField> and with_default(name, value) -> Self for builder-style override.

A library can also expose shared struct types in ComponentLibrary::data_models. Components reference them via StructRef(name). Each component still has its own main data_model on ComponentDef; the library list is for types reused across multiple components (e.g. UserProfile).

ComponentDefaultValue

#[repr(C, u8)]
pub enum ComponentDefaultValue {
    None,
    String(AzString),
    Bool(bool),
    I32(i32), I64(i64), U32(u32), U64(u64), Usize(usize),
    F32(f32), F64(f64),
    ColorU(ColorU),
    /// Default is an instance of another component
    ComponentInstance(ComponentInstanceDefault),
    /// Default callback function pointer name
    CallbackFnPointer(AzString),
    /// JSON string representing a complex default value
    Json(AzString),
}

The variants line up with ComponentFieldType for primitives. ComponentInstance carries a library, component, and a list of ComponentFieldOverride { field_name, source } so a StyledDom slot can default to a sub-component already configured. CallbackFnPointer is a fully-qualified function name ("my_app::handlers::on_click"); compiled components resolve it via dladdr, dynamic components emit it as a code-gen use import. Json is the escape hatch for complex defaults that don't fit any other variant. The serializer re-parses and re-emits the JSON in place via serde_json_default.

ComponentFieldValueSource: Literal vs Binding

#[repr(C, u8)]
pub enum ComponentFieldValueSource {
    Default,
    Literal(AzString),
    Binding(AzString),
}

This is the per-instance counterpart of ComponentDefaultValue. The debugger has two views:

  • Component preview. Fields use Default or Literal. Hardcoded values, instantiated for visual testing.
  • Application composition. Fields can also use Binding("app_state.user.name"), a dotted path resolved against the application's RefAny state. Auto-complete uses the type-hint on the bound RefAny plus any StructRef-defined fields.

ComponentFieldOverride { field_name, source } carries one of these for each overridden field of a default component instance.

ComponentFieldValue: the runtime value enum

#[repr(C, u8)]
pub enum ComponentFieldValue {
    String(AzString), Bool(bool),
    I32(i32), /* …same numeric set as ComponentDefaultValue … */
    ColorU(ColorU),
    None,
    Some(ComponentFieldValueBox),
    Vec(ComponentFieldValueVec),
    StyledDom(StyledDom),
    Struct(ComponentFieldNamedValueVec),
    Enum { variant: AzString, fields: ComponentFieldNamedValueVec },
    Callback(AzString),
    RefAny(RefAny),
}

Where ComponentFieldType is the class, ComponentFieldValue is the instance. It carries actual StyledDom subtrees, RefAnys, and resolved literals. The intended render-function signature passes a ComponentFieldNamedValueVec of these; the current signature still passes a &ComponentDataModel with default_value doubling as „current value“. See the divergence note at the end of the page.

ComponentSource

#[repr(C)]
pub enum ComponentSource {
    Builtin,
    Compiled,
    UserDefined,
}

Drives editability in the debugger. Builtin (the HTML elements baked into the DLL) and Compiled (Rust widgets like Button, TextInput) are read-only. Their data model is generated from compiled code, and the type cannot be edited. UserDefined components are JSON / XML-imported or built in the debugger and are fully editable. The exportable flag on ComponentLibrary follows: builtin and compiled libraries are not exportable; user-defined libraries are.

ComponentRenderFn and ComponentCompileFn

pub type ComponentRenderFn = fn(
    &ComponentDef,
    &ComponentDataModel,
    &ComponentMap,
) -> ResultStyledDomRenderDomError;

pub type ComponentCompileFn = fn(
    &ComponentDef,
    &CompileTarget,
    &ComponentDataModel,
    indent: usize,
) -> ResultStringCompileError;

render_fn is the live-rendering path; compile_fn emits source code for one of CompileTarget::{Rust, C, Cpp, Python}. Both are bare fn pointers (not closures), which keeps ComponentDef #[repr(C)] and FFI-safe. The &ComponentMap argument lets the function recursively look up sub-components.

For Builtin HTML elements, builtin_render_fn calls tag_to_node_type(def.id.name) to map "div", "a", "button", and so on to NodeType, builds a Dom, and styles it with the per-component def.css. builtin_compile_fn emits Dom::create_node(NodeType::Div) (Rust), AzDom_createDiv() (C), Dom::create_div() (C++), or Dom.div() (Python).

For user-defined components, user_defined_render_fn is a generic walker: it iterates data.fields, dispatches on each default_value variant, and for ComponentInstance looks up the sub-component in the ComponentMap and recurses. The def.css string is parsed via Css::from_string(...) and applied to the wrapper.

ComponentLibrary and ComponentMap

#[repr(C)]
pub struct ComponentLibrary {
    pub name: AzString,
    pub version: AzString,
    pub description: AzString,
    pub components: ComponentDefVec,
    pub exportable: bool,
    pub modifiable: bool,
    pub data_models: ComponentDataModelVec,
    pub enum_models: ComponentEnumModelVec,
}

#[repr(C)]
pub struct ComponentMap {
    pub libraries: ComponentLibraryVec,
}

The map provides several lookup strategies. ComponentMap::get(collection, name) is fully qualified and searches the named library only. get_unqualified(name) always searches "builtin". get_by_qualified_name("a:b") splits at : and falls back to unqualified. get_exportable_libraries() filters for exportable == true. all_components() returns a flat-mapped iterator over every library.

The "builtin" library is built once by build_builtin_library and registers every HTML element via builtin_component_def(tag, display_name, default_text, css), plus three control-flow components (if, for, map).

Type-string parser and serde format

ComponentFieldType::parse(s) accepts a small grammar: String, Option<Bool>, Vec<I32>, Callback(fn(...) -> Update), RefAny(MyAppData), EnumRef(ButtonVariant), StructRef(UserProfile). It's the entry point for XML args="..." attributes and the debugger „add field“ dialog. The inverse field_type_to_string() (serde-only) flattens back to the same syntax for export.

When the serde-json feature is enabled, ComponentDataModel, ComponentDataField, ComponentFieldType, and ComponentDefaultValue round-trip through JSON. Field types currently serialise as strings ("Option<String>"); the planned structured-JSON format from the design doc is not implemented.

Adding a new built-in HTML element

  1. Add a variant to NodeType.
  2. Add the tag arm to tag_to_node_type and the corresponding tag_to_nodetypetag.
  3. Add a builtin_component_def("tag", "Display Name", default_text, css) line to build_builtin_library.
  4. If the tag has a UA-CSS default, add an arm in apply_ua_css_to_compact. See Cascade, Inheritance, Restyle.
  5. Update the XML parser's tag table in layout/src/xml/mod.rs so XML / XHTML round-trips.

Adding a new field-type variant

  1. Add a variant to ComponentFieldType. Keep #[repr(C, u8)] discipline. No Box<T> — use ComponentFieldTypeBox for recursion.
  2. Add the parse arm in ComponentFieldType::parse and the inverse in field_type_to_string.
  3. Add a matching ComponentDefaultValue variant if the new type has a literal default form, and a ComponentFieldValue runtime variant.
  4. Update user_defined_render_fn and user_defined_compile_fn so the generic preview / compile paths know how to project it.
  5. Regenerate api.json so the new variant crosses the FFI boundary.

Divergences from the design doc

The design intent in scripts/COMPONENT_TYPE_SYSTEM_DESIGN.md is mostly implemented. Three deltas to be aware of:

  1. Render-function signature. The doc proposes &ComponentFieldNamedValueVec (actual runtime values). The implementation still passes &ComponentDataModel and treats default_value on each field as the current value. The runtime-value types exist (ComponentFieldValue, ComponentFieldNamedValue) but are not wired through render_fn yet.
  2. Variant names. The doc says Option(Box<...>) and Vec(Box<...>); the code uses OptionType and VecType to avoid colliding with core::option::Option / alloc::vec::Vec in scope.
  3. Debug-server JSON. The doc specifies structured JSON for field types; the current field_type_to_string() flattens to "Option<String>"-style strings, and the debugger re-parses them client-side. The serde implementation matches the legacy string form.

Code that constructs ComponentDef values must build the data model with the current value pre-baked into default_value. The ComponentDataModel::with_default(name, value) builder is the supported way to do it.

See also

Coming Up Next

Back to guide index