System Loaded Successfully■■Machine Status: Intelligent
Almighty Lisp
Thursday 15 October 2026Thu 15 Oct 2026

Building A Temperature Conversion Application

Now that we know how to open a simple window, we can finally get to work building a small application: a temperature converter. Not only will we learn how to build the GUI, but we will also learn how to write code that allows us to hotload changes into an open instance of the application.

Previously, in part 03

03

Opening a Window

After deciding to use ClozureCL to develop this first application, we learned how to open (and close) an application with ClozureCL's Cocoa bridge API. Along the way, we learned some of the vagaries of Objective-C and AppKit's API.

Return to part 03
ND-TU-261010 ■ 10 OCT 2026SERIES ■ MAC ■ PART 4 OF 6
01

Section 01 / 08

Now it's getting GUI

Now that we have a working system for making windows and starting an event loop, we want to add stuff to the window. We'll be building a temperature conversion application, so we'll need a place to type a number. To make it easy on ourselves, we'll only have one place to type the number, and then we'll have two buttons: one to convert from Fahrenheit to Celsius, and one for the reverse. They'll change the value of the number in the field where the original temperature was typed.

Understanding how to get a window open was challenging--we had to learn the inner workings of MacOS's GUI framework AppKit and how to use ClozureCL's Cocoa bridge to write Objective-C code in Common Lisp--but now that we understand the basics of working with AppKit, building the actual GUI isn't going to be that challenging.

Building the GUI is going to be fairly similar to building a CRUD application.

  1. We'll display a text input and buttons in a window.
  2. We'll code up a controller that has two actions: f-to-c and c-to-f.
  3. We'll write the code that models the "business logic" of converting temperatures.
  4. We'll wire up the model and controller code with the "views"--GUI widgets.

It'll end up feeling familiar to all my fellow web developers.

One important goal we have, however, is to write our code in a way that makes it hotswappable and interactive just like all Lisp code. We'll first muddle our way through the naive way before we upgrade to the more sophisticated approach. Future native application development will be made much easier after we learn the principles.

02

Section 02 / 08

A Refresher

First, let's just summarize what we covered in the last article:

  1. We learned how to interact with MacOS's GUI framework AppKit via ClozureCL's Cocoa bridge.
  2. We learned some of the details of Objective-C's syntax so we could understand how to write our Lisp code.
  3. We learned why we need an AppKit delegate to close our application and reopen the window in development.

Let's walk through the final code of that project piece by piece.

We begin by loading the Cocoa bridge.

(eval-when (:compile-toplevel :load-toplevel :execute)
;; We get some warnings from the CCL bridge that block compilation on the first try.
;; This blocks those warnings so we don't have to compile the file twice.
(handler-bind ((warning #'muffle-warning))
(require "OBJC-SUPPORT")))

We set up a delegate--an Objective-C object that AppKit asks whether it should quit the application when the window closes, and that we send a message to when we want the window reopened.

;; * Delegate
(defclass app-delegate (ns:ns-object)
((quit-on-close :initarg :quit-on-close
:initform nil
:reader quit-on-close))
(:metaclass ns:+ns-object))
(objc:defmethod (#/applicationShouldTerminateAfterLastWindowClosed: :<BOOL>)
((self app-delegate) app)
(declare (ignore app))
(quit-on-close self))
(objc:defmethod (#/reopenWindow: :void) ((self app-delegate) sender)
(declare (ignore sender))
(make-window))
(defvar *delegate* nil)

We split making a window and opening a window so that we can call make-window in the reopenWindow: method above without calling run again--which starts an event loop.

;; * Window
(defun make-window ()
(let ((window (make-instance 'ns:ns-window
:with-content-rect (ns:make-ns-rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled #$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil)))
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))
(defun open-window ()
(#/sharedApplication ns:ns-application)
(#/setActivationPolicy: #&NSApp #$NSApplicationActivationPolicyRegular)
(setf *delegate* (make-instance 'app-delegate :quit-on-close nil))
(#/setDelegate: #&NSApp *delegate*)
(make-window)
(#/run #&NSApp))

We wrote a function that sends a message on the main thread to the application's delegate that tells it to reopen the window.

(defun reopen-window ()
(#/performSelectorOnMainThread:withObject:waitUntilDone:
*delegate*
(objc:@selector #/reopenWindow:)
nil
t))

And we wrote the entry-point for starting the application.

;; * Open the first time with this
(ccl:process-interrupt ccl::*initial-process* #'open-window)
;; * Reopen with this
;; (reopen-window)

It was a lot to learn right from the beginning--selectors, delegates, main threads and event loops, an Objective-C and AppKit crash course, etc.

03

Section 03 / 08

Widgets

Thankfully, the next step in learning native programming isn't going to be quite as humbling. First, let's make the basic widgets, starting with a button.

Section 3.1

A Button

Making a button is easy thanks to ClozureCL's bridge--it has a convenient ns-button class that does the work of building the button in Objective-C. We just pass an ns-rect with the :with-frame argument. You can safely run this code without blowing up your REPL--you don't need to do it on the main thread.

REPL
(make-instance 'ns:ns-button
:with-frame (ns:make-ns-rect 20 20 125 32))
#<NS-BUTTON <NSButton: 0x10542c8f0> (#x10542C8F0)>

We can make a small alias around ns:make-ns-rect to make future code easier to write.

converter.lisp
(defun rect (x y width height)
(ns:make-ns-rect x y width height))

Then we can rewrite the make-instance call.

REPL
(make-instance 'ns:ns-button
:with-frame (rect 20 20 125 32))

The x and y coordinates refer to a position inside the view that the button will live in--for us, that will be the window's content view. If you're coming from the web, there's a trap here: the origin is the bottom-left corner, and y grows upward. A button at 0 0 sits in the bottom-left corner of the window, not the top-left.

Let's keep it in a variable so we can play with it.

REPL
(defparameter *button*
(make-instance 'ns:ns-button :with-frame (rect 20 20 125 32)))

A new button's title is an ns-string.

REPL
(#/title *button*)
#<NS-STRING "Button" (#xAFF1CB6628A66459)>

Which you can get back as a Lisp string using objc:lisp-string-from-nsstring.

REPL
(objc:lisp-string-from-nsstring (#/title button))
"Button"

...which isn't very helpful. We change it by sending the button the setTitle: message.

REPL
(#/setTitle: *button* #@"F to C")

The #@ in front of the string is important. A Lisp string is not an Objective-C string, and AppKit only understands its own NSString objects. #@"F to C" is ClozureCL's reader macro for writing an NSString constant directly in your Lisp code.

Section 3.2

A Text Field

We also need somewhere to type the temperature. Unsurprisingly, AppKit's text input is called NSTextField, and it's made exactly the same way as the button.

REPL
(make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24))
#<NS-TEXT-FIELD <NSTextField: 0x105445700> (#x105445700)>

Notice the y of 70. Since y grows upward, the text field is going to sit above the button we made at a y of 20.

Section 3.3

Putting Widgets in a Window

Making widgets in the REPL is safe, but we can't see them. A widget only appears on screen after it's added to a window. So let's start with the make-window function from last time, using our new rect function.

converter.lisp
(defun make-window ()
(let ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil)))
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))

Every window comes with a contentView--the view that fills the window below the title bar. Think of it as the <body> of the window. Widgets are views too, and we add them to the content view with addSubview:.

converter.lisp
(defun make-window ()
(let* ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil))
(content (#/contentView window))
(field (make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24))))
(#/addSubview: content field)
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))

Then we add the two buttons side by side under the text field.

converter.lisp
(defun make-window ()
(let* ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil))
(content (#/contentView window))
(field (make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24)))
(f-to-c-button (make-instance 'ns:ns-button :with-frame (rect 20 20 125 32)))
(c-to-f-button (make-instance 'ns:ns-button :with-frame (rect 155 20 125 32))))
(#/setTitle: f-to-c-button #@"F to C")
(#/setTitle: c-to-f-button #@"C to F")
(#/addSubview: content field)
(#/addSubview: content f-to-c-button)
(#/addSubview: content c-to-f-button)
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))

If you still have the application running from last time, close the window, compile the new make-window, and call (reopen-window). Otherwise, compile the whole file and start the application with the process-interrupt line. You should see a small window with a text field and two buttons.

You can type in the field and you can click the buttons. Nothing happens when you click them, of course. We haven't told them what to do yet.

04

Section 04 / 08

Controller

In web programming, if we want an interface to do something, we use a <form>.

<form method="post" id="my-form" action="/do-this">
...
<button>Submit</button>
</form>

When you click the button, it sends a message to the server (a request), which gets routed to the controller at do-this. The controller does something and replies to the message.

The same is true in AppKit. A button holds two things: a target and an action. The target is an object. The action is a selector--the name of a method. When you click the button, AppKit sends the action message to the target. This is called the target-action pattern, and it's used everywhere in AppKit.

So we need an object with two methods. converter is our controller. Just like the delegate, it needs to be an Objective-C object so that AppKit can send it messages. It also needs to know about the text field, because that's where it will read the temperature from and write the converted temperature to.

converter.lisp
(defclass converter (ns:ns-object)
((field :initarg :field
:reader field))
(:metaclass ns:+ns-object))

Just to reiterate, converter will be the target for our buttons, and each button will specify an action--the method that converter should call.

Now we need the two actions. An action method always has the same shape: it takes one argument--the sender, which is the widget that was clicked--and returns nothing, which is :void in Objective-C. Since it takes an argument, the selector's name ends with a :. For now, the actions will just write something into the text field so that we know the buttons are working.

converter.lisp
(objc:defmethod (#/fToC: :void) ((self converter) sender)
(declare (ignore sender))
(#/setStringValue: (field self) #@"F to C"))
(objc:defmethod (#/cToF: :void) ((self converter) sender)
(declare (ignore sender))
(#/setStringValue: (field self) #@"C to F"))

Objective-C's naming convention is camelCase, so our f-to-c and c-to-f actions become fToC: and cToF:. All that's left is the wiring.

In make-window, we make the controller and hand it the text field, then we set the target and action of each button.

converter.lisp
(defun make-window ()
(let* ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil))
(content (#/contentView window))
(field (make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24)))
(f-to-c-button (make-instance 'ns:ns-button :with-frame (rect 20 20 125 32)))
(c-to-f-button (make-instance 'ns:ns-button :with-frame (rect 155 20 125 32)))
(controller (make-instance 'converter :field field)))
(#/setTitle: f-to-c-button #@"F to C")
(#/setTarget: f-to-c-button controller)
(#/setAction: f-to-c-button (objc:@selector #/fToC:))
(#/setTitle: c-to-f-button #@"C to F")
(#/setTarget: c-to-f-button controller)
(#/setAction: c-to-f-button (objc:@selector #/cToF:))
(#/addSubview: content field)
(#/addSubview: content f-to-c-button)
(#/addSubview: content c-to-f-button)
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))

You might wonder why the variable is named controller and not converter. It's because I tried converter first and ClozureCL refused to compile the function.

Close the window, compile, and call (reopen-window) again. Click the buttons and the text field tells you which button you clicked. The view is talking to the controller.

05

Section 05 / 08

Logic

Now for the "business logic". This will just be some simple, relaxing Lisp.

converter.lisp
(defun fahrenheit-to-celsius (fahrenheit)
(* (- fahrenheit 32) 5/9))
(defun celsius-to-fahrenheit (celsius)
(+ (* celsius 9/5) 32))

Because it's just Lisp, we can test it in the REPL without opening a window at all. Water boils at 212 degrees Fahrenheit.

REPL
(fahrenheit-to-celsius 212)
100

And we can go back the other way.

REPL
(celsius-to-fahrenheit 100)
212

Since we wrote the fractions as 5/9 and 9/5, Common Lisp does the math with rational numbers and we never lose any precision. That also means that a temperature that doesn't divide evenly comes back as a fraction.

REPL
(fahrenheit-to-celsius 100)
340/9

That is the correct answer, but nobody wants to read 340/9 on a thermometer. We'll leave the logic precise and round the number only when we display it. format can do that for us.

REPL
(format nil "~,1f" (fahrenheit-to-celsius 100))
"37.8"

Keeping the logic away from the GUI like this is the same separation we make in web applications, and for the same reasons. We can test it, reuse it, and when we later build the UI again with a different toolkit, this code won't change at all.

06

Section 06 / 08

Naive implementation

We have the views, the controller, and the logic. Now we connect them. When a button is clicked, we want to:

  1. Read the number in the text field.
  2. Convert it.
  3. Write the result back into the text field.

Both actions do the same thing with a different conversion, so we'll write one function that takes the conversion as an argument.

converter.lisp
(defun convert-field (field conversion)
(let ((temperature (#/doubleValue field)))
(#/setStringValue: field
(objc:make-nsstring
(format nil "~,1f" (funcall conversion temperature))))))

doubleValue asks the text field to read its text as a number and gives us a Lisp double float. If the text isn't a number at all--it's empty, or somebody typed "hot"--we get 0.0d0 instead of an error. Good enough for now. setStringValue: needs an NSString, and since our string is made at runtime by format, we convert it with objc:make-nsstring.

Then we rewrite the actions to call it.

converter.lisp
(objc:defmethod (#/fToC: :void) ((self converter) sender)
(declare (ignore sender))
(convert-field (field self) #'fahrenheit-to-celsius))
(objc:defmethod (#/cToF: :void) ((self converter) sender)
(declare (ignore sender))
(convert-field (field self) #'celsius-to-fahrenheit))

Compile those three forms, but this time don't close the window. Type 212 into the text field that's already open and click "F to C". It says 100.0. Click "C to F" and it goes back to 212.0.

We redefined two Objective-C methods and the running application picked them up immediately. That's the Lisp experience we want. The same is true of convert-field and the conversion functions: redefine them and the very next click uses the new definition.

So the behavior of our application is already hotswappable. The trouble is everything else.

Section 6.1

The Trouble

Let's say we want to change the title of a button. We change the string in make-window, compile it, and... nothing. The window on the screen was built by the old make-window, and it has no idea that a new one exists. To see the change we have to close the window and call (reopen-window), and whatever we typed in the old window is gone.

That's the "edit, refresh the page" loop of web development. It's fine for three widgets. It won't be fine for a real application where getting back to the screen you were working on takes ten clicks.

What we really want is to reach into the running application from the REPL and change it. Let's imagine we had saved the window in a variable named *window* and tried to give it a title from the REPL.

REPL
(#/setTitle: *window* #@"Temperature")
Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'NSWindow geometry should only be modified on the main thread!'

No debugger, just a dead Lisp process and that message left behind in the *sly-inferior-lisp* buffer. It's the same lesson we learned last time when we tried to open a window: your REPL does not run on the main thread, and AppKit insists that its windows and views are only touched from the main thread. Some messages will kill the process like this one did, and some will just freeze your REPL. Either way, you're restarting Lisp.

We already know one way around this--it's how reopen-window works. We add a method to the delegate and use performSelectorOnMainThread: to send it the message. But that would mean writing a new Objective-C method for every single thing we ever want to do to the running application. ENOUGH WITH THE BUREAUCRACY!

07

Section 07 / 08

Making it Hotswappable With a Main Thread Caller

What we want is to be able to write any Lisp code at all and say, "run this on the main thread".

The problem is that performSelectorOnMainThread: doesn't know what a Lisp function is. It only knows how to send a message to an Objective-C object. So we'll give it what it wants: an Objective-C object whose only job is to hold a Lisp function and to call it when it receives a message.

converter.lisp
(defclass main-thread-caller (ns:ns-object)
((function-to-call :initarg :function-to-call
:reader function-to-call))
(:metaclass ns:+ns-object))
(objc:defmethod (#/callLispFunction: :void) ((self main-thread-caller) sender)
(declare (ignore sender))
(funcall (function-to-call self)))

That's the only Objective-C method we'll need to write for this, ever. Now we write the Lisp function that uses it. It makes a caller that holds our function, asks AppKit to send the caller the callLispFunction: message on the main thread, and waits for it to finish.

converter.lisp
(defun call-on-main-thread (function)
(let ((caller (make-instance 'main-thread-caller
:function-to-call function)))
(#/performSelectorOnMainThread:withObject:waitUntilDone:
caller
(objc:@selector #/callLispFunction:)
nil
t)
(#/release caller)))

The release at the end is new. Objective-C objects aren't garbage collected like Lisp objects are. Each one keeps a count of how many owners it has, and it's freed when the count reaches zero. We made the caller, so we own it, and when we're done with it we need to say so. Since we make a new caller for every call, we'd leak a little memory every time if we didn't.

This already works, but it throws away whatever our function returns. In a REPL we want to ask the running application questions, too. The function we pass and the function that calls it run on different threads, but a closure doesn't care about that. We can wrap our function in a lambda that saves the result in a variable, and return it after the main thread is done.

converter.lisp
(defun call-on-main-thread (function)
(let* ((result nil)
(caller (make-instance 'main-thread-caller
:function-to-call
(lambda ()
(setf result (funcall function))))))
(#/performSelectorOnMainThread:withObject:waitUntilDone:
caller
(objc:@selector #/callLispFunction:)
nil
t)
(#/release caller)
result))

There's one more thing to take care of. If our function signals an error, it signals it on the main thread, in the middle of the application's event loop. I want the error in the REPL where I can debug it. We can do the same trick again: catch the error on the main thread, save it, and signal it again in the thread that asked.

converter.lisp
(defun call-on-main-thread (function)
(let* ((result nil)
(problem nil)
(caller (make-instance 'main-thread-caller
:function-to-call
(lambda ()
(handler-case (setf result (funcall function))
(error (e) (setf problem e)))))))
(#/performSelectorOnMainThread:withObject:waitUntilDone:
caller
(objc:@selector #/callLispFunction:)
nil
t)
(#/release caller)
(when problem
(error problem))
result))

Finally, since we're going to be typing this a lot, a small macro saves us from writing the lambda every time.

converter.lisp
(defmacro on-main-thread (&body body)
`(call-on-main-thread (lambda () ,@body)))

Section 7.1

Holding On To The Window

To poke at the window from the REPL, we need to be able to access it. We'll save it in a global variable when we make it.

converter.lisp
(defvar *window* nil)

There is one catch. By default, a window that isn't managed by a window controller is released when it's closed. Our variable would be left pointing at freed memory, and the next message we sent to it would crash the process. We tell the window not to do that with setReleasedWhenClosed:.

converter.lisp
(defun make-window ()
(let* ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil))
(content (#/contentView window))
(field (make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24)))
(f-to-c-button (make-instance 'ns:ns-button :with-frame (rect 20 20 125 32)))
(c-to-f-button (make-instance 'ns:ns-button :with-frame (rect 155 20 125 32)))
(controller (make-instance 'converter :field field)))
(#/setReleasedWhenClosed: window nil)
(#/setTitle: f-to-c-button #@"F to C")
(#/setTarget: f-to-c-button controller)
(#/setAction: f-to-c-button (objc:@selector #/fToC:))
(#/setTitle: c-to-f-button #@"C to F")
(#/setTarget: c-to-f-button controller)
(#/setAction: c-to-f-button (objc:@selector #/cToF:))
(#/addSubview: content field)
(#/addSubview: content f-to-c-button)
(#/addSubview: content c-to-f-button)
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
(setf *window* window)))

Reopening the window was the one thing we could already do on the main thread, and it took a delegate method and a function to do it. Now it's one line, and we can delete the reopenWindow: method from the delegate.

converter.lisp
(defun reopen-window ()
(on-main-thread (make-window)))

Compile the new definitions, close the old window, and call (reopen-window). We don't even need to restart Lisp to switch over to the new code. Now let's try setting the title again--the same code that killed our Lisp process before.

REPL
(on-main-thread (#/setTitle: *window* #@"Temperature"))
NIL

The title bar changes while the application is running, and the REPL is still alive. We can ask questions, too.

REPL
(on-main-thread (objc:lisp-string-from-nsstring (#/title *window*)))
"Temperature"

And if we make a mistake, we get an ordinary Lisp error in an ordinary Lisp debugger. Abort it and both the REPL and the application carry on as if nothing happened.

REPL
(on-main-thread (error "Too hot!"))

Section 7.2

Hotswapping the Widgets

We can now change anything in the running application from the REPL, but changing things by hand isn't the same as changing the code. We still have the original trouble: when we edit make-window, the open window doesn't change.

Why did the two methods update the moment we compiled them, when the buttons don't? Because AppKit sends fToC: every time the button is clicked, and it looks the method up every time it sends it. Code that gets called over and over is hotswappable for free. make-window gets called once. By the time we compile a new one, the old one has already done its work and nobody is going to call the new one.

So that's the whole problem: the code that builds the window needs to run again, and it needs to do it without throwing the window away.

make-window can't be run again as it is, because it does two jobs. It makes a window, and it makes the content of the window. We split open-window last time for the same kind of reason, so let's split again. First, the buttons are getting repetitive, so they get a function of their own. It takes a Lisp string for the title, so we can finally have our arrows.

converter.lisp
(defun make-button (title frame target action)
(let ((button (make-instance 'ns:ns-button :with-frame frame)))
(#/setTitle: button (objc:make-nsstring title))
(#/setTarget: button target)
(#/setAction: button action)
button))

Then we move the widgets out of make-window and into a function that builds a fresh content view--an NSView, the plain rectangle that all widgets are built on--with everything already in it.

converter.lisp
(defun make-content ()
(let* ((content (make-instance 'ns:ns-view :with-frame (rect 0 0 300 120)))
(field (make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24)))
(controller (make-instance 'converter :field field)))
(#/addSubview: content field)
(#/addSubview: content (make-button "F to C" (rect 20 20 125 32)
controller (objc:@selector #/fToC:)))
(#/addSubview: content (make-button "C to F" (rect 155 20 125 32)
controller (objc:@selector #/cToF:)))
content))

That leaves make-window with only the window to worry about. Instead of adding widgets to the content view the window came with, it replaces the whole content view with ours using setContentView:.

converter.lisp
(defun make-window ()
(let ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil)))
(#/setReleasedWhenClosed: window nil)
(#/setContentView: window (make-content))
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
(setf *window* window)))

If a window's content view can be replaced when the window is made, it can be replaced at any time after that, too--as long as it's done on the main thread.

converter.lisp
(defun refresh ()
(on-main-thread (#/setContentView: *window* (make-content))))

Compile the new definitions, close the window, and call (reopen-window) one last time. Now go to make-content, change the two titles to "F → C" and "C → F", compile the function, and evaluate this in the REPL.

REPL
(refresh)

The buttons change in the open window. The window never closed and the Lisp process never restarted.

Section 7.3

Compiling Is the Refresh

It works, and I don't like it. We compile the function and then we have to go to the REPL and tell the application that we compiled the function. When I redefine a Lisp function, the next call uses it. I don't call (refresh) on my functions. As far as I'm concerned, this is unacceptable. Lisp can do better!

The thing that knows make-content has changed is the form that defines it. So let's have that form do the refreshing. We'll write a macro that defines the function just as defun would, and then, if there's a window open, refreshes it.

converter.lisp
(defmacro define-content (name () &body body)
`(progn
(defun ,name () ,@body)
(when *window*
(refresh))
',name))

The empty () does nothing but make it look like a defun. Now we change one word in make-content.

converter.lisp
(define-content make-content ()
(let* ((content (make-instance 'ns:ns-view :with-frame (rect 0 0 300 120)))
(field (make-instance 'ns:ns-text-field :with-frame (rect 20 70 260 24)))
(controller (make-instance 'converter :field field)))
(#/addSubview: content field)
(#/addSubview: content (make-button "F → C" (rect 20 20 125 32)
controller (objc:@selector #/fToC:)))
(#/addSubview: content (make-button "C → F" (rect 155 20 125 32)
controller (objc:@selector #/cToF:)))
content))

Compile it. Move a button and compile it. Make the text field narrower and compile it. Every time you compile, the open window changes. We never touch the REPL. The first time the file is loaded there is no window, so *window* is nil and the macro defines the function and leaves it at that.

If you've used a frontend framework, this will feel familiar: the content is whatever make-content returns, and we "re-render" by calling it again.

Section 7.4

Keeping What's Already There

It also has the problem that re-rendering has. Type 212 into the text field, then change a button's title and compile. The title changes and the 212 is gone. Of course it is: make-content made a brand new text field and threw the old one away, along with everything in it.

What we really want is a way to update the definition of make-content and then also have the GUI update so that the changes reflect a permanent change rather than a temporary, ad-hoc change.

Lisp already has a form that can help us. defvar, when initially compiled, assigns a value to a variable. If it is recompiled later with a different value, however, it retains the old value. To change the value, you have to change the value via setf. We want something like that for widgets, with one difference: a widget should be made only once, and set up every time its definition is compiled. Making a button is the part that must not happen again. Giving it a frame and a title can happen as often as we like.

So we'll need some global defvar definitions for our widgets. The controller needs one too. And since the text field will now have a name, *field*, the controller doesn't need to be handed the field when it's made, so we can drop the slot. It's cleaner.

converter.lisp
(defclass converter (ns:ns-object)
()
(:metaclass ns:+ns-object))
(defvar *controller* (make-instance 'converter))
converter.lisp
(objc:defmethod (#/fToC: :void) ((self converter) sender)
(declare (ignore sender))
(convert-field *field* #'fahrenheit-to-celsius))
(objc:defmethod (#/cToF: :void) ((self converter) sender)
(declare (ignore sender))
(convert-field *field* #'celsius-to-fahrenheit))

Now the macro. A widget definition has a name, a class, and the code that sets the widget up.

converter.lisp
(defvar *widgets* '())
(defun place-widget (name)
(let ((widget (funcall name))
(content (#/contentView *window*)))
(unless (#/isDescendantOf: widget content)
(#/addSubview: content widget))))
(defmacro define-widget (name class &body setup)
`(progn
(defvar ,name nil)
(defun ,name ()
(unless ,name
(setf ,name (make-instance ',class)))
,@setup
,name)
(pushnew ',name *widgets*)
(when *window*
(on-main-thread (place-widget ',name)))
',name))

define-widget does four things.

  1. It defines a variable for the widget with defvar, so compiling the definition again doesn't lose the widget we already have.
  2. It defines a function with the same name as the variable. In Common Lisp a symbol can name a variable and a function at the same time, and the two don't get in each other's way: *field* is the widget, and (*field*) calls the function. That function makes the widget only if there isn't one yet, then runs the setup code, then returns the widget.
  3. It adds the name to *widgets*, the list of every widget the window should have.
  4. If there's a window open, it calls place-widget right away, on the main thread.

place-widget calls that function by its name and puts the widget in the window if it isn't in there already. The first time, that makes the widget and adds it. Every time after, the widget is already there, and all that happens is the setup.

We won't be calling (*field*) ourselves. place-widget is its only caller. What we use is the definition, which we compile, and the variable, which we can poke at from the REPL with on-main-thread.

Here are our three widgets.

converter.lisp
(define-widget *field* ns:ns-text-field
(#/setFrame: *field* (rect 20 70 260 24)))
(define-widget *f-to-c* ns:ns-button
(#/setFrame: *f-to-c* (rect 20 20 125 32))
(#/setTitle: *f-to-c* (objc:make-nsstring "F → C"))
(#/setTarget: *f-to-c* *controller*)
(#/setAction: *f-to-c* (objc:@selector #/fToC:)))
(define-widget *c-to-f* ns:ns-button
(#/setFrame: *c-to-f* (rect 155 20 125 32))
(#/setTitle: *c-to-f* (objc:make-nsstring "C → F"))
(#/setTarget: *c-to-f* *controller*)
(#/setAction: *c-to-f* (objc:@selector #/cToF:)))

Put them above the two action methods in the file, since they're what declares *field*, and below *controller*, which the buttons use.

make-window goes back to using the content view the window came with, and places every widget on the list. It has to save the window in *window* first, because that's where place-widget looks for it.

converter.lisp
(defun make-window ()
(let ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled
#$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil)))
(#/setReleasedWhenClosed: window nil)
(setf *window* window)
(mapc #'place-widget *widgets*)
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))

make-button, make-content, define-content and refresh have nothing left to do. Delete them. Close the window, compile the file, and call (reopen-window), for what really is the last time.

Now type 212 into the text field and leave the cursor in it. Go to the definition of *f-to-c*, change its title to "To Celsius", and compile that one form. The title changes. The 212 is still there, and the cursor is still blinking in the field. It's the same button it was a moment ago with a new title.

Change the field's frame and compile: it moves, with its text. And adding a widget is just writing a new definition.

converter.lisp
(define-widget *clear* ns:ns-button
(#/setFrame: *clear* (rect 220 95 60 20))
(#/setTitle: *clear* (objc:make-nsstring "Clear")))

Compile it and a third button appears in the open window. It doesn't do anything yet, but you know how to fix that now: write a clear: method on the controller, set the button's target and action, and compile both.

There's a bonus we didn't ask for. Close the window and call (reopen-window). The 212 is still there. The widgets belong to their variables now and not to the window, so a new window gets the same old widgets.

That's hotswapping a native GUI, and it didn't take much code. The principles are these:

  1. Functions or methods called within our program are hotswappable for free.
  2. Keep a reference to the things you want to hotswap.
  3. Split making a thing from setting it up. Make it once. Set it up as often as you like.
  4. Let the definition do the updating, so that compiling is all it takes.
  5. Have one way to run any code on the main thread.
08

Section 08 / 08

Final Code

The final code looks like this:

converter.lisp
(eval-when (:compile-toplevel :load-toplevel :execute)
;; We get some warnings from the CCL bridge that block compilation on the first try.
;; This blocks those warnings so we don't have to compile the file twice.
(handler-bind ((warning #'muffle-warning))
(require "OBJC-SUPPORT")))
;; * Main Thread Caller
(defclass main-thread-caller (ns:ns-object)
((function-to-call :initarg :function-to-call
:reader function-to-call))
(:metaclass ns:+ns-object))
(objc:defmethod (#/callLispFunction: :void) ((self main-thread-caller) sender)
(declare (ignore sender))
(funcall (function-to-call self)))
(defun call-on-main-thread (function)
(let* ((result nil)
(problem nil)
(caller (make-instance 'main-thread-caller
:function-to-call
(lambda ()
(handler-case (setf result (funcall function))
(error (e) (setf problem e)))))))
(#/performSelectorOnMainThread:withObject:waitUntilDone:
caller
(objc:@selector #/callLispFunction:)
nil
t)
(#/release caller)
(when problem
(error problem))
result))
(defmacro on-main-thread (&body body)
`(call-on-main-thread (lambda () ,@body)))
;; * Delegate
(defclass app-delegate (ns:ns-object)
((quit-on-close :initarg :quit-on-close
:initform nil
:reader quit-on-close))
(:metaclass ns:+ns-object))
(objc:defmethod (#/applicationShouldTerminateAfterLastWindowClosed: :<BOOL>)
((self app-delegate) app)
(declare (ignore app))
(quit-on-close self))
(defvar *delegate* nil)
;; * Logic
(defun fahrenheit-to-celsius (fahrenheit)
(* (- fahrenheit 32) 5/9))
(defun celsius-to-fahrenheit (celsius)
(+ (* celsius 9/5) 32))
;; * Window
(defvar *window* nil)
;; * Controller
(defclass converter (ns:ns-object)
()
(:metaclass ns:+ns-object))
(defvar *controller* (make-instance 'converter))
;; * Widgets
(defun rect (x y width height)
(ns:make-ns-rect x y width height))
(defvar *widgets* '())
(defun place-widget (name)
(let ((widget (funcall name))
(content (#/contentView *window*)))
(unless (#/isDescendantOf: widget content)
(#/addSubview: content widget))))
(defmacro define-widget (name class &body setup)
`(progn
(defvar ,name nil)
(defun ,name ()
(unless ,name
(setf ,name (make-instance ',class)))
,@setup
,name)
(pushnew ',name *widgets*)
(when *window*
(on-main-thread (place-widget ',name)))
',name))
(define-widget *field* ns:ns-text-field
(#/setFrame: *field* (rect 20 70 260 24)))
(define-widget *f-to-c* ns:ns-button
(#/setFrame: *f-to-c* (rect 20 20 125 32))
(#/setTitle: *f-to-c* (objc:make-nsstring "F → C"))
(#/setTarget: *f-to-c* *controller*)
(#/setAction: *f-to-c* (objc:@selector #/fToC:)))
(define-widget *c-to-f* ns:ns-button
(#/setFrame: *c-to-f* (rect 155 20 125 32))
(#/setTitle: *c-to-f* (objc:make-nsstring "C → F"))
(#/setTarget: *c-to-f* *controller*)
(#/setAction: *c-to-f* (objc:@selector #/cToF:)))
;; * Actions
(defun convert-field (field conversion)
(let ((temperature (#/doubleValue field)))
(#/setStringValue: field
(objc:make-nsstring
(format nil "~,1f" (funcall conversion temperature))))))
(objc:defmethod (#/fToC: :void) ((self converter) sender)
(declare (ignore sender))
(convert-field *field* #'fahrenheit-to-celsius))
(objc:defmethod (#/cToF: :void) ((self converter) sender)
(declare (ignore sender))
(convert-field *field* #'celsius-to-fahrenheit))
;; * Opening the window
(defun make-window ()
(let ((window (make-instance 'ns:ns-window
:with-content-rect (rect 0 0 300 120)
:style-mask (logior #$NSWindowStyleMaskTitled #$NSWindowStyleMaskClosable)
:backing #$NSBackingStoreBuffered
:defer nil)))
(#/setReleasedWhenClosed: window nil)
(setf *window* window)
(mapc #'place-widget *widgets*)
(#/makeKeyAndOrderFront: window nil)
(#/activateIgnoringOtherApps: #&NSApp t)
window))
(defun open-window ()
(#/sharedApplication ns:ns-application)
(#/setActivationPolicy: #&NSApp #$NSApplicationActivationPolicyRegular)
(setf *delegate* (make-instance 'app-delegate :quit-on-close nil))
(#/setDelegate: #&NSApp *delegate*)
(make-window)
(#/run #&NSApp))
(defun reopen-window ()
(on-main-thread (make-window)))
;; * Open the first time with this
(ccl:process-interrupt ccl::*initial-process* #'open-window)
;; * Reopen with this
;; (reopen-window)
;; * Hotswap a widget by compiling its definition

It's not much of an application, but look at what we get to keep. The logic is plain Lisp that we can test in the REPL. The controller is two tiny methods we can redefine while the application is running. Each widget is a definition we can compile again whenever we like, and the widget in the open window changes without losing what's in it. And on-main-thread lets us reach into the running application from the REPL to inspect it and change it. We'll be using it in every native application we write from here on.

We can also imagine expanding this define-widget macro so that we, the users, don't even need to worry about Objective-C and can just focus on writing Lisp--which is our long term goal. make-button had the beginnings of that (make-button took a plain Lisp string for the title argument, releasing us from the burden of knowing about and using objc:make-nsstring), so it's not much of a stretch to hide the ClozureCL bridge API behind a Lispy one.

Finally we can write a native application in Lisp the Lisp Way using the power of macros.

ALMIGHTY LISP ■ CONTINUOUS OPERATION ■ LIVE IMAGE SYSTEMCOMMON LISP ■ EMACS ■ SLY ■ REPL: CONNECTED

End transmission

Standby...Next: part 5, queued

Building My First Native App

Native application is back. My body is ready. I am putting down the hypermedia and embracing deranged native GUI APIs--beginning with MacOS and Objective-C development (in Almighty Common Lisp, of course).

4 of 6 published

01Why Build Native Instead of Web Applications?IN-OP-26928 ■ 28 Sep02A Gift To My FatherND-CS-261001 ■ 01 Oct03Opening a WindowND-TU-261002 ■ 02 Oct
04Building A Temperature Conversion ApplicationMAC ■ You are here
05Charting Seven Years Of Bible ReadingsQueued
06Discovering A Bug In Local TimeQueued