The best kittens, technology, and video games blog in the world.

Thursday, May 10, 2007

Gtk DSL for RLisp

“I’m serious, it was this big!” by Kevin Steele from flickr (CC-NC)
I basically agree with Steve Yegge that "w=widget.create; w.set_attribute; c = widget.create; w.add_child(c)" is the most horrible ways of creating UIs. Well, graphical GUI builders generating obfuscated code and printing HTML as raw strings are close in suckiness.

The best I could do were custom XMLs. "Custom" is the point here, it must have application-specific to be of any use, generic toolkit XMLs are too limited. Building GUIs this way still sucked, but considerably less so. Here's a snippet from my old app, something like those single-page-wikis, just local and database-backed:

class PseudoTiddlyBox < Gtk::EventBox
def initialize(title)
@title = title
@links = nil
@text = ""

vbox = $g.parse_subtree("<vbox />")
button_bar_desc = "<text editable='false'><right>"+
" <button cmd='open_all'>Open all</button>"+
" <button cmd='close'>Close</button>"+
@button_bar = $g.parse_subtree(button_bar_desc)
@header = $g.parse_subtree("<text bg='#D0D0D0' editable='false'><huge>#{title}</huge></text>")
@body = $g.parse_subtree("<text editable='false' />")

vbox.pack_start(@button_bar, false, false, 0.0)
vbox.pack_start(@header, false, false, 0.0)
vbox.pack_start(@body, false, false, 0.0)


@button_tags = []
@button_tags.push(tag) if tag.is_button

signal_connect("enter-notify-event") {|w,e|
@button_tags.each{|tag| tag.foreground = 'black'}
signal_connect("leave-notify-event") {|w,e|
@button_tags.each{|tag| tag.foreground = 'white'}
# Attach a link/button controller
@button_bar.attach_link_observer {|w,href| open_link(href)}
@body.attach_link_observer {|w,href| open_link(href)}

@button_bar.attach_tag_observer {|tv,tag,new_state|
tag.background = new_state ? '#F0F0F0' : 'white' if tag.is_button
@body.attach_tag_observer {}

With custom XML it was possible to simplify building structure considerably, but attaching code to widgets was as hard as ever. XML is a horrible representation of code. It's awesome for data export, and markup, and configuration files, and many other things, just not code. The code should definitely be in some real language. Unfortunately in this case most languages fail miserably. What has the advantages of both programming languages and XML ? Lisp S-expressions obviously !

To get something running, I first coded a trivial GUI hello world in RLisp, Java style:
(ruby-require "gtk2")

(let w [Gtk::Window new "RLisp Gtk Hello World"])
[w border_width= 10]

(let v [Gtk::VBox new])
(let l [Gtk::Label new "Hello, World"])
(let b [Gtk::Button new "Quit"])
[v pack_start l]
[v pack_start b]

[b signal_connect "clicked" & (fn ()
(print "Hello, world!")
[Gtk main_quit]

[w add v]
[w show_all]

[Gtk main]

It's so ugly. Even in such trivial application it takes a lot of thinking to imagine what the result will looks like. And here's the same program, using macro-based DSL:
(gtk Window ("RLisp Gtk Hello World")
id => w
border_width => 10
(gtk VBox ()
(gtk Label ("Hello, World"))
(gtk Button ("Quit")
signal "clicked" => (fn ()
(print "Hello, world!")
[Gtk main_quit]))
[w show_all]
[Gtk main]

It feels like Zen. It's instantly obvious what the window will look like and even how it will behave. The difference is almost as huge as going from hand-coded C parsers to regular expressions.

The DSL is very simple, it's just one macro (gtk ...). The first argument is widget class, then constructor arguments, then attributes. id => var means local variable var should be bound to the widget (something I wanted to have in XML-based solution, but it was only possible to use global variables this way). signal sig => handler sets callbacks, property => value sets various attributes, and everything else is considered a child to add. (if (foo) (gtk some-widget) (gtk another-widget)) is a perfectly valid child, but in this DSL optional children must go through the usual "widget.add(child)" road.

Here's the DSL definition. It took just 40 minutes to write, what means that either I'm getting better at macros or it's a pretty simple one:
(ruby-require "gtk2")

(defun gtk-attrs (widget args)
(let tmp (gensym))
(defun parse-args (args)
(match args
('id '=> var . rest)
(cons `(let ,var ,tmp) (parse-args rest))
('signal sig '=> handler . rest)
(cons `[,tmp signal_connect ,sig & ,handler] (parse-args rest))
(prop '=> val . rest)
(cons `[,tmp ,["#{prop}=" to_sym] ,val] (parse-args rest))
(child . rest)
(cons `[,tmp add ,child] (parse-args rest))
(raise "Don't know what to do with: #{args}")))
(let ops (parse-args args))
(let ,tmp ,widget)

(defmacro gtk (widget constr_args . attributes)
(gtk-attrs `[,["Gtk::#{widget}" to_sym] new ,@constr_args] attributes))

No comments: