Scripton / Docs

Overview

Let's start with an example:

from scripton import ui from scripton.canvas import Canvas def render(canvas, state): # Update the canvas based on the given state canvas.clear() canvas.draw_circle( x=150, y=150, radius=state.radius, fill='#fff' ) # Show a slider for the user interface return ui.Slider( value=state.bind.radius, range=(10, 100), label='Radius' ) ui.run( render, canvas=Canvas(width=300, height=300), state=ui.State( radius=20.0 ) )

Broadly, this script proceeds as follows:

  1. Tells the IDE to display an interactive user interface and its current configuration (the slider returned by the render function)
  2. Waits for messages from the IDE informing it how the UI has changed (eg: when the user moves the slider to a new position)
  3. Reacts to that change by updating the ui.State instance and, potentially, re-invoking render.

The steps above repeat in a loop until the script is terminated.

Let's dig into each of these steps, starting with the ui.run function.

ui.run

def run(renderer, *args, **kwargs): ...
  • The run function has a single required argument - a function (renderer) that returns the current user interface. In the example above, this would be the render function that returns a single Slider as the UI.

  • All other positional and keyword arguments are bound to the renderer function (equivalent to functools.partial). These are useful for passing in expensive resources that you don't want to re-create every time the user interfaces changes (the Canvas instance in our example).

  • The renderer function is invoked at the beginning to render the initial UI. The run function then enters an infinite loop receiving and processing UI events (like slider drags, button clicks, etc).

  • The rendered UI is completely dynamic, and some events (like the slider drag in our example) can trigger a re-rendering by re-invoking the renderer function. You can also perform other updates here that are a function of the UI state (like the canvas drawing in our example).

ui.State

You might have noticed the ui.State instance in the example earlier:

... ui.run( render, canvas=Canvas(width=300, height=300), state=ui.State( radius=20.0 ) )

It provides a few key functionalities:

  • It acts as a simple namespace for grouping all mutable state for the user interface.

    state = ui.State( learning_rate=0.03, warmup=True ) # Simple access to the attributes assert type(state.warmup) == bool # Mutable state.learning_rate = 0.2
  • The special bind accessor provides a way for widgets (like ui.Slider) to "bind" their value to a particular state attribute.

    ui.Slider( # While state.radius is just an int or a float, state.bind.radius # is a special "Binding" instance that allows for both reading and # updating the value of state.radius value=state.bind.radius, range=(10, 100) )

    The value argument for ui.Slider can accept either a numeric type (int or float), or a Binding instance that acts as an indirection to both fetching and mutating a numeric value. This essentially allows the IDE to auto-update state.radius whenever the slider is changed.

  • Mutating any attributes of a ui.State instance (including via binding auto-updates) automatically triggers a UI re-rendering. In our slider example, this is how the render function is invoked whenever the user drags the radius slider.