Login
Pikchr UI Subset
Login

Pikchr UI Subset

Status: Normative
Version: PUS-1 (2026-08-16)
Parent decision: D3.2 — GP-Crystal Design Overview §3


1. Purpose

This page defines the Pikchr UI Subset (PUS-1): the exact set of Pikchr constructs that GP-Crystal UI blocks may emit. The subset is sized so that both target renderers can faithfully express every construct:

Any Pikchr construct not listed here is excluded. UI block emitters MUST NOT emit excluded constructs.


2. Element Table

2.1 box

box wid 1.20 ht 0.40 rad 0.03 with .nw at (0.10,3.20) fill 0x4C97FF color 0x4C97FF
Attribute Type Requirement Notes
wid float (Pikchr in) Required Width; converted to pixels by the layout pass (§3).
ht float (Pikchr in) Required Height.
rad float (Pikchr in) Optional Corner radius; 0 = sharp. Omit for square corners.
fill 0xRRGGBB hex Required Fill color.
color 0xRRGGBB hex Required Border/stroke color.
at (x, y) literal Required Anchor position; MUST be an explicit numeric literal (see §3).
with anchor selector Optional Which point of the box lands on at. Default .center. Subset allows .nw, .center, .ne, .sw, .se, .n, .s, .e, .w.

lvgl-crystal mapping:

Condition Widget Key calls
No op= annotation Lvgl::Object.new(parent) set_pos, set_size, set_style_bg_color(fill), set_style_border_color(color), set_style_radius(rad_px)
Has op= annotation Lvgl::Widgets::Button.new(parent) Same geometry/style calls; plus Lvgl::Event subscription for Code::Clicked (or ev= override) dispatching the annotated op.

2.2 text

Start
text "Start" color 0xFFFFFF at (0.70,3.00)
Attribute Type Requirement Notes
string quoted literal Required Visible label. Single line only; no embedded newlines.
at (x, y) literal Required Center of the text bounding box (absolute).
color 0xRRGGBB hex Optional Text color; default 0x000000 (black).

lvgl-crystal mapping: Lvgl::Widgets::Label.new(parent) with .text = <string> and set_style_text_color(color). Position: set_pos at the resolved center minus half the text bounds (font-dependent; best-effort on embedded).

Overlap rule: when a text element's bounding box lies within a box element's bounds, the lvgl translator MUST create the label as a child of that box widget, not as a sibling on the screen root.

2.3 line

line from (0.10,2.00) to (2.90,2.00) color 0xCCCCCC
Attribute Type Requirement Notes
from (x, y) literal Required Start coordinate (absolute).
to (x, y) literal Required End coordinate (absolute).
color 0xRRGGBB hex Optional Stroke color; default 0x000000.

lvgl-crystal mapping: LibLvgl.lv_line_create(parent_ptr) with a two-point lv_point_t array. (Lvgl::Widgets::Line is not yet a named wrapper; the translator calls LibLvgl directly.)

2.4 arrow

An arrow is a line with one or both arrowheads. All line attributes apply, plus:

Token Position Meaning
-> after keyword Arrowhead at the to end.
<- after keyword Arrowhead at the from end.
<-> after keyword Arrowheads at both ends.
arrow from (0.50,1.50) to (2.50,1.50) color 0x4C97FF

lvgl-crystal mapping: same as line; arrowheads are drawn as small filled triangles computed from the endpoint direction vector, rendered as additional lv_line segments.

2.5 circle

circle rad 0.20 at (1.50,2.80) fill 0xFF8C1A color 0xFF8C1A
Attribute Type Requirement Notes
rad float (Pikchr in) Required Radius.
at (x, y) literal Required Center position (absolute).
fill 0xRRGGBB hex Required Fill color.
color 0xRRGGBB hex Required Border color.

lvgl-crystal mapping: LibLvgl.lv_arc_create(parent_ptr) with arc range set to 0–360° (full circle); rad × 2 maps to set_size. The element is circle, not ellipse, precisely because lv_arc is circular — a non-circular ellipse has no faithful lvgl rendering, so the subset does not offer one.


3. Layout Resolution Rule

Pikchr's default placement model chains elements relative to previously placed objects. PUS-1 forbids implicit chaining and mandates explicit absolute coordinates.

Rule 1 — Mandatory at clause. Every element MUST include an explicit at (x, y) clause where both coordinates are numeric literals. Implicit Pikchr placement (chaining from the previous element's edge) is excluded.

Rule 2 — Mandatory scale line. Every PUS-1 diagram MUST open with:

scale = <N>

This resolves the pixel formula (both renderers apply it identically):

px = round(pikchr_inches × 72 × scale)

Typical value: scale = 0.75 (matches the gpcr renderer's current default).

Rule 3 — Y-axis inversion (lvgl only). Pikchr's origin is bottom-left; lvgl's is top-left. The embedded translator applies:

px_x = round(x_in × 72 × scale)
px_y = round((canvas_height_in − y_in) × 72 × scale)

canvas_height_in is computed from the bounding box of all declared elements.

Rule 4 — Anchor resolution. The translator resolves each element's top-left corner from the declared at point and with anchor before calling set_pos:

Rule 5 — No reflow. Dynamic layout (text wrapping, flex, grid) is out of scope for PUS-1. Screens are authored at fixed pixel dimensions matched to the target display.


4. Interactivity Annotation Convention

Pikchr only draws; it carries no event semantics. PUS-1 annotates interactive elements via a trailing Pikchr comment on the same source line as the element. Both the web JS layer and the lvgl event handler parse the identical comment text from the same Pikchr source, making the annotation the single source of truth for both renderers.

4.1 Syntax

<pikchr-element-line> # [id=<name>] [op=<json>] [ev=<event>]

Fields are space-separated within the comment. At least one of id= or op= MUST be present for the comment to be treated as an annotation (plain comments without these fields are ignored).

Field Format Description
id=<name> id= + [a-z][a-z0-9_]* Stable element identifier. Used as the SVG id attribute (web) and as the event registration key (lvgl). MUST be unique within a diagram.
op=<json> op= + compact JSON object (no internal spaces) GP-Crystal op dispatched on activation; identical JSON consumed by both renderers.
ev=<event> ev=clicked | ev=pressed | ev=short_clicked | ev=value_changed lvgl event code to subscribe to; default clicked. The web renderer always uses click regardless of ev=.

4.2 Web renderer behavior

A single external JS asset (no inline scripts; CSP-safe per D3.1) scans the Pikchr source that produced each rendered SVG page. For each annotated element it:

  1. Locates the SVG element whose id attribute matches id=<name>.
  2. Attaches a click listener that HTTP-POSTs the JSON from op= to the management API endpoint (ticket 21dfd164d6).
  3. Sets cursor: pointer on the element.

Element mapping is ordinal. Pikchr emits no id attributes in its SVG output; it does, however, emit graphics elements in source order. The scanner therefore reads the diagram's Pikchr source (the page embeds it — Fossil's toggle fences already do), and maps the k-th shape-producing line of the source to the k-th top-level graphics element of the SVG. id= is a pure annotation-namespace identifier and has no relationship to Pikchr tags:

box wid 1.20 ht 0.40 ... # id=blink_btn op={"op":"call","target":"blink"}

Pikchr tags (Label:) remain available for positioning within the diagram; note that Pikchr requires tag names to begin with an uppercase letter, another reason they are unsuitable as annotation ids.

4.3 lvgl-crystal behavior

The translator parses annotation comments before constructing widgets. For each annotated element it:

  1. Creates the widget per §2.
  2. Registers a Lvgl::Event subscription for the event code named by ev= (default Code::Clicked) on the new widget.
  3. On event fire, passes the op= JSON string to the local op-apply engine (the same engine that processes voice and model edits — Design Overview §1).

4.4 Annotated diagram example

Start Stop
scale = 0.75

box wid 1.20 ht 0.40 rad 0.03 with .nw at (0.10,3.20) fill 0x4C97FF color 0x4C97FF # id=start_btn op={"op":"call","target":"blink"}
text "Start" color 0xFFFFFF at (0.70,3.00)

box wid 1.20 ht 0.40 rad 0.03 with .nw at (1.50,3.20) fill 0xFF6680 color 0xFF6680 # id=stop_btn op={"op":"call","target":"stop_blink"}
text "Stop" color 0xFFFFFF at (2.10,3.00)

circle rad 0.15 at (2.75,3.05) fill 0xFF0000 color 0xFF0000 # id=led_status

line from (0.10,2.80) to (3.00,2.80) color 0xCCCCCC

In this example start_btn and stop_btn are interactive (Button on lvgl; click listeners in the web layer); led_status has an id for programmatic update but no op= (static on lvgl; addressable by the JS layer for visual state changes via SSE re-render); the line is a pure static separator.


5. Explicit Exclusions

5.1 Excluded shape keywords

ellipse, arc, cylinder, file, oval, diamond, dot, spline.

ellipse is excluded because lvgl has no ellipse (lv_arc is circular) — use circle. arc is excluded because Pikchr renders arcs as open paths with fill:none regardless of the fill attribute, so no filled dual rendering exists.

5.2 Excluded attributes and modifiers

Attribute / modifier Reason excluded
dashed, dotted No direct lvgl analogue in subset.
thickness Line weight not yet mapped.
chop Endpoint shortening has no lvgl analogue.
same Inheriting attributes from the previous element depends on implicit ordering.
fit Auto-sizing is incompatible with the fixed-layout rule (§3 Rule 5).
invisible, invis No lvgl analogue in subset.
Standalone string label on non-text elements Use a separate text element.
above, below, ljust, rjust Relative text placement; use explicit at instead.

5.3 Excluded placement forms

Form Example (excluded) Reason
Implicit chaining box; box Position depends on previous element without at.
Edge-relative expressions at last box.e + (0.2,0) Requires element tracking; all positions MUST be numeric literals.
Direction specifiers right 1.0 Relative cursor movement.
move statements move right 0.5 Implicit cursor movement.

5.4 Excluded control flow and macros

define, loop, until, print, assert.

5.5 Excluded interactivity

Event / widget Status
Hover Web-only; no lvgl analogue. Excluded.
Drag, scroll Out of scope for badge-class target. Excluded.
Long-press PUS-2 candidate.
Text input No Pikchr-renderable primitive. Excluded.
Lvgl::Widgets::Slider No PUS-1 Pikchr primitive maps to a slider shape. PUS-2 candidate.

6. Version History

Version Date Summary
PUS-1 2026-08-16 Initial normative subset. Five elements: box, text, line, arrow, ellipse. Annotation convention: # id= op= ev= trailing comment. Layout resolution: explicit at (x,y) + scale = + Y-inversion for lvgl.
PUS-2 2026-08-17 ellipse → circle (lv_arc is circular; no silent degradation); arc explicitly excluded (Pikchr arcs are open, unfillable paths); element mapping made ordinal (Pikchr emits no SVG ids).

PUS-2 will be drafted once the pixel formula is validated on real lvgl-crystal hardware and at least one working UI block has been rendered on both targets.


First artifact of D3.2. Implementation in a separate ticket.