# oxiforge
[](https://github.com/emobotics-dev/oxiforge/actions/workflows/ci.yml)
[](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.4"
[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
```sh
cargo +esp -Zbuild-std=alloc,core run --release
```
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.