Skip to content

Speculative Box Primitives

enumeratio edited this page Nov 1, 2026 · 10 revisions

Design: 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.

1. Inventory

What exists (packages/boxes/src/box.ts)

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.

What is missing

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.

Each element onto primitives

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.

2. The element set

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:

  1. Interactive: it takes input (a control box, InputFieldBox, LocatorPaneBox).
  2. 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).
  3. Accessibility: it carries a role and a label its parts don't (a GridBox that is a table, a GraphicsBox that is a figure, a slider's value text).
  4. Focus: it is a tab stop or traps focus (TooltipBox on 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.

3. Show and graphics

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 GraphicsBox whose content is produced per view, the way an adaptive Plot resamples on zoom. reduce already pins what a static environment can't drive: pinning the view gives a PlotRange, and a pinned lattice is bounded, so a print or a pipe gets a finite list too.
  • Rules stay data. ColorRules, ColorMixing and BoundaryStyle ride as options; styleElement (graphics-rules.ts, pure) resolves them per mark from has/value/relatedTo in every drawer. A selection change restyles without relowering.
  • Hit-testing is the drawer's, against tagged marks; a hit sets the Selection binding, 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.

4. The layout engine

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.

5. Hooks and hints

Resolution for one box, as settled in Speculative-Native-Rendering §4 and §6, cheapest first:

  1. 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.
  2. 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 the Environment record, whose name is one field, so a row can say name == 'tui' where a dependency forces it and touch where capability is enough. A row's body is boxes or vdom markup: no environment code.
  3. A code hook where data can't say it: the library's package.json lists enumeratio.environments, each an entry (./web, ./tui, ./native) whose default export is (ctx) => HeadRenderer, imported at mount. This is how ComplexPlot's WebGPU producer, MathLive's editor and xterm stay. A hook imports the primitive library's types only.
  4. The environment's own renderer for that box head.
  5. A reduction to simpler boxes (RadicalBox to a prefixed SqrtBox, GridBox to rows, PaneSelectorBox to the selected pane).
  6. 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.

6. Migration

Each slice is one PR. The first proves the contract outside the web, where it matters most.

  1. GraphicsBox and a TUI drawer for figure frames. Add GraphicsBox and GraphicsComplexBox (primitives as tiles-canvas.ts's GraphicsPrimitive, marks tagged by address) to boxes; split drawTiles into displayListOf (pure) and the canvas paint; a cell drawer in cli (generalizing textplot.ts) draws StrandDiagram, TreeDiagram, PathDiagram and CellDiagram at a TTY. Pass: notatio show 'Permutation([3, 1, 2])' draws strands in a terminal; the web's canvas goldens are unchanged. No element changes.
  2. Layout formatters as boxes, and layout v0. Row/Column/Grid/Panel/Labeled as makeBoxes rules; PanelBox, PaneBox; the web box renderer draws them as DOM; layout with cell metrics draws them in the TUI. Retires notatio-row, -column, -grid, -panel, -labeled into the renderer.
  3. 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.ts reads control boxes instead of declarations. The native spike (Speculative-Native-Rendering §5, steps 1–4) fits after this slice.
  4. <notatio-graphics>. Show and the 2-D plots (Plot, PolarPlot, ListPlot, VectorPlot, Chart, GraphPlot) lower to GraphicsBox; the SVG drawer replaces glyphs.ts and the plots' SVG strings for SSR and export. Retires notatio-show, notatio-plot, -polar-plot, -vector-plot, -chart, -graph-plot, -torus-square.
  5. notatio-out on boxes. Math runs as typeset leaves, everything else through the box renderer; a Grid of formulas stops being one LaTeX array.
  6. Prose and the rest. TextData/TextCell/ButtonBox through the renderer (Speculative-Prose-Pipeline); TooltipBox, PaneSelectorBox. Retires notatio-string-template and notatio-when.
  7. 3-D and rasters. Graphics3DBox and RasterBox; ComplexPlot's GPU producer as a web hook, the CPU path (gpu-eval.ts) elsewhere. notatio-manipulate retires into DynamicModuleBox once its playback menu is an AnimatorBox.

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.

7. Decisions (the leans, accepted)

  1. 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.
  2. Element tags: the box's head or the user's? Lean: the user's head (notatio-slider draws SliderBox); no -box tags, 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 :defined holds 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 and table-view-box in 2b, controls and dynamic-box / dynamic-module-box in 3, graphics-box in 4.
  3. One ControlBox(kind, …) or Wolfram's per-kind boxes? The native page sketched one. Lean: Wolfram's (SliderBox, PopupMenuBox, …), sharing controls.ts's contract; a kind's intent (continuous, choice, toggle) is a table over them, which the TUI and native read.
  4. Does Show keep its element? Lean: no; Show lowers to GraphicsBox as Wolfram's returns a Graphics, and <notatio-graphics> draws it.
  5. Do renderingOf and makeBoxes merge? Lean: yes; VISUAL_SYMBOLS rows become makeBoxes rules, and Rendering is what the web renderer emits, not a second lowering.
  6. Where do layout and the interface boxes live? Lean: boxes/layout and boxes in boxes (base, pure) until a second environment library forces a split; per-environment libraries (boxes-web, boxes-tui, native) as separate packages from the start.
  7. Math runs inside KaTeX: \htmlData or MathML Core? MathML Core keeps the box tree in the DOM and renders in every current engine. Lean: KaTeX with \htmlData now, matching your KaTeX-for-static answer; MathML Core as a measured swap later.
  8. Cell math in the TUI: our own or toText? Lean: our own small 2-D set in layout (fractions, scripts, grids); toText stays for pipes.
  9. Lazy layers in a box. A lattice GraphicsBox holds a producer, not data. Lean: a DynamicBox-like producer inside GraphicsBox, keyed by view, and reduce pins the view for static environments so their list is finite.
  10. collection-table. Lean: keep the element until PaneBox virtualizes rows; its columns are a GridBox already, so the move is the table's chrome, not its data. Amended 2026-10-08: the lazy table is its own TableViewBox over a row source, not a lazy GridBox in a PaneBox; see Speculative-Lazy-Grid (slice 2b).

Home

Project

Design: every design page, settled, speculative and archived

Clone this wiki locally