Skip to content

UI Mode

UI mode turns a worksheet into a fill-in form. Mark the inputs of your calculation with the #UI keyword, switch the results pane to input mode, and those lines are rendered as text boxes, drop-downs, radio buttons, checkboxes or editable grids instead of plain results. Type a new value and the whole document recalculates.

The same file is still an ordinary worksheet. In the preview, report, and PDF/Word exports — the #UI keyword changes nothing about how the line looks unless this is explicitly set (see reportStyle property).

#UI L = 6m
#UI w = 25kN/m
M = w*L^2/8

Preview and Report show three ordinary lines. In Input mode L and w become text boxes, and M follows whatever you type into them.

Turning it on

A document that declares #UI controls exists to be filled in, so the first time you open one it comes up as its input form. You can turn this behavior off with Open #UI Documents in Input Mode on the panel's Settings tab.

VS Code

The input form and the report each open as their own panel — see Live preview for the buttons and commands that open them, and Exports for Print Report to PDF.

Closing the form asks whether to save first; declining discards them. The report preview is not tied to the form: open it on its own to read the print layout beside the editor, and it stays open when the form closes. However, it is recommended to save and close the input form before making changes to the CalcpadCE file.

Desktop App

Input takes over the window and hides the editor. Its toolbar also offers Report (the report beside the form), Save values (once you have changed something) and Exit input mode, which leaves you in Report. In Input and Report, a Print PDF button exports the report.

#pre and #post

The two directives split a document into the part that is filled in and the part that is read back. #pre is hidden in a report; #post is hidden while the form is on screen:

Mode #pre shown #post shown Entered #UI values applied
Preview yes yes no, unless a setting is changed
Input yes no yes
Report no yes yes
#pre
'<p>Enter the span below.</p>
#end pre
#UI L = 6m
#post
'<p>Span used: 'L'</p>
#end post

Preview is the suggested mode to write your CalcpadCE file in: it shows everything at once, and it ignores input values so you see the default values of the document.

Turning on the Apply #UI Values in Preview setting will render the preview with the input values applied. This can make debugging issues from values input into the form easier.

Writing a #UI line

#UI { JSON properties } name = value

The JSON properties are optional. Without it, the input properties are derived from the right-hand side.

#UI is only allowed where the right-hand side is a plain value or a vector/matrix function by itself:

Right-hand side Accepted Example
Number, with or without a unit ✔ #UI L = 10m, #UI n = 4, #UI q = 3kN/m
Vector or matrix literal ✔ #UI v = [1; 2; 3], #UI M = [1; 2 \| 3; 4]
vector() / matrix() constructor ✔ #UI Z = vector(5), #UI G = matrix(r; c)
An expression (unless allowExpression is true) ✘ #UI k = 2*E, #UI k = max(v), #UI v = [1; sqrt(4)]
Text ✘ #UI text = 'text'

Some considerations:

  • Exponent notation is not valid: write the full number instead.
  • Saved values are matched to controls by variable name, so be careful when renaming variables — see Editing a document that has saved values.

Labels

An inline comment on the line can label the control:

#UI 'Span, 'L = 6m

Several controls on one line

Comment segments separate assignments, so one #UI line can declare more than one control. They share the line's JSON properties but are saved and overridden separately.

#UI 'b = 'b = 200mm', h = 'h = 400mm

JSON properties

Property Type Applies to Meaning
type string all entry, datagrid, dropdown, radio, checkbox. Auto-detected when omitted
mode string all Only number is currently accepted; string inputs are planned but not yet supported
style string all CSS class applied to the input element
reportStyle string all CSS class applied to the line in the report
forceUnits bool entry, datagrid Default true: the unit stays in the document and only the number is editable. false puts the unit in the control
allowExpression bool entry, datagrid Default false. true accepts any expression, written to the document as typed. Implies forceUnits: false
rows number datagrid Grid rows. Auto-detected when omitted
columns number datagrid Grid columns. Auto-detected when omitted
rowHeaders array datagrid Row header labels
columnHeaders array datagrid Column header labels
width number or percentage datagrid Total grid width in pixels, or a percentage of the line such as "75%". Natural width when omitted
rowHeaderWidth number datagrid Width of the row header column, in pixels. Default 50
columnWidths array datagrid Column widths in pixels. May be shorter than the column count; the rest take the default 80
keys array dropdown, radio The labels shown to the user
values array dropdown, radio The values substituted into the calculation, one per key

keys and values are both required for a drop-down or radio group, and must be the same length. The datagrid properties throw an error on any other control. forceUnits and allowExpression will error on a drop-down, radio group or checkbox, which always substitute the whole value from their own values anyway.

You don't have to write JSON by hand — the Properties tab has a form for it that fills in the fields that apply to the control type it detects.

The control types

entry: an input box

The default for a numeric right-hand side. The unit stays in the document beside the box, so only the number is editable; the box accepts digits, a decimal point and a sign, and rejects anything else as you type.

#UI L = 10m
#UI {"type": "entry"} W = 5m

Two properties loosen that, at the cost of the simple, unit-safe input.

forceUnits: false moves the unit into the box, so the whole value is editable and the entered one replaces the right-hand side outright — which is what lets an input change its unit as well as its magnitude.

#UI {"forceUnits": false} q = 3kN/m

allowExpression: true goes further and accepts anything, writing it into the document as typed. The box shows the expression as written rather than the number it came to, so a formula stays a formula across edits. Nothing is validated as you type: an entry the parser cannot read leaves the control in place and reports the error on the line below it, the same as any other line. It implies forceUnits: false — a formula cannot carry an appended unit.

#UI {"allowExpression": true} A = b*h

keys are shown, values are substituted. A value may carry its own unit, so a drop-down can switch units as well as magnitudes.

#UI {"type": "dropdown", "keys": ["Low", "Medium", "High"], "values": ["1", "2", "3"]} grade = 1

radio: a button group

Same keys/values pairing as a drop-down, laid out as radio buttons.

#UI {"type": "radio", "keys": ["Steel", "Concrete"], "values": ["200GPa", "25GPa"]} E = 200GPa

checkbox: a toggle

Checked is 1, unchecked is 0.

#UI {"type": "checkbox"} useSteel = 1

datagrid: an editable grid

The default whenever the right-hand side is a vector/matrix literal or a vector()/matrix() call.

#UI v = [1; 2; 3]
#UI M = [1; 2; 3 | 4; 5; 6]
#UI Z = vector(5)
#UI G = matrix(3; 4)

Sizes computed from a variable or expression work too — matrix(r; c), matrix(len(x); len(y)) — the grid is sized from the value the line produced.

A grid can use default values from any expression. Whatever the line evaluates to fills the cells:

#UI G = matrix(3; 3) + 5
#UI H = vec2row(x)

Declaring rows and columns explicitly fits the literal to that shape: missing cells become 0, extra ones are dropped.

#UI {"type": "datagrid", "rows": 2, "columns": 3, "columnHeaders": ["a", "b", "c"], "rowHeaders": ["r1", "r2"]} T = [0; 0; 0 | 0; 0; 0]

The grid's size comes from the directive or default size, so rows and columns cannot be inserted or deleted in the form.

Units in a grid

By default the cells hold plain numbers and a cell holding anything else is put back to 0. When a default value carries a unit, the unit is kept aside rather than shown. The cells stay numeric, hovering the cell shows the unit that will be applied in the report. So U = matrix(2; 2)*5m fills the grid with 5 and writes [5m; 5m | 5m; 5m], and a literal default such as M = [1m; 2m] behaves the same way.

It is also recommended to put units in column or row headers.

forceUnits: false puts the units in the cells instead, so 5m can be typed into one, and allowExpression: true accepts any expression in the cell. Note that this is still subject to the limitations of the calculation engine, putting a vector or matrix into a cell will throw an error.

Sizing

By default each column is sized to the longest single word it has to show, its header included, with a minimum of 80 pixels. width overrides that total using a number of pixels or a percentage of the line such as "75%" ("100%" and "full" both fill the entire line). rowHeaderWidth and columnWidths set each row/column in the order they are declared. When width is set and the parts add up to more than it, they are all scaled down together, keeping their relative sizes. This means columnWidths can be written as plain ratios. No column is scaled below its own longest word, so a width too small for the content is overrun and the grid scrolls rather than clipping every column.

#UI {"type": "datagrid", "width": "100%", "rowHeaderWidth": 140, "columnWidths": [2, 1, 1]} M = matrix(2; 3)

Text wraps at spaces only, and the rows grow to fit. A word too long for its column is cut off at the cell border. Click into the cell or hover over a header to read it in full.

Note: Setting a datagrid with a row length of 1 will output a vector instead of matrix due to limitations in how vectors/matricies are input. This is planned to be fixed in a future version. As this is mostly an issue when dynamically defining row lengths from a variable, you can check if the row length is one and convert it to a matrix using vec2row() where this is an issue.

Styling with CSS

Two properties attach CSS classes:

  • style — added to the input control itself, and only in Input mode.
  • reportStyle — added to the element wrapping the line, everywhere the line is not a control: Preview, Report, and every export but the input form.

The base classes

Every control also carries a class of its own, which is what your class combines with:

Type HTML Element Base class
entry <input type="text"> calcpad-ui-input
dropdown <select> calcpad-ui-dropdown
radio <span> wrapping the buttons calcpad-ui-radio, each button's <label> is calcpad-ui-radio-label
checkbox <input type="checkbox"> calcpad-ui-checkbox
datagrid <div> holding the grid calcpad-ui-datagrid

Write your selector as the base class plus your own class, so it only hits the controls you marked: .calcpad-ui-input.highlight, not .highlight. A reportStyle class lands on the line's element, which is a paragraph, so target a class called boxed as p.boxed.

Where to put the style sheet

Put the CSS in a comment block, and wrap that block in #val … #end val so the lines are emitted as written instead of each being wrapped in a paragraph — a <p> tag inserted into the middle of a <style> element breaks the rules around it.

#val
'<style>
'  .calcpad-ui-input.highlight { background-color: #eeeeee; border: 1px solid #aaaaaa; }
'  .calcpad-ui-dropdown.primary { font-weight: 600; }
'  .calcpad-ui-radio.compact .calcpad-ui-radio-label { margin-right: 4px; }
'  .calcpad-ui-checkbox.switch { accent-color: #2a8f3f; }
'  .calcpad-ui-datagrid.bordered { border: 2px solid #444444; }
'  p.boxed { border: 1px solid #cccccc; padding: 2px 4px; }
'</style>
#end val

Then name the classes on the directives:

#UI {"style": "highlight"} depth = 2m
#UI {"reportStyle": "boxed"} P = 25kN
#UI {"style": "highlight", "reportStyle": "boxed"} q = 3kN/m
#UI {"type": "dropdown", "style": "primary", "keys": ["Low", "High"], "values": ["1", "2"]} g = 1
#UI {"type": "radio", "style": "compact", "keys": ["Steel", "Concrete"], "values": ["200GPa", "25GPa"]} E = 200GPa
#UI {"type": "checkbox", "style": "switch"} flag = 1
#UI {"type": "datagrid", "style": "bordered"} T = [1; 2 | 3; 4]

depth is highlighted in the form and unremarkable in the report; P is boxed in the report and an ordinary text box in the form; q gets both.

Also, several classes can be listed at once — "style": "highlight wide"

Inside a datagrid

A grid is a third-party widget, so a style class reaches its outer container but not the cells, headers or context menu inside it.

There is limited customization available using the datagrid properties, and more customization is planned.

Saving what was entered

Saving input values writes them into a metadata comment at the top of the document, which is what restores them the next time you open it:

'<!--{"uiOverrides":{"L:1":"8","q:1":"30"}}-->

The keys are control identities: the variable name, a number indicating the order that variable appears in when it is re-defined (L:1 is the first L), then a number representing the order it was re-defined inside a loop (y:1:2). A saved value replaces the right-hand side of its assignment in the form and in the report, so the report shows the numbers that were entered. Hand-writing an entry in the uiOverrides data works too, and the Properties tab can help make editing easier. This is useful if you changed the CalcpadCE file after applying inputs and need to re-map renamed variables or want to remove deleted entries.

See Metadata Comments for the comment format itself. The comment has to be the first line of the file to take effect. See #UI overrides and includes for more information.

Values do not have to be saved to be exported: an export made while the form is filled in uses what is currently entered. Saving is what makes them survive closing the document.

Editing a document that has saved values

Because a saved value is tied to a variable name and to which declaration of that name it is, editing a document can move or orphan the values already saved in it. The document still calculates correctly — the risk is that a filled-in form comes back with a value on the wrong control, or with a control reset to the default value.

The ordinal counts the #UI declarations of that name in the order the document runs them, so what matters is not where a line sits in the file but how many declarations of the same name come before it:

  • Renaming a variable orphans its value. L:1 no longer matches anything, the control comes up with the default value, and the stale entry stays in the metadata comment until it is overwritten by the next save or purged in the Properties tab.
  • Deleting one of several #UI lines that declare the same name renumbers the ones after it. Delete the first of three L controls and the old L:2 and L:3 values land on what are now L:1 and L:2 — the values survive, attached to the wrong controls.
  • Inserting a new #UI declaration of a name that already exists shifts every later declaration of that name the same way.
  • Moving, editing or deleting lines that declare other names is safe. So is reordering, as long as the declarations that use the same name keep their relative order.

If you plan to use #UI elements on a document where the CalcpadCE file can change, the most stable arrangement is one #UI declaration per variable name. Give each input its own name instead of re-assigning one, and every key is name:1: it cannot be renumbered by anything you do elsewhere in the document, and only renaming or removing that input affects it.

If a document's values do end up scrambled, the metadata comment is plain text: fix the keys by hand, or clear the uiOverrides entry to start the form from the document's own values again. Editing the comment takes effect on the next render, and what it says replaces what is currently entered — including values typed into the form but not yet saved.

The Properties tab does this without hand-editing: its Saved #UI values list shows every entry, lets you edit one in place or jump to its control, flags entries that no longer match a control still in the document, and has a Purge unused button to drop them.

Exporting

A document with #UI controls exports like any other — see Exports for the variants, the formats, and where each one lives.

An input form exported to HTML is static. Its controls render, but nothing is behind them to recalculate.

Compiled (.cpdz) worksheets

A compiled worksheet is a .cpdz file with its source locked and only the #UI form left editable. It changes a few things from the ordinary-file behaviour described above:

  • It always opens as its input form — the "first time you open it" auto-detection under Turning it on doesn't apply, since there is no other mode to default to.
  • Exporting drops the Preview and Unwrapped variants: there is no source to render them from, so only Report and Input form are offered.
  • Saving values uses a separate command in VS Code — Save Values to Compiled Worksheet rather than Save #UI Values to Document — since the values are written back into the compiled file itself, not a .cpd source file.

#UI in macros, conditions and loops

The keyword works anywhere a normal assignment does.

#def Beam$(span$)
    #UI load = 10kN/m
    M = load*span$^2/8
#end def
Beam$(6m)

Inside #if, flipping the branch does not renumber the controls that follow (each branch gets its own override value that you can switch between):

#UI {"type": "checkbox"} useSteel = 1
#if useSteel ≡ 1
    #UI E = 200GPa
#else
    #UI E = 25GPa
#end if

Inside a loop each pass renders its own control, and each is entered separately:

#repeat 3
    #UI y = 2
    y
#loop

#pause in UI mode

The input form recalculates the whole document every time a value changes. Put #pause after inputs that come before something slow such as a long loop, a large matrix solve, heavy JavaScript, etc. The form stops there, so editing values stays responsive, and the slow part runs only once the user presses Next.

#pause splits the input form into steps. The form calculates down to the first #pause and stops there. Next continues to the following one, adding the next section below the ones already shown, and Previous steps back. Earlier inputs stay on screen and can still be changed.

#UI x = 2
#UI n = 1000000
#pause
#hide
s = 0
#for i = 1 : n
    s = s + x/i^2
#loop
#end hide
s

The Next and Previous buttons appear only when the document has at least one #pause. In the desktop and web app they sit in the toolbar beside Save values. In VS Code, they are arrows in the input form's title bar (beside the save button).

Outside the input form, #pause is ignored and the whole document is calculated.

Each step is tied to the #pause it stops at, so the form stays where it is while inputs change:

  • Loops: a #pause inside a loop stops once per pass. If an input lowers the loop count below the pass you are on, the form stops at the first #pause after the loop.
  • Conditions: a #pause in a branch that is not taken is skipped. If flipping an input moves the branch you are on, the form stops at the next #pause that is reached.
  • Edits: editing the source while the form is open keeps the current step, unless a #pause or a loop is added, removed or moved.

The step is not saved with the values, so the form starts again from the first step when it is reopened.

Diagnostics

#UI problems are reported under CPD-3415 by the linter and also appear at the offending line by the calculation engine when you run the document:

Message Cause
The #UI keyword requires a variable assignment. The line assigns nothing
#UI does not support expressions unless 'allowExpression' is true. The right-hand side is computed, not a value, and allowExpression is off
String mode is not supported by the #UI keyword. The variable ends with $, or "mode" is not number
Improper format for #UI keyword. Missing closing brace. The JSON block is unterminated
Invalid JSON in #UI. The block is not valid JSON
A #UI value has the wrong type. A property was given the wrong kind of value
The #UI type '…' is not recognized… "type" is not one of the five
The #UI … requires both keys and values arrays. A drop-down or radio group is missing one
The #UI … keys and values arrays must have the same length. They are paired, so the counts must match
The #UI … has n entries but the grid has m … More headers than rows or columns
#UI '…' must not be negative. rows or columns was given a negative number