Skip to main content

Layout and Widgets

layout.json lists the widgets on a Kamoji Mini App's screen, from back to front, and can attach Lua code to gestures and events.

{
"widgets": [
{ "type": "circle", "cx": 120, "cy": 120, "r": 100, "fill": "none", "border": "#333333" },
{ "type": "label", "font": "large", "align": "center", "text": "{{ fmt('%.2f g', imu.lat_g or 0) }}" }
],
"on": {
"top_tap": "state.page = (state.page or 1) % 3 + 1"
}
}

The file can also be just the array of widgets, without "widgets" and "on". Widgets are created in order, so later ones are drawn on top.

Values and bindings​

Every property takes either a constant or an expression:

  • A constant is applied once: "y": 40, "color": "grey", "text": "Hello". Numbers may also be written as strings ("y": "40").
  • A string containing {{ … }} is a binding: a Lua expression Kamoji re-evaluates 10 times a second, updating the widget only when the result changes. See Expressions and Lua.

Properties marked bindable below accept expressions; the others must be constants.

Position and size​

The mini app's area is 240 × 240 px, with (0, 0) at the top left and the centre of the round screen at (120, 120).

  • Without align, x and y are the widget's top-left corner in pixels.
  • With align, the widget is placed at that point of the screen and x and y are offsets from it. "align": "center" with no x / y centres the widget.
PropertyBindableMeaning
alignNocenter, top_left, top_mid, top_right, left_mid, right_mid, bottom_left, bottom_mid, bottom_right
x, yYesPosition, or offset when aligned (px)
w, hNoWidth and height (px), as numbers. What they do depends on the widget
hiddenYestrue hides the widget
opaYesOpacity: 0–1, or 0–255
colorYesThe widget's main colour (below)

Colours​

Colours are "#RRGGBB", "#RGB", or one of these names:

NameColourNameColour
white#FFFFFFblack#000000
red#FF3B30green#34C759
blue#0A84FFyellow#FFD60A
orange#FF9F0Acyan#64D2FF
magenta#FF2D95grey / gray#8E8E93
gold#E8B23A

An expression may also return a number, 0xRRGGBB. A colour Kamoji cannot read is ignored, and the widget keeps its previous colour.

label​

Text.

{ "type": "label", "font": "huge", "align": "center", "text": "{{ fmt('%02d:%02d', time.hour or 0, time.min or 0) }}" }
PropertyBindableDefaultMeaning
textYesemptyThe text. Numbers are written without trailing zeros (3, 2.5, 0.33); nil is empty
fontNomediumsmall, medium, large, huge, or a built-in font builtin:<name>
colorYeswhiteText colour
text_alignNocenterleft, center or right, within the label's width
wNofits the textWidth; long text wraps onto more lines
x, y, align, hidden, opaAs above

The fonts are Barlow Condensed:

FontSizeCharacters
small18 pxPrintable ASCII and ¢ £ ¥ © ® ° · × ÷ – — ‘ ’ “ ” • … € ™ −
medium28 pxSame as small
large44 pxSame as small
huge96 pxDigits and space . , : ; - + % / ° ' " ( ) only: for clocks, counters and gauges

A character the font does not have is not drawn, so accented letters, other scripts and emoji do not show.

Mini apps can also use some of the fonts of Kamoji's own screens, as "font": "builtin:<name>":

FontSizeUsed by
builtin:exciteometer_valueBarlow Condensed Bold, 36 pxThe Excite-O-Meter's numbers
builtin:exciteometer_unitBarlow Condensed, 19 pxThe Excite-O-Meter's %
builtin:statusBarlow Condensed, 24 pxKamoji's status labels

An unknown built-in name falls back to medium, with a warning in the log. Your own fonts are coming later.

rect​

A rectangle, filled or outlined.

{ "type": "rect", "x": 40, "y": 100, "w": 160, "h": 40, "radius": 8, "fill": "#1C1C1E", "border": "gold" }
PropertyBindableDefaultMeaning
x, y, w, hx, yPosition and size. Always give w and h
fillYeswhiteFill colour, or "none" for an outline only
radiusNo0Corner radius (px)
borderNononeBorder colour
border_widthNo2Border width (px), with border
colorYesSame as fill

circle​

A circle, filled or outlined.

{ "type": "circle", "r": 9, "fill": "gold", "cx": "{{ 120 + (imu.lat_g or 0) * 100 }}", "cy": 120 }
PropertyBindableDefaultMeaning
cx, cyYesCentre (px). With align, offsets like x / y (with "align": "center", from the centre of the screen)
rNo10Radius (px)
fillYeswhiteFill colour, or "none" for an outline only
borderNononeBorder colour
border_widthNo2Border width (px), with border
colorYesSame as fill

arc​

A ring gauge, or part of one: a background track and a coloured indicator.

{ "type": "arc", "align": "center", "w": 220, "width": 10, "min": 0, "max": 59,
"start_angle": 270, "end_angle": 269, "track": "#1C1C1E", "color": "cyan",
"value": "{{ time.sec or 0 }}" }
PropertyBindableDefaultMeaning
valueYesThe value; the indicator runs from the track's start to it
min, maxNo0, 100Range of value (whole numbers)
start_angle, end_angleSee below135, 45Angles of the track in degrees: 0 is 3 o'clock, clockwise. The default is a 270° gauge open at the bottom; 270 to 269 is a full ring starting at 12 o'clock
w, hNo200, same as wDiameter (px)
widthNo12Thickness of the ring (px)
colorYesIndicator colour
trackNo#333333Track colour

When start_angle or end_angle is an expression, it sets the indicator's own angles instead of the track's, for an indicator that does not start at the track's start (a needle segment, a centre-zero gauge). Use value or angle expressions, not both.

bar​

A straight progress bar. It is vertical when h is larger than w.

{ "type": "bar", "align": "center", "y": 60, "w": 160, "h": 12, "color": "green", "value": "{{ state.level }}" }
PropertyBindableDefaultMeaning
valueYes0The value
min, maxNo0, 100Range of value (whole numbers)
w, hNo160, 16Size (px)
colorYesIndicator colour
trackNo#333333Background colour

line​

A line through two or more points: tick marks, clock hands, needles, simple graphs.

{ "type": "line", "width": 4, "color": "white", "points": [[120, 120], [120, 30]] }
PropertyBindableDefaultMeaning
pointsYes[[x, y], …] in screen pixels. An expression returns a Lua table of the same shape, { {x, y}, … }. At most 64 points
widthNo3Line width (px)
roundedNotrueRounded line ends
colorYeswhiteLine colour
{ "type": "line", "width": 2, "color": "gold", "points": "{{ hand(time.sec * 6, 94) }}" }

where hand is a function in main.lua that returns { {120, 120}, {x, y} } (see the clock example).

chart​

A live chart: each series' value is read every every_ms and added on the right, the older points shift left. Good for "the last N seconds" of a sensor.

{ "type": "chart", "align": "center", "w": 180, "h": 110, "points": 60, "every_ms": 200,
"min": -100, "max": 100, "grid": [3, 0], "width": 3,
"series": [
{ "value": "{{ clamp((imu.lat_g or 0) * 100, -100, 100) }}", "color": "#E8B23A" },
{ "value": "{{ clamp((imu.long_g or 0) * 100, -100, 100) }}", "color": "#64D2FF" }
] }
PropertyBindableDefaultMeaning
series[{ "value", "color" }, …]: one line (or set of bars) each. value is a number or an expression; a non-number (e.g. nil) leaves a gap
every_msNo1000How often the series are sampled (at least 50)
pointsNo30Points kept on screen (2–240): points × every_ms is the time span shown
min, maxNo0, 100Value range of the vertical axis (whole numbers: scale small values up, e.g. g × 100)
chart_typeNolineline or bar
gridNo[3, 0]Horizontal and vertical grid lines
widthNo3Line width (px)
w, hNo200, 100Size (px)

The chart has no background or border, so it sits directly on the black screen. Sampling runs only while the mini app is on screen, under the same CPU budget as the other bindings; a hidden chart (hidden) keeps sampling.

image​

A picture. For now only images built into Kamoji's firmware can be shown, named builtin:<name>; images from your mini app's own files are coming later.

{ "type": "image", "src": "builtin:exciteometer_vignette", "x": 0, "y": 0, "opa": "{{ state.flash }}" }
PropertyBindableDefaultMeaning
srcNobuiltin:<name>. An unknown name skips the widget, with a warning in the log
x, y, align, hidden, opaAs above. The image keeps its own size
ImageWhat it is
builtin:exciteometer_vignetteA full-screen (240 × 240) red vignette, the Excite-O-Meter's "hard move" flash

Gestures and events: on​

on maps a gesture or event name to Lua statements that run when it happens:

"on": {
"top_tap": "state.face = state.face % 5 + 1",
"top_hold": "state.n = 0",
"pinch": "state.n = state.n + 10",
"bump": "state.bumps = state.bumps + 1"
}

The names are the same as the Lua handlers without on_: top_tap, top_hold, pinch, enter, exit, and the events bump, hard_brake, hard_accel, lean. An event's magnitude is only passed to the Lua handler functions, not to on code; see Expressions and Lua.

Not available yet​

Images from your own files, gif, canvas and free drawing are designed but not built: an unknown widget type is skipped with a warning in the log.