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.
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 and g-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.)
The Codio menus now provide two modes of running your project:
- Running the gdemo, lightbulb, or paint 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)
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's width and height in the given graphics context, with the lower-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
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).
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.
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 wigdets 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 one Widget.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.
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 clickingat 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 at LineStartMode in the Gctx.MouseDown case of paint_action. Here, the application enters LineEndMode (x, y). We did this for you! Notice that we have stored this initial mouse position in the LineEndMode constructor. At this point the mouse button is currently down.
-
In LineEndMode (x, y), the behavior
in paint_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 to paint.shapes. Also, you need to reset paint.mode back to LineStartMode, so that the user can draw another line.
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 occured. 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;
}
When there's nothing that needs to be previewed, paint.preview should simply be None. However, when the user drags the mouse in LineEndMode, paint.preview will be some appropriate shape (in this case, a Line).
To achieve this, there are several changes to make to paint_action:
- Change Paint.paint_action to update paint.preview appropriately. When the user drags the mouse (generating an event with constructor Gctx.MouseDrag) and is in LineEndMode, you need to set paint.preview to a Line whose two points are the point where the mouse was first clicked (provided in LineEndMode) and the current mouse position (which is the variable p bound on the first line of the function).
-
Change Paint.paint_action to
update paint.preview for mouse releases. When the
user releases the mouse (which is an event with
constructor
Gctx.MouseUp) and is in LineEndMode, you need to remove the line
preview:
paint.preview <- None
You also need to modify the function associated with the Undo button to reset the paint.preview shape to None. In addition, if the paint.mode is LineEndMode, then reset it to LineStartMode. These changes prevent strange behavior when the user starts drawing a line, drags the mouse off the canvas, and then clicks the Undo button.
Finally, amend the Paint.repaint function to draw the preview if needed.
Note that Paint.repaint should draw the previewed item last, since it's the most recent action and thus highest in the z-order. Whenever repaint is called, it should draw all of the shapes already present in the shapes deque, and after that draw the shape stored in paint.preview (if it is not None).
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.)
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
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 while paint.mode = PointMode, a Points 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 the Gctx.MouseDown event. Store this Points value in paint.preview.
-
If a mouse drag (Gctx.MouseDrag) is detected while
in PointMode, you need to update the preview to include
that point as part of the existing
Points value. Since paint.preview
is an option type, you'll need to a pattern match
to get the list of points so far. The following
code might be useful:
let points =
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.
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 drtawing 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 and paint_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)!
Task 5: Checkboxes and Line Thickness
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 new change_listener to the value_controller's list of change listeners.
- get_value returns the value stored by the value_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 the value_controller has stored, with the newly set value as the argument to each.
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_control that takes an initial value returns a value controller storing that value (with no listeners yet). The type of make_control 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_control. Use the type definition and these instructions to figure this out!
We have provided several test cases for make_control 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_controllerThe 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.
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 the get_value function of the bool value_controller to determine whether it should be turned on or not.
- LISTENER LIGHT, which registers the change of lightbulb color via a change_listener added to the bool 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.
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 radiof 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 uncheck 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.
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% 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.