Snow Tutorial

Last modified 2009-11-26

  1. Getting and Installing Snow
  2. Building Snow from source (optional)
  3. The Snow REPL
  4. Basic Concepts
  5. Layout
  6. Event handling
  7. Embedding Snow
  8. Data Binding
  9. What's more?

Getting and Installing Snow

You can download the latest Snow binary distribution from
http://common-lisp.net/projects/snow/. It contains Snow and all its dependencies in a single Zip file. Since Snow can be used both in Lisp and Java applications, procedures for installing it can vary in each of the two cases. Currently Snow, when run from the jar, requires a temporary directory to load itself; make sure your application has write permissions on your OS's tmp directory. Snow should automatically clear its temporary files when the application exits.

Building Snow from source (optional)

Snow is built using the Ant program (a Java make-like tool). You can get it from <
http://ant.apache.org/>. To obtain the source code for Snow, either download a source release from the project page or, if you want the latest & greatest stuff, follow the instructions at <http://common-lisp.net/faq.shtml> to checkout it from the SVN repository. Once you have the source in a given directory, cd to that directory and issue the command ant snow.jar to build Snow. ant snow.clean removes all compiled files.

The Snow REPL

Being based on Lisp, Snow offers a REPL (read-eval-print-loop), an interactive prompt that allows you to evaluate arbitrary pieces of code. If you launch Snow through its main class (snow.Snow) with no command-line arguments, it will show a window containing the REPL (which is nothing more than a wrapped ABCL REPL). It should print

SNOW-USER(1):

SNOW-USER is the active package (namespace), (1) is the line number of the REPL. Now, the obligatory hello world:
(frame (:title "Snow Example")
  (button :text "Hello, world!"
          :on-action (lambda (event)
                       (print "Hello, world!")))
  (pack self)
  (show self))
Evaluating this will show a window containing a single button which, when pressed, will output "Hello, world!". The terminology should be familiar to Swing developers. Actually, the output from the button will NOT go to the REPL, but to the OS console instead; I'll explain this later, please ignore it for now.
The REPL is great for experimenting: the code you input is immediately executed by an interpreter. You can also compile your code, either on the fly in the REPL or from a file; this is outside the scope of this tutorial, but you can find more information in any decent tutorial or book about Common Lisp (I suggest the free ebook Practical Common Lisp by Peter Seibel, available at
http://gigamonkeys.com/book/). However, experiments sometimes go wrong; if you make a mistake - for example, evaluating an unexisting function - you will end in the debugger. Try typing the function call
(oh-no!)
- you should see something like this:

Debugger window

The restarts are the actions the system can perform to recover the situation; this is an important feature of Common Lisp, worth studying by itself. You'll always be able to choose the TOP-LEVEL restart, which will bring you back to the REPL.
You can quit the REPL (and terminate the application) any time by closing the REPL window, or by typing (quit)1.

Basic Concepts

As you can see from the previous examples, Snow code consists of a tree of widgets; nesting in the code means nesting in the widget hierarchy, for example:
(frame (:visible-p t)
  (panel (:layout "wrap")
    (label :text "1")
    (label :text "2")
    (label :text "3"))
  (panel ()
    (label :text "4")
    (label :text "5")
    (label :text "6"))
  (pack self))
Creates a frame with two children, both panels with 3 children each - one has labels from 1 to 3, the other from 4 to 6.
You can set the properties of the widgets using keyword-value pairs like :visible-p t which means setVisible(true) (-p is a suffix traditionally used in Lisp to name predicates, T is the canonical true value). Containers must have their properties set in a list after the widget name -
(panel (...here go the properties...) ...here goes the body...)
- the list serves to separate them from the body. Non-containers have no body and thus their properties do not require to be wrapped in a list:
(label ...here go the properties...)

How does this work?

The Snow API consists of a set of macros that can be used to declaratively construct a tree of widgets. These macros are designed in such a way to make the tree structure of Lisp source code closely mirror the GUI widget tree structure (in the general case). The macros expand to code that uses a functional interface to create widgets, however it is not recommended to use this functional API directly since it depends on the context established by the macros. The aspects of such context of interest to the user are:

lexical variable self

Holds the current widget being processed. Example:
(frame ()
  (print self)) 
will output something like:
#<javax.swing.JFrame ...frame.toString()... {identityHashCode}>

special variable *parent*

Holds the current parent widget. When *parent* is non-nil, any widget created through a macro will be automatically added to the container referenced by *parent*. All Snow widget macros process the forms in their body in the following way: These rules make the nesting of Snow widget macros work in an intuitive way: Snow provides operators to alter this default behavior:

widget id

Additionally, all container widget macros support a pseudo-property called id which can be used to bind a lexical variable of choice to the widget locally to the macro body. Example:
(frame (:id foo)
  (print foo)) 
will output something like:
#<javax.swing.JFrame ...frame.toString()... {identityHashCode}>

Layout

By default, Snow uses
MiG Layout as the layout manager to organize components inside a container. When you create a component that will be automatically added to *parent* by Snow, you can use the pseudo-property :layout to specify (as a string) additional information for the layout manager. If you use add-child, instead, you have to pass this string to add-child as its last optional parameter (I hope I can fix this inconsistency). Here's a quick cheat sheet of the constraints you can use with MiG Layout: http://www.migcalendar.com/miglayout/cheatsheet.html (look for "Component Constraints").
You can use another layout instead of MiG: to do so, use the layout-manager property of the container. The values you can pass are:

Event handling

Certain widgets can trigger events on certain types of user actions. These events can be handled by user code. Event-handling callbacks can be set using properties named :on-event-name (for example, :on-action for handling clicks on buttons, or ActionEvents in Swing/AWT parlance). Currently extremely few events are supported! I'll add new ones in future releases.
A callback for an event is either a Lisp function with a single argument (the event object), or an appropriate native Java event handler for the event (e.g., an instance of java.awt.ActionListener).
Events happen on a dedicated thread (in Swing's terminology, the EDT - Event Dispatching Thread). That's why, in the Hello World example, the string got printed to the console and not to the REPL! In fact, the REPL has its own dynamic, thread-local context, which rebinds the value of *terminal-io* to a stream that reads and writes on the REPL; the event, instead, is run in another thread, which doesn't have access to this context, and thus uses the global value of *terminal-io*. If you want to capture the value of a dynamic variable from the thread that creates the event handler, you have to explicitly do so like this:
(button :on-action (let ((tmp *some-thread-local-variable*))
                     (lambda (event)
                       (let ((*some-thread-local-variable* tmp))
                         ...do stuff...))))

Embedding Snow

Snow can easily be embedded in a Java application by using JSR-223. The snow.Snow class has some static methods that can be used to load some Snow source code from a .lisp file (or classpath resource), or to obtain an instance of javax.script.ScriptEngine which you can use for more advanced stuff (e.g. compiling Lisp code, or calling specific Lisp functions). When embedding Snow to define (part of) the application's GUI, it is recommended that you modularize the Snow code in functions, which you'll call from Java to obtain the GUI components:

file.lisp

(in-package :snow-user)

(defun create-main-frame (&rest args)
  ...snow code...)

MyClass.java

...
Snow.evalResource(new FileReader("file.lisp"));	
JFrame mainFrame = (JFrame) Snow.getInvocable().invokeFunction("create-main-frame", args);
...

Data Binding

Keeping the GUI state in sync with the application objects state is generally tedious and error-prone. Data Binding is the process of automating the synchronization of state between two objects, in this case a GUI component and an application-level object. Snow supports several kinds of data binding, and it uses two libraries to do so:
JGoodies Binding on the Java side and Cells on the Lisp side.

General concepts

There are two general ways to bind or connect a widget to some object's property: one is by using the :binding property of the widget, letting the framework choose which property of the widget to bind, e.g. the text property for a text field; for example:
(text-field :binding (make-simple-data-binding x))
the other is to provide as the value of a widget's property an object representing the binding, as in
(button :enabled-p (make-simple-data-binding x))
this will connect the specific property of the widget with the user-provided object or property.

Types of data binding

Snow supports several types of data binding; some are more indicated for Lisp applications, others for Java applications.

Syntactic sugar

To avoid the verbosity of make-foo-data-binding, Snow provides convenient syntax to cover the most common cases of data binding. You can enable this special syntax by evaluating the form (in-readtable snow:syntax), for example placing it in every source file right after the (in-package :snow-user) form at the top of the file.
This special syntax is accessed by using the prefix $. It covers the following cases:

What's more?

I haven't covered which widgets are supported and how much of their API is supported. At this stage, Snow is in a early stage of development, so very little of the Swing API is covered. The best way to learn about Snow usage is to look at the examples included with Snow: the debugger (debugger.lisp), inspector (inspector.lisp) and the REPL (repl.lisp and swing/swing.lisp). Also, I haven't talked about how to use your custom widgets with Snow, and probably other things. Drop me a line at alessiostalla @ Google's mail service, and I'll be happy to help you.

Footnotes

  1. If you really mess things up, you can change the package of the REPL to one where the symbol QUIT is not visible. If you find yourself in this situation, type (ext:quit) to exit.
  2. Snow provides a convenient Java class - org.armedbear.lisp.Callback - that you can subclass to create your custom callback function. You have just to implement the appropriate overload of the call method, or alternatively use one of the static methods Callback.fromXXX to create a Callback from several kinds of Java callbacks.