User Interface Patterns in Wolfram Language

Lou D’Andria, Wolfram Research

#WolframTechConf

ABSTRACT

In this talk, we’ll explore common pitfalls developers encounter when building dynamic interfaces in the Wolfram Language, and present effective patterns to avoid them. We’ll cover a wide range of topics, including synchronization, initialization, alignment, and other frequently overlooked issues.

Expect slow things to be slow

There are two evaluation modes for dynamic content.

Manipulate[Zeta[x],{x,0,1}]

c=0;While[True,Pause[2];Print[++c]]

“Synchronous” vs “Asynchronous”

“Asynchronous”
• queued
• same queue as shift-return
• can handle long running evaluations
• cell bracket highlights until evaluation is complete
• can be preempted
• doesn’t interfere with other front end interactions

“Synchronous”
• preemptive
• very responsive
• no indication on the cell bracket
• can handle short evaluations
• effectively pauses the front end while waiting
• automatically aborts after a few seconds

Dynamic option: SynchronousUpdating

Use SynchronousUpdating -> False for any evaluation that may take more than a couple seconds.

Dynamic[Pause[10];2+2]

Dynamic[Pause[10];2+2,SynchronousUpdating->False]

Button, EventHandler, ActionMenu option: Method

Some controls take an option for determining whether to evaluate synchronously (Method -> “Preemptive”) or asynchronously (Method -> “Queued”).

Like Dynamic and DynamicModule, these options are set to synchronous mode by default.

Button[Dynamic[If[!ColorQ[color], "pick a color", color]],color=SystemDialogInput["Color"],Method -> "Queued"]

DynamicModule option: SynchronousInitialization

Use SynchronousInitialization -> False, for any initialization that may take more than a couple seconds.

DynamicModule[{x=0},Dynamic[x],Initialization:>(Pause[10];x=1)]

DynamicModule[{x=0},Dynamic[x],Initialization:>(Pause[10];x=1),SynchronousInitialization -> False]

Consider the next session

Every instance of an interface needs to ensure any support code is loaded, and any relevant state is saved and applied.

Use autoloading features in the Wolfram Language (via DeclarePackage, Stub, …), or use an explicit Initialization option to that instance of DynamicModule, Dynamic, DynamicWrapper, or even Cell and Notebook.

CellPrint[ExpressionCell[123,"Output", Initialization:>AttachCell[Now]]]

DynamicModule is key!

A scoping construct that automatically saves state

Local variables in one instance of an object are automatically isolated from any other instances, and stored in the saved notebook

DynamicModule[{x},LabeledSlider[Dynamic[x]]]

Support for SaveDefinitions

This is sometimes convenient, but often saves too much. Use with caution.

Note that IncludedContexts and ExcludedContexts are now options to DynamicModule.

Support for initialization and progress indication

Details…

Initialization and progress indication

Initialize responsibly

Think carefully about what should happen when there are multiple copies of this interface on screen at the same time.

Use Needs; don't change global state; …

Sync Dynamics vs Async Initializations

Synchronous dynamics will be evaluated before asynchronous initializations, which makes it easy to show progress indication.

DynamicModule[{initdone},Dynamic[If[TrueQ[initdone],"ready","not ready"]],Initialization:>(loadYourCode[];Pause[2];initdone=True),SynchronousInitialization->False,UnsavedVariables:>{initdone}]

Please waitFEExpressionsEllipsisAnimator

Get arbitrarily clever in the “not ready” display.

DynamicModule[{initdone},Dynamic[If[TrueQ[initdone],"ready",Panel[Column[{"Loading. Please wait.",
ProgressIndicator[Appearance->"Percolate"]},Center]]]],Initialization:>(loadYourCode[];Pause[2];initdone=True),SynchronousInitialization->False,UnsavedVariables:>{initdone}]

That is exactly how Manipulate’s SynchronousInitialization option works.

Manipulate[x,{x,0,1},Initialization:>(Pause[3];x=0.5),SynchronousInitialization->False]

BONUS: More info about progress indicators

See extensive discussion in “The Principle of Evaluation Blocking” here.

Don’t move the user’s selection (unless you have to)

The selection belongs to the user!

Leave the selection alone during long-running tasks, or any other time it makes sense to do so.

If you must overwrite content, try to avoid SelectionMove.

SelectionMove

Table[Button["click", 
SelectionMove[EvaluationBox[],All,Expression, AutoScroll->False];
NotebookWrite[EvaluationNotebook[],ToBoxes @ "done"]],20]

Alternative: target objects directly

Table[Button["click", NotebookWrite[EvaluationBox[],ToBoxes @ "done"]],20]

The example of control-equal

Compare with interactive iconize / un-iconize

Range[100]

BONUS: Custom targets

Writing to an custom target.

targetObject /: MakeBoxes[t_targetObject, fmt_] := InterpretationBox["\"<targetObject>\"", t]

$defaultTargetDisplay = RawBoxes[StyleBox["✍", FontColor -> RGBColor[0,0,1]]];

newTarget[display_:$defaultTargetDisplay] :=
	Block[{id, displayboxes, reference},
		id = CreateUUID[];
		displayboxes = PaneBox[ToBoxes[display], BoxID -> id];
		Print[{RawBoxes @ displayboxes}];
		reference = FE`BoxReference[EvaluationNotebook[], id];
		targetObject[reference, displayboxes]
	]

targetWrite[targetObject[reference_, displayboxes_], expr_] :=
	MathLink`CallFrontEnd[FrontEnd`BoxReferenceReplace[
		reference,
		RowBox[{ToBoxes[expr], ",", displayboxes}]
	]]

t=newTarget[]

targetWrite[t,foo]

targetWrite[t,Plot[Sin[x],{x,0,10}]]

targetWrite[t,DateString[]]

Do[targetWrite[t,i],{i,10}]

Don’t forget about baselines

Most people know how to align objects there’s “rattle room” in Framed, Pane, Panel, Grid, Overlay, TabView, PaneSelector, …

Framed[x,ImageSize ->{100,100},Alignment->{Right,Top}]

Grid[{{1.111111},{22.2},{33333.3}}, Alignment -> "."]

Overlay[{
"a",
Item["b", Alignment -> {Right,Center}],
Item["c", Alignment -> {Center,Bottom}]}, ImageSize->{100,100}]

Lesser known is aligning objects by their baselines.

The baseline of a font character is typically the bottom of the character, excluding any descenders.

BaselinePosition

Every object has a baseline, and its position can be changed!

{x,
TabView[{x,y,z}],
Grid[{{a,b},{c,x}}],
Column[{x,y,z}],x}

Box generators typically have a BaselinePosition option.

{x,
TabView[{x,y,z},BaselinePosition-> Baseline],
Grid[{{a,b},{c,x}},BaselinePosition->{2,2}],
Column[{x,y,z},BaselinePosition->1],x}

For nested box generators, account for the baseline in every layer.

{x,TabView[{Grid[{{a,b},{c,Column[{z,y,x}]}}],y,z}],x}

{x,TabView[{Grid[{{a,b},{c,Column[{z,y,x},BaselinePosition->3]}},BaselinePosition->{2,2}],y,z},BaselinePosition->Baseline],x}

Challenge: Stop this cell label from moving.

PaneSelector[{
1->"a",
2->Item["b", Alignment -> {Right,Center}],
3->Item["c", Alignment -> {Center,Bottom}]}, Dynamic[choice], ImageSize->{100,100}, BaselinePosition -> Top]

Treat strings as text

str=StringTake[ExampleData[{"Text","GettysburgAddress"}],UpTo[400]]

First off… A string is not a box!

RawBoxes[str]

Use a text font.

Style[str, FontFamily -> "Arial"]

Get rid of that weird indentation.

Style[str, FontFamily->"Arial", LineIndent->0]

Fill lines more completely.

Style[str, FontFamily->"Arial",LineIndent->0, LinebreakAdjustments->{1.,10,1,0,1}]

Protect against winding up in an input cell.

Style[str, FontFamily->"Arial",LineIndent->0,LinebreakAdjustments->{1.,10,1,0,1},ShowStringCharacters->False]

Better yet…

Just use inline cells with existing named styles.

TextCell[str, "Text"]

Remove interpretation information.

TextCell[str, "Text", StripOnInput -> True]

Finally… Inline cells have baselines too, as well as internal alignment and/or justification!

Grid[{{x,TextCell[str, "Text", StripOnInput -> True,CellBaseline->Baseline,TextJustification->1],"x"}}]

Dark mode support

Go back and watch the talk from Noah Hardwicke earlier today. (link)

In brief…

The LightDark option determines the behavior of notebooks.

New “Standard” colors (StandardRed, StandardGreen, …) are generally useable as foregrounds or backgrounds in light and dark modes.

A new LightDarkSwitched color primitive gives more control when needed.

Style[Graphics[{LightDarkSwitched[Blue, Green],Disk[]},ImageSize -> Tiny]]

A new LightDarkAutoColorRules option gives other levers to push

Style[Graphics[{Blue,Disk[]},ImageSize -> Tiny], LightDarkAutoColorRules->All]

For vector graphics, use those levers.

BONUS…

For bitmap graphic resources, provide light and dark PNG files in the usual Bitmaps locations with appropriate file names and the front end will automatically switch between them.

For instance: ControlEqualAppearance@192dpi.theme-dark.9.png

Compatibility and steganography

Internal`LightDarkColor[Blue, Green]//InputForm

Style[Graphics[{Internal`LightDarkColor[Blue, Green],Disk[]},ImageSize -> Tiny],LightDarkAutoColorRules -> <|"MarkedColors" -> True|>]

DarkModeMigration paclet

Coming soon to the Wolfram Language Paclet Repository.

Conclusion

Expect slow things to be slow

Consider the next session

Don’t move the user’s selection (unless you have to)

Don’t forget about baselines

Treat strings as text

Dark mode support