oxiforge 0.4.0

YAML-to-Rust code generator for oxivgl LVGL UIs
Documentation

oxiforge

CI License: GPL-3.0-only

YAML-to-Rust code generator for oxivgl LVGL 9.5 UIs.

Describe your UI in YAML, oxiforge generates safe Rust code that calls oxivgl widget constructors at build time. Designed for no_std embedded targets (ESP32, etc.) but works on any platform oxivgl supports.

Concept

You describe your UI as a YAML file — screens, widgets, styles, layouts. During cargo build, oxiforge reads that YAML and generates Rust source code that creates the UI through oxivgl, a safe Rust binding layer for LVGL 9.5. The generated code is pulled into your crate via include!(), so it compiles as part of your application — no runtime YAML parsing, no interpreter overhead.

Under the hood, LVGL is a C library with its own object ownership: parents own their children, and widgets store raw pointers to styles. oxiforge bridges this gap by retaining every widget wrapper in the generated <Page>Widgets struct (id:-tagged widgets as public fields, decorative ones privately) and sharing one Rc-backed Style per distinct treatment — so the LVGL object tree stays intact while view teardown still frees every style and widget through normal Rust drop semantics. Nothing is leaked or forgotten.

YAML format

A UI file has one top-level lvgl: key containing pages, style definitions, gradients, and an optional theme.

lvgl:
  default_font: montserrat_16

  style_definitions:
    - id: card
      bg_color: 0x1E1E1E
      radius: 8
      pad_all: 10

  pages:
    - id: dashboard
      bg_color: 0x121212
      layout: flex
      flex_flow: column
      widgets:
        - label:
            text: "Battery: 13.8V"
            styles: card
            text_color: 0x4CAF50
        - arc:
            id: soc
            width: 140
            height: 140
            min_value: 0.0
            max_value: 100.0
            value: 75.0
            arc_width: 12
            part_indicator:
              arc_color: 0x4CAF50

Supported widgets

Core (always available): obj, label, value_label, button, switch, checkbox, slider, arc, bar, dropdown, roller, scale, led, image, line

Feature-gated (all on by default via all-widgets): spinner, spinbox, textarea, keyboard, chart, tabview, arc_label, calendar, list, table, tileview, win, msgbox, spangroup, buttonmatrix, canvas, imagebutton, anim_img, menu

Layouts

  • Flexlayout: flex, with flex_flow, flex_align_main/cross/track, flex_grow
  • Gridlayout: grid, with grid_columns, grid_rows, grid_cell on children

Styles

  • Inline — set properties directly on any widget (incl. background images: bg_image_src + recolor/tile props)
  • Style definitions — reusable named batches, applied via styles: name or styles: [a, b]
  • State stylesstate_pressed:, state_checked:, etc.
  • Widget partspart_indicator:, part_knob:, etc.
  • Gradients — named gradients: definitions referenced via bg_grad:

Events, views & toasts

  • Event bindingson: { clicked: my_handler } or a declarative { show_toast: <id>, args: [...] }
  • View codegengenerate_view: true wraps each page in an oxivgl::view::View impl with update_fn / on_event_fn hooks and keypad/encoder input_group support
  • Toasts — a toasts: section (plus toast_presets: [info, warn, error]) generates passive overlay views with raise helpers, parameterizable via params + bind_text
  • Resource budgetsbudget: { max_objects, max_depth } asserts a per-view widget census in debug builds

Full property reference: docs/yaml-language-spec.md. A generated JSON Schema (schemas/oxiforge.schema.json) gives editor autocomplete/validation via yaml-language-server.

Rust API

generate_from_str

Parse + validate + generate in memory. Returns generated Rust source as String.

let yaml = std::fs::read_to_string("ui.yaml").unwrap();
let code = oxiforge::generate_from_str(&yaml).unwrap();

generate

Parse + validate + generate from file, write .rs to output directory. Emits cargo::rerun-if-changed for build.rs use.

oxiforge::generate(
    std::path::Path::new("ui.yaml"),
    std::path::Path::new(&out_dir),
).unwrap();

Error reporting

Unknown YAML keys produce fuzzy "did you mean?" suggestions:

error: unknown property 'background_color'
  hint: did you mean 'bg_color'?

Project integration

1. Add dependencies

# Cargo.toml
[dependencies]
oxivgl = "0.6"

[build-dependencies]
oxiforge = "0.3"

Working inside a checkout of this repository (e.g. the bundled examples)? Replace each line with { path = "../oxiforge" } / { path = "../oxivgl" } as appropriate.

2. Create build.rs

fn main() {
    let yaml = std::path::Path::new("ui.yaml");
    let out_dir = std::env::var("OUT_DIR").unwrap();
    oxiforge::generate(yaml, std::path::Path::new(&out_dir))
        .expect("oxiforge codegen failed");
}

3. Include generated code

The generated file defines items (functions and structs), so include!() it at module level:

// main.rs
extern crate alloc;  // required for no_std targets

use oxivgl::prelude::*;

include!(concat!(env!("OUT_DIR"), "/ui.rs"));

fn build_ui(container: &Obj<'static>) -> Result<(), WidgetError> {
    // One create_<page_id>() per YAML page. Pages with id-tagged widgets
    // return a <PageId>Widgets struct holding handles for runtime updates —
    // keep it alive as long as the view exists.
    let widgets = create_dashboard(container)?;
    widgets.soc.set_value(75.0);
    Ok(())
}

With generate_view: true, each page additionally gets an oxivgl::view::View impl ready for oxivgl's navigator.

Building for ESP32 / ESP32-S3

The bundled examples form their own cargo workspace under examples/, built on the m5stack-core BSP. Each carries a fire27 / cores3 feature that selects the board, and puts LVGL's heap in PSRAM via oxivgl::mem. Run scripts pick the target and feature for you:

cd examples
./run_fire27.sh meter_view   # M5Stack Fire  (ESP32)   flash + monitor
./run_cores3.sh meter_view   # M5Stack CoreS3 (ESP32-S3) flash + monitor
./run_host.sh   meter_view   # host (SDL2 window), LVGL's internal pool

See examples/arc_gauge/ for a minimal working example, examples/keypad_menu/ for event bindings and keypad-navigable input groups, or examples/meter_view/ for a more substantial alternator-regulator dashboard (arc + scale + mode pill + battery/engine/field panels) ported from a real firmware screen.

License

oxiforge is licensed under GPL-3.0-only. This is a deliberate choice and has consequences worth understanding before you adopt it.

oxiforge runs as a build-dependency: it reads your YAML at build time and emits Rust source that your crate include!()s. That generated source is then compiled and linked into your binary. Under the GPL, code derived from a GPL-licensed work is itself bound by the GPL — and the source oxiforge produces qualifies as a derived work of oxiforge. The practical consequence is that a crate which include!()s oxiforge-generated code, and the binary built from it, must also be distributed under a GPL-compatible license.

This is intentional. If you want to ship a closed-source product that uses oxiforge, that license is not available off-the-shelf; please get in touch.

The runtime library oxiforge generates code againstoxivgl — is dual-licensed MIT OR Apache-2.0 and is unaffected by this. You can use oxivgl directly (writing widget code by hand) in a proprietary project without GPL obligations; only the YAML-to-Rust code path through oxiforge is GPL.