Repository navigation
Speculative Box Primitives
Speculative design. Open, not settled — thinking and proposals, not shipped behavior. See Roadmap for what's in flight.
Status: accepted (the maintainer, 2026-10-08) with every lean in §7; slices 1–7 landed. 1: #767 (GraphicsBox and the mark boxes; notatio show draws figures). 2: #770 (layout formatters as boxes, layout v0). 2b: #771 (TableViewBox over a row source; -box tags), #779 (TUI pager). 3: #776, #781, #794 (control boxes, DynamicBox/DynamicModuleBox, bars as Wolfram's single-entry boxes). 4: #784, #786 (Show lowers to GraphicsBox, drawn by <graphics-box>; lattice producers), #789, #798, #801, #809 (plots, vector fields, charts, graph plots). 5: #822 (notatio-out on boxes: math as KaTeX leaves, closed layouts as grid-box of leaves). 6: #859 (prose: TextData/TextCell/ButtonBox through the renderer, TooltipBox, PaneSelectorBox; notatio-string-template and notatio-when retired); #856 took the control boxes to Wolfram's ToBoxes shapes. 7: #869 (Graphics3DBox and its marks; Plot3D/ListPlot3D/BarChart3D lower by makeBoxes, <graphics-3d-box> draws them), #882 (RasterBox under contour and density plots; ComplexPlot lowers to a raster with a CPU producer, its element stays the GPU hook). Next: notatio-manipulate into DynamicModuleBox, once its playback menu is an AnimatorBox. Open from slice 4: carrier glyphs (glyphs.ts) still build SVG strings for their synchronous callers (table cells, pickers, prerendered pages, the CLI and terminal); Show frames already lower 11 of the 17 glyph kinds to GraphicsBox, so retiring the string builders is parked.
The short version. Boxes are the one contract every environment draws. A head that renders
lowers, by a makeBoxes rule, to a small set of box primitives, and each environment has a
primitive library that draws those boxes and nothing else. On the web, one renderer per
primitive, a custom element only where a primitive is interactive or lives on its own.
A math run is a leaf the environment typesets (KaTeX/MathLive, SwiftMath/AndroidMath).
Graphics are a GraphicsBox holding the frame contract's display list. Layout is general
(flow, tables, structures, rich and interactive text), with math as one client: boxes, the
graphics primitives and the vdom together are a semantic markup for anyone, the job HTML set
out to do.
| group | boxes |
|---|---|
| math |
SuperscriptBox, SubscriptBox, SubsuperscriptBox, OverscriptBox, UnderscriptBox, UnderoverscriptBox, FractionBox, SqrtBox, RadicalBox
|
| layout |
RowBox, GridBox, FrameBox, StyleBox
|
| meaning |
InterpretationBox, TagBox, ErrorBox, FormBox (TeX held as written) |
| prose |
TextBox, TextCell, TextData, ButtonBox (the one interactive box, prose only) |
| holes |
TemplateSlot, TemplateExpression
|
Every serializer (toLatex, toMathML, toText, toHtml, toMarkdown) is boxes to a
string. Nothing lays out, and no element draws a box directly: notatio-out goes
makeBoxes → toLatex → KaTeX/MathLive.
| box | Wolfram | holds | needed by |
|---|---|---|---|
GraphicsBox, Graphics3DBox
|
same | primitives (Disk, Line, Polygon, Text, Polyhedron), directives, GraphicsComplexBox, InsetBox
|
Show, plots, charts, graphs |
RasterBox |
same | a pixel grid, or a producer that fills one | density, complex plots |
DynamicBox |
same | an expression re-evaluated when its dependencies move |
Dynamic, readouts, holes |
DynamicModuleBox |
same | a scope: variables and their values, then a body |
DynamicModule, Manipulate
|
| control boxes |
SliderBox, CheckboxBox, PopupMenuBox, InputFieldBox, SetterBox, TogglerBox, AnimatorBox, Slider2DBox, LocatorPaneBox
|
a binding (Dynamic(x)), a domain, options |
the Control family |
PaneBox |
same | a box at a size, scrolling or scaling past it |
Pane, narrow screens, tables |
PanelBox |
same | a box on a background with a frame | Panel |
TooltipBox |
same | a box and what shows on hover or focus |
Tooltip, describe
|
PaneSelectorBox |
same | alternatives, one shown, picked by a dynamic value |
When, tabs, forms cycler |
ItemBox, AdjustmentBox
|
same | per-cell span/alignment/background; nudges | tables, fine layout |
TemplateBox |
same | arguments and a named display function | notation as data |
The layout formatters are not boxes; they are makeBoxes rules to them, as in Wolfram:
Row → RowBox; Column, Grid, Labeled → a TagBoxed GridBox (the tag keeps the head
readable back); Panel → PanelBox; Framed → FrameBox; Pane → PaneBox;
Tooltip → TooltipBox; Style → StyleBox.
| element | decomposes into | clean? |
|---|---|---|
row, column, grid, panel, labeled
|
RowBox, GridBox (tagged), PanelBox
|
yes |
dynamic |
DynamicBox |
yes |
dynamic-module |
DynamicModuleBox |
yes |
slider, vertical-slider, animator, checkbox, popup-menu, input-field, toggler, slider-2d, locator
|
their control box | yes |
setter-bar, radio-button-bar, toggler-bar, list-picker, interval-slider, color-slider
|
a RowBox/GridBox of SetterBox/TogglerBox, or a control box of their own |
mostly |
knob, stepper
|
SliderBox with a tangle Appearance; gears are environment behavior |
mostly |
string-template |
TextData with DynamicBox holes |
yes |
when |
PaneSelectorBox over DynamicBox(test)
|
yes |
manipulate |
DynamicModuleBox of a GridBox: control boxes over a PaneBox body |
yes, playback menu aside |
show, plot, polar-plot, vector-plot, chart, graph-plot
|
GraphicsBox |
yes |
contour-plot, density-plot
|
GraphicsBox with RasterBox and lines |
yes |
plot-3d, list-plot-3d, bar-chart-3d, curve-3d
|
Graphics3DBox |
yes |
complex-plot, complex-plot-3d
|
RasterBox whose producer is a WebGPU shader |
no: the producer is GPU code |
torus-square |
GraphicsBox; no head today, so a shorthand: a rendering rule instead |
yes |
collection-table |
GridBox with ItemBox headers in a PaneBox, rows as InterpretationBox
|
partly: lazy rows, sort, column editing |
test-result-object |
PanelBox of a GridBox (Wolfram's summary box) |
yes |
out |
boxes; math runs as a typeset leaf | yes |
in, cell, notebook
|
InputFieldBox around a math editor; Cells in a notebook |
no: the editor is MathLive |
code |
InputFieldBox with a language |
no: an editor |
terminal |
none: it hosts the TUI environment inside the page | no |
worksheet, clock, gradient, palette
|
no head yet (worksheet), or a control with no Wolfram box |
no |
What doesn't decompose is an editor, a host for another environment, or a GPU producer. Those stay elements, reached through the hooks in §5.
Every primitive has a web renderer, one function per box head, the web's half of the contract. A renderer registers a custom element only when the box meets one of these:
-
Interactive: it takes input (a control box,
InputFieldBox,LocatorPaneBox). -
Independent lifecycle: it re-renders without its parent (a
DynamicBox), owns a scope (DynamicModuleBox), a timer (AnimatorBox), a worker or GPU context (GraphicsBox,RasterBox), or observes its own size (PaneBox). -
Accessibility: it carries a role and a label its parts don't (a
GridBoxthat is a table, aGraphicsBoxthat is a figure, a slider's value text). -
Focus: it is a tab stop or traps focus (
TooltipBoxon focus, a popover menu).
Where HTML already has the element (ButtonBox is <button> or <a>), the renderer uses it.
Everything else is drawn by its parent as plain DOM: RowBox (flex or inline), a layout
GridBox (CSS grid), FrameBox, PanelBox, StyleBox, ItemBox, TagBox and
InterpretationBox (data attributes on the node they wrap, so hover and selection find the
expression), the holes.
| custom element | drawn by its parent |
|---|---|
control boxes, InputFieldBox, LocatorPaneBox
|
RowBox, layout GridBox, FrameBox, PanelBox, ItemBox
|
DynamicBox, DynamicModuleBox, PaneSelectorBox
|
StyleBox, TagBox, InterpretationBox, ErrorBox
|
GraphicsBox, Graphics3DBox, RasterBox
|
TextCell, TextData, TextBox, ButtonBox (native) |
PaneBox, TooltipBox, a table GridBox
|
every math box, inside a typeset leaf |
Tags follow heads. An element is named for the head it renders, never for the box:
Slider renders as SliderBox, and <notatio-slider> is that box's element, so the
control tags keep their names. A box whose head has no other spelling takes the
head's tag: <notatio-graphics>, <notatio-pane>, <notatio-tooltip>. No -box tags,
no shorthand elements.
Math runs are a leaf. A maximal subtree of math boxes inside a FormBox(…, TraditionalForm)
is one typeset leaf, drawn whole by the environment's typesetter: KaTeX for static output,
MathLive where it is edited, SwiftMath/AndroidMath natively (§6 of the native page). Our engine
lays out everything around it. A GridBox of formulas is then a CSS grid of leaves, not one
LaTeX array, so a table of results scrolls, wraps and selects like a table. Inside a leaf,
InterpretationBox reaches the DOM through KaTeX's \htmlData (to verify for MathLive's
static renderer), so a hover still finds its expression.
A layer is not a box; it lowers to one. A layer (StrandDiagram, CellDiagram,
TreeDiagram, PathDiagram, PolytopeFaces, LatticeTiles) is an expression Show holds.
Lowering asks the frame contract (place, mark, links, linkMark, addresses, label,
view) for a display list, and wraps it in a GraphicsBox:
Show(StrandDiagram(Permutation([3, 1, 2])), Selection -> _s)
→ GraphicsBox(
GraphicsComplexBox(places, [TagBox(Disk(…), (1, 0)), …, TagBox(Line(…), link)]),
ColorRules -> …, BoundaryStyle -> …, Selection -> DynamicBox(_s), ViewKind -> "fixed")
- Bounded layers (every figure frame) lower to a finite list: each mark a primitive tagged with its address, each link a primitive tagged with its members. That list is plain data, so SVG, a terminal and a native view draw it with no layer code.
-
Unbounded layers (a lattice, classified outward within a budget) lower to a
GraphicsBoxwhose content is produced per view, the way an adaptivePlotresamples on zoom.reducealready pins what a static environment can't drive: pinning the view gives aPlotRange, and a pinned lattice is bounded, so a print or a pipe gets a finite list too. -
Rules stay data.
ColorRules,ColorMixingandBoundaryStyleride as options;styleElement(graphics-rules.ts, pure) resolves them per mark fromhas/value/relatedToin every drawer. A selection change restyles without relowering. -
Hit-testing is the drawer's, against tagged marks; a hit sets the
Selectionbinding, as any control's change does.
tiles-canvas.ts becomes the web canvas drawer of a GraphicsBox, and nothing more.
drawTiles mixes two jobs today: walking addresses within a budget (the layer's side) and
painting batches by mixed color (the drawer's). They split: a pure
displayListOf(layer, view, budget) in frontend, and paint(ctx, list, style) in the web
library. An SVG drawer over the same list replaces the SVG-string functions (glyphs.ts,
plot.ts's output) for print, SSR and export; @enumeratio/raster rasterizes that SVG.
Show then lowers to GraphicsBox as Wolfram's Show returns a Graphics, Labeled and
Legended around it to a tagged GridBox, and <notatio-show> retires into
<notatio-graphics>. Camera frames are Graphics3DBox with ViewPoint and kin; Inset
places a box (a formula, a table) inside graphics, laid out by §4 and drawn as a leaf.
layout(box: Box, style: Style, metrics: Metrics): DisplayList;
interface Metrics {
measure(text: string, font: Font): { advance: number; ascent: number; descent: number };
leaf(box: Box): Size; // a typeset run, a control, a graphic: sized by the environment
readonly em: number;
readonly cell?: boolean; // integer character cells (a terminal)
}Pure, in a base package with no DOM: positioned glyph runs, rules, rectangles and the graphics
primitives, plus slots, rectangles reserved for a leaf the environment draws itself (a math
run, a control, a GraphicsBox). Layout never draws an interactive thing; it reserves its room.
It covers flow and line breaking (TextData), GridBox (spans, alignment, rules, headers),
PaneBox sizing and Priority on narrow widths, frames and panels, and Inset.
| environment | flow and grids | graphics | math runs |
|---|---|---|---|
| web | DOM and CSS (flex, grid, native text); layout only for Inset, SVG export and goldens |
canvas or SVG from the list | KaTeX / MathLive leaf |
| TUI |
layout with cell metrics: 2-D text, box-drawing frames and rules |
braille and box-drawing cells | a small cell math in layout
|
| native |
layout with CoreText / Paint metrics, absolutely placed views |
Skia or native paths | SwiftMath / AndroidMath leaf |
| static |
layout to SVG |
SVG | KaTeX's or MathJax's SVG, or text |
The web keeps the DOM for flow because it gives selection, find-in-page, zoom, right-to-left,
screen readers and font fallback for free. The TUI gains the two-dimensional text
serializer (Boxes "Next" 5): a fraction stacks, a Grid gets ruled cells, a Panel a
frame, a strand or tree frame its braille or box-drawing picture, all from one function.
The terminal has no typesetter to borrow, so layout carries a small cell math (stacked
fractions, raised scripts, grids), not TeX quality. Native gets everything but the math leaf.
Resolution for one box, as settled in Speculative-Native-Rendering §4 and §6, cheapest first:
-
An option hint on the box. Namespaced, ignorable keys in the option bag:
{"tui.Width": 40, "native.Hint": "list", "web.Element": "notatio-complex-plot"}. Unknown keys are ignored, as Wolfram ignores unknown options. -
A registry row keyed by (head, capability predicate), in the library's data beside
its notation:
{head: "CollectionTable", when: "touch && layout == 'compact'", lower: <boxes markup>}. The predicate is over theEnvironmentrecord, whosenameis one field, so a row can sayname == 'tui'where a dependency forces it andtouchwhere capability is enough. A row's body is boxes or vdom markup: no environment code. -
A code hook where data can't say it: the library's
package.jsonlistsenumeratio.environments, each an entry (./web,./tui,./native) whose default export is(ctx) => HeadRenderer, imported at mount. This is howComplexPlot's WebGPU producer, MathLive's editor and xterm stay. A hook imports the primitive library's types only. - The environment's own renderer for that box head.
-
A reduction to simpler boxes (
RadicalBoxto a prefixedSqrtBox,GridBoxto rows,PaneSelectorBoxto the selected pane). -
toText(box)in a label. Boxes always draw.
A value's default picture stays a rendering rule, data: VALUE_FRAMES' rows become
makeBoxes rules (Permutation → Show(StrandDiagram(·)) → GraphicsBox), so the TUI and a
native host inherit them. The same goes for VISUAL_SYMBOLS: each row's argument map is a
makeBoxes rule, and renderingOf shrinks to the web renderer's output. A host that renders
nothing but boxes draws every head that has a rule.
Where a host lacks a capability a box needs (no GPU, no BigInt), it reduces or declines that box, never answers wrong.
Each slice is one PR. The first proves the contract outside the web, where it matters most.
-
GraphicsBoxand a TUI drawer for figure frames. AddGraphicsBoxandGraphicsComplexBox(primitives astiles-canvas.ts'sGraphicsPrimitive, marks tagged by address) toboxes; splitdrawTilesintodisplayListOf(pure) and the canvaspaint; a cell drawer incli(generalizingtextplot.ts) drawsStrandDiagram,TreeDiagram,PathDiagramandCellDiagramat a TTY. Pass:notatio show 'Permutation([3, 1, 2])'draws strands in a terminal; the web's canvas goldens are unchanged. No element changes. -
Layout formatters as boxes, and
layoutv0.Row/Column/Grid/Panel/LabeledasmakeBoxesrules;PanelBox,PaneBox; the web box renderer draws them as DOM;layoutwith cell metrics draws them in the TUI. Retiresnotatio-row,-column,-grid,-panel,-labeledinto the renderer. -
DynamicBox,DynamicModuleBox, control boxes.Row([Slider(k, (0, 5)), Dynamic(k^2)])is boxes end to end; the control elements become their boxes' elements (tags unchanged);cli/src/drive.tsreads control boxes instead ofdeclarations. The native spike (Speculative-Native-Rendering §5, steps 1–4) fits after this slice. -
<notatio-graphics>.Showand the 2-D plots (Plot,PolarPlot,ListPlot,VectorPlot,Chart,GraphPlot) lower toGraphicsBox; the SVG drawer replacesglyphs.tsand the plots' SVG strings for SSR and export. Retiresnotatio-show,notatio-plot,-polar-plot,-vector-plot,-chart,-graph-plot,-torus-square. -
notatio-outon boxes. Math runs as typeset leaves, everything else through the box renderer; aGridof formulas stops being one LaTeX array. -
Prose and the rest.
TextData/TextCell/ButtonBoxthrough the renderer (Speculative-Prose-Pipeline);TooltipBox,PaneSelectorBox. Retiresnotatio-string-templateandnotatio-when. -
3-D and rasters.
Graphics3DBoxandRasterBox;ComplexPlot's GPU producer as a web hook, the CPU path (gpu-eval.ts) elsewhere.notatio-manipulateretires intoDynamicModuleBoxonce its playback menu is anAnimatorBox.
Stay as elements: the control boxes' elements, dynamic, dynamic-module, out, in,
cell, notebook, code, terminal, worksheet, collection-table (until GridBox in a
PaneBox handles lazy rows), the GPU plots behind their hook.
-
A custom element for every box primitive, or only where §2's criteria hold? "A web
component per box primitive" read literally gives
<notatio-row-box>around every row. Lean: a web renderer for every primitive, a custom element only where it is interactive, independent, a landmark or a tab stop; the rest is plain DOM. -
Element tags: the box's head or the user's? Lean: the user's head (
notatio-sliderdrawsSliderBox); no-boxtags, no renames for the controls. Amended 2026-10-08 (decisions 1 and 2): every box primitive is drawn as its own tag, the box head in kebab case:RowBox→row-box,TableViewBox→table-view-box,Graphics3DBox→graphics-3d-box(a break before each capital and each run of digits). The suffix meets the custom-element hyphen rule without a brand, so the primitives read as generic, and web markup is the box tree itself. Every box tag is registered: the interactive boxes (§2's criteria) with their behavior, the rest with an empty class, so:definedholds and a collision with another library surfaces at definition (we log it, and CSS keyed on the tag keeps drawing). A host may register them under other names through a scoped registry.notatio-stays for the notebook and app shell, which are not boxes:notatio-notebook,-cell,-in,-out,-terminal,-code,-worksheet. Each slice renames the elements it touches: layout boxes andtable-view-boxin 2b, controls anddynamic-box/dynamic-module-boxin 3,graphics-boxin 4. -
One
ControlBox(kind, …)or Wolfram's per-kind boxes? The native page sketched one. Lean: Wolfram's (SliderBox,PopupMenuBox, …), sharingcontrols.ts's contract; a kind's intent (continuous, choice, toggle) is a table over them, which the TUI and native read. -
Does
Showkeep its element? Lean: no;Showlowers toGraphicsBoxas Wolfram's returns aGraphics, and<notatio-graphics>draws it. -
Do
renderingOfandmakeBoxesmerge? Lean: yes;VISUAL_SYMBOLSrows becomemakeBoxesrules, andRenderingis what the web renderer emits, not a second lowering. -
Where do
layoutand the interface boxes live? Lean:boxes/layoutand boxes inboxes(base, pure) until a second environment library forces a split; per-environment libraries (boxes-web,boxes-tui, native) as separate packages from the start. -
Math runs inside KaTeX:
\htmlDataor MathML Core? MathML Core keeps the box tree in the DOM and renders in every current engine. Lean: KaTeX with\htmlDatanow, matching your KaTeX-for-static answer; MathML Core as a measured swap later. -
Cell math in the TUI: our own or
toText? Lean: our own small 2-D set inlayout(fractions, scripts, grids);toTextstays for pipes. -
Lazy layers in a box. A lattice
GraphicsBoxholds a producer, not data. Lean: aDynamicBox-like producer insideGraphicsBox, keyed by view, andreducepins the view for static environments so their list is finite. -
collection-table. Lean: keep the element untilPaneBoxvirtualizes rows; its columns are aGridBoxalready, so the move is the table's chrome, not its data. Amended 2026-10-08: the lazy table is its ownTableViewBoxover a row source, not a lazyGridBoxin aPaneBox; see Speculative-Lazy-Grid (slice 2b).