oxiforge 0.4.0

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

[![CI](https://github.com/emobotics-dev/oxiforge/actions/workflows/ci.yml/badge.svg)](https://github.com/emobotics-dev/oxiforge/actions/workflows/ci.yml)
[![License: GPL-3.0-only](https://img.shields.io/badge/license-GPL--3.0--only-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)

YAML-to-Rust code generator for [oxivgl](https://github.com/emobotics-dev/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](https://github.com/emobotics-dev/oxivgl),
a safe Rust binding layer for [LVGL 9.5](https://lvgl.io). 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.

```yaml
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

- **Flex**`layout: flex`, with `flex_flow`, `flex_align_main/cross/track`, `flex_grow`
- **Grid**`layout: 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 styles**`state_pressed:`, `state_checked:`, etc.
- **Widget parts**`part_indicator:`, `part_knob:`, etc.
- **Gradients** — named `gradients:` definitions referenced via `bg_grad:`

### Events, views & toasts

- **Event bindings**`on: { clicked: my_handler }` or a declarative
  `{ show_toast: <id>, args: [...] }`
- **View codegen**`generate_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 budgets**`budget: { max_objects, max_depth }` asserts a
  per-view widget census in debug builds

Full property reference: [`docs/yaml-language-spec.md`](docs/yaml-language-spec.md).
A generated JSON Schema ([`schemas/oxiforge.schema.json`](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`.

```rust
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.

```rust
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

```toml
# 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`

```rust
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:

```rust
// 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/`](examples/),
built on the [`m5stack-core`](https://github.com/emobotics-dev/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:

```sh
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/`](examples/arc_gauge/) for a minimal working example,
[`examples/keypad_menu/`](examples/keypad_menu/) for event bindings and
keypad-navigable input groups, or [`examples/meter_view/`](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 *against* — [oxivgl](https://github.com/emobotics-dev/oxivgl) —
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.