Chapter 8 · Part V

A Live Desktop Dashboard

A real window, with real buttons, wired to real Java event listeners written in Nex. This is the one chapter in this book that crosses out of the language entirely, into the host platform's own GUI toolkit. It is the one place where "does this even work" had to be answered before any design could be trusted.

Four buttons (+, -, reset, quit), each wired to a real javax.swing.ActionListener implemented as a Nex class, updating a counter shown in a live window. The counter itself is trivial. What makes this chapter different from every one before it is that Swing is not Nex's own type system reasoning about Nex's own values. It is a real Java GUI toolkit, calling back into Nex code from inside the JVM's own event dispatch machinery, and that boundary was genuinely untested territory when this project began.

Problem

Before any of this project's design was settled, a separate feasibility spike answered a narrower question first: can a Nex class actually implement a Java interface, get handed to a real widget, and have a real click fire a real method call back into Nex? That question was not rhetorical. The spike found that the answer is yes, but with two specific, narrow bugs along the way, one of which shapes every line of code in this chapter.

The spike's headline result, confirmed end to end: a Nex class can inherit ActionListener, be wired to a real JButton via addActionListener, and have actionPerformed fire on an actual doClick() (on the JVM backend, no fallback, correct state throughout), provided the listener class holds no field typed as an imported Java class. That proviso is not a footnote. It is the design constraint this entire project is built around.

Design as Contract

The spike found that a Nex class field typed as an imported Java class (a JLabel field on a listener that wants to update a label directly, the single most natural-looking design for this kind of code) fails to compile cleanly on the JVM backend, whether or not that class implements any interface at all. So the design this project follows is not "the listener updates the label." It is a split, validated in the spike before this chapter's code was written: capture the event in plain Nex state; render that state separately.

Dashboard_State holds nothing but plain Nex fields — no Java type anywhere in sight, by construction, not by care taken not to slip one in:

class Dashboard_State
create
  make() do
    count := 0
    should_quit := false
  end
feature
  count: Integer
  should_quit: Boolean

  increment() do count := count + 1 end
  decrement() do count := count - 1 end
  reset() do count := 0 end
  quit() do should_quit := true end
end

Each listener holds exactly one field (a Dashboard_State) and does exactly one thing when its event fires: call the one method on that state which describes what happened.

class Increment_Listener
inherit
  ActionListener
create
  make(state: Dashboard_State) do
    this.state := state
  end
feature
  state: Dashboard_State

  actionPerformed(e: ActionEvent) do
    state.increment()
  end
end

Dashboard_State is a Nex class, not a Java one, so a field typed with it is exactly the kind of field the spike's finding does not apply to. This listener sidesteps the bug entirely because of what it is built to hold, not because of any special-case handling inside it. Three more listeners (Decrement, Reset, Quit) follow the identical shape.

Build

Source

The complete, current code for this project is at examples/contracts_at_work/08_dashboard on GitHub.

dashboard_main.nex is the only file in this project that ever touches a JLabel; every widget lives here, and the listeners never see one:

with "java" do
  let frame: JFrame := JFrame.new("Contracts at Work — Dashboard")
  let label: JLabel := JLabel.new("count: 0")
  let inc_button: JButton := JButton.new("+")
  ...
  inc_button.addActionListener(create Increment_Listener.make(state))
  ...
  frame.setVisible(true)

  from
  until
    state.should_quit
  do
    Thread.sleep(100)
    label.setText("count: " + state.count)
  end

  frame.dispose()
end

The polling loop is deliberately the simplest thing that could work — no Task, just a tight sleep-and-redraw cycle reading state.count every hundred milliseconds. It is the single point of contact between "what happened" (an event, captured as a state mutation by a listener running on Swing's own event-dispatch thread) and "what the user sees" (a label's text, mutated only here, only from this loop). Even should_quit (a listener setting a flag rather than closing the window directly) follows the same rule: the listener describes an intent, and only the loop that owns the window acts on it.

This split follows directly from the constraint above, and it is kept, in this project's own judgment, because it is arguably the better design on its own merits: it separates "what happened" from "how it's shown," the same separation of concerns this book has argued for since Chapter 1's split between Word_Stats and its CLI wrapper.

Test

checks.nex drives the listeners through real JButton.doClick() calls, not direct method calls on the listener objects, the same style of check the spike itself used and the only style that actually proves the Swing wiring works rather than proving the underlying state methods work in isolation:

inc_button.doClick()
inc_button.doClick()
inc_button.doClick()
c.check("three increments", "3", "" + state.count)

dec_button.doClick()
c.check("one decrement", "2", "" + state.count)

quit_button.doClick()
c.check("quit after click", "true", "" + state.should_quit)

No visible window is required for this: JButton and ActionListener both function without ever being shown on screen, which is what makes it possible to check real event dispatch in an automated suite at all, rather than only by a human clicking a real window.

Takeaways

When a design's whole point is to react to a real external event, test it by triggering that real event, not by calling the code underneath it directly. A real JButton.doClick() proves the wiring works in a way a direct call on the listener object never could — the same principle behind every integration test in this book, just applied here to a GUI instead of a socket or an HTTP connection. And capturing an event as a plain state mutation, then rendering that state separately from one single loop, is a pattern worth reaching for anywhere "what happened" and "how it's shown" can be pulled apart — it keeps a UI's own bookkeeping out of the code that actually responds to the user.

Answering the one question that can sink an entire project's design — can this even work, mechanically — with a small, disposable spike before writing any of the project's real code buys a kind of confidence no amount of careful planning can substitute for.