
Homework 5: Paint
Computing’s core challenge is how not to make a mess of it.
— Edsger W. Dijkstra
Resources
- Homework files
- Online documentation
- Video Overview
- CIS 120 OCaml programming style guide
- HW 5 FAQ’s
Assignment Overview
In this assignment, we will use OCaml to build a paint program, as well as the GUI toolkit that supports it.
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:

The supplied code for this project is an extremely primitive version of the GUI toolkit and paint program, comprising a few 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
- Paint application:
paint.ml
g-native.ml.x
andg-js.ml.x
: wrappers for the 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. Finally, paint
is the paint application itself.
Feel free to use any function in the OCaml standard library in your solution. 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 Programs In Codio.
The Codio menus now provide two modes of running your project:
- Running the
gdemo
,lightbulb
, orpaint
applications in your browser.
- First, use “Build Project” to compile your program. (This also generates the corresponding javascript version of the program.)
- Then use one of “gdemo”, “lightbulb”, “notifierdemo”, 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 for the GUI applications to work in your browser.
- 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.
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, you should start by rereading the instructions carefully.
Pay careful 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 rewarding 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 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 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 earlier in these instructions, 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.
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 argued gctx. Look at Gctx.draw_line
for an example of how to do this.
Note that calling any native drawing method (such as Graphics.draw_rect) also requires conversion of the coordinates from our widget-local top-left-origin system 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 to use 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 around 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. And even worse, you’ll have to draw your own camel. No points for that. :-)

At this stage, the GUI consists of a few buttons and a “canvas” for drawing, but it’s quite minimal. 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 some things about how it works.
What are widgets…
As we mentioned above, our 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 make for a generic, versatile datatype that allows our paint program to offer any arbitrary features we’d like!
Understanding paint
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 be helpful to read 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 order and 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 originally drawn 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 “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 with 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. This handling of MouseDown
events is written 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 drawings, 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, 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! (Try to draw a camel, if you have the patience…)
Task 1: Better Layout
Suffice it to say, the paint program we’ve designed so far is pretty ugly. The first problem is with the layout: we only have Widget.hpair, so everything just marches off to the right. 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 you 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 implementatin of a vertical pair differs from the horizontal pair! (Hint: Do NOT copy-and-paste from hpair
for this function!)
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
.
Once you have completed your implementation, you can 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 correctly when 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) …))
. Given this behavior, you can implement list_layout
as a fold over the list of widgets. (You may find the function List.fold_right useful here!) 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
.
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 allow users to create lines (rather than our 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 in a width and height parameters (such as Graphics.draw_rect
) will react very badly when given a negative or zero value 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 need to 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 implemented 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; a “drop” is when the user releases 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.
The necessary logic changes are straightforward:
- Recall that when we get a click at some point
(x, y)
, we arrive atLineStartMode
in theGctx.MouseDown
case ofpaint_action
. Here, the application entersLineEndMode (x, y)
. We did this for you! Notice that we have stored this initial mouse position in theLineEndMode
constructor. At this point the mouse button is currently down. - In
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, this is 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 a masterpiece like our camel. 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. 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. To make things simple, points will always be one pixel in size. 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
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
.
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 here!)
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
Next, recall that we store the “history” of the canvas in the shapes deque, where each item in the list represents some kind of basic drawing command. You will need to extend the definition of shapes so that “draw some points” is one of these possible commands.
You will also need 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, you need to 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?)
Modify the shape type in paint.ml
to include a 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.
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
so the following takes place:
- 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 to 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 Lines
and another for Points
(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 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, we 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.
This gif was created using an old implementation (hence the different look and extra feature)!
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
Task 5 is often considered to be more difficult than tasks 0-4. 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.
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 an event_listener
that is attached to buttons. Event listeners are fired whenever events like mouse clicks happen. change_listeners
, on the other hand, 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 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 use the helper function defined above 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 indicated 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 testcases 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)
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). While 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 desried behavior should translate to differences between the the two implementations!
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
tests whether or not you are toggling the state correctly within your checkbox. The LISTENER LIGHT
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 bit.
Using a Checkbox: Another Widget and More State
Once you have successfully implemented the checkbox function, you will need to 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 non-zero 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 drawn when the checkbox is not checked should always be thin, and shapes drawn when the checkbox is checked should always be 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 or 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 Piazza 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 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 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 (as we have done 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 this 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. 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. 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 action 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.
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 thus 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. (Note that the submission system will not run any tests; it will only verify that your code compiles. We will, however, run the test cases provided in widgetTest.ml
when we grade your program and use that as part of your score.)
The grade breakdown is as follows:
- 50% Tasks 0 through 4, including:
- Task 0 (drawing practice): 4 points manually assigned
- Task 1 (better layout): 6 points autograded, 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
- 20% Task 5 (checkboxes): 10 points autograded, 10 points manually assigned
- 20% Task 6 (something cool): 20 points manually assigned
- 10% autograded programming style
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.
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 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 file! We won’t see your changes, and the testers may not work. In particular, 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.