wiki:CocoaBridge

Cocoa Bridge

As discussed in the section on the OpenMCL FFI, OpenMCL has a very powerful interface to the world of libraries and components that exist outside of the Lisp image. Foremost among these is the Cocoa Bridge, a binding layer to the Mac OS X user interface.

A good introductory Cocoa text is Aaron Hillegass's  Cocoa Programming for Mac OS X.

Elementary Usage

Here's a very simple example of how to create a window and draw to it.

(in-package "CL-USER")

(require "COCOA")

(defclass red-view (ns:ns-view)
  ()
  (:metaclass ns:+ns-object))

(objc:defmethod (#/drawRect: :void) ((self red-view) (rect :<NSR>ect))
  (#/set (#/redColor ns:ns-color))
  (#_NSRectFill (#/bounds self)))

(defun show-red-window ()
  (ccl::with-autorelease-pool
   (let* ((rect (ns:make-ns-rect 0 0 300 300))
	  (w (make-instance 'ns:ns-window
			    :with-content-rect rect
			    :style-mask (logior #$NSTitledWindowMask
					       #$NSClosableWindowMask
					       #$NSMiniaturizableWindowMask)
			    :backing #$NSBackingStoreBuffered
			    :defer t)))
     (#/setTitle: w #@"Red")
     (#/setContentView: w (#/autorelease (make-instance 'red-view)))
     (#/center w)
     (#/orderFront: w nil)
     (#/contentView w))))

Load the file containing these forms, evaluate (show-red-window), and you'll see a red window.

More drawing

The general assumption in Cocoa is that you will do your drawing in your custom view's drawRect: method. It's often nice, though, to be able to draw by evaluating forms at the lisp top-level.

One problem with that, at least in OpenMCL, is that parts of Cocoa are not thread-safe. Fortunately, creating windows on secondary threads is supported. So is drawing to a view, provided that the drawing is bracketed with calls to lockFocusIfCanDraw and unlockFocus.

So, to keep from forgetting to unlockFocus, define this simple macro. (If you forget to unlockFocus, you'll get a spinning beach ball and have to kill the lisp.)

(defmacro with-focused-view (view &body forms)
  `(when (#/lockFocusIfCanDraw ,view)
     (unwind-protect
	  (progn ,@forms)
       (#/unlockFocus ,view)
       (#/flushGraphics (#/currentContext ns:ns-graphics-context))
       (#/flushWindow (#/window ,view)))))

The flushGraphics/flushWindow business makes sure that your immediate drawing will get displayed.

First, create a window. (Notice that show-red-window returns a view instance.)

(setf *v* (show-red-window))

Now, with a view in hand, you can draw.

(with-focused-view *v*
    (let* ((path (#/bezierPath ns:ns-bezier-path)))
      (#/moveToPoint: path (ns:make-ns-point 10 10))
      (#/lineToPoint: path (ns:make-ns-point 100 100))
      (#/stroke path)
      (#/drawAtPoint:withAttributes: #@"hello world"
		  		     (ns:make-ns-point 10 100)
		  		     +null-ptr+)))

Getting a little fancier, we can write code to grab an image from the web and draw it in our view. This time we create the NSImage instance using an Objective-C-style idiom. We could also have written (make-instance 'ns:ns-image :with-contents-of-url url).

Note that you still have to release the NSImage instance, no matter how you create it. (The NSWindow instance created in show-red-window above will be released when the window is closed, so there's no leak there. But that's a special feature of the NSWindow class.)

(defun draw-earth (view)
  (let* ((url (#/URLWithString: ns:ns-url #@"http://nssdc.gsfc.nasa.gov/thumbnail/planetary/earth/apollo17_earth.gif"))
	 (image (#/initWithContentsOfURL: (#/alloc ns:ns-image) url))
	 (alpha (float 1.0 ns:+cgfloat-zero+)))
    (with-focused-view view
      (ns:with-ns-rect (z 0 0 0 0)
	 (#/drawAtPoint:fromRect:operation:fraction:
	  image (ns:make-ns-point 40 40)
	  z
	  #$NSCompositeCopy
	  alpha)))
    (#/release image)))

Do the actual drawing:

(draw-earth *v*)

The drawing that we're doing by evaluating forms at the listener can be looked at as "immediate mode" drawing. This is a fun way to experiment, but if the view is ever told to redisplay itself (e.g., if you minimize and then restore the window), your drawing will be wiped out. You need to arrange for your drawing routines to be called from the view's drawRect: method in order for it to be "permanent."

See also: GradientWindow, EasyGuiCurrencyConverter.