Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,16 @@ my_project/
</component>
```

### The three sigils
### The sigils

| Prefix | Means | Example |
| --- | --- | --- |
| `$name` | An `<api>` property of this element | `<lv_label text="$title"/>` |
| `#name` | A constant from `<consts>` or `globals.xml` | `pad="#space_md"` |
| `{ ... }` | An expression, evaluated once at creation | `hidden="{!icon}"` |
| `@{ ... }` | The same expression as a binding: re-evaluated whenever a subject or variant in it changes | `hidden="@{subject_count == 0}"` |

Inside `{ }` you write bare identifiers, no `$` or `#`.
Inside `{ }` and `@{ }` you write bare identifiers, no `$` or `#`.

### `view` and `extends`

Expand Down Expand Up @@ -123,6 +124,12 @@ Three ways, in order of preference:

<!-- 3. Bound style, applied when a subject matches -->
<bind_style name="style_dark" subject="subject_dark_theme_on" ref_value="1"/>

<!-- 3b. Bound style, applied while an expression is true (no @{} wrapper) -->
<bind_style name="style_warning" if="subject_temp > 10 and subject_temp &lt;= 30"/>

<!-- 2b. Computed local style property -->
<lv_label style_text_color-pressed="@{subject_error ? 0xf00 : 0xaaa}"/>
```

Prefix style names with `style_`. Selectors combine parts and states with `|`.
Expand All @@ -140,6 +147,19 @@ But constants can be used:

Pass the property to a *local* style property instead: `<lv_slider style_border_width-knob="$thickness"/>`.

A `<transition>` child animates a style's properties on state changes. It animates *into* the state of the style holding it, so for both directions add one to the default style too:

```xml
<style name="style_card" bg_color="#color_panel">
<transition props="bg_color" duration="300" easing="ease_out"/>
</style>
<style name="style_card_pressed" bg_color="#color_panel_pressed">
<transition props="bg_color" duration="80"/>
</style>
```

One transition per style, numeric and color properties only, and `<bind_style>` never animates.

## Data binding

Subjects are the interface between the UI and the application. Define them in `globals.xml`:
Expand All @@ -161,15 +181,46 @@ Only `int`, `string` and `float` are supported.
<!-- Conditional: child element binding -->
<bind_flag_if_eq subject="subject_mode" flag="hidden" ref_value="0"/>
<bind_state_if_gt subject="subject_temp" state="checked" ref_value="30"/>

<!-- Generic: any widget attribute bound to any expression -->
<lv_label text="@{'Battery: ' . subject_battery . '%'}"/>
<lv_obj width="@{subject_columns * 100}" style_bg_color="@{subject_on ? 0x0f0 : 0x333}"/>
```

`bind_flag_*` takes a `flag`, `bind_state_*` takes a `state`. Both come in `_eq`, `_not_eq`, `_gt`, `_ge`, `_lt`, `_le`. The `lv_obj-` prefix is optional.

States: `default`, `checked`, `focused`, `focus_key`, `edited`, `hovered`, `pressed`, `scrolled`, `disabled`.
Common flags: `hidden`, `clickable`, `checkable`, `scrollable`, `floating`, `ignore_layout`.

`@{ }` is `{ }` that re-runs whenever a referenced subject or variant changes. It works on **widget** attributes (including `style_*` locals) and on a component instance's **variant** attributes. Not in `<styles>` (initialized once) and not on a component's own props or slots. It must reference at least one subject or variant, and inside it only `type="subject"` props may appear; other props are an error. A failed re-evaluation (e.g. `/0`) keeps the previous value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: A non-subject $prop inside @{ } is described as "an error", which contradicts both this paragraph's own soft-failure theme and the authoritative docs/syntax/data-binding.mdx, where it is "skipped with a warning, but the widget is still created normally". Consider rewording to "skipped with a warning" so users don't expect a hard failure that doesn't occur.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At AGENTS.md, line 195:

<comment>A non-subject `$prop` inside `@{ }` is described as "an error", which contradicts both this paragraph's own soft-failure theme and the authoritative docs/syntax/data-binding.mdx, where it is "skipped with a warning, but the widget is still created normally". Consider rewording to "skipped with a warning" so users don't expect a hard failure that doesn't occur.</comment>

<file context>
@@ -161,15 +181,46 @@ Only `int`, `string` and `float` are supported.
 States: `default`, `checked`, `focused`, `focus_key`, `edited`, `hovered`, `pressed`, `scrolled`, `disabled`.
 Common flags: `hidden`, `clickable`, `checkable`, `scrollable`, `floating`, `ignore_layout`.
 
+`@{ }` is `{ }` that re-runs whenever a referenced subject or variant changes. It works on **widget** attributes (including `style_*` locals) and on a component instance's **variant** attributes. Not in `<styles>` (initialized once) and not on a component's own props or slots. It must reference at least one subject or variant, and inside it only `type="subject"` props may appear; other props are an error. A failed re-evaluation (e.g. `/0`) keeps the previous value.
+
+To give each instance its own data, declare `<prop name="temp" type="subject"/>` and pass a subject name at the call site: `<room_card temp="subject_kitchen"/>`.
</file context>


To give each instance its own data, declare `<prop name="temp" type="subject"/>` and pass a subject name at the call site: `<room_card temp="subject_kitchen"/>`.

**Binding beats callbacks.** A radio group, a theme switch, or a value readout needs no C at all: write the subject with `subject_set_int_event`, read it with `bind_state_if_eq`.

## Variants

A component's named visual states, declared in `<api>`. Per-instance and reactive, so they are the component-scoped counterpart of global subjects.

```xml
<api>
<variants>
<variant name="size" options="small large" default="small"/>
<variant name="tone" options="normal danger" default="normal"/>
</variants>
</api>
<view extends="lv_button">
<style name="style_normal"/>
<bind_style name="style_danger" subject="tone" ref_value="danger"/>
<bind_style name="style_large" subject="size" ref_value="large"/>
<lv_label text="Subtitle" hidden="@{size == small}"/>
</view>
```

Read a variant with `<bind_style subject="<variant>" ref_value="<option>">` (preferred for anything visual) or in `@{ }`, where the variant name is the current option and an option name is a constant.

Pick an option on the instance, `<my_badge size="large" tone="@{subject_level > 100 ? danger : normal}"/>`, or from C with the exported `my_badge_set_size(obj, MY_BADGE_SIZE_LARGE)` (`lv_xml_set_variant(obj, "size", "large")` at runtime). An unknown option on the instance falls back to `default` with a warning; in `lv_xml_set_variant()` it's refused and the option is left unchanged. Option names must be unique across a component's variants, a variant name shadows a same-named prop/const/subject, and reordering `options` breaks already exported C.

## Events

All are children of a widget, all take `trigger` (`clicked`, `long_pressed`, `value_changed`, ...):
Expand All @@ -196,14 +247,16 @@ Evaluated **once at creation**, not reactive. For anything that changes at runti
<lv_obj style_bg_color="{is_on ? 0x00ff00 : 0x333333}"/>
```

`.` concatenates. Strings use single quotes. There is no `&&` or `||`, comparisons cannot be chained, and ternaries cannot be nested.
`.` concatenates. Strings use single quotes. Comparisons cannot be chained (`a < b < c`) and ternaries cannot be nested.

`&&` and `||` exist, but `&` and `<` must be XML-escaped in an attribute value, so prefer the `and` / `or` keywords: `hidden="{a > 10 and a &lt;= 30}"`. Both sides are always evaluated, there is no short-circuiting.

## Animations

```xml
<animations>
<timeline name="timeline_load">
<animation prop="translate_x" target="self" start="-30" end="0" duration="500"/>
<animation prop="translate_x" target="self" start="-30" end="0" duration="500" easing="ease_out"/>
<animation prop="opa" target="label" start="0" end="255" duration="500" delay="200"/>
<include_timeline target="icon" timeline="show_up" delay="300"/>
</timeline>
Expand All @@ -212,6 +265,8 @@ Evaluated **once at creation**, not reactive. For anything that changes at runti

`target="self"` is the `view`; anything else is matched against a child's `name`. Play with `<play_timeline_event>`.

`easing` (on `<animation>` and `<transition>`) is `linear` (default), `ease_in`, `ease_out`, `ease_in_out`, `overshoot`, `bounce`, `step`, `bezier(x1 y1 x2 y2)` with `x` in `0..1`, or a callback registered with `lv_xml_register_easing_cb()`.

## Slots

Expose an internal object as a place where the caller can add children:
Expand All @@ -237,7 +292,8 @@ The slot target is `<component_name-slot_name>`, and you can set normal object p

- Inventing an attribute instead of reading `lvgl_widgets_xml/`.
- Putting `$prop` into a `<style>`. Use a local style property.
- Expecting `{ }` to update at runtime. It does not, that's data binding.
- Expecting `{ }` to update at runtime. It does not, write `@{ }` for that.
- Putting a non-subject `$prop` inside `@{ }`, or `@{ }` on a component's own prop or in a `<style>`. None of them can update.
- Using `bind_state_*` with a `flag=` attribute, or `bind_flag_*` with `state=`.
- `screen_load_event` on a screen that isn't `permanent="true"`.
- Hard-coding `pad="8"` and `bg_color="0x1E232E"` when `#space_md` and `#color_dark_panel` already exist in `globals.xml`.
Expand Down
50 changes: 49 additions & 1 deletion docs/syntax/animations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,58 @@ Within each `timeline`, add individual `<animation>` elements to describe each s
- `target` - Name of the UI element to animate. `self` refers to the root element of the Component (the `view`).
- `start` - Start value (integer only).
- `end` - End value (integer only).
- `duration` - Duration of the animation in milliseconds.
- `duration` - Duration of the animation in milliseconds. Default is `1000`.
- `delay` - Delay before starting in milliseconds. Default is 0.
- `early_apply` - If `true`, the start value is applied immediately, even during the delay. Default is `false`.
- `easing` - The animation path, e.g. `ease_out`. See below. Default is `linear`.

### Easing

The `easing` property tells how the value should progress from `start` to `end` during the `duration`. Besides `<animation>`, style [`<transition>`](./styles#transitions)s support it too.

These built-in paths can be used:

- `linear` - Constant speed. This is the default.
- `ease_in` - Slow start.
- `ease_out` - Slow end.
- `ease_in_out` - Slow start and end.
- `overshoot` - Goes above the end value and settles back.
- `bounce` - Bounces back a few times at the end.
- `step` - Stays at the start value and jumps to the end value at the very end.

```xml
<animation prop="translate_y" target="self" start="-30" end="0" duration="500" easing="ease_out"/>
```

For full control, `bezier(x1 y1 x2 y2)` describes a cubic bezier curve with its two control points, just like `cubic-bezier()` in CSS:

```xml
<animation prop="translate_y" target="self" start="-30" end="0" duration="500"
easing="bezier(0.34 1.56 0.64 1)"/>
```

`x1` and `x2` need to be in the `0..1` range, while `y1` and `y2` can be outside it to overshoot. At most four decimals are used from each value. Ready to use curves can be picked from e.g. [easings.net](https://easings.net).

Finally, an animation path implemented in C can be registered by name and then referenced in `easing`:

```c
static int32_t my_easing(const lv_anim_t * a)
{
return lv_map(a->act_time, 0, a->duration, a->start_value, a->end_value);
}

/* `NULL` registers the name globally, so every XML file can use it.
* Pass `lv_xml_component_get_scope("my_component")` to keep it local. */
lv_xml_register_easing_cb(NULL, "my_easing", my_easing);
```

The callback returns the value to apply at the current moment, so it interpolates between `start_value` and `end_value` itself.

```xml
<animation prop="translate_y" target="self" start="-30" end="0" duration="500" easing="my_easing"/>
```

Register the callback **before** the XML that uses it, as the name is resolved while the XML is being parsed. In the exported code the name is emitted as a plain C function reference, so the function has to be visible to the generated file.

### Include External Timelines

Expand Down
2 changes: 2 additions & 0 deletions docs/syntax/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ lv_obj_t * my_component_create(lv_obj_t * parent, int32_t prop1, const char * pr

These properties are set once at creation time, and there are no specific `set` functions to modify the property later. LVGL's general API can still be used to modify any widget in the component, but no dedicated API functions are generated.

For a value that has to change after creation, use a [variant](./variants) (a named set of options declared in `<api>`, with a generated setter) or a [subject](./data-binding).

### Slots

With the help of a "slot," any UI element in the component can be easily exposed as a parent where children can be created later.
Expand Down
98 changes: 98 additions & 0 deletions docs/syntax/data-binding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,104 @@ The most commonly bound `LV_OBJ_FLAG_*` flags, exposed by the same names without

For the full list of states and flags see LVGL's [Object basics](https://lvgl.io/docs/open/intro/basics) and [Style states](https://lvgl.io/docs/open/main-modules/style#states) reference.

## Expression binding

The `bind_*` attributes and elements can bind only what they were designed for. To bind **any** widget attribute to **any** expression of subjects, wrap the expression in `@{ }`:

```xml
<lv_label text="@{'Battery: ' . subject_battery . '%'}"/>
<lv_obj width="@{subject_column_count * 100}"/>
<lv_obj hidden="@{subject_unread == 0}"/>
<lv_obj style_bg_color="@{subject_temp > 30 ? 0xff0000 : 0x00ff00}"/>
```

The expression is evaluated when the widget is created, and **re-evaluated whenever any subject it references changes**. The result is applied to the attribute exactly as if you had typed the computed value there.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

`@{ }` and `{ }` share the same syntax, operators, and type rules, see [Evaluate expressions](./evaluate). The difference is only *when* they run:

```xml
<!-- Updates whenever subject_volume changes -->
<lv_label text="@{'Volume: ' . subject_volume}"/>

<!-- Evaluated once, when the label is created -->
<lv_label text="{'Volume: ' . subject_volume}"/>
```

### What can be referenced

Inside `@{ }` names are written as **bare identifiers**, without `$` or `#`:

- **Subjects** - defined in `globals.xml`.
- **Variants** - the Component's own reactive state. A variant name reads the currently selected option, and the option names can be used as constants. See [Variants](./variants).
- **Constants** - they are inlined as literals.
- **`type="subject"` properties** - see [Per-instance subjects](#per-instance-subjects) below.

An expression has to reference at least one subject or variant. Otherwise it could never update, so it's skipped with a warning.

Properties of any other type **cannot** be used, because they exist only while the element is being created. Such a binding is skipped with a warning, but the widget is still created normally.

Several subjects can be used in one expression, and a change of any of them re-evaluates it:

```xml
<lv_obj width="@{subject_cell_width * subject_column_count}"/>
```

### Per-instance subjects

A binding written in a Component is shared by all of its instances, so referencing a subject by name makes every instance follow the *same* subject. To let each instance follow a *different* one, declare a `type="subject"` property and pass the subject's name where the Component is used:

```xml
<!-- room_card.xml -->
<component>
<api>
<prop name="temperature" type="subject"/>
</api>

<view extends="lv_obj">
<lv_label text="@{temperature . ' °C'}"/>
<lv_obj style_bg_color="@{temperature > 25 ? 0xff8800 : 0x0088ff}"/>
</view>
</component>
```

```xml
<!-- screen -->
<room_card temperature="subject_kitchen_temp"/>
<room_card temperature="subject_bedroom_temp"/>
```

The value has to name an existing subject. If it doesn't, the binding is skipped with a warning. In the exported code such a property becomes an `lv_subject_t *` argument of the Component's create function.

### Where it can be used

`@{ }` works on the attributes of **widgets**: on the `<view>` itself, on any widget inside it, and on local style properties (`style_bg_color`, `style_pad_all-pressed`, ...). It also works on a Component instance's [variant](./variants) attributes.

It can't be used in the `<styles>` section, as [styles](./styles) are initialized only once, nor on the normal properties of a Component instance:

```xml
<!-- ❌ A style is initialized once, so it can't be bound -->
<style name="style_main" bg_color="@{subject_theme_color}"/>

<!-- ❌ `title` is a property of the my_card Component -->
<my_card title="@{subject_user_name}"/>

<!-- ✅ Bind inside the Component, and pass the subject to it
(`title_subject` is a `type="subject"` property of my_card) -->
<my_card title_subject="subject_user_name"/>
```

To switch a whole style at runtime, use [`<bind_style>`](./styles#style-binding), which takes the same kind of expression in its `if` attribute:

```xml
<bind_style name="style_warning" if="subject_temp > 10 and subject_temp &lt;= 30"/>
```

### Errors

An expression that can't be parsed is reported with a warning and the binding is skipped, so the attribute keeps its default value. The widget itself is always created.

If the evaluation fails only later, for example on a division by zero after a subject became `0`, a warning is logged and the attribute **keeps its previous value** instead of being set to something meaningless.

## Selection groups via a shared subject

A common pattern is to express "exactly one of these widgets is active" with a single integer subject and one `ref_value` per option. Combining `subject_set_int_event` (writes the subject on click) with `bind_state_if_eq` (reads it to drive the `checked` state) produces a runtime-driven radio group with no application code.
Expand Down
Loading
Loading