Declarative Dataflow: GUI Programming Example

Accessing Java code in Vodka is easy: Any Java class that is available on the Vodka runtime's classpath can be imported with an import native declaration. Moreover and thanks to the partial-application operator ->, we can write code that, on the first glance, looks almost like regular Java code:

# 
# Create a swing frame with a button inside that will print a message
# when clicked
# 

import library.util;

import native javax.swing.JButton;
import native javax.swing.JLabel;
import native javax.swing.JFrame;
import native java.awt.FlowLayout;

import library.swingUtil;


button = JButton("Button");

button->addActionListener(ActionListener(actionPerformed:
                            fn e: Console.print("Click!")));


frame = JFrame("MyFrame");

frame->getContentPane()->setLayout(FlowLayout());
frame->getContentPane()->add(JLabel("Click here:"));
frame->getContentPane()->add(button);

frame->pack();
frame->show();

stop();
	

In addition to calling Java methods ourselves, we can create objects that implement particular Java interfaces, as is the case with the above action listener. In fact, we can turn any record with appropriately named function-value fields into an implementation of a Java interface by passing the record to the interface constructor (in this case ActionListener).

Introducing Data Dependencies

Although feeling at home with the API may be a nice thing, it would not be much of an improvement, if writing Swing code "the old way" was the sole means of GUI construction in our language. To illustrate our more declarative approach to GUI development (found e.g. in OpenLaszlo), which concentrates on the data dependencies rather than on state-changes, we are going to construct the following window:

original window

Dragging one of sliders changes the width or height of the green rectangle, whose area is displayed as a numberic value inside. If the green rectangle grows larger than 20,000 square pixels, a text notice is shown. In the lower half of the window, the selection from the popup menu determines from which of the text fields the content of the label at the bottom is drawn. Editing that text field also changes the text below interactively.

window after edititing

In implementing this functionality, we make use of reference cells, another kind of advanced mutable variables we can implement in the language itself. Reference cells allow us to define data dependencies between variables and have state changes be tracked automatically.

In order to achieve this, the set operation on reference cells no longer takes a value that defines the contents of the variable, but a function that is executed to (re-)compute the actual value. Within this function, other variables may be referenced by calling their subscribe operation.

#
# Upper region of the window
#


sliderX = VSlider(0, 200);
sliderY = VSlider(0, 200);

areaValue = VCell
(
    fn t: sliderX.value.subscribe(t) * sliderY.value.subscribe(t)
);


areaLabel = VLabel("-");

areaLabel.value.set
(
    fn t: toString(areaValue.value.subscribe(t))
);


areaPanel = VFlowPanel[areaLabel];
areaPanel.comp->setBackground(Color(0, 255, 0));

areaPanel.size.set
(
    fn t: (sliderX.value.subscribe(t), sliderY.value.subscribe(t))
);


notice = VLabel("wow, this is HUGE ... !!");

notice.visibility.set
(
    fn t: areaValue.value.subscribe(t) > 20000
);

In constructing the lower half of the window, we make use of the fact that reference cells, too, can be treated as first class values. The value of the selection cell is one of the text fields, and the value of the selectionText label is defined as the value of that text field, which is the value of the selection cell.

#
# Lower half components
#

textA = VTextField("A quick brown fox");
textB = VTextField("jumped over the lazy dogs");

textA.size.set(fn t: (200, 22));
textB.size.set(fn t: (200, 22));

menu = VComboBox(["Text A","Text B"]->toArray());

selectionText = VLabel("-");
selectionText.size.set(fn t: (200, 22));


selection = VCell
(
    fn t: (if menu.value.subscribe(t)->equals("Text A") then textA else textB)
);


selectionText.value.set
(
    fn t: (selection.value.subscribe(t)).value.subscribe(t)
);

Assembling components into panels is a little easier, too, with the help of generator literals:

#
# JFrame assembly 
#

sliders = VFlowPanel
[
    VLabel("Width:"), sliderX,
    VLabel("Height:"), sliderY,
    areaPanel,
    notice
];

sliders.size.set(fn t: (220, 300));



texts = VFlowPanel
[
    VLabel("Text A:"), textA,
    VLabel("Text B:"), textB,
    VLabel("Selection:"), menu,
    selectionText
];

texts.size.set(fn t: (220, 250));


base = VFlowPanel [sliders, texts];


frame = JFrame("MyFrame");
frame->setContentPane(base.comp);
frame->setSize(220, 550);
frame->show();


stop();

Putting these code snippets together yields exactly the required functionality, without writing a single callback or update function ourselves. Moreover, the reference cell implementation is clever enough to recompute just those cells that have actually changed, and to promote those changes in a topologically sorted way.