A simple drawing program

A simple drawing program

Aside from widgets, GtkReactive also adds canvas interactions, specifically handling of mouse clicks and scroll events. We can explore some of these tools by building a simple program for drawing lines.

Let's begin by creating a window with a canvas in it:

using Gtk.ShortNames, GtkReactive, Graphics, Colors

win = Window("Drawing")
c = canvas(UserUnit)       # create a canvas with user-specified coordinates
push!(win, c)

A few concepts from Cairo are important here:

Here we specified UserUnit units for our drawing and mouse-position units; we chose these to be (0,0) for the top left and (1,1) for the bottom right. With this choice, if a user resizes the window by dragging its border, our lines will stay in the same relative position.

We're going to set this up so that a new line is started when the user clicks with the left mouse button; when the user releases the mouse button, the line is finished and added to a list of previously-drawn lines. Consequently, we need a place to store user data. We'll use Signals, so that our Canvas will be notified when there is new material to draw:

const lines = Signal([])   # the list of lines that we'll draw
const newline = Signal([]) # the in-progress line (will be added to list above)

Now, let's make our application respond to mouse-clicks. An important detail about a GtkReactive.Canvas object is that it contains a MouseHandler, accessible with c.mouse; this object contains Reactive.Signal objects for mouse button press/release events, mouse movements, and scrolling:

const drawing = Signal(false)  # this will become true if we're actively dragging

# c.mouse.buttonpress is a `Reactive.Signal` that updates whenever the
# user clicks the mouse inside the canvas. The value of this signal is
# a MouseButton which contains position and other information.

# We're going to define a callback function that runs whenever the
# button is clicked. If we just wanted to print the value of the
# returned button object, we could just say
#     map(println, c.mouse.buttonpress)
# However, here our function is longer than `println`, so
# we're going to use Julia's do-block syntax to define the function:
sigstart = map(c.mouse.buttonpress) do btn
    # This is the beginning of the function body, operating on the argument `btn`
    if btn.button == 1 && btn.modifiers == 0 # is it the left button, and no shift/ctrl/alt keys pressed?
        push!(drawing, true)   # activate dragging
        push!(newline, [btn.position])  # initialize the line with the current position
    end
end

sigstart is also a signal; we won't do anything with it, but we assigned it to a variable to prevent it from being garbage-collected. (We could use GtkReactive.gc_preserve(win, sigstart) if we wanted to keep it alive for at least as long as win is active.)

Once the user clicks the button, drawing holds value true; from that point forward, any movement of the mouse extends the line by an additional vertex:

const dummybutton = MouseButton{UserUnit}()
# See the Reactive.jl documentation for `filterwhen`
sigextend = map(filterwhen(drawing, dummybutton, c.mouse.motion)) do btn
    # while dragging, extend `newline` with the most recent point
    push!(newline, push!(value(newline), btn.position))
end

Notice that we made this conditional on drawing by using filterwhen; dummybutton is just a default value of the same type as c.mouse.motion to provide for filterwhen.

Finally, when the user releases the mouse button, we stop drawing, store newline in lines, and prepare for the next line by starting with an empty newline:

sigend = map(c.mouse.buttonrelease) do btn
    if btn.button == 1
        push!(drawing, false)  # deactivate dragging
        # append our new line to the overall list
        push!(lines, push!(value(lines), value(newline)))
        # For the next click, make sure `newline` starts out empty
        push!(newline, [])
    end
end

At this point, you could already verify that these interactions work by monitoring lines from the command line by clicking, dragging, and releasing.

However, it's much more fun to see it in action. Let's set up a draw method for the canvas, which will be called (1) whenever the window resizes (this is arranged by Gtk.jl), or (2) whenever lines or newline update (because we supply them as arguments to the draw function):

# Because `draw` isn't a one-line function, we again use do-block syntax:
redraw = draw(c, lines, newline) do cnvs, lns, newl  # the function body takes 3 arguments
    fill!(cnvs, colorant"white")   # set the background to white
    set_coordinates(cnvs, BoundingBox(0, 1, 0, 1))  # set coordinates to 0..1 along each axis
    ctx = getgc(cnvs)   # gets the "graphics context" object (see Cairo/Gtk)
    for l in lns
        drawline(ctx, l, colorant"blue")  # draw old lines in blue
    end
    drawline(ctx, newl, colorant"red")    # draw new line in red
end

function drawline(ctx, l, color)
    isempty(l) && return
    p = first(l)
    move_to(ctx, p.x, p.y)
    set_source(ctx, color)
    for i = 2:length(l)
        p = l[i]
        line_to(ctx, p.x, p.y)
    end
    stroke(ctx)
end

A lot of these commands come from Cairo.jl and/or Graphics.jl.

Our application is done! (But don't forget to showall(win).) Here's a picture of me in the middle of a very fancy drawing:

drawing

You can play with the completed application in the examples/ folder.