Scene JSON Guide
Scene JSON describes the build-time UI: logical size, hierarchy, appearance, assets and interactions. Product state, I/O and display-driver setup remain in application/BSP code. Use this guide for common decisions and the generated authoring reference to look up every accepted field and default.
From a description or reference image
Section titled “From a description or reference image”Lock the logical resolution, pixel format, display orientation, available fonts/assets, dynamic content and user actions first. Use a screenshot or design image as a visual reference:
- Model visible elements as containers, labels, controls, shapes and reusable images. A screenshot asset cannot provide their states or interactions.
- Scale geometry when the reference and display resolutions differ.
- Record assumptions for missing fonts, exact colors and interaction states.
A screenshot asset does not define pressed/disabled state, scrolling, navigation, loading or error behavior; declare those behaviors in the scene and application.
Minimal scene
Section titled “Minimal scene”{ "screen": "dashboard", "w": 320, "h": 240, "screen_bg": "#101820", "objects": [ { "type": "container", "parent": -1, "name": "card", "x": 20, "y": 24, "w": 280, "h": 192, "bg_color": "#1B263B", "radius": 16 }, { "type": "progress", "parent_name": "card", "name": "load", "x": 20, "y": 64, "w": 240, "h": 18, "value": 30, "fg_color": "#4CC9F0" }, { "type": "toggle", "parent_name": "card", "name": "power", "callback": "power_changed", "x": 108, "y": 124, "w": 64, "h": 32, "fg_color": "#4361EE" } ]}Required top-level fields are screen, w, h and a non-empty objects
array. Scene size is in logical pixels and must match the bundle/BSP display
path. w and h must be greater than zero. default_font_size, when present,
must be 1–255. Build uses the same field type, enum, range and semantic checks
as gspc diagnose, so unsupported combinations fail before lowering. The
documented rectangle aliases retain their legacy color fields; unknown fields
on registered widgets are errors.
Ordinary parent references may point forward or backward in the object array. Inherited hidden state and template membership do not depend on that ordering; the compiler preserves authored draw order.
Reusable project components
Section titled “Reusable project components”Grid/wrap and component expansion require GSPC 0.7.0, paired with ESP-GSP 1.6.0. See the 1.6.0 release notes.
A scene’s components map, or JSON files loaded with --components-dir, defines
parameterized subtrees. Each definition has an objects array and an optional
params array of required argument names. A whole string such as "$title" is
replaced by the corresponding argument, preserving its JSON type. Strings that
do not start with $ also support textual substitution, for example
"Reading: $value"; non-string arguments become JSON text in this form. A string
starting with $ is treated as one complete parameter reference, not as a
prefix followed by a template suffix. Substitution does not evaluate arithmetic
expressions.
Instantiate a definition with use. The use site can override name, x, y
and either parent or parent_name on the root. Named parents can be declared
later in the scene. Other use-site fields must be declared parameters and used
explicitly by the definition. A child’s numeric parent inside the definition
is a local object index; omitted parents attach to the definition’s root.
parent_name references scene-wide names. Give each repeated instance distinct
names for any exposed children, for example through required name parameters.
Definitions can contain ordinary widgets and built-in composites such as
TabView, Dropdown, Keyboard and MsgBox. Their generated controls participate in
the same event and overlay paths as directly authored controls. Nested use
inside a definition is diagnosed; combine definitions at scene level or flatten
shared subtrees. Scene-level styles and root font defaults remain available.
Asset paths resolve relative to the scene, not to the component definition file.
The component patterns example
provides settings rows, icon buttons and badges. These are editable project
components, not new runtime widget types. Expanded objects still consume command,
state and resource capacity; compare against an equivalent hand-authored scene.
Use gspc diagnose --components-dir <directory> <scene.json> for expansion and
build-semantic diagnostics. A daemon request with level: "schema" checks source
structure only; use level: "compile" for the complete validation path.
Appearance
Section titled “Appearance”This card combines a rounded background, an outside outline, a hard shadow and column layout. The two bars are ordinary child rectangles:
{ "screen": "styled_card", "w": 240, "h": 160, "screen_bg": "#101827", "objects": [ {"type":"container","name":"card","x":20,"y":20,"w":200,"h":104, "bg_color":"#24334A","radius":12,"outline_color":"#5AA9E6","outline_width":2, "shadow_color":"#000000","shadow_offset_y":6,"shadow_opacity":96, "layout":"column","padding":16,"gap":12,"align_cross":"stretch"}, {"type":"rect","parent_name":"card","w":168,"h":16,"bg_color":"#5AA9E6"}, {"type":"rect","parent_name":"card","w":168,"h":16,"bg_color":"#5AA9E6","bg_opacity":96} ]}Save it as styled_card.json and run gspc pack styled_card.json --deployable -o styled_card.gspb.
The fields below describe how to adapt the appearance and layout.
For content and structural widgets, bg_opacity (0–255, default 255) affects
only the background. Its effective alpha is
round(round(opacity * bg_opacity / 255) * color_alpha / 255).
border_opacity independently scales an authored border. A non-255 bg_opacity
requires literal opacity.
Widgets with the outline fields use outline_color, outline_width,
outline_pad and outline_opacity for an outside rounded outline. The outline
requires a color and a positive width. border_side is available on content and
structural widgets with the values all (default), none, top, bottom,
left, right, horizontal and vertical. Partial borders are inside the
original box and require static w, h and radius: 0; width, color and
opacity are shared. Opposite thick sides are split so a pixel is not blended
twice. all keeps the rounded-border behavior, while none suppresses only
the border.
shadow_offset_x/y, shadow_spread, shadow_radius, shadow_color and
shadow_opacity enable a hard, filled rounded shadow. Defaults are black,
opacity 96, zero offset/spread and the element radius; spread increases the
paint bounds and shadow radius. Decorations use the element box, not the image
alpha silhouette or the union of child bounds. They expand painted and dirty
regions without changing layout size or hit targets, and ancestor clipping
still applies. Each enabled outline or shadow adds at most one primitive before
optimization; zero effective opacity emits none. A partial border adds one strip
per selected side, up to two. Translucent pixels and larger dirty regions add
rendering cost.
Scene position groups and whole template instances can move decorations with
their element. Outlines and shadows require static w/h/radius.
Template members with partial borders require literal x/y and
a literal or static-theme border color. Template members with outlines or
shadows require literal x/y and literal or static-theme decoration colors.
Layout
Section titled “Layout”layout resolves direct children at build time. row and column keep their
existing behavior when layout_wrap is omitted or false. In row/column layout,
margin_left, margin_right, margin_top and margin_bottom override the
corresponding sides of margin.
For row/column, align_main accepts start, center, end and
space_between; align_cross accepts start, center, end and stretch.
In non-wrapped row/column, omitting align_cross preserves the authored
cross-axis position. Set layout_wrap: true to partition children into lines
using each child’s bounded authored main size, including margins. gap separates
adjacent children and lines. An oversized child remains a single item. Alignment
is applied independently to each line, and wrapped row/column defaults to
cross-axis start. A wrapped line distributes extra main-axis space to grow
children from their authored bounded size, subject to their maximum bounds.
layout: "grid" arranges authored children; it is distinct from the virtualized
data Grid widget, type: "grid", which recycles cells.
layout: "grid" requires grid_columns from 1 to 65535. It creates equal-width
column tracks; when the inner width is not divisible evenly, the remainder goes
to the leading columns. Children are placed in authored order, filling rows from
left to right; a grid_column_span (default 1) may occupy 1 through
grid_columns columns and moves to the next row when it does not fit. A grid row
height is the maximum authored bounded child height, including its vertical
margins. Grid child width comes from the spanned track width minus horizontal
margins, clamped by the child’s width bounds. align_main (start, center or
end) positions a max-width-capped child within its cell; space_between is
invalid for grid. align_cross (start, center, end or stretch) positions
or stretches a child within its row. Grid children cannot use grow.
min_width, max_width, min_height and max_height (0–65535 pixels)
constrain static dimensions, including row/column grow, cross-axis stretch and
grid cell sizing.
The minimum must not exceed the maximum, and a bound cannot share its dimension
with a bounded dynamic declaration. Grow allocation reserves minima, distributes
remaining space by grow weight, and returns space from children that reach their
maxima. align_main places any space left after all grow children reach their
maxima. If minima do not fit, they are preserved and content overflows. These
constraints do not add runtime reflow. Layouts with explicit alignment or size
constraints reject bounded dynamic child positions or grow sizes that they would
overwrite; when these fields are omitted, the legacy path remains permissive.
Grid and wrapped row/column reject bounded dynamic x, y, w or h on direct
children because the solver owns or uses that geometry. Nested layout parents
resolve before their child layouts, including when a parent is declared later in
the object array. Runtime setters do not reflow siblings. Existing non-wrapped
row/column scenes remain unchanged when these new fields are not used. Move the
enclosing group when the compiled arrangement should move together.
An expanded composite used as a direct Grid/Wrap child may move when its root
keeps its authored size. If layout would change the expanded root’s actual
w/h, compilation reports a diagnostic because the composite’s internal
geometry was fixed during expansion. Keep the composite at its authored fixed
size inside a container and make that container the direct Grid/Wrap child; the
container can take the layout-assigned geometry while the composite retains its
internal geometry.
Generated composite overlays and placeholders are excluded from Grid/Wrap
placement and never consume cells.
This complete scene shows a three-column grid with a spanning child and a wrapped
row. The child x/y positions are omitted because the selected layout supplies
them:
{ "screen": "grid_wrap", "w": 320, "h": 240, "screen_bg": "#101820", "objects": [ { "type": "container", "name": "grid", "x": 16, "y": 16, "w": 288, "h": 104, "layout": "grid", "grid_columns": 3, "padding": 8, "gap": 8, "align_main": "center", "align_cross": "center", "bg_color": "#1B263B" }, { "type": "rect", "parent_name": "grid", "w": 64, "h": 24, "max_width": 72, "bg_color": "#4CC9F0" }, { "type": "rect", "parent_name": "grid", "w": 64, "h": 24, "max_width": 72, "bg_color": "#90BE6D" }, { "type": "rect", "parent_name": "grid", "w": 128, "h": 24, "grid_column_span": 2, "max_width": 152, "bg_color": "#F9C74F" }, { "type": "container", "name": "wrapped", "x": 16, "y": 136, "w": 288, "h": 88, "layout": "row", "layout_wrap": true, "padding": 8, "gap": 8, "align_main": "center", "bg_color": "#1B263B" }, { "type": "rect", "parent_name": "wrapped", "w": 100, "h": 20, "bg_color": "#F94144" }, { "type": "rect", "parent_name": "wrapped", "w": 90, "h": 24, "margin_right": 4, "bg_color": "#F3722C" }, { "type": "rect", "parent_name": "wrapped", "w": 120, "h": 18, "bg_color": "#577590" } ]}Static scene text supports text_line_space (0–4096 extra pixels between rows)
and text_vertical_align (auto, top, center, bottom). Alignment applies
to the complete visible text block, including empty rows. auto keeps
single-line text centered and multiline text top-aligned. The values are applied
at compile time and add no runtime text-layout work.
Non-default spacing or vertical alignment requires unbound scene text, input
disabled and literal w/h. Text bindings, input fields, template text and
bounded dynamic w/h reject these non-default values. Color and visibility
bindings remain available.
Images
Section titled “Images”The image widget’s image_opacity (0–255, default 255) affects image pixels
without changing the widget background or border. It also applies to ordinary
template image blits. A fixed, non-animated raster can combine image opacity
with runtime fit, scaling and rotation when opacity is static 255 (the default), the
effective codec is unset (codec omitted, auto or default) or store, and
store_scale is 1. With codec omitted, the legacy compress: true setting
selects a lossless container and cannot use this conversion. Dynamic image sources,
template dynamic_image, animated assets and SVG do not use opacity baking;
combinations that require baking produce a compiler error.
When baking is used, the compiler multiplies source alpha by image_opacity / 255
and emits a lossless store resource variant, without the profile’s automatic
compression policy. The alpha plane and multiple opacity variants can increase
resource size; identical encoded payloads are deduplicated. Runtime transforms
then use the baked alpha without an extra opacity operation.
For a translucent raster image, literal rotation: 0 is normalized to no
runtime rotation property and emits a warning. Opaque images retain their
rotation property at zero. A bounded dynamic rotation remains a runtime
property even when its default is zero.
Field decisions
Section titled “Field decisions”| Purpose | Fields | Rule |
|---|---|---|
| Type and hierarchy | type, parent or parent_name |
Use one parent form. Parent-relative coordinates and object-array paint order apply. |
| Geometry | x, y, w, h |
Author in logical pixels. Parents normally appear before children. |
| Application API | name |
Add to application-controlled elements; it drives generated typed helpers. |
| Application event | callback |
Use when product code must react; keep the resulting callback non-blocking. |
| Declarative event | events |
Use for scene-local show/hide, value or navigation actions. |
| Advanced raw state | bind, bind_target |
Declare when the desired runtime property requires a bind; see the declaration rules below. |
| Appearance | bg_color, fg_color, border_*, radius, opacity |
The selected widget must accept the field. Colors use #RRGGBB or #RRGGBBAA. |
| Text and assets | text, font*, image, fit |
Paths are relative to the scene. Declare font_charset for runtime text beyond static/ASCII characters. |
| Layout | layout, layout_wrap, grid_columns, child grid_column_span, padding, gap, child margin/grow |
Row/column controls its main axis; grid assigns equal tracks; direct child geometry is compiler-owned. |
| Reuse | styles, themes, components, template |
Styles/themes/components are compile-time reuse; templates create/recycle runtime instances. |
Later objects paint and hit-test above earlier objects. Keep Drawer descendants
after their Drawer. Use hidden for initial visibility and enabled only when
runtime enabled/disabled behavior is required.
Runtime-created template instances
Section titled “Runtime-created template instances”When application code calls a generated *_create() helper or
esp_gsp_widget_create(), declare the maximum number of simultaneously live
copies on that template. For example, code that may keep four cards alive uses:
{ "type": "container", "template": "card", "max_instances": 4, "w": 100, "h": 100}gspc adds this quota to the bundle’s automatic shared-instance requirement,
so the application does not calculate or duplicate a total pool size in
Kconfig. Generated gsp_<bundle>_<template>_create() helpers retain the same
typed API. If a fifth copy is requested, the runtime reports the template name
ID, live count and the JSON setting to raise. Keep
CONFIG_ESP_GSP_CONTEXT_DEFAULT_INSTANCES=0 unless consuming an externally
compiled bundle that cannot be regenerated.
Choose the right structure
Section titled “Choose the right structure”| Requirement | Start with |
|---|---|
| Fixed content | label, image, shape, container |
| Values and switches | progress, slider, arc, toggle, checkbox |
| Small fixed choices | dropdown, wheel, tabview |
| Dynamic sequence | list plus row_template |
| Album | grid plus cell_template |
| Conversation | message_list |
| Camera/video pixels | authored image target plus Canvas API |
| Navigation | page_flow, stackview, drawer, layer |
item_count is logical dataset length. List/Grid Kconfig and runtime limits
cover only simultaneously active controls and recycled rows/cells.
Validate
Section titled “Validate”Prefer the owning ESP-IDF build because it applies the actual bundle options, target profile and Kconfig limits:
idf.py buildScene compilation is integrated by gsp_add_bundle(); ordinary applications
do not need compiler source files or private runtime headers. Rebuild before
using generated APIs. Check layout and interaction in the simulator, then verify display, touch and
animation on the target board.
Dynamic property scope
Section titled “Dynamic property scope”A runtime-update marker identifies fields that can generate runtime properties. The declaration determines which properties are generated:
namegives an object a stable identity; it does not make every static field dynamic.- For bounded geometry and opacity fields, scalar literals remain static. To expose a runtime property, use
{"default":128,"min":0,"max":255,"property":"alpha"}. The optionalpropertydefaults to the field name. - Raster images use dynamic
x/y,rotationandscalablefor position, rotation and scale; use literals forw/h/opacity/radius. Scene SVG images also support boundedw/h; template images use literals for all four fields. - Dynamic
x/ytranslate the object and its subtree. Dynamicw/h/radius/opacityaffect the object’s own drawing; they do not relayout children or apply opacity to an entire subtree as a unit. - A layer’s dynamic
w/h/radius/opacitycontrols its background fill; declare a background color when using these properties. - Widget values, text, colors, visibility and template-instance properties each have their own declarations and APIs.
The field tables distinguish scene objects from template members. “Own fill”
means the property controls the object’s background; text, images and children
keep their own appearance. “Image” requires an image source, and “SVG” applies
to scene SVG images. Enable runtime_style where the table specifies it.
Template helpers use the following units: bounded width/height/radius/opacity
setters use a 0..100 value, while the JSON default is authored in the declared units.
For templates, omit property or use the field name; custom names are rejected.
Template slot names follow member and field names; scene property names select the
corresponding generated scene-property API.
Declaring runtime controls
Section titled “Declaring runtime controls”For C-side visibility of an ordinary visible object, declare bind: "eye_visible"
with bind_target: "visible". Declare a text Bind for mutable text. Value controls,
bounded dynamic fields and template slots also generate their corresponding APIs,
with no additional Bind declaration. Static objects can omit bindings.
A named object with hidden: true retains its initial hidden state, visibility API
and required glyphs. An unnamed static hidden subtree without visibility bindings
or declarative visibility references is removed at compile time. Statically hidden
template members are not drawn.
The bundle header exports GSP_<SCENE>_OBJ_KEY_<NAME> by default, for example
GSP_HOME_OBJ_KEY_EYE. These stable keys require neither LEGACY nor RAW_IDS switches.
Renaming the JSON object makes use of its old symbol fail during application compilation.
Prefer generated typed helpers; use qualified keys for generic component APIs rather
than hard-coded FNV values. Legacy unqualified names remain opt-in to avoid collisions.
Press actions and default feedback
Section titled “Press actions and default feedback”Ordinary clickable controls already have a default pressed overlay; a pointer observer
is not required. JSON press, release, cancel, long and click actions using call arrive
as ESP_GSP_EVENT_CALL with their respective callback IDs.
Click eligibility covers the entire contact. Reaching the configured tap tolerance,
leaving the target beyond its edge margin, or parent gesture takeover cancels the
contact even if the finger returns. Small edge jitter may return within the margin
(one quarter of tap tolerance, capped at 4 scene pixels); release must still hit the
original control. Pointer injection and hardware touch frames use the same rules.
Use click for navigation or submission; press fires immediately on DOWN.
Holding an ordinary control for 500 ms without dragging fires long once and
suppresses click on release. Leaving its actual hit area restarts the long-press
timer. Cancellation restores pressed feedback and emits cancel once instead of
release/click; bind cancel to stop application actions started by press.
Hiding, disabling or leaving the scene also cancels the contact. Restoring a control
while held does not start another press. Slider/Arc changes and commits follow the
range-control contract in the workflow guide. Keep callbacks nonblocking.
Container bounds and clipping
Section titled “Container bounds and clipping”A Container or Layer groups children and provides their coordinate origin.
Set clip_children: true to clip child drawing and hit testing to its rectangular
bounds. Nested clips intersect and move with their parent. Use fixed w and h
for the clipping viewport; radius styles the background and border, and
overflow controls text overflow.
List, PageFlow, StackView and Drawer provide their own rectangular viewports. The child viewport is rectangular. For static circular or path masks, bake the mask into the image alpha channel.
Shared UI
Section titled “Shared UI”Place a shared header, navigation bar or keyboard outside the page roots of a PageFlow/StackView in one scene. The pages can then change while the shared controls keep their state. Use component definitions to reuse the same UI across separate scenes; each scene has its own control instances.
For application-drawn overlays, register esp_gsp_set_overlay_contributor(). It draws
rounded rectangles and caller-owned A8 glyph bitmaps, and rebuilds them against the
active scene after a scene switch. Handle custom overlay input through the top-level
input interceptor; use scene widgets for built-in layout, focus and hit handling.
A Keyboard declaration generates its pages and keys at build time. The active page handles rendering and input, while the bundle stores all pages. Sharing one keyboard across pages keeps both the JSON and compiled resources smaller.