DisplayScript

DisplayScript is a programming language and editing environment designed specifically for making user interfaces. It's inspired by simple drawing systems like PostScript and Processing, where you describe graphics using a step-by-step procedure (e.g., draw a circle here, then draw a square around it, then draw an image on top). The editing environment is completely "live": changes to a DisplayScript program are reflected immediately without reloading or losing your spot.

The DisplayScript programming language looks mostly like JavaScript:

function greeting(thingToGreet) {
    return "Hello, \(thingToGreet)!"
}
draw fill(gray(0.8))
var string = greeting("world")
draw centered(text(string))

(This is a complete program. You can run these code examples by pasting them into the DisplayScript editor.)

Running a DisplayScript program produces a sequence of low-level drawing operations (like fill this region with a color or draw this image at that position). This sequence of operations is called a frame. Whenever the interface needs to be redrawn, the entire program is run again to produce a new frame.

The main principle of DisplayScript is that each UI element is described by the process of drawing it. Unlike traditional object-oriented UI systems, there is no persistent tree of elements (that is, no views, layers, nodes, components, widgets, morphs, or anything like that). Each element is a procedure that draws itself and any other elements it contains.

To be more concrete, let's look at an example. We'll make a very simple interface: a red bar that starts at 50 pixels by 50 pixels, then gets 10 pixels wider each time you click.

To draw an element in DisplayScript, we use the draw keyword. We'll use a fill element to represent our bar:

draw fill(red)

Try pasting this code into the DisplayScript editor to run it! You'll see that this fill extends across the entire screen. To restrict it to 50 by 50 pixels, we can use at:

draw at(0, 0, 50, 50, fill(red))

We put the width of the bar into a state variable so we can change it:

state barLength = 50
draw at(0, 0, barLength, 50, fill(red))

State variables are values that can change based on events or other actions. Unlike other variables, their values persist across re-draws. We'll discuss them in more detail later.

To finish the program, we draw a clickHandler element which increases the barLength each time you click.

state barLength = 50
draw at(0, 0, barLength, 50, fill(red))
draw clickHandler(function () {
    next barLength = barLength + 10
})

And that's it! When you click, the barLength increases by 10, and the bar itself is redrawn automatically at its new length.

If you're familiar with traditional, persistent-tree systems, you can appreciate how this immediate-mode style simplifies UI updates. In these persistent-tree systems, elements are updated by setting properties like position and size directly. It's up to you to keep track of which elements need to be updated after a user action or state change. DisplayScript's model avoids manual dependency-tracking: everything is always redrawn using the current state.

Now that we've created the bar, we may want to use it in more than one place. Let's put the procedure for drawing a bar into a new drawable function:

drawable bar() {
    state barLength = 50
    draw at(0, 0, barLength, 50, fill(red))
    draw clickHandler(function () {
        next barLength = barLength + 10
    })
}
draw bar()

Calling a drawable function like bar() returns a drawable, DisplayScript's representation of a UI element. Each drawable holds a procedure describing the element it represents. You can use draw on a drawable to run it.

Once we have a bar() drawable function, we can draw multiple bars easily:

drawable bar() {
    state barLength = 50
    draw at(0, 0, barLength, 50, fill(red))
    draw clickHandler(function () {
        next barLength = barLength + 10
    })
}
draw at(0, 0, @width, 50, bar())
draw at(0, 100, @width, 50, bar())

Notice how we use at (like we did before with fill) to restrict the bounds of a drawable. @width and @height give you access to the current bounds. We can make our bar grow to match its height by using @height as the bar height instead of 50:

drawable bar() {
    state barLength = 50
    draw at(0, 0, barLength, @height, fill(red))
    draw clickHandler(function () {
        next barLength = barLength + 10
    })
}
draw at(0, 0, @width, 50, bar())
draw at(0, 100, @width, 100, bar())

This lets us change the height of the bar. What if we want to change its color? We can make the color a parameter of our drawable function:

drawable bar(color) {
    state barLength = 50
    draw at(0, 0, barLength, @height, fill(color))
    draw clickHandler(function () {
        next barLength = barLength + 10
    })
}
draw at(0, 0, @width, 50, bar(red))
draw at(0, 100, @width, 100, bar(gray(0.5)))

Describing elements as procedures makes this sort of abstraction easy.

There's certainly more to say about drawables (like how to measure their size), but we'll leave that for the language reference. For now, let's move on and talk a little bit about state variables.

In our bar program, we created a state variable called barLength. This variable maintains the current length of the bar, and is increased on every click.

state barLength = 50
draw at(0, 0, barLength, 50, fill(red))
draw clickHandler(function () {
    next barLength = barLength + 10
})

Most languages let you store state in any old variable. This doesn't work in DisplayScript:

var barLength = 50
draw at(0, 0, barLength, 50, fill(red))
draw clickHandler(function () {
    // Can't do this!
    barLength = barLength + 10
})

Remember that the entire program runs again to produce a frame; barLength would go right back to 50, even if DisplayScript allowed the assignment. Normal var bindings are re-made each time the program runs.

State variables, on the other hand, store their values in state cells which persist between runs. Accessing a state variable will produce the value stored in the corresponding state cell. Once the state variable is no longer accessed, DisplayScript cleans up its state cell as well.

These state cells are the only persistent values available to the program, and the only way to keep a value between frames is to use a state variable. This is another important principle of DisplayScript: persistent state is tracked explicitly.

Tracking state explicitly lets us keep state cells around not only when the program is re-run, but also when it is modified. And since elements are just procedures, changes to those procedures show up right away when the program runs again to produce the next frame. The combination of these two principles enables a really nice live editing experience:

At some point, a DisplayScript program needs to interact with the rest of the system it's running on. When UI code invokes a system function, it can end up accidentally blocking the UI for many milliseconds. Specifically, operations like text layout and image decoding often take a while to complete.

In DisplayScript, communication with external code is asynchronous. To hit a smooth 60 frames per second, a frame needs to be produced every 16 milliseconds; we can't wait on other parts of the system. DisplayScript lets you decide how to handle an incomplete operation instead of being forced to block: you could show a spinner, a blank tile, or a preview image, to give a few examples. You can still block the UI, too, if that's what you want (e.g., to avoid images popping in). See Asynchronous Operations and Readiness in the language reference for more details.

Asynchronous communication also lets us transparently move pieces of the system onto another thread, process, or machine without affecting the programming model. For example, A (hypothetical) DisplayScript window server could run DisplayScript programs on behalf of applications: the application could provide data and otherwise communicate with its DisplayScript program using asynchronous IPC (like NeWS). Or a DisplayScript "browser" could load programs from the network which communicate with servers using HTTP (like the web).

The final principle I'd like to mention is that DisplayScript should have no huge mountain of code underneath. It's tempting to build on web technologies or to try to generalize native UI frameworks: it saves a ton of work and eases interoperation with existing code written in these frameworks. It's certainly the practical choice in the short term.

But in the long term, trying to define a "simple" system in terms of a complex system just makes things more complex. Tracking down difficult bugs and diagnosing performance problems requires a real understanding of what is happening in your program. You ultimately have to understand both the layer you've added on top and the complex system lurking underneath to get this sort of real understanding.

Hopefully this introduction gave you a basic idea of what DisplayScript is. You can check out the language reference for more in-depth information, look at some examples, or go back to the home page.