use std::{collections::HashSet, fmt::Write};
use crate::model::*;
fn parse_custom_font(font: &str) -> Option<&str> {
let s = font.strip_prefix("custom(")?;
let name = s.strip_suffix(')')?;
if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
Some(name)
}
fn font_expr(font: &str) -> String {
if let Some(name) = parse_custom_font(font) {
format!("FONT_{}", name.to_uppercase())
} else {
format!("oxivgl::fonts::{}", font.to_uppercase())
}
}
fn collect_custom_fonts(doc: &UiDoc) -> Vec<String> {
let mut fonts = Vec::new();
let mut seen = HashSet::new();
if let Some(defs) = &doc.lvgl.style_definitions {
for def in defs {
check_font_str(&def.style.text_font, &mut fonts, &mut seen);
}
}
for page in &doc.lvgl.pages {
collect_fonts_from_common(&page.common, &mut fonts, &mut seen);
}
fonts
}
fn check_font_str(font: &Option<String>, fonts: &mut Vec<String>, seen: &mut HashSet<String>) {
if let Some(f) = font
&& let Some(name) = parse_custom_font(f)
&& seen.insert(name.to_owned())
{
fonts.push(name.to_owned());
}
}
fn collect_image_symbols(doc: &UiDoc) -> Vec<String> {
let mut syms = Vec::new();
let mut seen = HashSet::new();
if let Some(defs) = &doc.lvgl.style_definitions {
for def in defs {
collect_bg_image_sym(&def.style, &mut syms, &mut seen);
}
}
for page in &doc.lvgl.pages {
collect_image_syms_from_common(&page.common, &mut syms, &mut seen);
}
if let Some(toasts) = &doc.lvgl.toasts {
for toast in toasts {
collect_image_syms_from_common(&toast.common, &mut syms, &mut seen);
}
}
syms
}
#[cfg(feature = "widget-animimg")]
fn doc_uses_animimg_frames(doc: &UiDoc) -> bool {
fn widgets_use_frames(widgets: &[WidgetKind]) -> bool {
widgets.iter().any(|w| {
if let WidgetKind::AnimImg(p) = w
&& p.frames.as_deref().is_some_and(|f| !f.is_empty())
{
return true;
}
w.common().widgets.as_deref().is_some_and(widgets_use_frames)
})
}
let pages = doc.lvgl.pages.iter().map(|p| &p.common);
let toasts = doc.lvgl.toasts.iter().flatten().map(|t| &t.common);
pages.chain(toasts).any(|c| c.widgets.as_deref().is_some_and(widgets_use_frames))
}
fn collect_bg_image_sym(style: &StyleProps, syms: &mut Vec<String>, seen: &mut HashSet<String>) {
if let Some(sym) = &style.bg_image_src
&& seen.insert(sym.clone())
{
syms.push(sym.clone());
}
}
fn collect_image_syms_from_common(common: &CommonProps, syms: &mut Vec<String>, seen: &mut HashSet<String>) {
collect_bg_image_sym(&common.style, syms, seen);
for nested in [
&common.state_default,
&common.state_pressed,
&common.state_focused,
&common.state_checked,
&common.state_disabled,
&common.state_scrolled,
&common.part_indicator,
&common.part_knob,
&common.part_items,
&common.part_scrollbar,
&common.part_selected,
&common.part_cursor,
]
.into_iter()
.flatten()
{
collect_bg_image_sym(nested, syms, seen);
}
if let Some(widgets) = &common.widgets {
collect_image_syms_from_widgets(widgets, syms, seen);
}
}
fn collect_image_syms_from_widgets(widgets: &[WidgetKind], syms: &mut Vec<String>, seen: &mut HashSet<String>) {
for w in widgets {
match w {
#[cfg(feature = "widget-animimg")]
WidgetKind::AnimImg(p) => {
if let Some(frames) = &p.frames {
for f in frames {
if seen.insert(f.clone()) {
syms.push(f.clone());
}
}
}
}
#[cfg(feature = "widget-imagebutton")]
WidgetKind::Imagebutton(p) => {
for src in [&p.src_released, &p.src_pressed, &p.src_disabled].into_iter().flatten() {
for name in [&src.left, &src.mid, &src.right].into_iter().flatten() {
if seen.insert(name.clone()) {
syms.push(name.clone());
}
}
}
}
_ => {}
}
collect_image_syms_from_common(w.common(), syms, seen);
}
}
fn collect_referenced_styles(common: &CommonProps) -> Vec<String> {
let mut ids = Vec::new();
let mut seen = HashSet::new();
collect_style_refs_from_common(common, &mut ids, &mut seen);
ids
}
fn collect_style_refs_from_common(common: &CommonProps, ids: &mut Vec<String>, seen: &mut HashSet<String>) {
let add = |style_ref: &Option<StyleRef>, ids: &mut Vec<String>, seen: &mut HashSet<String>| {
if let Some(r) = style_ref {
for name in r.names() {
if seen.insert(name.to_string()) {
ids.push(name.to_string());
}
}
}
};
add(&common.styles, ids, seen);
for part in [
&common.part_indicator,
&common.part_knob,
&common.part_items,
&common.part_scrollbar,
&common.part_selected,
&common.part_cursor,
]
.into_iter()
.flatten()
{
add(&part.styles, ids, seen);
}
if let Some(widgets) = &common.widgets {
for w in widgets {
collect_style_refs_from_common(w.common(), ids, seen);
}
}
}
fn collect_fonts_from_common(common: &CommonProps, fonts: &mut Vec<String>, seen: &mut HashSet<String>) {
check_font_str(&common.style.text_font, fonts, seen);
for s in [
&common.state_default,
&common.state_pressed,
&common.state_focused,
&common.state_checked,
&common.state_disabled,
&common.state_scrolled,
]
.into_iter()
.flatten()
{
check_font_str(&s.text_font, fonts, seen);
}
for s in [
&common.part_indicator,
&common.part_knob,
&common.part_items,
&common.part_scrollbar,
&common.part_selected,
&common.part_cursor,
]
.into_iter()
.flatten()
{
check_font_str(&s.text_font, fonts, seen);
}
if let Some(widgets) = &common.widgets {
for w in widgets {
collect_fonts_from_common(w.common(), fonts, seen);
#[cfg(feature = "widget-tabview")]
if let WidgetKind::Tabview(p) = w
&& let Some(tabs) = &p.tabs
{
for tab in tabs {
collect_fonts_from_common(&tab.common, fonts, seen);
}
}
#[cfg(feature = "widget-tileview")]
if let WidgetKind::Tileview(p) = w
&& let Some(tiles) = &p.tiles
{
for tile in tiles {
collect_fonts_from_common(&tile.common, fonts, seen);
}
}
}
}
}
pub(crate) fn collect_id_widgets(widgets: &[WidgetKind]) -> Vec<(String, String)> {
let mut result = Vec::new();
for w in widgets {
if let Some(id) = &w.common().id {
result.push((id.clone(), widget_rust_type(w).to_owned()));
}
if let Some(children) = &w.common().widgets {
result.extend(collect_id_widgets(children));
}
#[cfg(feature = "widget-tabview")]
if let WidgetKind::Tabview(p) = w
&& let Some(tabs) = &p.tabs
{
for tab in tabs {
if let Some(children) = &tab.common.widgets {
result.extend(collect_id_widgets(children));
}
}
}
#[cfg(feature = "widget-tileview")]
if let WidgetKind::Tileview(p) = w
&& let Some(tiles) = &p.tiles
{
for tile in tiles {
if let Some(children) = &tile.common.widgets {
result.extend(collect_id_widgets(children));
}
}
}
}
result
}
fn collect_retained_widgets(widgets: &[WidgetKind]) -> Vec<String> {
let mut result = Vec::new();
fn walk(widgets: &[WidgetKind], out: &mut Vec<String>) {
for w in widgets {
if w.common().id.is_none() {
out.push(widget_rust_type(w).to_owned());
}
if let Some(children) = &w.common().widgets {
walk(children, out);
}
#[cfg(feature = "widget-tabview")]
if let WidgetKind::Tabview(p) = w
&& let Some(tabs) = &p.tabs
{
for tab in tabs {
if let Some(children) = &tab.common.widgets {
walk(children, out);
}
}
}
#[cfg(feature = "widget-tileview")]
if let WidgetKind::Tileview(p) = w
&& let Some(tiles) = &p.tiles
{
for tile in tiles {
if let Some(children) = &tile.common.widgets {
walk(children, out);
}
}
}
}
}
walk(widgets, &mut result);
result
}
fn widget_rust_type(kind: &WidgetKind) -> &'static str {
match kind {
WidgetKind::Obj(_) => "Obj",
WidgetKind::Label(_) => "Label",
WidgetKind::Button(_) => "Button",
WidgetKind::Switch(_) => "Switch",
WidgetKind::Checkbox(_) => "Checkbox",
WidgetKind::Slider(_) => "Slider",
WidgetKind::Arc(_) => "Arc",
WidgetKind::Bar(_) => "Bar",
WidgetKind::Dropdown(_) => "Dropdown",
WidgetKind::Roller(_) => "Roller",
WidgetKind::Scale(_) => "Scale",
WidgetKind::Led(_) => "Led",
WidgetKind::Image(_) => "Image",
WidgetKind::Line(_) => "Line",
WidgetKind::ValueLabel(_) => "ValueLabel",
#[cfg(feature = "widget-spinner")]
WidgetKind::Spinner(_) => "Spinner",
#[cfg(feature = "widget-spinbox")]
WidgetKind::Spinbox(_) => "Spinbox",
#[cfg(feature = "widget-textarea")]
WidgetKind::Textarea(_) => "Textarea",
#[cfg(feature = "widget-keyboard")]
WidgetKind::Keyboard(_) => "Keyboard",
#[cfg(feature = "widget-chart")]
WidgetKind::Chart(_) => "Chart",
#[cfg(feature = "widget-tabview")]
WidgetKind::Tabview(_) => "Tabview",
#[cfg(feature = "widget-arc-label")]
WidgetKind::ArcLabel(_) => "ArcLabel",
#[cfg(feature = "widget-calendar")]
WidgetKind::Calendar(_) => "Calendar",
#[cfg(feature = "widget-list")]
WidgetKind::List(_) => "List",
#[cfg(feature = "widget-table")]
WidgetKind::Table(_) => "Table",
#[cfg(feature = "widget-tileview")]
WidgetKind::Tileview(_) => "Tileview",
#[cfg(feature = "widget-win")]
WidgetKind::Win(_) => "Win",
#[cfg(feature = "widget-msgbox")]
WidgetKind::Msgbox(_) => "Msgbox",
#[cfg(feature = "widget-spangroup")]
WidgetKind::Spangroup(_) => "Spangroup",
#[cfg(feature = "widget-buttonmatrix")]
WidgetKind::Buttonmatrix(_) => "Buttonmatrix",
#[cfg(feature = "widget-canvas")]
WidgetKind::Canvas(_) => "Canvas",
#[cfg(feature = "widget-imagebutton")]
WidgetKind::Imagebutton(_) => "Imagebutton",
#[cfg(feature = "widget-animimg")]
WidgetKind::AnimImg(_) => "AnimImg",
#[cfg(feature = "widget-menu")]
WidgetKind::Menu(_) => "Menu",
}
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|seg| {
let mut c = seg.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect()
}
pub fn generate(doc: &UiDoc) -> Result<String, std::fmt::Error> {
let mut out = String::new();
if let Some(spdx) = &doc.lvgl.spdx_license {
writeln!(out, "// SPDX-License-Identifier: {spdx}")?;
}
writeln!(out, "// Generated by oxiforge — do not edit")?;
writeln!(out)?;
if let Some(defs) = &doc.lvgl.style_definitions {
for def in defs {
gen_style_def(&mut out, def, doc)?;
}
writeln!(out)?;
}
let custom_fonts = collect_custom_fonts(doc);
if !custom_fonts.is_empty() {
writeln!(out, "unsafe extern \"C\" {{")?;
for name in &custom_fonts {
writeln!(out, " static {name}: u8;")?;
}
writeln!(out, "}}\n")?;
for name in &custom_fonts {
let const_name = name.to_uppercase();
writeln!(
out,
"static FONT_{const_name}: Font = unsafe {{ Font::from_extern(core::ptr::addr_of!({name}) as *const ()) }};"
)?;
}
writeln!(out)?;
}
let image_symbols = collect_image_symbols(doc);
if !image_symbols.is_empty() {
writeln!(out, "unsafe extern \"C\" {{")?;
for name in &image_symbols {
writeln!(out, " static {name}: lv_image_dsc_t;")?;
}
writeln!(out, "}}\n")?;
}
#[cfg(feature = "widget-animimg")]
if doc_uses_animimg_frames(doc) {
writeln!(out, "struct AnimImgFrames<const N: usize>([*const core::ffi::c_void; N]);")?;
writeln!(out, "// SAFETY: the pointers address immutable extern image descriptors.")?;
writeln!(out, "unsafe impl<const N: usize> Sync for AnimImgFrames<N> {{}}")?;
writeln!(out)?;
}
for page in &doc.lvgl.pages {
if page.skip == Some(true) {
continue;
}
gen_page(&mut out, page, doc, &[])?;
}
if doc.lvgl.generate_view == Some(true) {
gen_view_impls(&mut out, doc)?;
}
gen_toasts(&mut out, doc)?;
Ok(out)
}
fn gen_view_impls(out: &mut String, doc: &UiDoc) -> std::fmt::Result {
writeln!(out, "// ── View wrappers ──────────────────────────────────────────────────────")?;
writeln!(out)?;
let pages: Vec<&Page> = doc.lvgl.pages.iter().filter(|p| p.skip != Some(true)).collect();
for page in &pages {
gen_page_view(out, page)?;
}
if let Some(first) = pages.first() {
let alias = if let Some(name) = &doc.lvgl.app_name {
format!("{}RootView", to_pascal_case(name))
} else {
format!("{}RootView", to_pascal_case(&first.id))
};
let first_view = format!("{}View", to_pascal_case(&first.id));
writeln!(out, "/// Root view — pass to `run_app` or `run_app_nav` as the initial view.")?;
writeln!(out, "pub type {alias} = {first_view};")?;
writeln!(out)?;
}
Ok(())
}
fn gen_page_view(out: &mut String, page: &Page) -> std::fmt::Result {
let page_pascal = to_pascal_case(&page.id);
let view_name = format!("{page_pascal}View");
let widgets_struct = format!("{page_pascal}Widgets");
let has_widgets = page
.common
.widgets
.as_deref()
.map(|w| !collect_id_widgets(w).is_empty() || !collect_retained_widgets(w).is_empty())
.unwrap_or(false);
let declarative_group = page.input_group.as_ref();
if has_widgets {
writeln!(out, "pub struct {view_name} {{")?;
writeln!(out, " widgets: Option<{widgets_struct}>,")?;
if declarative_group.is_some() {
writeln!(out, " _input_group: Option<oxivgl::group::Group>,")?;
}
writeln!(out, "}}")?;
writeln!(out)?;
writeln!(out, "impl Default for {view_name} {{")?;
if declarative_group.is_some() {
writeln!(out, " fn default() -> Self {{ Self {{ widgets: None, _input_group: None }} }}")?;
} else {
writeln!(out, " fn default() -> Self {{ Self {{ widgets: None }} }}")?;
}
writeln!(out, "}}")?;
} else {
writeln!(out, "pub struct {view_name};")?;
writeln!(out)?;
writeln!(out, "impl Default for {view_name} {{")?;
writeln!(out, " fn default() -> Self {{ Self }}")?;
writeln!(out, "}}")?;
}
writeln!(out)?;
writeln!(out, "impl oxivgl::view::View for {view_name} {{")?;
writeln!(
out,
" fn create(&mut self, _container: &oxivgl::widgets::Obj<'static>) -> Result<(), oxivgl::widgets::WidgetError> {{"
)?;
if let Some(group) = declarative_group {
writeln!(out, " let widgets = create_{}(_container)?;", page.id)?;
writeln!(out, " let _group = oxivgl::group::Group::new()?;")?;
for member in &group.members {
writeln!(out, " _group.add_obj(&widgets.{member});")?;
}
writeln!(out, " self._input_group = Some(_group);")?;
writeln!(out, " self.widgets = Some(widgets);")?;
} else if has_widgets {
writeln!(out, " self.widgets = Some(create_{}(_container)?);", page.id)?;
} else {
writeln!(out, " create_{}(_container)?;", page.id)?;
}
writeln!(out, " Ok(())")?;
writeln!(out, " }}")?;
if has_widgets {
writeln!(out)?;
writeln!(out, " fn will_hide(&mut self) {{")?;
writeln!(out, " self.widgets = None;")?;
if declarative_group.is_some() {
writeln!(out, " self._input_group = None;")?;
}
writeln!(out, " }}")?;
}
if let Some(update_fn) = &page.update_fn {
writeln!(out)?;
writeln!(out, " fn update(&mut self) -> Result<oxivgl::view::NavAction, oxivgl::widgets::WidgetError> {{")?;
if has_widgets {
writeln!(out, " {update_fn}(&mut self.widgets)")?;
} else {
writeln!(out, " {update_fn}()")?;
}
writeln!(out, " }}")?;
}
if let Some(on_event_fn) = &page.on_event_fn {
writeln!(out)?;
writeln!(out, " fn on_event(&mut self, event: &oxivgl::event::Event) -> oxivgl::view::NavAction {{")?;
if has_widgets {
writeln!(out, " {on_event_fn}(&mut self.widgets, event)")?;
} else {
writeln!(out, " {on_event_fn}(event)")?;
}
writeln!(out, " }}")?;
}
if declarative_group.is_some() {
writeln!(out)?;
writeln!(out, " fn input_group(&self) -> Option<oxivgl::group::GroupRef> {{")?;
writeln!(out, " self._input_group.as_ref().map(|g| g.as_ref())")?;
writeln!(out, " }}")?;
} else if let Some(input_group_fn) = &page.input_group_fn {
writeln!(out)?;
writeln!(out, " fn input_group(&self) -> Option<oxivgl::group::GroupRef> {{")?;
if has_widgets {
writeln!(out, " {input_group_fn}(&self.widgets)")?;
} else {
writeln!(out, " {input_group_fn}()")?;
}
writeln!(out, " }}")?;
}
writeln!(out, "}}")?;
writeln!(out)?;
Ok(())
}
fn gen_toasts(out: &mut String, doc: &UiDoc) -> std::fmt::Result {
let Some(toasts) = &doc.lvgl.toasts else { return Ok(()) };
if toasts.is_empty() {
return Ok(());
}
writeln!(out, "// ── Toasts ─────────────────────────────────────────────────────────────")?;
writeln!(out)?;
for toast in toasts {
let params: &[String] = toast.params.as_deref().unwrap_or(&[]);
let page = Page {
id: toast.id.clone(),
skip: None,
update_fn: None,
on_event_fn: None,
input_group: None,
input_group_fn: None,
budget: toast.budget.clone(),
common: toast.common.clone(),
};
gen_page(out, &page, doc, params)?;
let pascal = to_pascal_case(&toast.id);
let view_name = format!("{pascal}Toast");
let widgets_struct = format!("{pascal}Widgets");
let has_widgets = toast
.common
.widgets
.as_deref()
.map(|w| !collect_id_widgets(w).is_empty() || !collect_retained_widgets(w).is_empty())
.unwrap_or(false);
writeln!(out, "pub struct {view_name} {{")?;
for p in params {
writeln!(out, " {p}: alloc::string::String,")?;
}
if has_widgets {
writeln!(out, " widgets: Option<{widgets_struct}>,")?;
}
writeln!(out, "}}")?;
writeln!(out)?;
if params.is_empty() {
let init = if has_widgets { " widgets: None " } else { "" };
writeln!(out, "impl Default for {view_name} {{")?;
writeln!(out, " fn default() -> Self {{ Self {{{init}}} }}")?;
writeln!(out, "}}")?;
} else {
let new_args =
params.iter().map(|p| format!("{p}: impl Into<alloc::string::String>")).collect::<Vec<_>>().join(", ");
writeln!(out, "impl {view_name} {{")?;
writeln!(out, " /// Construct the toast with its bound text.")?;
writeln!(out, " pub fn new({new_args}) -> Self {{")?;
writeln!(out, " Self {{")?;
for p in params {
writeln!(out, " {p}: {p}.into(),")?;
}
if has_widgets {
writeln!(out, " widgets: None,")?;
}
writeln!(out, " }}")?;
writeln!(out, " }}")?;
writeln!(out, "}}")?;
}
writeln!(out)?;
let create_args: String = params.iter().map(|p| format!(", &self.{p}")).collect();
writeln!(out, "impl oxivgl::view::View for {view_name} {{")?;
writeln!(
out,
" fn create(&mut self, _container: &oxivgl::widgets::Obj<'static>) -> Result<(), oxivgl::widgets::WidgetError> {{"
)?;
if has_widgets {
writeln!(out, " self.widgets = Some(create_{}(_container{create_args})?);", toast.id)?;
} else {
writeln!(out, " create_{}(_container{create_args})?;", toast.id)?;
}
writeln!(out, " Ok(())")?;
writeln!(out, " }}")?;
if has_widgets {
writeln!(out)?;
writeln!(out, " fn will_hide(&mut self) {{")?;
writeln!(out, " self.widgets = None;")?;
writeln!(out, " }}")?;
}
writeln!(out, "}}")?;
writeln!(out)?;
// Raise helper: returns a ShowToast action ready to return from a
// view's update()/on_event().
let duration = toast_duration_expr(toast);
let helper_args =
params.iter().map(|p| format!("{p}: impl Into<alloc::string::String>")).collect::<Vec<_>>().join(", ");
let ctor = if params.is_empty() {
format!("{view_name}::default()")
} else {
format!("{view_name}::new({})", params.join(", "))
};
writeln!(out, "/// Raise the `{}` toast (`NavAction::ShowToast`).", toast.id)?;
writeln!(out, "pub fn {}_toast({helper_args}) -> oxivgl::view::NavAction {{", toast.id)?;
writeln!(out, " oxivgl::view::NavAction::show_toast({ctor}, {duration})")?;
writeln!(out, "}}")?;
writeln!(out)?;
writeln!(out, "/// Post the `{}` toast from any task (`post_toast_with`).", toast.id)?;
writeln!(out, "pub fn post_{}({helper_args}) {{", toast.id)?;
if params.is_empty() {
writeln!(out, " oxivgl::navigator::post_toast_with({view_name}::default, {duration});")?;
} else {
for p in params {
writeln!(out, " let {p} = {p}.into();")?;
}
writeln!(
out,
" oxivgl::navigator::post_toast_with(move || {view_name}::new({}), {duration});",
params.join(", ")
)?;
}
writeln!(out, "}}")?;
writeln!(out)?;
}
Ok(())
}
fn toast_duration_expr(toast: &ToastDef) -> String {
if toast.persistent == Some(true) {
"None".to_string()
} else {
let ms = toast.duration_ms.unwrap_or(2500);
format!("Some(core::time::Duration::from_millis({ms}))")
}
}
fn gen_style_def(out: &mut String, def: &StyleDef, doc: &UiDoc) -> std::fmt::Result {
writeln!(out, "fn style_{}() -> Style {{", def.id)?;
writeln!(out, " let mut b = StyleBuilder::new();")?;
gen_style_obj_setters(out, "b", &def.style, " ", doc)?;
writeln!(out, " b.build()")?;
writeln!(out, "}}")?;
writeln!(out)?;
Ok(())
}
fn gen_page(out: &mut String, page: &Page, doc: &UiDoc, params: &[String]) -> std::fmt::Result {
let id_widgets = page.common.widgets.as_deref().map(collect_id_widgets).unwrap_or_default();
let retained_widgets = page.common.widgets.as_deref().map(collect_retained_widgets).unwrap_or_default();
let struct_name = if !id_widgets.is_empty() || !retained_widgets.is_empty() {
let name = format!("{}Widgets", to_pascal_case(&page.id));
writeln!(out, "#[derive(Debug)]")?;
writeln!(out, "pub struct {name} {{")?;
for (id, ty) in &id_widgets {
writeln!(out, " pub {id}: {ty}<'static>,")?;
}
for (i, ty) in retained_widgets.iter().enumerate() {
writeln!(out, " _retained_{}: {ty}<'static>,", i + 1)?;
}
writeln!(out, "}}")?;
writeln!(out)?;
Some(name)
} else {
None
};
let ret_type = match &struct_name {
Some(name) => format!("Result<{name}, WidgetError>"),
None => "Result<(), WidgetError>".to_string(),
};
// Toast params arrive as `&str` arguments, referenced by `bind_text:`.
let param_args: String = params.iter().map(|p| format!(", {p}: &str")).collect();
writeln!(out, "pub fn create_{}(container: &oxivgl::widgets::Obj<'static>{param_args}) -> {ret_type} {{", page.id)?;
// Hoist each referenced style: built once, shared by every referencing
// widget (add_style Rc-retains it). The locals drop at the end of
// create_*() while the widgets keep the lv_style_t alive.
for id in collect_referenced_styles(&page.common) {
writeln!(out, " let style_{id} = style_{id}();")?;
}
// Structurally identical anonymous styles share one local (and one
// lv_style_t) within this create_*() — see emit_or_reuse_style.
let mut dedup = StyleDedup::new();
// Apply style references to container
gen_screen_style_refs(out, &page.common, " ")?;
// Apply page-level inline style props to container as a shared Style —
// the Obj inline setters are deprecated since oxivgl 0.4.0 (each call
// allocated a per-object local style), and this path supports the full
// StyleProps set instead of the old screen-only subset.
gen_obj_inline_style(out, "container", &page.common.style, " ", doc, &mut dedup)?;
// Layout on container
gen_screen_layout(out, &page.common, " ")?;
// Child widgets
let mut counter = 0u32;
if let Some(widgets) = &page.common.widgets {
for w in widgets {
gen_widget(out, w, "container", &mut counter, " ", doc, &mut dedup)?;
}
}
// Debug-only resource tripwire: a widget-count regression fails here
// (CI/HIL) instead of OOMing a constrained target. No-op in release.
if let Some(budget) = &page.budget {
let max_depth = budget.max_depth.map_or("u16::MAX".to_string(), |d| d.to_string());
writeln!(
out,
" oxivgl::diag::assert_budget(container, &oxivgl::diag::Budget {{ max_objects: {}, max_depth: {max_depth} }});",
budget.max_objects
)?;
}
if let Some(name) = &struct_name {
writeln!(out, " Ok({name} {{")?;
for (id, _) in &id_widgets {
writeln!(out, " {id},")?;
}
for i in 0..retained_widgets.len() {
let n = i + 1;
writeln!(out, " _retained_{n}: _w{n},")?;
}
writeln!(out, " }})")?;
} else {
writeln!(out, " Ok(())")?;
}
writeln!(out, "}}")?;
writeln!(out)?;
Ok(())
}
fn gen_widget(
out: &mut String,
widget: &WidgetKind,
parent: &str,
counter: &mut u32,
indent: &str,
doc: &UiDoc,
dedup: &mut StyleDedup,
) -> std::fmt::Result {
// `doc` is threaded through for future reference resolution (e.g. gradient
// lookups).
let _ = doc;
let common = widget.common();
let var = match &common.id {
Some(id) => id.clone(),
None => {
*counter += 1;
format!("_w{}", counter)
}
};
// Constructor + widget-specific props
match widget {
WidgetKind::Obj(_) => {
writeln!(out, "{indent}let {var} = Obj::new({parent})?;")?;
}
WidgetKind::Label(p) => {
writeln!(out, "{indent}let {var} = Label::new({parent})?;")?;
if let Some(param) = &p.bind_text {
// Bound to a toast param (an in-scope `&str` create-fn arg).
writeln!(out, "{indent}{var}.text({param});")?;
} else if let Some(text) = &p.text {
writeln!(out, "{indent}{var}.text({:?});", text)?;
}
if let Some(mode) = &p.long_mode {
writeln!(out, "{indent}{var}.set_long_mode(LabelLongMode::{});", enum_variant(mode))?;
}
}
WidgetKind::Button(_) => {
writeln!(out, "{indent}let {var} = Button::new({parent})?;")?;
}
WidgetKind::Switch(_) => {
writeln!(out, "{indent}let {var} = Switch::new({parent})?;")?;
}
WidgetKind::Checkbox(p) => {
writeln!(out, "{indent}let {var} = Checkbox::new({parent})?;")?;
if let Some(text) = &p.text {
writeln!(out, "{indent}{var}.text({:?});", text)?;
}
}
WidgetKind::Slider(p) => {
writeln!(out, "{indent}let {var} = Slider::new({parent})?;")?;
if let Some(min) = p.min_value
&& let Some(max) = p.max_value
{
writeln!(out, "{indent}{var}.set_range({min}, {max});")?;
}
if let Some(v) = p.value {
writeln!(out, "{indent}{var}.set_value({v});")?;
}
if let Some(o) = &p.orientation {
writeln!(out, "{indent}{var}.set_orientation(SliderOrientation::{});", enum_variant(o))?;
}
}
WidgetKind::Arc(p) => {
writeln!(out, "{indent}let {var} = Arc::new({parent})?;")?;
if let Some(min) = p.min_value
&& min != 0.0
&& let Some(max) = p.max_value
{
writeln!(out, "{indent}{var}.set_range_raw({}, {});", min as i32, max as i32)?;
} else if let Some(max) = p.max_value {
writeln!(out, "{indent}{var}.set_range({max:.1});",)?;
}
if let Some(v) = p.value {
writeln!(out, "{indent}{var}.set_value({v:.1});")?;
}
if let Some(start) = p.start_angle {
writeln!(out, "{indent}{var}.set_bg_start_angle({start});")?;
}
if let Some(end) = p.end_angle {
writeln!(out, "{indent}{var}.set_bg_end_angle({end});")?;
}
if let Some(r) = p.rotation {
writeln!(out, "{indent}{var}.set_rotation({r});")?;
}
if let Some(mode) = &p.mode {
writeln!(out, "{indent}{var}.set_mode(ArcMode::{});", enum_variant(mode))?;
}
if let Some(false) = p.adjustable {
writeln!(out, "{indent}{var}.remove_flag(ObjFlag::CLICKABLE);")?;
}
}
WidgetKind::Bar(p) => {
writeln!(out, "{indent}let {var} = Bar::new({parent})?;")?;
if let Some(min) = p.min_value
&& min != 0.0
&& let Some(max) = p.max_value
{
writeln!(out, "{indent}{var}.set_range_raw({}, {});", min as i32, max as i32)?;
} else if let Some(max) = p.max_value {
writeln!(out, "{indent}{var}.set_range({max:.1});")?;
}
if let Some(v) = p.value {
writeln!(out, "{indent}{var}.set_value({v:.1});")?;
}
if let Some(mode) = &p.mode {
writeln!(out, "{indent}{var}.set_mode(BarMode::{});", enum_variant(mode))?;
}
if let Some(o) = &p.orientation {
writeln!(out, "{indent}{var}.set_orientation(BarOrientation::{});", enum_variant(o))?;
}
}
WidgetKind::Dropdown(p) => {
writeln!(out, "{indent}let {var} = Dropdown::new({parent})?;")?;
if let Some(opts) = &p.options {
let joined = opts.join("\\n");
writeln!(out, "{indent}{var}.set_options({:?});", joined)?;
}
if let Some(sel) = p.selected {
writeln!(out, "{indent}{var}.set_selected({sel});")?;
}
if let Some(dir) = &p.dir {
writeln!(out, "{indent}{var}.set_dir(DdDir::{});", enum_variant(dir))?;
}
}
WidgetKind::Roller(p) => {
writeln!(out, "{indent}let {var} = Roller::new({parent})?;")?;
if let Some(opts) = &p.options {
let joined = opts.join("\\n");
let mode = p.mode.as_ref().map_or("Normal", |m| match m {
RollerMode::Normal => "Normal",
RollerMode::Infinite => "Infinite",
});
writeln!(out, "{indent}{var}.set_options({:?}, RollerMode::{mode});", joined)?;
}
if let Some(n) = p.visible_row_count {
writeln!(out, "{indent}{var}.set_visible_row_count({n});")?;
}
if let Some(sel) = p.selected {
writeln!(out, "{indent}{var}.set_selected({sel}, false);")?;
}
}
WidgetKind::Scale(p) => {
writeln!(out, "{indent}let {var} = Scale::new({parent})?;")?;
if let Some(mode) = &p.mode {
writeln!(out, "{indent}{var}.set_mode(ScaleMode::{});", enum_variant(mode))?;
}
if let Some(n) = p.total_tick_count {
writeln!(out, "{indent}{var}.set_total_tick_count({n});")?;
}
if let Some(n) = p.major_tick_every {
writeln!(out, "{indent}{var}.set_major_tick_every({n});")?;
}
if let Some(show) = p.label_show {
writeln!(out, "{indent}{var}.set_label_show({show});")?;
}
if let Some(min) = p.range_min
&& let Some(max) = p.range_max
{
writeln!(out, "{indent}{var}.set_range({min}, {max});")?;
}
if let Some(r) = p.rotation {
writeln!(out, "{indent}{var}.set_rotation({r});")?;
}
if let Some(a) = p.angle_range {
writeln!(out, "{indent}{var}.set_angle_range({a});")?;
}
if let Some(len) = p.major_tick_length {
writeln!(out, "{indent}{var}.set_tick_length(Part::Indicator, {len});")?;
}
if let Some(len) = p.minor_tick_length {
writeln!(out, "{indent}{var}.set_tick_length(Part::Items, {len});")?;
}
}
WidgetKind::Led(p) => {
writeln!(out, "{indent}let {var} = Led::new({parent})?;")?;
if let Some(c) = &p.color {
writeln!(out, "{indent}{var}.set_color({});", color_make_expr(c))?;
}
if let Some(b) = p.brightness {
writeln!(out, "{indent}{var}.set_brightness({b});")?;
}
}
WidgetKind::Image(p) => {
writeln!(out, "{indent}let {var} = Image::new({parent})?;")?;
if let Some(src) = &p.src {
writeln!(out, "{indent}{var}.set_src({:?});", src)?;
}
}
WidgetKind::Line(p) => {
writeln!(out, "{indent}let {var} = Line::new({parent})?;")?;
if let Some(pts) = &p.points {
let pts_str: Vec<String> = pts
.iter()
.map(|p| format!("lv_point_precise_t {{ x: {}, y: {} }}", p[0] as i32, p[1] as i32))
.collect();
writeln!(
out,
"{indent}static {}_POINTS: [lv_point_precise_t; {}] = [{}];",
var.to_uppercase(),
pts.len(),
pts_str.join(", ")
)?;
writeln!(out, "{indent}{var}.set_points(&{}_POINTS);", var.to_uppercase())?;
}
}
WidgetKind::ValueLabel(p) => {
writeln!(out, "{indent}let {var} = ValueLabel::new({parent}, {:?})?;", p.unit)?;
if let Some(text) = &p.text {
writeln!(out, "{indent}{var}.text({:?});", text)?;
}
}
#[cfg(feature = "widget-spinner")]
WidgetKind::Spinner(p) => {
writeln!(out, "{indent}let {var} = Spinner::new({parent})?;")?;
if let Some(time) = p.anim_time
&& let Some(len) = p.arc_length
{
writeln!(out, "{indent}{var}.set_anim_params({time}, {len});")?;
}
}
#[cfg(feature = "widget-spinbox")]
WidgetKind::Spinbox(p) => {
writeln!(out, "{indent}let {var} = Spinbox::new({parent})?;")?;
if let Some(min) = p.min_value
&& let Some(max) = p.max_value
{
writeln!(out, "{indent}{var}.set_range({min}, {max});")?;
}
if let Some(v) = p.value {
writeln!(out, "{indent}{var}.set_value({v});")?;
}
if let Some(dc) = p.digit_count
&& let Some(dp) = p.decimal_point
{
writeln!(out, "{indent}{var}.set_digit_format({dc}, {dp});")?;
}
if let Some(s) = p.step {
writeln!(out, "{indent}{var}.set_step({s});")?;
}
if let Some(r) = p.rollover {
writeln!(out, "{indent}{var}.set_rollover({r});")?;
}
}
#[cfg(feature = "widget-textarea")]
WidgetKind::Textarea(p) => {
writeln!(out, "{indent}let {var} = Textarea::new({parent})?;")?;
if let Some(text) = &p.text {
writeln!(out, "{indent}{var}.set_text({:?});", text)?;
}
if let Some(ph) = &p.placeholder {
writeln!(out, "{indent}{var}.set_placeholder_text({:?});", ph)?;
}
if let Some(pw) = p.password_mode {
writeln!(out, "{indent}{var}.set_password_mode({pw});")?;
}
if let Some(ol) = p.one_line {
writeln!(out, "{indent}{var}.set_one_line({ol});")?;
}
if let Some(ml) = p.max_length {
writeln!(out, "{indent}{var}.set_max_length({ml});")?;
}
}
#[cfg(feature = "widget-keyboard")]
WidgetKind::Keyboard(p) => {
writeln!(out, "{indent}let {var} = Keyboard::new({parent})?;")?;
if let Some(mode) = &p.mode {
writeln!(out, "{indent}{var}.set_mode(KeyboardMode::{});", enum_variant(mode))?;
}
}
#[cfg(feature = "widget-chart")]
WidgetKind::Chart(p) => {
writeln!(out, "{indent}let {var} = Chart::new({parent})?;")?;
if let Some(ct) = &p.chart_type {
writeln!(out, "{indent}{var}.set_type(ChartType::{});", enum_variant(ct))?;
}
if let Some(n) = p.point_count {
writeln!(out, "{indent}{var}.set_point_count({n});")?;
}
if let Some(h) = p.div_lines_h {
let v = p.div_lines_v.unwrap_or(0);
writeln!(out, "{indent}{var}.set_div_line_count({h}, {v});")?;
} else if let Some(v) = p.div_lines_v {
writeln!(out, "{indent}{var}.set_div_line_count(0, {v});")?;
}
}
#[cfg(feature = "widget-tabview")]
WidgetKind::Tabview(p) => {
writeln!(out, "{indent}let {var} = Tabview::new({parent})?;")?;
if let Some(pos) = &p.tab_bar_position {
writeln!(out, "{indent}{var}.set_tab_bar_position(DdDir::{});", enum_variant(pos))?;
}
if let Some(size) = p.tab_bar_size {
writeln!(out, "{indent}{var}.set_tab_bar_size({size});")?;
}
if let Some(tabs) = &p.tabs {
for tab in tabs {
let tab_var = format!("tab_{}", tab.name.replace(' ', "_").to_lowercase());
writeln!(out, "{indent}let {tab_var} = {var}.add_tab({:?});", tab.name)?;
gen_common_props(out, &tab_var, &tab.common, indent)?;
gen_style_ref_apply(out, &tab_var, &tab.common, indent)?;
gen_obj_inline_style(out, &tab_var, &tab.common.style, indent, doc, dedup)?;
if let Some(children) = &tab.common.widgets {
for child in children {
gen_widget(out, child, &format!("&{tab_var}"), counter, indent, doc, dedup)?;
}
}
}
}
}
#[cfg(feature = "widget-arc-label")]
WidgetKind::ArcLabel(p) => {
writeln!(out, "{indent}let {var} = ArcLabel::new({parent})?;")?;
if let Some(text) = &p.text {
writeln!(out, "{indent}{var}.set_text_static(c{:?});", text)?;
}
if let Some(r) = p.radius {
writeln!(out, "{indent}{var}.set_radius({r});")?;
}
if let Some(a) = p.angle_start {
writeln!(out, "{indent}{var}.set_angle_start({a:.1});",)?;
}
if let Some(a) = p.angle_size {
writeln!(out, "{indent}{var}.set_angle_size({a:.1});",)?;
}
if let Some(d) = &p.dir {
writeln!(out, "{indent}{var}.set_dir(ArcLabelDir::{});", enum_variant(d))?;
}
}
#[cfg(feature = "widget-calendar")]
WidgetKind::Calendar(p) => {
writeln!(out, "{indent}let {var} = Calendar::new({parent})?;")?;
if let Some(y) = p.today_year
&& let Some(m) = p.today_month
&& let Some(d) = p.today_day
{
writeln!(out, "{indent}{var}.set_today_date({y}, {m}, {d});")?;
}
if let Some(y) = p.shown_year
&& let Some(m) = p.shown_month
{
writeln!(out, "{indent}{var}.set_month_shown({y}, {m});")?;
}
}
#[cfg(feature = "widget-list")]
WidgetKind::List(p) => {
writeln!(out, "{indent}let {var} = List::new({parent})?;")?;
if let Some(items) = &p.items {
for item in items {
match item {
ListItem::Text(t) => {
writeln!(out, "{indent}{var}.add_text({:?});", t)?;
}
ListItem::Button(t) => {
writeln!(out, "{indent}{var}.add_button(None, {:?});", t)?;
}
}
}
}
}
#[cfg(feature = "widget-table")]
WidgetKind::Table(p) => {
writeln!(out, "{indent}let {var} = Table::new({parent})?;")?;
if let Some(cols) = p.columns {
writeln!(out, "{indent}{var}.set_column_count({cols});")?;
}
if let Some(widths) = &p.column_widths {
for (i, w) in widths.iter().enumerate() {
writeln!(out, "{indent}{var}.set_column_width({i}, {w});")?;
}
}
if let Some(rows) = &p.rows {
for (r, row) in rows.iter().enumerate() {
for (c, cell) in row.iter().enumerate() {
writeln!(out, "{indent}{var}.set_cell_value({r}, {c}, {:?});", cell)?;
}
}
}
}
#[cfg(feature = "widget-tileview")]
WidgetKind::Tileview(p) => {
writeln!(out, "{indent}let {var} = Tileview::new({parent})?;")?;
if let Some(tiles) = &p.tiles {
for (i, tile) in tiles.iter().enumerate() {
let tile_var = format!("{var}_tile{i}");
let dir = tile.scroll_dir.as_ref().map_or("All", |d| match d {
ScrollDir::None => "NONE",
ScrollDir::Hor => "HOR",
ScrollDir::Ver => "VER",
ScrollDir::All => "ALL",
});
writeln!(
out,
"{indent}let {tile_var} = {var}.add_tile({}, {}, ScrollDir::{dir});",
tile.col, tile.row
)?;
gen_common_props(out, &tile_var, &tile.common, indent)?;
gen_style_ref_apply(out, &tile_var, &tile.common, indent)?;
gen_obj_inline_style(out, &tile_var, &tile.common.style, indent, doc, dedup)?;
if let Some(children) = &tile.common.widgets {
for child in children {
gen_widget(out, child, &format!("&{tile_var}"), counter, indent, doc, dedup)?;
}
}
}
}
}
#[cfg(feature = "widget-win")]
WidgetKind::Win(p) => {
writeln!(out, "{indent}let {var} = Win::new({parent})?;")?;
if let Some(title) = &p.title {
writeln!(out, "{indent}{var}.add_title({:?});", title)?;
}
}
#[cfg(feature = "widget-msgbox")]
WidgetKind::Msgbox(p) => {
writeln!(out, "{indent}let {var} = Msgbox::new(Some({parent}))?;")?;
if let Some(title) = &p.title {
writeln!(out, "{indent}{var}.add_title({:?});", title)?;
}
if let Some(text) = &p.text {
writeln!(out, "{indent}{var}.add_text({:?});", text)?;
}
if let Some(true) = p.close_button {
writeln!(out, "{indent}{var}.add_close_button();")?;
}
if let Some(buttons) = &p.buttons {
for btn in buttons {
writeln!(out, "{indent}{var}.add_footer_button({:?});", btn)?;
}
}
}
#[cfg(feature = "widget-spangroup")]
WidgetKind::Spangroup(p) => {
writeln!(out, "{indent}let {var} = Spangroup::new({parent})?;")?;
if let Some(o) = &p.overflow {
writeln!(out, "{indent}{var}.set_overflow(SpanOverflow::{});", enum_variant(o))?;
}
if let Some(m) = &p.span_mode {
writeln!(out, "{indent}{var}.set_mode(SpanMode::{});", enum_variant(m))?;
}
if let Some(n) = p.max_lines {
writeln!(out, "{indent}{var}.set_max_lines({n});")?;
}
if let Some(n) = p.indent {
writeln!(out, "{indent}{var}.set_indent({n});")?;
}
if let Some(spans) = &p.spans {
for (i, span) in spans.iter().enumerate() {
let svar = format!("{var}_span{i}");
writeln!(out, "{indent}let {svar} = {var}.add_span()?;")?;
writeln!(out, "{indent}{svar}.set_text(c{:?});", span.text)?;
if let Some(c) = &span.text_color {
writeln!(out, "{indent}{svar}.set_text_color({});", color_make_expr(c))?;
}
if let Some(o) = span.text_opa {
writeln!(out, "{indent}{svar}.set_text_opa({o});")?;
}
if let Some(d) = &span.text_decor {
let dval = match d {
TextDecor::None => "NONE",
TextDecor::Underline => "UNDERLINE",
TextDecor::Strikethrough => "STRIKETHROUGH",
};
writeln!(out, "{indent}{svar}.set_text_decor(TextDecor::{dval});")?;
}
}
writeln!(out, "{indent}{var}.refresh();")?;
}
}
#[cfg(feature = "widget-buttonmatrix")]
WidgetKind::Buttonmatrix(p) => {
writeln!(out, "{indent}let {var} = Buttonmatrix::new({parent})?;")?;
if let Some(rows) = &p.button_rows {
// Build flat button list with newline separators
let mut all_labels: Vec<String> = Vec::new();
for (i, row) in rows.iter().enumerate() {
if i > 0 {
all_labels.push("\\n".to_string());
}
all_labels.extend(row.iter().cloned());
}
let map_name = format!("{}_MAP", var.to_uppercase());
let labels: Vec<String> = all_labels.iter().map(|l| format!("c{:?}", l)).collect();
writeln!(
out,
"{indent}static {map_name}: &ButtonmatrixMap = oxivgl::btnmatrix_map!({});",
labels.join(", ")
)?;
writeln!(out, "{indent}{var}.set_map({map_name});")?;
}
if let Some(oc) = p.one_checked {
writeln!(out, "{indent}{var}.set_one_checked({oc});")?;
}
}
#[cfg(feature = "widget-canvas")]
WidgetKind::Canvas(p) => {
let w = p.buf_width.unwrap_or(100);
let h = p.buf_height.unwrap_or(100);
let cf = match p.color_format {
Some(CanvasColorFormat::Rgb565) => "ColorFormat::RGB565",
_ => "ColorFormat::ARGB8888",
};
let buf_var = format!("{var}_buf");
writeln!(out, "{indent}let {buf_var} = DrawBuf::create({w}, {h}, {cf}).unwrap();")?;
writeln!(out, "{indent}let {var} = Canvas::new({parent}, {buf_var})?;")?;
}
#[cfg(feature = "widget-imagebutton")]
WidgetKind::Imagebutton(p) => {
writeln!(out, "{indent}let {var} = Imagebutton::new({parent})?;")?;
for (state, src) in
[("Released", &p.src_released), ("Pressed", &p.src_pressed), ("Disabled", &p.src_disabled)]
{
if let Some(s) = src {
let left = s.left.as_ref().map_or("None".to_string(), |n| format!("Some(unsafe {{ &{n} }})"));
let mid = s.mid.as_ref().map_or("None".to_string(), |n| format!("Some(unsafe {{ &{n} }})"));
let right = s.right.as_ref().map_or("None".to_string(), |n| format!("Some(unsafe {{ &{n} }})"));
writeln!(out, "{indent}{var}.set_src(ImagebuttonState::{state}, {left}, {mid}, {right});")?;
}
}
}
#[cfg(feature = "widget-menu")]
WidgetKind::Menu(p) => {
writeln!(out, "{indent}let {var} = Menu::new({parent})?;")?;
if let Some(mode) = &p.header_mode {
let mode_str = match mode {
MenuHeaderMode::TopFixed => "TopFixed",
MenuHeaderMode::TopUnfixed => "TopUnfixed",
MenuHeaderMode::BottomFixed => "BottomFixed",
};
writeln!(out, "{indent}{var}.set_mode_header(MenuHeaderMode::{mode_str});")?;
}
if let Some(enabled) = p.root_back_button {
writeln!(out, "{indent}{var}.set_mode_root_back_button({enabled});")?;
}
}
#[cfg(feature = "widget-animimg")]
WidgetKind::AnimImg(p) => {
writeln!(out, "{indent}let {var} = AnimImg::new({parent})?;")?;
if let Some(frames) = &p.frames {
let arr_name = format!("{}_FRAMES", var.to_uppercase());
let ptrs: Vec<String> = frames
.iter()
.map(|f| format!("unsafe {{ &{f} as *const lv_image_dsc_t as *const core::ffi::c_void }}"))
.collect();
// Frame pointers are extern-static addresses (constants), so the
// array lives in a function-local static — shared by every
// create_*() call, never allocated, never leaked.
writeln!(
out,
"{indent}static {arr_name}: AnimImgFrames<{}> = AnimImgFrames([{}]);",
ptrs.len(),
ptrs.join(", ")
)?;
writeln!(out, "{indent}{var}.set_src(&{arr_name}.0);")?;
}
if let Some(d) = p.duration {
writeln!(out, "{indent}{var}.set_duration({d});")?;
}
if let Some(r) = p.repeat_count {
writeln!(out, "{indent}{var}.set_repeat_count({r});")?;
}
if let Some(true) = p.auto_start {
writeln!(out, "{indent}{var}.start();")?;
}
}
}
// Common properties (position, size, align, flags)
gen_common_props(out, &var, common, indent)?;
// Style references
gen_style_ref_apply(out, &var, common, indent)?;
// Inline style properties (Obj API)
gen_obj_inline_style(out, &var, &common.style, indent, doc, dedup)?;
// Layout
gen_layout(out, &var, common, indent)?;
// State styles (Style objects applied with ObjState selector)
gen_state_styles(out, &var, common, indent, doc, dedup)?;
// Part styles (Style objects applied with Part selector)
gen_part_styles(out, &var, common, indent, doc, dedup)?;
// Event bindings
gen_event_bindings(out, &var, common, indent)?;
// Recurse children
if let Some(children) = &common.widgets {
for child in children {
gen_widget(out, child, &format!("&{var}"), counter, indent, doc, dedup)?;
}
}
// Decorative (id-less) widgets are moved into a private `_retained_N`
// field on the page's *Widgets struct by `gen_page` — keeping their `Obj`
// alive for the view's lifetime, so style property maps are freed on
// teardown rather than leaked. The let-binding above remains in scope
// until then.
Ok(())
}
/// Emit position/size/align/flag calls on an Obj (or Obj-deref widget).
fn gen_common_props(out: &mut String, var: &str, common: &CommonProps, indent: &str) -> std::fmt::Result {
if let Some(w) = &common.width {
writeln!(out, "{indent}{var}.width({});", dim_expr(w))?;
}
if let Some(h) = &common.height {
writeln!(out, "{indent}{var}.height({});", dim_expr(h))?;
}
if let Some(align) = &common.align {
// x/y become offsets from alignment point (LVGL align semantics).
let x_ofs = common.x.as_ref().map_or("0".into(), dim_expr);
let y_ofs = common.y.as_ref().map_or("0".into(), dim_expr);
writeln!(out, "{indent}{var}.align(Align::{}, {x_ofs}, {y_ofs});", enum_variant(align))?;
} else {
if let Some(x) = &common.x {
writeln!(out, "{indent}{var}.x({});", dim_expr(x))?;
}
if let Some(y) = &common.y {
writeln!(out, "{indent}{var}.y({});", dim_expr(y))?;
}
}
if let Some(true) = common.hidden {
writeln!(out, "{indent}{var}.add_flag(ObjFlag::HIDDEN);")?;
}
if let Some(grow) = common.flex_grow {
writeln!(out, "{indent}{var}.set_flex_grow({grow});")?;
}
if let Some(cell) = &common.grid_cell {
let col_align = cell.column_align.as_ref().map_or("Stretch".to_string(), |a| format!("{a:?}"));
let row_align = cell.row_align.as_ref().map_or("Stretch".to_string(), |a| format!("{a:?}"));
writeln!(
out,
"{indent}{var}.set_grid_cell(GridCell::new(GridAlign::{col_align}, {}, {}), GridCell::new(GridAlign::{row_align}, {}, {}));",
cell.column, cell.column_span, cell.row, cell.row_span,
)?;
}
Ok(())
}
/// Apply named style references via `add_style`.
///
/// References the per-create hoisted `style_<id>` local (one shared
/// `lv_style_t` for all referencing widgets). Style is Rc-wrapped —
/// `add_style` clones the Rc internally, keeping the `lv_style_t` alive as
/// long as the widget exists.
fn gen_style_ref_apply(out: &mut String, var: &str, common: &CommonProps, indent: &str) -> std::fmt::Result {
if let Some(style_ref) = &common.styles {
for name in style_ref.names() {
writeln!(out, "{indent}{var}.add_style(&style_{name}, Selector::DEFAULT);")?;
}
}
Ok(())
}
/// Apply named style references for screen (same API as Obj for add_style).
fn gen_screen_style_refs(out: &mut String, common: &CommonProps, indent: &str) -> std::fmt::Result {
if let Some(style_ref) = &common.styles {
for name in style_ref.names() {
writeln!(out, "{indent}container.add_style(&style_{name}, Selector::DEFAULT);")?;
}
}
Ok(())
}
/// Map from canonically-rendered setter body to the local variable already
/// holding that anonymous Style within the current `create_*()`.
///
/// Structurally identical inline/state/part styles across widgets reuse one
/// `lv_style_t` instead of each building their own (`add_style` Rc-retains, so
/// sharing is free) — the anonymous-style counterpart of the hoisted
/// `style_definitions` refs (docs/cr-style-ref-not-shared.md, finding 1).
type StyleDedup = std::collections::HashMap<String, String>;
/// Emit `let <svar> = { StyleBuilder ... };` for `style`, unless a
/// structurally identical anonymous style was already built in this
/// `create_*()` — then reuse its local. Returns the variable to `add_style`.
fn emit_or_reuse_style(
out: &mut String,
svar: &str,
style: &StyleProps,
indent: &str,
doc: &UiDoc,
dedup: &mut StyleDedup,
) -> Result<String, std::fmt::Error> {
// Canonical key: the setter body rendered without indentation.
let mut key = String::new();
gen_style_obj_setters(&mut key, "b", style, "", doc)?;
if let Some(existing) = dedup.get(&key) {
return Ok(existing.clone());
}
writeln!(out, "{indent}let {svar} = {{")?;
writeln!(out, "{indent} let mut b = StyleBuilder::new();")?;
gen_style_obj_setters(out, "b", style, &format!("{indent} "), doc)?;
writeln!(out, "{indent} b.build()")?;
writeln!(out, "{indent}}};")?;
dedup.insert(key, svar.to_string());
Ok(svar.to_string())
}
/// Emit inline style properties on a widget. Builds a local Style and applies
/// it with `Selector::DEFAULT`, ensuring ALL StyleProps are emitted (not just
/// the subset that has direct Obj API methods).
fn gen_obj_inline_style(
out: &mut String,
var: &str,
style: &StyleProps,
indent: &str,
doc: &UiDoc,
dedup: &mut StyleDedup,
) -> std::fmt::Result {
if !has_inline_style_props(style) {
return Ok(());
}
let svar = emit_or_reuse_style(out, &format!("{var}_inline"), style, indent, doc, dedup)?;
writeln!(out, "{indent}{var}.add_style(&{svar}, Selector::DEFAULT);")?;
Ok(())
}
/// Layout calls on screen (set_flex_flow, set_flex_align, remove_scrollable).
fn gen_screen_layout(out: &mut String, common: &CommonProps, indent: &str) -> std::fmt::Result {
if let Some(LayoutKind::Flex) = common.layout {
if let Some(flow) = &common.flex_flow {
writeln!(out, "{indent}container.set_flex_flow(FlexFlow::{});", enum_variant(flow))?;
}
if common.flex_align_main.is_some() || common.flex_align_cross.is_some() || common.flex_align_track.is_some() {
let main = common.flex_align_main.as_ref().map_or("Start", flex_align_str);
let cross = common.flex_align_cross.as_ref().map_or("Start", flex_align_str);
let track = common.flex_align_track.as_ref().map_or("Start", flex_align_str);
writeln!(
out,
"{indent}container.set_flex_align(FlexAlign::{main}, FlexAlign::{cross}, FlexAlign::{track});"
)?;
}
}
if let Some(false) = common.scrollable {
writeln!(out, "{indent}container.remove_scrollable();")?;
}
Ok(())
}
/// Layout calls on Obj (set_layout, set_flex_flow, set_flex_align, grid).
fn gen_layout(out: &mut String, var: &str, common: &CommonProps, indent: &str) -> std::fmt::Result {
match common.layout {
Some(LayoutKind::Flex) => {
writeln!(out, "{indent}{var}.set_layout(Layout::Flex);")?;
if let Some(flow) = &common.flex_flow {
writeln!(out, "{indent}{var}.set_flex_flow(FlexFlow::{});", enum_variant(flow))?;
}
if common.flex_align_main.is_some()
|| common.flex_align_cross.is_some()
|| common.flex_align_track.is_some()
{
let main = common.flex_align_main.as_ref().map_or("Start", flex_align_str);
let cross = common.flex_align_cross.as_ref().map_or("Start", flex_align_str);
let track = common.flex_align_track.as_ref().map_or("Start", flex_align_str);
writeln!(
out,
"{indent}{var}.set_flex_align(FlexAlign::{main}, FlexAlign::{cross}, FlexAlign::{track});"
)?;
}
}
Some(LayoutKind::Grid) => {
writeln!(out, "{indent}{var}.set_layout(Layout::Grid);")?;
if common.grid_columns.is_some() || common.grid_rows.is_some() {
// Track lists are compile-time constants (grid_fr is a const
// fn) — function-local statics replace the per-create heap
// allocation LVGL would never free. An absent axis gets an
// empty track list (terminator only).
let track_array = |tracks: &Option<Vec<TrackSize>>| -> (usize, String) {
let tracks = tracks.as_deref().unwrap_or_default();
let exprs: String = tracks.iter().map(|t| format!("{}, ", track_expr(t))).collect();
(tracks.len() + 1, format!("{exprs}GRID_TEMPLATE_LAST"))
};
let upper = var.to_uppercase();
let (n_cols, cols) = track_array(&common.grid_columns);
let (n_rows, rows) = track_array(&common.grid_rows);
writeln!(out, "{indent}static {upper}_COLS: [i32; {n_cols}] = [{cols}];")?;
writeln!(out, "{indent}static {upper}_ROWS: [i32; {n_rows}] = [{rows}];")?;
writeln!(out, "{indent}{var}.set_grid_dsc_array(&{upper}_COLS, &{upper}_ROWS);")?;
}
}
_ => {}
}
if let Some(false) = common.scrollable {
writeln!(out, "{indent}{var}.remove_scrollable();")?;
}
Ok(())
}
/// Generate state-specific Style objects and apply via ObjState selector.
fn gen_state_styles(
out: &mut String,
var: &str,
common: &CommonProps,
indent: &str,
doc: &UiDoc,
dedup: &mut StyleDedup,
) -> std::fmt::Result {
let states = [
(&common.state_default, "DEFAULT"),
(&common.state_pressed, "PRESSED"),
(&common.state_focused, "FOCUSED"),
(&common.state_checked, "CHECKED"),
(&common.state_disabled, "DISABLED"),
(&common.state_scrolled, "SCROLLED"),
];
for (maybe_style, state_name) in &states {
if let Some(style) = maybe_style {
let svar = format!("{var}_state_{}", state_name.to_lowercase());
let svar = emit_or_reuse_style(out, &svar, style, indent, doc, dedup)?;
writeln!(out, "{indent}{var}.add_style(&{svar}, ObjState::{state_name});")?;
}
}
Ok(())
}
/// Generate part-specific Style objects and apply via Part selector.
fn gen_part_styles(
out: &mut String,
var: &str,
common: &CommonProps,
indent: &str,
doc: &UiDoc,
dedup: &mut StyleDedup,
) -> std::fmt::Result {
let parts = [
(&common.part_indicator, "Indicator"),
(&common.part_knob, "Knob"),
(&common.part_items, "Items"),
(&common.part_scrollbar, "Scrollbar"),
(&common.part_selected, "Selected"),
(&common.part_cursor, "Cursor"),
];
for (maybe_style, part_name) in &parts {
if let Some(style) = maybe_style {
// Apply named style refs with the Part selector
if let Some(style_ref) = &style.styles {
for name in style_ref.names() {
writeln!(out, "{indent}{var}.add_style(&style_{name}, Part::{part_name});")?;
}
}
// Apply inline properties as a local Style
if has_inline_style_props(style) {
let svar = format!("{var}_part_{}", part_name.to_lowercase());
let svar = emit_or_reuse_style(out, &svar, style, indent, doc, dedup)?;
writeln!(out, "{indent}{var}.add_style(&{svar}, Part::{part_name});")?;
}
}
}
Ok(())
}
/// Emit `widget.on(EventCode::X, ...);` calls for all declared event bindings.
///
/// A `Handler` action names a plain `fn(&oxivgl::event::Event)` in scope at the
/// `include!()` site. A `ShowToast` action emits a non-capturing closure that
/// raises the toast via its `post_<id>` helper.
fn gen_event_bindings(out: &mut String, var: &str, common: &CommonProps, indent: &str) -> std::fmt::Result {
if let Some(on_map) = &common.on {
for (code, action) in on_map {
let ev = code.as_event_code_ident();
match action {
EventAction::Handler(handler) => {
writeln!(out, "{indent}{var}.on(oxivgl::enums::EventCode::{ev}, {handler});")?;
}
EventAction::ShowToast(st) => {
// Raise the toast from a non-capturing closure via its
// cross-task post_<id> helper (literal string args).
let args = st.args.iter().map(|a| format!("{a:?}")).collect::<Vec<_>>().join(", ");
writeln!(
out,
"{indent}{var}.on(oxivgl::enums::EventCode::{ev}, |_| post_{}({args}));",
st.show_toast
)?;
}
}
}
}
Ok(())
}
/// Check if a StyleProps has any inline properties set (excluding `styles`
/// ref). Must cover every field that `gen_style_obj_setters` emits.
fn has_inline_style_props(s: &StyleProps) -> bool {
s.opa.is_some()
|| s.bg_color.is_some()
|| s.bg_opa.is_some()
|| s.bg_image_src.is_some()
|| s.bg_image_opa.is_some()
|| s.bg_image_recolor.is_some()
|| s.bg_image_recolor_opa.is_some()
|| s.bg_image_tiled.is_some()
|| s.border_color.is_some()
|| s.border_width.is_some()
|| s.border_opa.is_some()
|| s.border_side.is_some()
|| s.outline_color.is_some()
|| s.outline_width.is_some()
|| s.outline_opa.is_some()
|| s.outline_pad.is_some()
|| s.shadow_color.is_some()
|| s.shadow_width.is_some()
|| s.shadow_ofs_x.is_some()
|| s.shadow_ofs_y.is_some()
|| s.shadow_spread.is_some()
|| s.shadow_opa.is_some()
|| s.pad_all.is_some()
|| s.pad_top.is_some()
|| s.pad_bottom.is_some()
|| s.pad_left.is_some()
|| s.pad_right.is_some()
|| s.radius.is_some()
|| s.text_color.is_some()
|| s.text_opa.is_some()
|| s.text_font.is_some()
|| s.text_decor.is_some()
|| s.line_color.is_some()
|| s.line_width.is_some()
|| s.line_rounded.is_some()
|| s.arc_color.is_some()
|| s.arc_width.is_some()
|| s.arc_opa.is_some()
|| s.arc_rounded.is_some()
|| s.translate_y.is_some()
|| s.line_opa.is_some()
|| s.pad_row.is_some()
|| s.pad_column.is_some()
|| s.clip_corner.is_some()
|| s.transform_angle.is_some()
|| s.transform_zoom.is_some()
|| s.bg_grad.is_some()
}
/// Emit setters on a Style object (`&mut self` API with `_hex` color variants).
/// Used for style_definitions, state styles, and part styles.
fn gen_style_obj_setters(
out: &mut String,
var: &str,
style: &StyleProps,
indent: &str,
doc: &UiDoc,
) -> std::fmt::Result {
if let Some(o) = &style.opa {
writeln!(out, "{indent}{var}.opa({});", opa_u8_expr(o))?;
}
if let Some(c) = &style.bg_color {
writeln!(out, "{indent}{var}.bg_color_hex({});", color_u32_expr(c))?;
}
if let Some(o) = &style.bg_opa {
writeln!(out, "{indent}{var}.bg_opa({});", opa_u8_expr(o))?;
}
if let Some(sym) = &style.bg_image_src {
writeln!(out, "{indent}{var}.bg_image_src(unsafe {{ &{sym} }});")?;
}
if let Some(o) = &style.bg_image_opa {
writeln!(out, "{indent}{var}.bg_image_opa({});", opa_u8_expr(o))?;
}
if let Some(c) = &style.bg_image_recolor {
writeln!(out, "{indent}{var}.bg_image_recolor_hex({});", color_u32_expr(c))?;
}
if let Some(o) = &style.bg_image_recolor_opa {
writeln!(out, "{indent}{var}.bg_image_recolor_opa({});", opa_u8_expr(o))?;
}
if let Some(tiled) = style.bg_image_tiled
&& tiled
{
writeln!(out, "{indent}{var}.bg_image_tiled(true);")?;
}
if let Some(c) = &style.border_color {
writeln!(out, "{indent}{var}.border_color_hex({});", color_u32_expr(c))?;
}
if let Some(w) = style.border_width {
writeln!(out, "{indent}{var}.border_width({w});")?;
}
if let Some(o) = &style.border_opa {
writeln!(out, "{indent}{var}.border_opa({});", opa_u8_expr(o))?;
}
if let Some(side) = &style.border_side {
writeln!(out, "{indent}{var}.border_side(BorderSide::{});", border_side_str(side))?;
}
if let Some(c) = &style.outline_color {
writeln!(out, "{indent}{var}.outline_color({});", color_make_expr(c))?;
}
if let Some(w) = style.outline_width {
writeln!(out, "{indent}{var}.outline_width({w} as i32);")?;
}
if let Some(o) = &style.outline_opa {
writeln!(out, "{indent}{var}.outline_opa({});", opa_u8_expr(o))?;
}
if let Some(p) = style.outline_pad {
writeln!(out, "{indent}{var}.outline_pad({p} as i32);")?;
}
if let Some(c) = &style.shadow_color {
writeln!(out, "{indent}{var}.shadow_color({});", color_make_expr(c))?;
}
if let Some(w) = style.shadow_width {
writeln!(out, "{indent}{var}.shadow_width({w} as i32);")?;
}
if let Some(x) = style.shadow_ofs_x {
writeln!(out, "{indent}{var}.shadow_offset_x({x} as i32);")?;
}
if let Some(y) = style.shadow_ofs_y {
writeln!(out, "{indent}{var}.shadow_offset_y({y} as i32);")?;
}
if let Some(s) = style.shadow_spread {
writeln!(out, "{indent}{var}.shadow_spread({s} as i32);")?;
}
if let Some(o) = &style.shadow_opa {
writeln!(out, "{indent}{var}.shadow_opa({});", opa_u8_expr(o))?;
}
if let Some(p) = style.pad_all {
writeln!(out, "{indent}{var}.pad_all({p} as i32);")?;
}
if let Some(p) = style.pad_top {
writeln!(out, "{indent}{var}.pad_top({p} as i32);")?;
}
if let Some(p) = style.pad_bottom {
writeln!(out, "{indent}{var}.pad_bottom({p} as i32);")?;
}
if let Some(p) = style.pad_left {
writeln!(out, "{indent}{var}.pad_left({p} as i32);")?;
}
if let Some(p) = style.pad_right {
writeln!(out, "{indent}{var}.pad_right({p} as i32);")?;
}
if let Some(r) = style.radius {
writeln!(out, "{indent}{var}.radius({r});")?;
}
if let Some(c) = &style.text_color {
writeln!(out, "{indent}{var}.text_color_hex({});", color_u32_expr(c))?;
}
if let Some(f) = &style.text_font {
writeln!(out, "{indent}{var}.text_font({});", font_expr(f))?;
}
if let Some(o) = &style.text_opa {
writeln!(out, "{indent}{var}.text_opa({});", opa_u8_expr(o))?;
}
if let Some(d) = &style.text_decor {
writeln!(out, "{indent}{var}.text_decor(TextDecor::{});", text_decor_str(d))?;
}
if let Some(c) = &style.line_color {
writeln!(out, "{indent}{var}.line_color({});", color_make_expr(c))?;
}
if let Some(w) = style.line_width {
writeln!(out, "{indent}{var}.line_width({w} as i32);")?;
}
if let Some(rounded) = style.line_rounded
&& rounded
{
writeln!(out, "{indent}{var}.line_rounded(true);")?;
}
if let Some(c) = &style.arc_color {
writeln!(out, "{indent}{var}.arc_color({});", color_make_expr(c))?;
}
if let Some(w) = style.arc_width {
writeln!(out, "{indent}{var}.arc_width({w} as i32);")?;
}
if let Some(o) = &style.arc_opa {
writeln!(out, "{indent}{var}.arc_opa({});", opa_u8_expr(o))?;
}
if let Some(rounded) = style.arc_rounded
&& rounded
{
writeln!(out, "{indent}{var}.arc_rounded(true);")?;
}
if let Some(o) = &style.line_opa {
writeln!(out, "{indent}{var}.line_opa({});", opa_u8_expr(o))?;
}
if let Some(p) = style.pad_row {
writeln!(out, "{indent}{var}.pad_row({p} as i32);")?;
}
if let Some(p) = style.pad_column {
writeln!(out, "{indent}{var}.pad_column({p} as i32);")?;
}
if let Some(clip) = style.clip_corner
&& clip
{
writeln!(out, "{indent}{var}.clip_corner(true);")?;
}
if let Some(y) = style.translate_y {
writeln!(out, "{indent}{var}.translate_y({y} as i32);")?;
}
if let Some(a) = style.transform_angle {
writeln!(out, "{indent}{var}.transform_rotation({a} as i32);")?;
}
if let Some(z) = style.transform_zoom {
writeln!(out, "{indent}{var}.transform_scale({z} as i32);")?;
}
if let Some(grad_id) = &style.bg_grad
&& let Some(grads) = &doc.lvgl.gradients
&& let Some(gdef) = grads.iter().find(|g| g.id == *grad_id)
{
let dir_method = match gdef.dir.as_str() {
"hor" => "Hor",
"ver" => "Ver",
"linear" => "Linear",
"radial" => "Radial",
"conical" => "Conical",
_ => "Ver",
};
let colors: Vec<String> = gdef.stops.iter().map(|s| color_make_expr(&s.color)).collect();
let fracs: Vec<String> = gdef.stops.iter().map(|s| s.position.to_string()).collect();
writeln!(out, "{indent}{{")?;
writeln!(out, "{indent} let mut g = GradDsc::new();")?;
writeln!(out, "{indent} g.init_stops(&[{}], &[], &[{}]);", colors.join(", "), fracs.join(", "),)?;
writeln!(out, "{indent} g.set_dir(GradDir::{dir_method});")?;
writeln!(out, "{indent} {var}.bg_grad(g);")?;
writeln!(out, "{indent}}}")?;
}
Ok(())
}
// ── Helpers ──────────────────────────────────────────────────────────
/// Color as u32 literal (for Obj inline style methods that take u32 directly).
fn color_u32_expr(c: &Color) -> String {
match c {
Color::Hex(v) => format!("0x{v:06X}"),
Color::Named(n) => format!("Palette::{n}()"),
}
}
/// Color as `color_make(r, g, b)` (for methods that take lv_color_t).
fn color_make_expr(c: &Color) -> String {
match c {
Color::Hex(v) => {
let r = (v >> 16) & 0xFF;
let g = (v >> 8) & 0xFF;
let b = v & 0xFF;
format!("color_make({r}, {g}, {b})")
}
Color::Named(n) => format!("palette_main(Palette::{n:?})"),
}
}
/// Opacity as u8 value.
/// Opacity as u8 value (Style API takes raw u8, not Opa type).
fn opa_u8_expr(o: &Opacity) -> String {
match o {
Opacity::Value(v) => format!("{v}"),
Opacity::Named(n) => match n.as_str() {
"transp" => "0".into(),
"cover" => "255".into(),
_ => format!(" 255"),
},
}
}
fn dim_expr(d: &Dimension) -> String {
match d {
Dimension::Pixels(v) => format!("{v}"),
Dimension::Percent(v) => format!("lv_pct({v})"),
Dimension::SizeContent => "LV_SIZE_CONTENT".into(),
}
}
fn track_expr(t: &TrackSize) -> String {
match t {
TrackSize::Pixels(v) => format!("{v}"),
TrackSize::Fr(v) => format!("grid_fr({v})"),
TrackSize::Content => "(GRID_TEMPLATE_LAST - 101)".into(),
}
}
fn flex_align_str(a: &FlexAlign) -> &'static str {
match a {
FlexAlign::Start => "Start",
FlexAlign::End => "End",
FlexAlign::Center => "Center",
FlexAlign::SpaceEvenly => "SpaceEvenly",
FlexAlign::SpaceAround => "SpaceAround",
FlexAlign::SpaceBetween => "SpaceBetween",
}
}
fn border_side_str(s: &BorderSide) -> &'static str {
match s {
BorderSide::None => "NONE",
BorderSide::Top => "TOP",
BorderSide::Bottom => "BOTTOM",
BorderSide::Left => "LEFT",
BorderSide::Right => "RIGHT",
BorderSide::Full => "FULL",
}
}
fn text_decor_str(d: &TextDecor) -> &'static str {
match d {
TextDecor::None => "NONE",
TextDecor::Underline => "UNDERLINE",
TextDecor::Strikethrough => "STRIKETHROUGH",
}
}
fn enum_variant<T: std::fmt::Debug>(v: &T) -> String {
format!("{v:?}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::parse_str;
#[test]
fn generates_label_on_screen() {
let yaml = r#"
lvgl:
pages:
- id: main
bg_color: 0x000000
widgets:
- label:
text: "Hello"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("fn create_main"));
assert!(code.contains("Label::new"));
assert!(code.contains(r#"text("Hello")"#));
// Page-level style props build a shared Style — the Obj inline
// setters are deprecated since oxivgl 0.4.0.
assert!(code.contains("b.bg_color_hex(0x000000)"));
assert!(code.contains("container.add_style(&container_inline, Selector::DEFAULT)"));
assert!(!code.contains("container.bg_color("));
}
#[test]
fn generates_spdx_passthrough() {
let yaml = "# SPDX-License-Identifier: MIT\nlvgl:\n pages:\n - id: main\n";
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains(" }
#[test]
fn generates_style_defs_and_refs() {
let yaml = r#"
lvgl:
style_definitions:
- id: card
bg_color: 0x1E1E1E
radius: 8
pages:
- id: main
widgets:
- obj:
styles: card
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("style_card"));
assert!(code.contains("bg_color_hex"));
assert!(code.contains("add_style"));
}
#[test]
fn generates_part_styles() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- slider:
part_indicator:
bg_color: 0xFF9800
part_knob:
bg_color: 0xFFFFFF
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Part::Indicator"));
assert!(code.contains("Part::Knob"));
}
#[test]
fn generates_state_styles() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
state_pressed:
bg_color: 0xFF0000
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("ObjState::PRESSED"));
}
#[test]
fn generates_arc_with_range() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- arc:
id: gauge
min_value: 0.0
max_value: 150.0
value: 75.0
start_angle: 130
end_angle: 330
width: 200
height: 200
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Arc::new"));
assert!(code.contains("set_range(150.0)"));
assert!(code.contains("set_value(75.0)"));
assert!(code.contains("set_bg_start_angle(130)"));
assert!(code.contains("set_bg_end_angle(330)"));
}
#[test]
fn generates_flex_layout() {
let yaml = r#"
lvgl:
pages:
- id: main
layout: flex
flex_flow: column
flex_align_main: space_between
flex_align_cross: center
widgets:
- label:
text: "A"
flex_grow: 1
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("set_flex_flow(FlexFlow::Column)"));
assert!(code.contains("FlexAlign::SpaceBetween"));
assert!(code.contains("FlexAlign::Center"));
assert!(code.contains("set_flex_grow(1)"));
}
#[test]
fn generates_grid_layout() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
id: grid_container
layout: grid
grid_columns:
- 1fr
- 1fr
grid_rows:
- content
widgets:
- label:
text: "Left"
grid_cell:
column: 0
row: 0
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("grid_fr(1)"));
assert!(code.contains("GRID_TEMPLATE_LAST"));
assert!(code.contains("set_grid_dsc_array"));
assert!(code.contains("set_grid_cell"));
assert!(code.contains("static "));
assert!(!code.contains("Box::leak"));
}
#[test]
fn grid_with_only_columns_emits_empty_row_dsc() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
layout: grid
grid_columns: ["1fr", "1fr"]
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("_ROWS: [i32; 1] = [GRID_TEMPLATE_LAST];"));
assert!(code.contains("set_grid_dsc_array"));
}
#[test]
fn screen_fn_takes_screen_ref() {
let yaml = "lvgl:\n pages:\n - id: main\n";
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("container: &oxivgl::widgets::Obj<'static>"));
}
#[test]
fn widget_constructor_uses_ref_not_mut() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "hi"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Label::new(container)"));
assert!(!code.contains("let mut _w"));
}
#[test]
fn style_def_uses_hex_methods() {
let yaml = r#"
lvgl:
style_definitions:
- id: dark
bg_color: 0x111111
text_color: 0xFFFFFF
pages:
- id: main
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("bg_color_hex(0x111111)"));
assert!(code.contains("text_color_hex(0xFFFFFF)"));
}
#[test]
fn generates_checkbox() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- checkbox:
id: cb
text: "Enable"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Checkbox::new"));
assert!(code.contains(r#"text("Enable")"#));
}
#[test]
fn generates_slider_with_range() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- slider:
id: vol
min_value: 0
max_value: 100
value: 50
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Slider::new"));
assert!(code.contains("set_range(0, 100)"));
assert!(code.contains("set_value(50)"));
}
#[test]
fn generates_bar_with_range() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- bar:
id: progress
min_value: 0.0
max_value: 100.0
value: 75.0
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Bar::new"));
assert!(code.contains("set_range(100.0)"));
assert!(code.contains("set_value(75.0)"));
}
#[test]
fn generates_dropdown() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- dropdown:
id: dd
options:
- "One"
- "Two"
- "Three"
selected: 1
dir: top
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Dropdown::new"));
assert!(code.contains("set_options"));
assert!(code.contains("set_selected(1)"));
assert!(code.contains("DdDir::Top"));
}
#[test]
fn generates_roller() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- roller:
id: r
options:
- "A"
- "B"
selected: 0
visible_row_count: 3
mode: infinite
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Roller::new"));
assert!(code.contains("set_options"));
assert!(code.contains("RollerMode::Infinite"));
assert!(code.contains("set_visible_row_count(3)"));
assert!(code.contains("set_selected(0, false)"));
}
#[test]
fn generates_scale() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- scale:
id: s
mode: round_inner
total_tick_count: 31
major_tick_every: 5
label_show: true
range_min: 0
range_max: 100
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Scale::new"));
assert!(code.contains("ScaleMode::RoundInner"));
assert!(code.contains("set_total_tick_count(31)"));
assert!(code.contains("set_major_tick_every(5)"));
assert!(code.contains("set_label_show(true)"));
assert!(code.contains("set_range(0, 100)"));
}
#[test]
fn generates_led() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- led:
id: status
color: 0x00FF00
brightness: 200
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Led::new"));
assert!(code.contains("set_color(color_make(0, 255, 0))"));
assert!(code.contains("set_brightness(200)"));
}
#[test]
fn generates_image() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- image:
id: logo
src: "S:logo.bin"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Image::new"));
assert!(code.contains("set_src(\"S:logo.bin\")"));
}
#[test]
fn generates_line() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- line:
id: l
points:
- [0.0, 0.0]
- [100.0, 50.0]
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Line::new"));
assert!(code.contains("lv_point_precise_t { x: 0, y: 0 }"));
assert!(code.contains("lv_point_precise_t { x: 100, y: 50 }"));
assert!(code.contains("static"));
assert!(code.contains("set_points"));
}
#[test]
fn generates_label_long_mode() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "Long text"
long_mode: scroll_circular
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("LabelLongMode::ScrollCircular"));
}
#[test]
fn generates_arc_mode() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- arc:
mode: reverse
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("ArcMode::Reverse"));
}
#[test]
fn generates_hidden_flag() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
hidden: true
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("ObjFlag::HIDDEN"));
}
#[test]
fn generates_xy_without_align() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "pos"
x: 10
y: 20
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains(".x(10)"));
assert!(code.contains(".y(20)"));
}
#[test]
fn generates_inline_obj_styles() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
radius: 12
text_opa: 128
text_font: "montserrat_14"
text_align: center
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("_inline"));
assert!(code.contains("StyleBuilder::new()"));
assert!(code.contains("radius(12)"));
assert!(code.contains("text_opa(128)"));
assert!(code.contains("text_font(oxivgl::fonts::MONTSERRAT_14)"));
assert!(code.contains("Selector::DEFAULT"));
}
#[test]
fn generates_style_def_with_border_outline_shadow() {
let yaml = r#"
lvgl:
style_definitions:
- id: fancy
border_color: 0xFF0000
border_width: 2
border_opa: 200
border_side: top
outline_color: 0x00FF00
outline_width: 3
outline_opa: 150
outline_pad: 4
shadow_color: 0x0000FF
shadow_width: 10
shadow_ofs_x: 2
shadow_ofs_y: 3
shadow_spread: 5
shadow_opa: 180
pages:
- id: main
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("border_color_hex(0xFF0000)"));
assert!(code.contains("border_width(2)"));
assert!(code.contains("border_opa(200)"));
assert!(code.contains("BorderSide::TOP"));
assert!(code.contains("outline_color(color_make(0, 255, 0))"));
assert!(code.contains("outline_width(3 as i32)"));
assert!(code.contains("outline_opa(150)"));
assert!(code.contains("outline_pad(4 as i32)"));
assert!(code.contains("shadow_color(color_make(0, 0, 255))"));
assert!(code.contains("shadow_width(10 as i32)"));
assert!(code.contains("shadow_offset_x(2 as i32)"));
assert!(code.contains("shadow_offset_y(3 as i32)"));
assert!(code.contains("shadow_spread(5 as i32)"));
assert!(code.contains("shadow_opa(180)"));
}
#[test]
fn generates_style_def_with_line_and_transform() {
let yaml = r#"
lvgl:
style_definitions:
- id: line_style
line_color: 0xFF0000
line_width: 3
line_rounded: true
text_decor: underline
translate_y: 10
pages:
- id: main
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("line_color(color_make(255, 0, 0))"));
assert!(code.contains("line_width(3 as i32)"));
assert!(code.contains("line_rounded(true)"));
assert!(code.contains("TextDecor::UNDERLINE"));
assert!(code.contains("translate_y(10 as i32)"));
}
#[test]
fn generates_obj_flex_layout_on_widget() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
layout: flex
flex_flow: row
flex_align_main: center
flex_align_cross: end
flex_align_track: start
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("set_layout(Layout::Flex)"));
assert!(code.contains("set_flex_flow(FlexFlow::Row)"));
assert!(code.contains("FlexAlign::Center"));
assert!(code.contains("FlexAlign::End"));
assert!(code.contains("FlexAlign::Start"));
}
#[test]
fn generates_screen_style_ref() {
let yaml = r#"
lvgl:
style_definitions:
- id: dark_bg
bg_color: 0x000000
pages:
- id: main
styles: dark_bg
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("container.add_style"));
assert!(code.contains("style_dark_bg()"));
}
#[test]
fn generates_inline_padding_on_widget() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
pad_top: 5
pad_bottom: 10
pad_left: 15
pad_right: 20
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("pad_top(5 as i32)"));
assert!(code.contains("pad_bottom(10 as i32)"));
assert!(code.contains("pad_left(15 as i32)"));
assert!(code.contains("pad_right(20 as i32)"));
}
#[test]
fn retains_anonymous_widgets_in_widgets_struct() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "anon"
- label:
id: lbl
text: "named"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(!code.contains("core::mem::forget"));
assert!(code.contains("_retained_1: Label<'static>"));
assert!(code.contains("_retained_1: _w1"));
assert!(code.contains("pub lbl: Label<'static>"));
}
#[test]
fn generates_style_builder_for_defs() {
let yaml = r#"
lvgl:
style_definitions:
- id: s1
bg_color: 0x000000
pages:
- id: main
widgets:
- obj:
styles: s1
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("StyleBuilder::new()"));
assert!(code.contains(".build()"));
assert!(!code.contains("Box::leak"));
assert!(code.contains("let style_s1 = style_s1();"));
assert!(code.contains("add_style(&style_s1,"));
assert!(!code.contains("add_style(&style_s1()"));
}
#[test]
fn generates_budget_assert() {
let yaml = r#"
lvgl:
pages:
- id: main
budget:
max_objects: 12
max_depth: 3
widgets:
- label:
text: "x"
- id: unbudgeted
widgets:
- label:
text: "y"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains(
"oxivgl::diag::assert_budget(container, &oxivgl::diag::Budget { max_objects: 12, max_depth: 3 });"
));
assert_eq!(code.matches("assert_budget").count(), 1);
}
#[test]
fn budget_without_depth_is_unbounded() {
let yaml = r#"
lvgl:
pages:
- id: main
budget:
max_objects: 5
widgets:
- label:
text: "x"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Budget { max_objects: 5, max_depth: u16::MAX }"));
}
#[test]
fn identical_inline_styles_deduplicated() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
id: card_a
bg_color: 0x1A2333
radius: 4
- obj:
id: card_b
bg_color: 0x1A2333
radius: 4
- slider:
id: s1
min_value: 0
max_value: 10
part_indicator:
bg_color: 0x1A2333
radius: 4
- obj:
id: card_c
bg_color: 0x99AABB
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert_eq!(code.matches("let card_a_inline = {").count(), 1);
assert!(!code.contains("let card_b_inline"));
assert!(!code.contains("let s1_part_indicator"));
assert!(code.contains("card_b.add_style(&card_a_inline, Selector::DEFAULT);"));
assert!(code.contains("s1.add_style(&card_a_inline, Part::Indicator);"));
assert!(code.contains("let card_c_inline = {"));
assert_eq!(code.matches("b.bg_color_hex(0x1A2333);").count(), 1);
}
#[test]
fn shared_style_built_once_for_many_refs() {
let yaml = r#"
lvgl:
style_definitions:
- id: chip
bg_color: 0x112233
pages:
- id: main
widgets:
- label:
text: "a"
styles: chip
- label:
text: "b"
styles: chip
- slider:
min_value: 0
max_value: 10
part_indicator:
styles: chip
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert_eq!(code.matches("style_chip()").count(), 2); assert_eq!(code.matches("let style_chip = style_chip();").count(), 1);
assert_eq!(code.matches("add_style(&style_chip,").count(), 3);
}
#[test]
fn generates_bg_image_style_props() {
let yaml = r#"
lvgl:
pages:
- id: main
bg_image_src: img_backdrop
bg_image_recolor: 0x102030
bg_image_recolor_opa: 128
bg_image_opa: 200
bg_image_tiled: true
widgets:
- obj:
bg_image_src: img_texture
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("static img_backdrop: lv_image_dsc_t;"));
assert!(code.contains("static img_texture: lv_image_dsc_t;"));
assert!(code.contains("b.bg_image_src(unsafe { &img_backdrop });"));
assert!(code.contains("b.bg_image_src(unsafe { &img_texture });"));
assert!(code.contains("b.bg_image_recolor_hex(0x102030);"));
assert!(code.contains("b.bg_image_recolor_opa(128);"));
assert!(code.contains("b.bg_image_opa(200);"));
assert!(code.contains("b.bg_image_tiled(true);"));
}
#[test]
fn generates_bar_with_mode_and_orientation() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- bar:
id: progress
max_value: 100.0
value: 50.0
mode: symmetrical
orientation: vertical
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Bar::new"));
assert!(code.contains("set_range(100.0)"));
assert!(code.contains("set_mode(BarMode::Symmetrical)"));
assert!(code.contains("set_orientation(BarOrientation::Vertical)"));
}
#[test]
fn generates_bar_with_nonzero_min() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- bar:
id: b
min_value: 10.0
max_value: 200.0
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("set_range_raw(10, 200)"));
}
#[test]
fn generates_arc_with_nonzero_min() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- arc:
id: a
min_value: 5.0
max_value: 100.0
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("set_range_raw(5, 100)"));
}
#[test]
fn generates_slider_with_orientation() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- slider:
id: s
min_value: 0
max_value: 100
orientation: horizontal
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Slider::new"));
assert!(code.contains("set_orientation(SliderOrientation::Horizontal)"));
}
#[cfg(feature = "widget-spinner")]
#[test]
fn generates_spinner() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- spinner:
anim_time: 1000
arc_length: 60
width: 50
height: 50
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Spinner::new"));
assert!(code.contains("set_anim_params(1000, 60)"));
}
#[cfg(feature = "widget-spinbox")]
#[test]
fn generates_spinbox() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- spinbox:
id: sb
value: 42
min_value: 0
max_value: 999
digit_count: 3
decimal_point: 0
step: 1
rollover: true
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Spinbox::new"));
assert!(code.contains("set_range(0, 999)"));
assert!(code.contains("set_value(42)"));
assert!(code.contains("set_digit_format(3, 0)"));
assert!(code.contains("set_step(1)"));
assert!(code.contains("set_rollover(true)"));
}
#[cfg(feature = "widget-textarea")]
#[test]
fn generates_textarea() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- textarea:
id: input
text: "hello"
placeholder: "Type here..."
password_mode: false
one_line: true
max_length: 64
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Textarea::new"));
assert!(code.contains("set_text(\"hello\")"));
assert!(code.contains("set_placeholder_text(\"Type here...\")"));
assert!(code.contains("set_password_mode(false)"));
assert!(code.contains("set_one_line(true)"));
assert!(code.contains("set_max_length(64)"));
}
#[cfg(feature = "widget-keyboard")]
#[test]
fn generates_keyboard() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- keyboard:
id: kb
mode: number
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Keyboard::new"));
assert!(code.contains("set_mode(KeyboardMode::Number)"));
}
#[cfg(feature = "widget-chart")]
#[test]
fn generates_chart() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- chart:
id: ch
chart_type: line
point_count: 20
div_lines_h: 5
div_lines_v: 3
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Chart::new"));
assert!(code.contains("set_type(ChartType::Line)"));
assert!(code.contains("set_point_count(20)"));
assert!(code.contains("set_div_line_count(5, 3)"));
}
#[cfg(feature = "widget-tabview")]
#[test]
fn generates_tabview() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- tabview:
tab_bar_position: top
tab_bar_size: 40
tabs:
- name: "Settings"
widgets:
- label:
id: settings_lbl
text: "Settings content"
- name: "Info"
widgets:
- label:
text: "Info content"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Tabview::new"));
assert!(code.contains("set_tab_bar_position(DdDir::Top)"));
assert!(code.contains("set_tab_bar_size(40)"));
assert!(code.contains("add_tab(\"Settings\")"));
assert!(code.contains("add_tab(\"Info\")"));
assert!(code.contains("Label::new"));
assert!(code.contains("settings_lbl"));
}
#[cfg(feature = "widget-arc-label")]
#[test]
fn generates_arc_label() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- arc_label:
text: "RPM"
radius: 80
angle_start: 0.0
angle_size: 180.0
dir: clockwise
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("ArcLabel::new"));
assert!(code.contains("set_text_static(c\"RPM\")"));
assert!(code.contains("set_radius(80)"));
assert!(code.contains("set_angle_start(0.0)"));
assert!(code.contains("set_angle_size(180.0)"));
assert!(code.contains("set_dir(ArcLabelDir::Clockwise)"));
}
#[cfg(feature = "widget-calendar")]
#[test]
fn generates_calendar() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- calendar:
id: cal
today_year: 2026
today_month: 3
today_day: 27
shown_year: 2026
shown_month: 3
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Calendar::new"));
assert!(code.contains("set_today_date(2026, 3, 27)"));
assert!(code.contains("set_month_shown(2026, 3)"));
}
#[cfg(feature = "widget-list")]
#[test]
fn generates_list() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- list:
items:
- text: "Section"
- button: "Item 1"
- button: "Item 2"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("List::new"));
assert!(code.contains("add_text(\"Section\")"));
assert!(code.contains("add_button(None, \"Item 1\")"));
assert!(code.contains("add_button(None, \"Item 2\")"));
}
#[cfg(feature = "widget-table")]
#[test]
fn generates_table() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- table:
id: tbl
columns: 2
column_widths: [100, 200]
rows:
- ["Name", "Value"]
- ["Voltage", "12.5"]
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Table::new"));
assert!(code.contains("set_column_count(2)"));
assert!(code.contains("set_column_width(0, 100)"));
assert!(code.contains("set_column_width(1, 200)"));
assert!(code.contains("set_cell_value(0, 0, \"Name\")"));
assert!(code.contains("set_cell_value(0, 1, \"Value\")"));
assert!(code.contains("set_cell_value(1, 0, \"Voltage\")"));
assert!(code.contains("set_cell_value(1, 1, \"12.5\")"));
}
#[cfg(feature = "widget-tileview")]
#[test]
fn generates_tileview() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- tileview:
tiles:
- col: 0
row: 0
scroll_dir: hor
widgets:
- label:
id: tile0_lbl
text: "Page 1"
- col: 1
row: 0
scroll_dir: hor
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Tileview::new"));
assert!(code.contains("add_tile(0, 0, ScrollDir::HOR)"));
assert!(code.contains("add_tile(1, 0, ScrollDir::HOR)"));
assert!(code.contains("tile0_lbl"));
}
#[cfg(feature = "widget-win")]
#[test]
fn generates_win() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- win:
id: w
title: "Settings"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Win::new"));
assert!(code.contains("add_title(\"Settings\")"));
}
#[cfg(feature = "widget-msgbox")]
#[test]
fn generates_msgbox() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- msgbox:
title: "Error"
text: "Something failed"
close_button: true
buttons:
- "OK"
- "Cancel"
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Msgbox::new(Some("));
assert!(code.contains("add_title(\"Error\")"));
assert!(code.contains("add_text(\"Something failed\")"));
assert!(code.contains("add_close_button()"));
assert!(code.contains("add_footer_button(\"OK\")"));
assert!(code.contains("add_footer_button(\"Cancel\")"));
}
#[cfg(feature = "widget-spangroup")]
#[test]
fn generates_spangroup() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- spangroup:
overflow: ellipsis
span_mode: expand
max_lines: 3
indent: 10
spans:
- text: "Hello "
text_color: 0xFF0000
- text: "World"
text_decor: underline
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Spangroup::new"));
assert!(code.contains("set_overflow(SpanOverflow::Ellipsis)"));
assert!(code.contains("set_mode(SpanMode::Expand)"));
assert!(code.contains("set_max_lines(3)"));
assert!(code.contains("set_indent(10)"));
assert!(code.contains("add_span()"));
assert!(code.contains("set_text(c\"Hello \")"));
assert!(code.contains("set_text_color("));
assert!(code.contains("set_text(c\"World\")"));
assert!(code.contains("set_text_decor(TextDecor::UNDERLINE)"));
assert!(code.contains("refresh()"));
}
#[cfg(feature = "widget-buttonmatrix")]
#[test]
fn generates_buttonmatrix() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- buttonmatrix:
id: btnm
button_rows:
- ["A", "B", "C"]
- ["D", "E"]
one_checked: true
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Buttonmatrix::new"));
assert!(code.contains("btnmatrix_map!"));
assert!(code.contains("set_map("));
assert!(code.contains("set_one_checked(true)"));
}
#[cfg(feature = "widget-canvas")]
#[test]
fn generates_canvas() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- canvas:
id: cv
buf_width: 200
buf_height: 150
color_format: rgb565
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("DrawBuf::create(200, 150, ColorFormat::RGB565)"));
assert!(code.contains("Canvas::new"));
}
#[cfg(feature = "widget-canvas")]
#[test]
fn generates_canvas_default_format() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- canvas:
buf_width: 100
buf_height: 100
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("ColorFormat::ARGB8888"));
}
#[cfg(feature = "widget-imagebutton")]
#[test]
fn generates_imagebutton() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- imagebutton:
id: ibtn
src_released:
left: img_btn_left
mid: img_btn_mid
right: img_btn_right
src_pressed:
mid: img_btn_mid_pressed
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("static img_btn_left: lv_image_dsc_t"));
assert!(code.contains("static img_btn_mid: lv_image_dsc_t"));
assert!(code.contains("Imagebutton::new"));
assert!(code.contains("ImagebuttonState::Released"));
assert!(code.contains("Some(unsafe { &img_btn_left })"));
assert!(code.contains("ImagebuttonState::Pressed"));
}
#[cfg(feature = "widget-animimg")]
#[test]
fn generates_anim_img() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- anim_img:
id: anim
frames:
- frame1
- frame2
- frame3
duration: 500
repeat_count: 0
auto_start: true
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("static frame1: lv_image_dsc_t"));
assert!(code.contains("static frame2: lv_image_dsc_t"));
assert!(code.contains("AnimImg::new"));
assert!(code.contains("struct AnimImgFrames"));
assert!(code.contains("static "));
assert!(code.contains("_FRAMES: AnimImgFrames<3>"));
assert!(!code.contains("Box::leak"));
assert!(code.contains("set_src("));
assert!(code.contains("set_duration(500)"));
assert!(code.contains("set_repeat_count(0)"));
assert!(code.contains("start()"));
}
#[cfg(feature = "widget-menu")]
#[test]
fn generates_menu() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- menu:
id: nav
header_mode: top_fixed
root_back_button: true
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("Menu::new"));
assert!(code.contains("set_mode_header(MenuHeaderMode::TopFixed)"));
assert!(code.contains("set_mode_root_back_button(true)"));
}
#[test]
fn generates_transform_rotation() {
let yaml = r#"
lvgl:
style_definitions:
- id: rotated
transform_angle: 450
transform_zoom: 512
pages:
- id: main
widgets: []
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("transform_rotation(450 as i32)"));
assert!(code.contains("transform_scale(512 as i32)"));
}
#[test]
fn generates_arc_and_line_opa() {
let yaml = r#"
lvgl:
style_definitions:
- id: s1
arc_opa: 128
arc_rounded: true
line_opa: 200
pages:
- id: main
widgets: []
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("arc_opa(128)"));
assert!(code.contains("arc_rounded(true)"));
assert!(code.contains("line_opa(200)"));
}
#[test]
fn generates_bg_grad() {
let yaml = r#"
lvgl:
gradients:
- id: sunset
dir: ver
stops:
- color: 0xFF6600
position: 0
- color: 0x330066
position: 255
style_definitions:
- id: grad_style
bg_grad: sunset
pages:
- id: main
widgets: []
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("GradDsc::new()"));
assert!(code.contains("init_stops("));
assert!(code.contains("set_dir(GradDir::Ver)"));
assert!(code.contains("bg_grad(g)"));
}
#[test]
fn generates_pad_row_column_clip() {
let yaml = r#"
lvgl:
style_definitions:
- id: grid_style
pad_row: 8
pad_column: 12
clip_corner: true
pages:
- id: main
widgets: []
"#;
let doc = parse_str(yaml).unwrap();
let code = generate(&doc).unwrap();
assert!(code.contains("pad_row(8 as i32)"));
assert!(code.contains("pad_column(12 as i32)"));
assert!(code.contains("clip_corner(true)"));
}
}