6. Now let's play!

In one sense, we finished “rubifying” our application when we converted its Objective-C code to Ruby. But in another, we still have one more thing to do. Let’s replace those multicolored .tiff images we’ve been drawing with a red circle using NSBezierPath.bezierPathWithOvalInRect. Change your CustomView drawRect method to the following:

CustomView drawRect [ruby]
    def drawRect(rect)
        OSX::NSColor.clearColor.set
        OSX::NSRectFill(frame) 	
        OSX::NSColor.redColor.set
        OSX::NSBezierPath.bezierPathWithOvalInRect(frame).fill
        window.invalidateShadow
    end

Now when you run it, your application’s window should look like this:

That’s nice, but we can do better. Let’s now illustrate one of the very cool characteristics of transparent windows: they can be nonconvex, and even noncontiguous! Instead of drawing one big circle in our window, we will draw four small ones, one at each corner. Try making the change yourself, or paste in the following new version of drawRect:

Another drawRect [ruby]
    def drawRect(rect)
        OSX::NSColor.clearColor.set
        OSX::NSRectFill(frame) 
        OSX::NSColor.redColor.set
        h,w = frame.size.height, frame.size.width
        s = 0.25 * ((h < w) ? h : w)
        [[0,0], [w-s, 0], [0, h-s], [w-s, h-s]].each {|x,y| 
            OSX::NSBezierPath.bezierPathWithOvalInRect([x, y, s, s]).fill
        }
        window.invalidateShadow
    end

Here’s what your application’s window should look like now:

Mouse down on one of the red circles and drag the window around. Then click in any of the transparent areas and see that your clicks go straight through to whatever is beneath the window: as far as the windowing system is concerned, in its transparent regions, your window just isn’t there!

And now for something I hope you’ll really like: let’s animate our window and have it dynamically change shape as its contents move around. How about having the dots move along the edges of the window? OK, replace your CustomView implementation with the following code:

Animated CustomView [ruby]
MAXCOUNT = 100

class CustomView < OSX::NSView
    def awakeFromNib
        @count = 0
        @timer = OSX::NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(1.0/30.0, self, :tick, nil, true)
        setNeedsDisplay(true)
    end
    
    def tick(sender)
        @count += 1
        @count = 0 if @count == MAXCOUNT
        setNeedsDisplay(true)
        window.oc_display
    end
    
    ns_overrides :acceptsFirstMouse_
    def acceptsFirstMouse(event)
        true
    end
    
    ns_overrides :drawRect_
    def drawRect(rect)
        OSX::NSColor.clearColor.set
        OSX::NSRectFill(frame) 
        OSX::NSColor.redColor.set
        h,w = frame.size.height, frame.size.width
        s = 0.25 * ((h < w) ? h : w)	
        dh = (h-s)*@count/MAXCOUNT
        dw = (w-s)*@count/MAXCOUNT 
        [[dw,0], [w-s, dh], [0, h-s-dh], [w-s-dw, h-s]].each {|x,y| 
            OSX::NSBezierPath.bezierPathWithOvalInRect([x, y, s, s]).fill
        }
        window.invalidateShadow
    end
end

If you’ve tried it, you’re probably saying “whoa, that’s really, uh, weird.”

OK, so we can’t all win Apple Design Awards, but with RubyCocoa we can definitely push our applications in interesting new directions—and get there faster and with less code than using Objective-C only.

Be sure to drag the window around a bit by clicking and dragging those moving circles. Notice that once you’ve started dragging the window your mouse doesn’t need to be on a red circle. I don’t know how to change that, but this example has gotten weird enough already, wouldn’t you say?

A few comments on the code, for the curious:

  • We use an NSTimer to run our animation. You’ve seen this before if you’ve read my Ruby Rocks article. It is configured to call tick thirty times a second.
  • The tick function increments a counter and uses setNeedsDisplay to tell the windowing system to redraw the window. We also have to call window.oc_display to regenerate the window’s shape.
  • drawRect uses the counter to move the origins of our circles’ enclosing rectangles parametrically along the edges of the window.
  • We override acceptsFirstMouse so that the window will respond to the first mouse click inside its boundaries. Normally that first mouse click selects the window and then is discarded. But that would mean that our window’s mouseDown function wouldn’t get called until our second click, and those moving circles are hard enough to click on once!

There’s one more thing I want to do with this example. Let’s make the window full-screen. Add the following code to your CustomWindow initWithContentRect_styleMask_backing_defer method:

Full-screen CustomWindow [ruby]
screenFrame = OSX::NSScreen.mainScreen.frame
setFrame_display(screenFrame, true)

Run it. Oops, it’s full-screen, but our view is stuck in the lower left corner. What happened? Open the nib file, MainMenu.nib, and use the size inspector (command-3) to look at the view. Reconfigure it so that the view resizes with the window. To do that, click on the springs and lines until the size inspector window looks like this:

Now save and close the nib file, rebuild, and run your application. Yeah, whoa.

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (0) post