Homework 5: Paint

Homework 5: Paint
Computing’s core challenge is how not to make a mess of it. — Edsger W. Dijkstra
Resources
Assignment Overview
In this assignment, we will use OCaml to build a paint program, as well as the GUI toolkit that supports it.
Here are some suggested checkpoints for this assignment. It is a long assignment, so try to do a little each day. We also strongly encourage you to read the content of Chapter 18 of the lecture notes, which will also allow you to work ahead if you like.
- Checkpoint 0: Complete Task 0 after Lecture 18 (Gctx and Widgets)
- Checkpoint 1: Complete Task 1 after Lecture 18 (Gctx and Widgets)
- Checkpoint 2: Complete Task 2 after Lecture 19 (Widgets and Layout)
- Checkpoint 3: Complete Task 3 after Lecture 19 (Widgets and Layout)
- Checkpoint 4: Complete Task 4 after Lecture 19 (Widgets and Layout)
- Checkpoint 5: Complete Task 5 after Lecture 20 (Widgets and Events)
- Checkpoint 6: Complete Task 6 after Lecture 20 (Widgets and Events)
We will cover the overall design of this GUI toolkit in class. (An overview of the design is also available in the lecture notes.) As a reminder of how the different parts fit together, here is a diagram of the software architecture:

A more detailed description of the above diagram can be found in the lecture notes. The supplied code for this project is an extremely primitive version of the GUI toolkit and paint program, comprising the following files:
- Gctx (graphics context) module:
gctx.ml
,gctx.mli
- Widget module:
widget.ml
,widget.mli
- Eventloop module:
eventloop.ml
,eventloop.mli
- Deque implementation:
deque.ml
,deque.mli
- Some unit tests:
widgetTest.ml
(uses the Assert module) - Some testing programs:
gdemo.ml
,lightbulb.ml
,pairdemo.ml
- Paint application:
paint.ml
g-native.ml.x
andg-js.ml.x
: wrappers for the OCaml Graphics module (see below)
The modules gctx
, widget
, and eventloop
are the main components of the
GUI toolkit. The gdemo
and lightbulb
programs test some of the functions
you must add to the gctx
and widget
modules. pairdemo
shows how
to use the provided debug widget. Finally, paint
is the paint application itself.
Feel free to use any function in the OCaml standard library in your code. We’ve provided some useful guidelines in this description and in comments in the source code. The documentation for the OCaml standard library is available online. In particular, you will need to read the documentation for the Graphics module. (Have a quick look at it now so that you will know what’s there when you need it later.)
Running Your Project In Codio.
The Codio menus now provide two modes of running your project:
- Running the
gdemo
,lightbulb
,pairdemo
, orpaint
applications in your browser.
- First, use “Build Project” to compile your program. (This also generates the corresponding javascript version of the program.)
- Then select one of “gdemo”, “lightbulb”, “pairdemo”, or “paint” from the View drop-down menu (to the right of “Build Project”) to run the program in your web browser. This command will open a new tab in your web browser.
- You can refresh your tab to re-run the application
- NOTE: You must enable pop-up windows in your browser for the GUI applications to work.
- Running the test cases in widgetTest.ml, with output in the terminal, as usual.
To make these two modes work, the project includes two “wrapper” files,
g-js.ml.x
and g-native.ml.x
, for the OCaml Graphics module. Do not edit
these files. They simply provide the same functions as found in OCaml’s
Graphics library, but suited to either javascript of native execution.
To prevent bugs, we strongly recommend using Chrome for this assignment.
How to Approach this Project
This project is designed to get you comfortable working with larger software projects with many moving parts.
The project will be significantly easier if you approach it methodically!
Use the instructions to clarify any confusion or edge cases you encounter. You should read through all of the instructions (and skim through all the provided code files) first to get the big picture of what needs to be done. Then, for each task, start by rereading the instructions for that task carefully.
Pay close attention to your coding style as you complete this assignment. Things like modularity of design will come naturally if you follow the layout of the tasks listed below and plan ahead. Other stylistic concerns, such as naming conventions and formatting, will require more of an ongoing effort. This assignment has a lot of related components, and you will probably find it helpful to maintain a fair amount of stylistic discipline.
Be sure to test your program after every task to be sure it works (and to make sure that you haven’t broken any previous functionality that you have already implemented!). We’ve put reminders to do this in each of the tasks.
Task 0: More Drawing
The first thing to do is to read over the file gctx.ml
and become familiar
with how to put graphics on the screen. For this problem, you will implement
Gctx.draw_rect
and Gctx.draw_ellipse
. Note: do not change any of the
provided function signatures in this homework.
You will know that these functions work correctly when running gdemo produces roughly the layout pictured below. (It will be missing the red square and blue circle at first.)

Understanding gctx—Graphics Contexts
As you can see in the diagram above, the paint program consists of three main
layers. The application layer at the top (paint.ml
) communicates with the GUI
toolkit layer in the middle (widget.ml
and gctx.ml
), which communicates
with the native graphics layer (OCaml’s Graphics module) at the bottom. So, if
the paint application does not directly communicate with OCaml graphics, how
does it draw shapes? This is the job of gctx.ml
. Your job in this task is
to read through gctx.ml
, make sure you understand what it does, and implement
a couple of missing drawing routines to check your understanding.
The file gctx.ml
provides three crucial components for our paint application:
- the
gctx
type - a simple interface for drawing shapes
- a simple interface for working with events (discussed later)
A gctx, or graphics context, stores information about how things are being
drawn – things like the position and color of the “pen” currently being used
for drawing. All drawing functions are passed a graphics context as an
argument, and they all must call the helper function set_graphics_state
before doing any actual drawing. This function sets the “active” color used by
the OCaml Graphics library to the one stored in the argument gctx. Look at
Gctx.draw_line
for an example of how to do this.
Calling any native drawing method (such as
Graphics.draw_rect)
also requires conversion of the coordinates from the widget-local,
top-left-origin system that we are using for this course to the
bottom-left-origin system that the Graphics library assumes. The
Gctx.ocaml_coords
function will be helpful here. Again, look at
Gctx.draw_line
for an example of how to do this.
Drawing Rectangles
The draw_rect
function takes in a graphics context, a point, and a dimension.
The type of draw_rect
is therefore gctx -> position -> dimension -> unit
.
Given these, it draws a rectangle with the dimensions’ width and height in the
given graphics context, with the upper-left corner situated at the given
point.
Fill in the implementation of draw_rect
in gctx.ml
.
Here
is the documentation for
Graphics.draw_rect. You’ll
need this function to complete the implementation of
Gctx.draw_rect
.
Drawing Ellipses
The Gctx.draw_ellipse
function is similar. Look at the documentation for
Graphics.draw_ellipse
for more help.
This concludes Task 0.
Interlude: Introducing the Paint application
Before proceeding, try running the Paint application and playing with it for a bit to see what the various parts of the user interface do. See the box above for instructions on running graphics programs under codio.
The finished Paint application is pictured at the very top of this page. Yours will look simpler at first: right now, the GUI consists of a few buttons and a “canvas” for drawing.

You can:
- draw lines with two clicks (the first click sets the start point of the line, and the second click sets the end point)
- change the color by clicking a color button
- undo a drawn shape
Before you start expanding the paint application, it will be useful to understand a few things about how it works.
What are widgets…
As mentioned above, the paint application is structured in three layers. The
top layer is the “painting logic.” The bottom layer is the drawing functions
provided by gctx.ml
. In the middle are a collection of “widgets” that simplify
the job of assembling user interfaces. All of the visible components of the user
interface are widgets… Buttons are widgets! The canvas is a widget! Even the
entire paint application is just one big widget that contains lots of other
widgets! As we will see later, some widgets are invisible and only affect the
processing of the user’s actions, which is why a mouse click in one part of the
application (like on the drawing canvas) can do something different than a click
elsewhere (like on a color selection button).
The file widget.ml
provides three crucial components for our paint
application:
- the widget type
- several functions to make useful widgets (label, notifier, button, …)
- several functions to make widgets useful (layout functions, mouseclick_listener, key_listener, …)
…and how do they work?
A widget is a record with three fields, which are all functions. These functions mean that a widget “knows”…
- how to draw itself (repaint),
- how to handle events that happen in its region of the screen (handle), and
- how big it is at any given moment (size).
Together, these functions give us a generic, versatile way of working with widgets.
Using the debug widget
The debug widget is provided to help you understand the flow of
events through widgets, which can be useful for debugging issues with them.
To test it out, run the “pairdemo” application from the Codio menu and hover
over the buttons. You should also read the documentation for the debug widget
in widget.ml
to understand how to use it. You are not required to use the
debug widget in any way to complete this assignment, but it’s there to help
you!

Understanding paint.ml
Below is a brief overview of how paint.ml
works. Again, you should read
through these instructions and the provided files before writing any
code. It will probably also be helpful to refer to these instructions while
looking through the paint.ml
file.
The paint program uses a mutable record (called state
) to store its state.
This record stores the sequence of shapes that the user has drawn
(state.shapes
), the input mode of the paint program (state.mode
), and the
currently selected pen color (state.color
).
The GUI of the paint program starts with three components: the mode_toolbar
,
the color_toolbar
, and the paint_canvas
. These three widgets are laid out
horizontally at the end of paint.ml
(see below).
Shapes
Initially, the paint program can only draw lines. The shape type has only one
constructor, Line
, which stores a color
, a start point p1
, and an end
point p2
.
The state.shapes
field is a shape Deque.deque
which records the sequence
of shapes drawn by the user. Our application requires (1) that shapes be
drawn in order, and (2) that the newest shape at any time be undoable. A
deque is a good data structure for this because it stores elements in the
order they were inserted, and it supports undo via deletion from the tail.
Paint Canvas: Repainting
We do the actual drawing in the repaint function, which is used to update the
paint_canvas
.
Every time the canvas is repainted, we go through the deque of shapes,
drawing each one on the canvas in the order they were added by the user.
(Deque.iter
applies a given command to each element of a deque, in order,
from head to tail.)
Since later shapes are drawn “on top of” older ones, they are sometimes said to be “higher in the z-order” (where z refers to the z-axis of a three-dimensional coordinate system).
Paint Canvas: Event Handling
We handle mouse clicks inside of the canvas using the paint_action
function.
This function is added as an event_listener
to the canvas via its controller,
paint_canvas_controller
. This means that whenever an event (the ones defined
in gctx.ml
) occurs in the canvas, the paint_action
function is called.
In the simple version of the code we provide to you, users draw lines by making two clicks in the canvas, one for each endpoint.
To implement this, we define two drawing modes: LineStartMode
and
LineEndMode
. A user’s first click sets the start point of the line and
switches the mode; the second click sets the end point and switches the mode
back. The handling of MouseDown
events to do this is in paint_action
. We
keep track of the first point by sticking it into the LineEndMode
constructor; after we get the second click, we add the line to state.shapes
;
then we go back to LineStartMode
. (Notice that we use state.color
as the
color field of the stored line.)
Toolbars and Layout
The rest of the file paint.ml
, marked TOOLBARS, sets up the various buttons
and their event handlers.
To undo, we simply remove entries from state.shapes
; this is implemented
in the Paint.undo
function.
The color buttons aren’t like normal buttons: we don’t want to draw a label
on them, but instead we want to draw a colored box. To accomplish this, we
define the widget-producing function Paint.color_button
. Whenever a color
button is clicked, it sets state.color
appropriately. The current color is
also displayed via the color_indicator
. The repaint function of this
colored square always displays itself with the currently selected color.
The last bit of this section just creates the toolbars and sets them up with
Widget.hpair
layouts. Once this is all done, we just need to run the eventloop
on the top-level layout widget to run the program.
But don’t just take our word for it: run it! (And try to draw a camel, if you have the patience…)
Task 1: Improving the Layout
The paint program we’ve designed so far is (we admit) pretty ugly. The first problem is with the layout: we only have Widget.hpair, so everything just marches off to the right of the screen. Half of the painting canvas is cut off!
We can solve this problem in a few steps: first, you’ll add a new vertical
layout widget, Widget.vpair
; then you’ll extend both of the pair layouts so
they can be used with lists of more than two widgets.
Defining Widget.vpair
To begin, fill in the implementation of the function Widget.vpair
so that we
can stack widgets vertically, just as Widget.hpair
allows us to stack them
horizontally.
This function has the same type as Widget.hpair
: widget -> widget -> widget
. Obviously, however, the logic in the implementation of a vertical
pair differs from the horizontal pair! (So… hint: Do NOT copy-and-paste from
hpair
when implementing this function! Think it through from scratch.)
In particular, the coordinate translations in the repaint and handle functions
need to be changed: rather than translating on the x-axis, vpair
must
translate on the y-axis. Similarly, the size function needs to be adjusted:
instead of summing along the x-axis and taking the maximum y-axis value,
vpair
must sum along the y-axis and take the maximum x-axis value.
Our hpair
implementation uses the library functions fst
and snd
. These
useful functions take a tuple of two elements as input and evaluate to either
the first or second element, respectively. For example, fst (3, 2)
yields 3
,
and snd (3, 2)
yields 2
. You may also want to use these.
Once you have completed your implementation, you should verify its correctness
by testing it! We have given you some test cases for a few functions in the
Widget library; these can be run by executing widgetTest
. Note that we can’t
automatically test the repaint function of a widget, so you will have to check
visually that this part works, once we get to writing some code that uses it.
Defining Widget.list_layout
Before we change paint.ml
to use your swanky new vertical pair widget, let’s
improve things a bit so that we can lay out a whole list of widgets either
horizontally or vertically. To do this, define a function Widget.list_layout
with type (widget -> widget -> widget) -> widget list -> widget
.
This higher-order function takes a pair layout function (for example,
hpair
or vpair
) and a list of widgets, and it uses this pair layout
function to arrange the widgets in order. So, for example, list_layout hpair [w1;w2;w3;…;wn]
is the same the as hpair w1 (hpair w2 (… (hpair wn-1 wn) …))
.
You can implement list_layout
as a fold over the list of widgets. (You may
find the function
List.fold_right
useful.) As the base case, you can use a trivial spacer widget, space (0,0)
, which takes up no space on the screen.
Creating Widget.vlist and Widget.hlist
Using Widget.list_layout
, you can now define two functions of type
widget list -> widget
, called Widget.hlist
and Widget.vlist
. Implement
these functions in widget.ml
.
Primitive and Composite methods
In general, there are two ways to implement widgets for this assignment. The first might be called the “primitive method,” where you implement a widget by directly specifying the repaint, handle, and size functions. The second is the “composite method,” where a widget is implemented by combining other widgets together (e.g., as we did for Widget.vpair, Widget.hpair, Widget.vlist and Widget.hlist above).
The point of composite methods is to avoid duplicating code and functionality. As you go through this assignment, take advantage of composite methods to avoid repeating yourself!
Changing the Toolbars to use vlist and hlist
Now you can lay out your paint program much more cleanly! Do the following:
- Change the color toolbar and mode toolbar in
paint.ml
to use oneWidget.hlist
widget each. - Lay out the toolbars and the canvas using a
Widget.vlist
so that the canvas is above the undo button, which is above the color buttons. Use the screenshot of our final version at the top of this page as a guide!
Task 2: Improving the Interface for Drawing Lines
The next step toward making the paint program more usable is adding drag-and-drop drawing of lines. This is the way most drawing editors do it (rather than our clunky initial click-two-endpoints method).
You’ll make this change in two steps. First, you’ll make the application draw lines via dragging and releasing the mouse; second, you’ll add previewing so that the program continuously displays the line being added as the user is drawing it.
One thing to bear in mind as you complete this and later parts is that
many of the OCaml Graphics functions that take width and height
parameters (such as Graphics.draw_rect
) will react very badly
when given negative or zero values for either of these parameters. If
you are having bizarre crashes on this part, make sure you are only
passing positive values to these functions!
Line Drawing with “Drag and Drop” Live Previewing
In the provided paint program, we create a Line and add it to the
“history” (state.shapes deque
) each time the user clicks while in
LineEndMode
(but not if the user is in LineStartMode
). We
implement the necessary logic by pattern matching on event_types
and
paint modes within the Paint.paint_action
function. Before you move
on, read over this function and make sure you understand the logic in
each case.
To add drag-and-drop line drawing, you need to change the way drawing is handled. A “drag” is when the user clicks and holds the mouse button down; a “drop” is when they release the mouse button. The goal is to draw a line by clicking at the start point, holding and dragging the mouse, and dropping at the end point.
Recall that, when we get a click at some point (x, y)
, we arrive at
LineStartMode
in the Gctx.MouseDown
case of paint_action
. Here, the
application changes mode to LineEndMode (x, y)
. Notice that we have stored
this initial mouse position in the LineEndMode
constructor. At this point the
mouse button is currently down.
The necessary changes are pretty straightforward:
- If the current mode is
LineEndMode (x, y)
, the behavior inpaint_action
depends on whether or not the mouse button has been released:- If the mouse button has not been released yet, then the user is dragging
(
Gctx.MouseDrag
). You’ll have to handle this event in the next step to support previewing, but you can ignore it for now. - If the mouse button is released (
Gctx.MouseUp
), then the line should be drawn from the initial position to this point of release. You should add a line from(x, y)
to the mouse’s current coordinates topaint.shapes
. Also, you need to resetpaint.mode
back toLineStartMode
, so that the user can draw another line.
- If the mouse button has not been released yet, then the user is dragging
(
Once you have tested that drag-and-drop works, remove the (now unnecessary)
code associated with the original “two-click” implementation of line drawing.
Specifically, remove the LineEndMode
case of MouseDown
; justify to yourself
why this is no longer needed!
Previewing
Currently, when drawing a line, the user doesn’t see anything until the “drop”. This makes it difficult to draw masterpieces like our dragon. Previewing will draw a “preview” line from the first click’s location to the current mouse position.
If the first click in a line has already been made, then
paint.mode = LineEndMode (x, y)
where the point (x, y)
is the location of
first click. We need the current mouse position so that the preview line can be
drawn from (x, y)
to the current mouse position.
There are two things to consider when implementing preview functionality:
- How do we get the current position of the mouse over the paint canvas?
- How do we draw the preview line?
Notice that the first line of the Paint.paint_action
function defines a
variable p
. This is the position at which the event occurred. For mouse events
(e.g. MouseDown
, MouseMove
, …), it is the position of the mouse.
To draw the preview line, you first need to be able to keep track of it in the
state of the application. You don’t want to add the preview line to
state.shapes
, since the line shouldn’t be permanent yet. Instead, you need to draw
a different preview line every time the mouse moves. The solution requires
adding a new field to the state record type that keeps track of the shape
currently being previewed, if any. A shape option
is a good candidate here,
since there may be Some
preview shape being drawn or None
at any point in
time.
Extend the state
type definition in paint.ml to include the following:
type state = {
...
mutable preview : shape option;
}
Task 3: Drawing Points
With lines done, let’s implement points. This task is a little more complex than the one above.
Here are the steps to implement drawing points.
Updating the Interface Files
You will add two functions, draw_point
and draw_points
. First, update the
gctx.mli
file and add the functions’ signatures.
val draw_point : gctx -> position -> unit
val draw_points : gctx -> position list -> unit
You may remember that an mli file acts as an interface constraint for an
ml
file, so other files can only access values and functions which are
explicitly added to a module’s interface. (Think back to homework 3, which
used a set interface to constrain which values defined in listset.ml
and
treeset.ml
could be seen from outside the module.)
For most of the later functions and values we ask you to implement (as well as
any you write yourself), you will need to add the function or value’s type
signature to the appropriate mli
file.
Implementing Gctx.draw_point
First add a draw_point
function to gctx.ml
. This is a function that takes in
a graphics context and the coordinates of a point and draws a point in that
location using the context. The type is therefore gctx -> position -> unit
.
(To make things a bit simpler, points will always be one pixel in size.)
Your draw_point
function should be similar to the functions in Task 0. Use
Graphics.plot
to draw the point on the screen.
Implementing Gctx.draw_points
You also need to support drawing multiple points at the same time, so the next
step will be using draw_point
to write another function that iterates over a
list of points and draws each of them using the given graphics context. Add a
function Gctx.draw_points
, of type gctx -> position list -> unit
. (You may
find the function
List.iter
useful.)
Adding a Point-Drawing Mode
We now have low-level functions that draw points and lists of points on the screen. Your next job is to hook these into the paint program by adding a “point drawing mode” to the user interface.
First, add a PointMode
constructor to the mode type. The PointMode
constructor doesn’t need to carry any data, since, unlike line drawing (which
needs to remember the first point of the line), we can draw a point with a
single click.
Extending the shape Type
Recall that the shapes
deque stores the “history” of the canvas. Each item in the
list represents some kind of basic drawing command. Here, we will extend the type
definition of shape
so that “draw some points” is one of these possible commands.
Specifically, modify the shape type in paint.ml
to include a new constructor:
Points of { color: Gctx.color; points: point list }
The point
list called points
will hold all of the points generated in one
mouse drag. We use a list because we want to allow a user to draw a series of
points by dragging their mouse with the button down. After the user releases
the mouse button, we add all of the points that the user drew to the deque of
shapes. (It’s better to add all the points at once, when the mouse button goes
up, rather than adding each point as a separate shape every time the mouse
moves with the button down, because this will interact better with Undo
. Do
you see why?)
Updating Paint.repaint
Now update repaint
so that when it sees Points ps
in shapes, it will
appropriately call the Gctx.draw_points
function you defined earlier and draw
each point in ps.points
on the canvas. Don’t forget to set the color in the
graphics context!
Updating Paint.paint_action
Next you’ll need to create a Points
shape and add it to the “history” (shapes)
each time we click (or click and drag) while in PointMode
(but, of course, not
if we’re in some other mode like LineStartMode
).
As discussed above, because the user of your application should be to be able to
drag the mouse to draw several points at once, you can’t just add every new
point you encounter to the shapes deque. Instead, while the user has not yet let
go of the mouse button, you can use the paint.preview
feature to store all the
points they’ve drawn on the canvas so far.
Update paint_action
as follows:
- If a click (
Gctx.MouseDown
) is detected whilepaint.mode = PointMode
, aPoints
record is created. Associated with this data constructor you need to store the current color (color
) of the graphics context, plus a list of points (points
), which at the moment only contains a single point at the current location of theGctx.MouseDown
event. Store thisPoints
value inpaint.preview
. - If a mouse drag (
Gctx.MouseDrag
) is detected while inPointMode
, you need to update the preview to include that point as part of the existingPoints
value. Sincepaint.preview
is anoption
type, you’ll need a pattern match to get the list of points so far. The following code might be useful:
let points_list =
begin match paint.preview with
| Some (Points ps) -> ps.points
| _ -> []
end in ...
- Finally, if a mouse release event (
Gctx.MouseUp
) is detected, this means the user has stopped dragging the mouse to draw points. At this point, you should do the following:- Extract the list of points from
paint.preview
. - Clear the preview.
- Insert all the points as a
Points
shape into the history deque.
- Extract the list of points from
Adding Buttons to the Toolbar
The final step is to add buttons to the toolbar that allow toggling between
Point
and Line
modes.
You will need to create two buttons: one for Line
s and another for Point
s
(see the reference image at the top of the page). When the user clicks on the
Line
button, it will set the current mode to LineStartMode
; when the user
clicks on the Point
button, it will set the current mode to PointMode
. You
can use mouseclick_listeners
to set up these actions, following the example of
undo.
Once the buttons have been added and you can switch modes, you should be able to draw points when running your application. Make sure to give it a try!
To recap, here are the steps you took to implement drawing points:
- Add some functions in
Gctx
that allow the user to draw points on a canvas. - Add a new mode, which allows you to figure out if you should be drawing points or lines when the user clicks.
- Update the type
shape
, so you can store the color and location of drawn points. - Update
Paint.repaint
to link the points to be drawn (step 3) with the drawing operation (step 1). - Update
Paint.paint_action
so when the user clicks in the new mode (step 2), it stores the desired point by creating a new shape item (step 3) and adding it to the deque of shapes. - Add buttons to the toolbar so the user can switch between line and point drawing modes.
Task 4: Drawing Ellipses
For your next task, you will add the ability to draw ellipses via dragging and dropping. As with lines, you will need to display a preview of the shape as the user drags on the canvas; the shape should only be saved to the history when the user releases the mouse.
You’ll need to make a number of changes to get this working, following a similar pattern to the one we outlined for point drawing:
- Add appropriate constructors to the shape and mode types.
- Change the
repaint
andpaint_action
functions to support the added shapes and modes. - Add a button to the toolbar to let the user draw ellipses.
There are different ways to draw ellipses. We’d like you to implement ellipse drawing using a “bounding box” method. The starting and ending points of the mouse drag are treated as two opposite corners of a rectangle, with the ellipse drawn so that it fits exactly inside of this rectangle. The bounding box itself should not be drawn, only the ellipse itself. Reference the GIF below for an illustration of how this works.

Note that this image was created using an old implementation (hence the different look and extra feature)! You don’t need to implement anything other than what the current instructions specify, so don’t worry if your implementation looks different from the above.
draw_ellipse
takes in the center of the ellipse as its position argument.
You’ll need to do a little math to calculate the midpoint between the saved
mouse position when the button was pressed and the current position of the
mouse.
Task 5: Checkboxes and Line Thickness
The first tasks were meant to familiarize you with the project structure. This one will require you to synthesize what you have learned to create entirely new functionality.
In this task, we will extend the widget library with checkbox widgets. These widgets toggle between “checked” and “unchecked” modes when they are clicked, and they update the state of the application when this happens. Then you will use a checkbox to allow the paint application to toggle between thin and thick pens for drawing lines and ellipses.
Warning: Task 5 is often considered to be more difficult than tasks 0-4.
The 'a value_controller
type
A 'a value_controller
is an object that stores a value and allows you to
interact with it. It also stores a list of change_listener
objects, which are
functions (of type 'a -> unit
) that will be called whenever the value stored
in the value_controller
is updated.
You can think of a change_listener
as being similar to the event_listener
s
that are attached to buttons. Event listeners are fired whenever events like
mouse clicks happen. Similarly, change_listeners
are called when the value
within a 'a value_controller
changes.
A 'a value_controller
is an object with three methods:
add_change_listener
adds a newchange_listener
to thevalue_controller
’s list of change listeners.get_value
returns the value stored by thevalue_controller
.change_value
does two things when called:- it updates the value of the
value_controller
to the provided value, and - it calls all of the
change_listeners
thevalue_controller
has stored, with the newly set value as the argument to each.
- it updates the value of the
The type is written in OCaml as:
type 'a value_controller =
{ add_change_listener : ('a -> unit) -> unit;
get_value : unit -> 'a;
change_value : 'a -> unit }
Making a 'a value_controller
Your first goal in task 5 is to implement a generic helper function
make_controller
that takes an initial value and returns a value controller
storing that value (with no listeners yet). The type of make_controller
is
'a -> 'a value controller
.
Think carefully about what internal state needs to be stored with each
instance of a value_controller
returned by make_controller
. Use the type
definition and these instructions to figure this out!
We have provided several test cases for make_controller
for you in
widgetTest.ml
.
Checkbox: Another Widget
The next goal is to implement a generic checkbox widget. Although we will want to use the checkbox specifically for controlling thickness below, we want the checkbox widget to be a general purpose component that could be used in any GUI application. Therefore, it should have the following type signature:
val checkbox : bool -> string -> widget * bool value_controller
The first argument indicates the initial state of the checkbox (i.e., should it
start checked or not?), and the second argument is string label to be displayed
next to the checkbox. The checkbox function returns two things: the checkbox
widget itself and a bool value_controller
.
The bool value_controller
associated with a checkbox is what actually keeps
track of whether or not the checkbox is checked. The widget portion of the
tuple returned by checkbox doesn’t have any internal mutable state of its own.
The aesthetics of your checkbox – how it gets drawn on the screen – are up to you. Simply make sure that your checkbox has a label, and that it clearly indicates whether or not it is toggled. For example, our solution draws the checkbox as a black rectangle with (when it is checked) an X inside.
For the checkbox test cases in widgetTest.ml
to pass, the entire checkbox
widget has to be clickable. (i.e. if the checkbox has a text component that
says ‘Thickness’, the word ‘Thickness’ has to be clickable as well). Note
that clicking the checkbox should not affect how points are drawn – only
ellipses and lines.
Checkboxes vs Buttons. Checkboxes are somewhat similar to buttons, but they have local state that must be maintained and updated between clicks, whereas buttons simply hand off events to listeners. So, although understanding the button widget in code we’ve provided will give you some useful insight into how to complete this task, we strongly advise you not to simply copy over the button code and randomly tweak things in hopes that it will eventually work. Take the time first to understand how the differences in desired behavior should translate to differences between the the two implementations!
Two ways of implementing widgets. As mentioned above, in general, there are two ways to implement widgets for this assignment: the “primitive method” and the “composite method”. We strongly encourage you to implement the checkbox using the composite method.
What is lightbulb…
To help you debug your checkbox implementation, we have provided some test cases
for checkbox in widgetTest.ml
.
We’ve also provided an adaptation of the lightbulb code presented in lecture
(lightbulb.ml
) that uses the checkbox widget. There are actually two versions
of the lightbulb:
STATE LIGHT
, which uses theget_value
function of thebool value_controller
to determine whether it should be turned on or not.LISTENER LIGHT
, which registers the change of lightbulb color via achange_listener
added to thebool value_controller
.
…and how does it help?
These two small examples will help you verify that your checkbox implementation
works. The STATE LIGHT
version tests whether or not you are toggling the
state correctly within your checkbox. The LISTENER LIGHT
version tests that
you implemented the change listener functionality properly. We advise you to
get these examples and the tests in widgetTest.ml
completely working before
going on to the next part of the assignment.
Using a Checkbox: Another Widget and More State
Once you have successfully implemented the checkbox function, put a checkbox
into your paint program and configure it so that it can be used to toggle line
thickness. This task requires that you modify the paint program such that it
knows whether it should be drawing thin or thick lines at any point in
time. You can control the thickness of figures drawn by the OCaml Graphics
library through the through the
Graphics.set_line_width
function. Remember that you can use the functions defined in the 'a value_controller
type to add change listeners to or get the status of the
checkbox on which the thickness toggle will be based.
Graphics.set_line_width
must be passed a positive integer for Graphics to
work properly. Passing in 0 or a negative number may result in unintended
consequences!
Handling line thickness: You’ll want to consider how the paint program we provide handles information about colors and implement line thickness in a similar fashion. Note that shapes added when the checkbox is not checked should always be drawn thin, and shapes added when the checkbox is checked should always be drawn thick. Changing the current color doesn’t change the color of past shapes, and similarly toggling the thickness checkbox should not change the thickness of past shapes!
Task 6: A Cool New Widget!
In this final task you will develop a cool new widget of your choice and use it in your paint application.
Below, we offer two concrete possibilities for this new widget: Sliders and Radio Buttons. But these are not the only options! If you have an alternate idea, feel free to run it by us using a private Ed post. (If you choose this route, please remember that your proposal should involve (a) creating a new type of widget and (b) using it in your paint application, and (c) it should be of reasonable difficulty. If you think what you have in mind would be substantially easier than the ones we are suggesting, it is probably too easy.)
Sliders
You could create a slider widget for your widget library, which lets the user select from a range of values by displaying a bar whose length is proportional to the currently selected value and allowing the user to click-and-drag to change the length of the bar; then use it in your paint application.
If you choose this option, please note the following:
-
You should start by making a widget for a single slider that which lets the user select from a range of values by dragging (it’s okay if clicking changes the value as well, but the slider must be draggable).
You may choose to either have all sliders have some fixed range of values, or to have the slider take its max value as an argument when it is constructed.
-
As with your checkboxes, these sliders must be general-purpose. In particular, you must provide a way for a user to add listeners that are called when the slider is manipulated and that perform some action based on the slider’s current value.
-
Once your slider widget is completed, the next step is to use it to control some value within your paint program. For example, you might use it to control the thickness of lines. Or you could use three sliders to offer a simple color picker to the user. (We have done this in our solution, pictured at the top of this page. Our implementation also draws each slider in a different color, but this is not required; we did it just for fun.)
-
Your slider must not break any other features in your paint program. For example, if you use a slider for line thickness, it must work in some reasonable way with the (required) thickness checkbox. E.g., the thickness level set by the slider might only be applied to shapes when the “Thick Lines” checkbox is checked, or clicking the checkbox might also set the thickness shown by the slider. Note that only the thickness of shapes, not points, should be affected if you choose to implement a thickness slider. Similarly, if you use sliders to choose colors, then pressing the old color buttons should interact in a reasonable way with your new color sliders.
Radio Buttons
Or you could create a radio button group widget. A group of radio buttons looks and functions like a row of interconnected checkboxes: checking one member of a radio button group should _un_check all the other members of the group, maintaining the invariant that exactly one box is checked at any given time.
-
Your widget must be general-purpose, in that it could be used in an application other than your paint program. This means you need to have some interface for users to attach change listeners to the radio buttons.
You might notice that the given mode buttons in the initial paint program already behave a bit like radio buttons. While this is loosely true, the goal of this extra feature is to create a reusable, generic widget for creating any arbitrary group of radio buttons to control any arbitrary value(s)! In particular, any logic that has to do with the radio buttons themselves should be with the widget code.
-
As with checkboxes, there should be some visible indication of which radio button is selected.
Radio Buttons vs. Checkboxes Although conceptually a group of radio buttons might appear to be roughly similar to a group of checkboxes, we advise against actually organizing your implementation this way. (Why might using a group of checkboxes not be a good approach??) Better to think about the desired radio-button functionality from scratch.
OCaml StyleChecker
Before submitting to Gradescope, you should run the stylechecker in Codio to make sure you have no style violations.
- In your project, select “Style Check Project” from the dropdown menu at the top of the screen where “Build Project” lives.
- Each style violation will appear with its corresponding file, line number, and a suggestion on how to fix it. If you have no violations, “No style violations” will be printed.
- An assignment with “No style violations” in Codio will receive 10/10 pts for Autograded Style Score in Gradescope.
If you are ever unsure about OCaml style, check out the CIS 1200 OCaml Programming Style Guide.
Submission and Grading
Grading
Although some components of the widget library are automatically graded, it is notoriously difficult to automatically test an entire user interface. Furthermore, we want to give you the freedom in Task 6 to implement whatever you like. We will therefore also be testing most of your homework by hand. You can submit your homework as many times as you like, but we will only look at your last submission.
The grade breakdown is as follows:
- (26%) Autograded Score
- Task 1 (better layout): 6 points autograded
- Task 5 (checkboxes): 10 points autograded
- Autograded Style: 10 points autograded
- (74%) Manually Graded Score
- Task 0 (drawing practice): 4 points manually assigned
- Task 1 (better layout): 4 points manually assigned
- Task 2 (drawing lines): 15 points manually assigned
- Task 3 (drawing points): 8 points manually assigned
- Task 4 (drawing ellipses): 13 points manually assigned
- Task 5 (checkboxes): 10 points manually assigned
- Task 6 (something cool): 20 points manually assigned
A quick note on style: while earlier assignments have not had too much emphasis on commenting (since we already gave you specifications for most of the code), please keep in mind that you should be commenting your code appropriately for this assignment, especially for Task 6. While your comments themselves won’t be graded, you should still be commenting as a general rule as you move into more complex assignments or codebases in the future.
Submission Instructions
You will submit the following 5 files for this assignment:
- gctx.ml, gctx.mli
- widget.ml, widget.mli
- paint.ml
As always, you can simply use the Zip for Submission button in the dropdown menu in Codio where you build and run your project to generate the appropriate, submission-ready zip file.
Do not edit any other files! We won’t see your changes, and the testers may not work.
We will use the versions of Deque and Eventloop provided to you in the assignment download for testing, so there’s no need to submit those.