paint

Homework 5: Paint

Computing’s core challenge is how not to make a mess of it.
     — Edsger W. Dijkstra


Assignment Overview

In this assignment, you will use OCaml to build a paint program from scratch. In particular, your task is to extend a GUI toolkit and implement a paint application that uses it.

We will cover the overall design of this GUI toolkit in class. (An overview of this 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:

gui architecture

The supplied code for this project is an extremely primitive GUI toolkit and paint program, comprised of a few files:

  • Gctx (read: 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 three modules—Gctx, Widget, and Eventloop—are the three libraries that form our GUI toolkit. The Gdemo and Lightbulb programs test some of the functions you must add to the Gctx and Widget modules. Finally, Paint is where we implement our paint application.

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 the 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.

Important Note About Running Your Programs In Codio.

The Codio menus now provide two modes of running your project:

  1. Running the gdemo, lightbulb, or paint applications in your browser.
    • First "Build Project" to compile your program. (This also generates the corresponding javascript version of the program.)
    • Then use "gdemo", "paint" or "lightbulb" 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 permit pop-up windows for the GUI applications to work in your browser.
    • NOTE: The "Quit" button in the paint program will not work in the browswer pop-up window.
  2. 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 large software projects with a lot of moving parts.

This project will be significantly easier if you approach it methodically and follow the program design process!

Use the instructions to clarify any confusion or edge cases you encounter. You should read through all of the instructions first to get a big picture idea of what needs to be done. Then, for each task, you should reread the instructions for clarity.

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 make sure that you haven't broken any previous functionality that you have implemented!


Task 0: More Drawing

The first (tiny) part of this assignment is to read over the file gctx.ml and become familiar with graphics drawing before you dive into the Widget library and Paint program. 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.)

gdemo

Understanding gctx—Graphics Context

As you can see in the image above, our paint application 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), and that toolkit communicates with the native graphics layer (OCaml Graphics Module) at the bottom. If our paint application does not directly communicate with OCaml graphics, how does it draw shapes? This is the job of gctx.ml!

gctx.ml provides three crucial components for our paint application:

  1. the gctx type
  2. a simple interface for drawing shapes
  3. a simple interface for working with events (discussed later)
A gctx, or graphics context, stores information about how things are being drawn. Specifically, the provided gctx type stores the coordinates where drawing takes places and the color 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. 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's width and height in the given graphics context, with the lower-left corner situated at the given point.

Remember: paint.ml does not interact directly with OCaml Graphics, but gctx.ml does! 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.


Introducing: The Paint Application

Our finished Paint application is pictured at the top of the page.

Yours looks a little simpler at first. And even worse, you'll have to draw your own dinosaur. (No points for that.)

your paint app

At first, your GUI consists of a few buttons and 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
  • quit the program
Before you expand your paint application, you'll need some more details on how it works.

What are widgets...

As we mentioned above, our paint application exists in layers. We saw that the top layer was able to draw via the simple interface provided by gctx.ml. How, though, does the paint application know what to draw? Or set the color when a button is clicked? Or perform nearly any action? This is the job of widget.ml!

widget.ml provides three crucial components for our paint application:

  1. the widget type
  2. several functions to make useful widgets
    • Such as label, notifier, button, …
  3. several functions to make widgets useful
    • Such as the 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:

  1. how to draw itself (repaint),
  2. how to handle events (handle), and
  3. how big it is (size).
Together, these functions make for a generic, versatile datatype that allows our paint program to offer any arbitrary features we'd like!

Buttons are widgets! The canvas is a widget! The entire paint application is actually just one big widget that consists of other widgets! Whereas gctx.ml provides the application layer the ability to draw, widget.ml provides the application layer actual components to draw.

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.

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, and an end point.

state.shapes 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 drawings are "on top of" older drawings, 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 clicks inside of the canvas using the paint_action function. This funcion 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 version of the code we provide 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 component of the stored line.)

Toolbars and Layout

The rest of the file, 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. To quit, we call the exit function. (The integer argument can be used to indicate that we're exiting with some kind of error—0 means there was no problem.)

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 dinosaur, 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 goes off to the right. Half of the canvas is cut off!

You can solve this problem in a few steps: first, you'll add a new vertical layout, 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

First, 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.

It 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 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.

Notice that our hpair implementation uses the 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 verify that this part works correctly by running a GUI program. (But keep reading before you do that!)

Defining Widget.list_layout

Before you reconfigure paint.ml to use your swanky new vertical pair widget, you should go one better! Why not have horizontal and vertical list layouts so that you can make rows or columns from goups of widgets? To do this, you'll 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 as arguments, and uses the pair layout function to arrange the wigdets in order.

That is, 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 your base case, you can use a trivial spacer, space (0,0), which takes up no space and won't change anything.

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! You must:

  1. Change the color toolbar and mode toolbar in paint.ml to each use a Widget.hlist.
  2. Lay out the toolbars and the canvas using a Widget.vlist so that the canvas is above the undo and quit buttons which are above the color buttons.
      Use the screenshot of our program above as a guide!


Task 2: Improving the Interface for Drawing Lines

The next step to make 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; then you'll add previewing.

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 parameter (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!

Updating Line Drawing: Drag-and-Drop

In the provided paint program, we need to create a Line (c, p1, p) 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; 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 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 shortly to support previewing.
    • 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 dinosaur. 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:

  1. How do we get the current position of the mouse over the paint canvas?
  2. 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 argued event occured—for mouse events (e.g. MouseDown, MouseMove, …) this 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. Your solution requires adding a new component to the state type that keeps track of the shape currently being previewed, if any. A shape option type is a good candidate here since there may be Some preview shape 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 will 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 do this, there are several changes to make to paint_action:

  1. Change Paint.paint_action to manage paint.preview for dragging. When the user drags the mouse (which is an event of type Gctx.MouseDrag) and is in LineEndMode, you need to set paint.preview to some Line with two points: the point where the mouse was first clicked (provided in LineEndMode) and the current mouse position (which is the variable p created on the first line of the function).
  2. Change Paint.paint_action to manage paint.preview for mouse releases. When the user releases the mouse (which is an event of type 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—this prevents 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.

Note that Paint.repaint should draw the previewed item last, since it's the most recent action and thus highest in the z-order. So whenever repaint is called, 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 addtwo functions, draw_point and draw_points. First, update the gctx.mli file and add the functions' signatures.

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.)

Add the following function signatures to the mli for Gctx:

val draw_point : gctx -> position -> unit
val draw_points : gctx -> position list -> unit
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 define a draw_point function in gctx.ml. This is a function that takes in a graphics context and a point, and draws a point in that location in 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 concurrently, so the next step will be using draw_point to write another function which iterates over a list of points and draws each of them inside the given graphics context. Define the function Gctx.draw_points, of type gctx -> position list -> unit. (You may find the function List.iter useful here!)

Adding a Point-Drawing Mode

You now have a function that draws a point and a function that draws list of points in some graphics context. The next job is to hook this into the paint program by adding a mode to distinguish when the user want to draw lines versus points. Add a PointMode constructor to the mode type.

The PointMode constructor doesn't need to carry any data: unlike line drawing (which needs to remember the first point of the line), we can draw a point in a single click, so we don't have to track anything in our mode.

Extending the shape Type

Recall that we are storing the "history" of the canvas in the shapes deque, where each item in the list represents some kind of basic drawing command. You need to make it so that "drawing points" is one of these possible commands.

You also need to allow a user to draw a series of points at once 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. (A command to draw a set of points just requires knowledge of the current color, and the points' locations.)

Modify the shape type in paint.ml to include a constructor Points of Gctx.color * point list. The point list will hold all of the points generated in one mouse drag.

Updating Paint.repaint

Now update repaint so that when it sees Points (c, ps) in shapes, it will appropriately call the Gctx.draw_points function defined earlier and draw each point in ps on the canvas. Don't forget to set the color in the graphics context!

Updating Paint.paint_action

You need to create a Points (c, ps) and add it the "history" (shapes) each time we click while in PointMode, but not if we're in some other mode, like LineStartMode).

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) was detected while paint.mode = PointMode, a Points (c, ps) is created. Associated with this data constructor you need to store the current color c of the graphics context, and create a list of points ps, which at the moment only contains a single point at the current location of the Gctx.MouseDown event. Store this Points in paint.preview.
  • If a mouse drag (Gctx.MouseDrag) was detected while in PointMode, you need to update the preview to include that point as part of the existing Points constructor. Since paint.preview is an option type, you'll need to pattern match it in order 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
      | _ -> []
      end in
    ...
  • Finally, if a mouse release event (Gctx.MouseUp) was detected, the user has stopped dragging the mouse to draw points. In this case, do the following:
    • Extract the list of points from paint.preview, if there is a value present, and then clear the preview.
    • If the list of points is not empty, then insert a Points shape into the deque. (It wouldn't make sense to insert an empty shape into the deque, since it would a waste of space.)

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 above). 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 examples in undo and quit.

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:

  1. Add some functions in Gctx that allow the user to draw points on a canvas.
  2. Add a new mode, which allows you to figure out if you should be drawing points or lines when the user clicks.
  3. Update the type shape, so you can store the color and location of drawn points.
  4. Update Paint.repaint to link the points to be drawn (step 3) with the drawing operation (step 1).
  5. 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.
  6. Add a button to the toolbar so the user can switch to the new mode (from step 2).


Task 4: Drawing Ellipses

For your next task, you must add the ability to draw ellipses via dragging and dropping to your paint program. As with lines, you will need to display a preview of the shape to be drawn as the user drags on the canvas; the shape should only be saved to the canvas 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.

your paint app
This gif was created using an old implementation (hence the different look and extra feature)!

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 into entirely new functionality.

In this task, you will extend the widget library with a checkbox widget. This widget can be toggled between "checked" and "unchecked" on click, and can update the state of the application based on its state. In the case of the paint application, you will use a checkbox to toggle line thickness. After you complete this task, both lines and ellipses should be able to be drawn using thick lines.

Introducing the 'a value_controller

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_listeners, which are functions that are called whenever the value_controller's value is updated. A 'a value_controller is a record of functions, which are:

  • add_change_listener: this function adds a new change_listener to the value_controller's list of change listeners. A change listener is simply a function of type 'a  -> unit.
  • get_value: this function returns the value stored by the value_controller.
  • change_value: this function actually does two things:
    1. It updates the value of the value_controller to the argued value.
    2. It calls all of the change_listeners the value_controller has stored, with the newly set value as the argument to each.
A final note on change listeners:
They are functions (of type 'a  -> unit) that are called whenever the value of a value_controller is updated via its change_value. They wait to run until the value is updated. They listen for changes to the value, and execute when a change occurs.

The type is written 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_control that takes a generic value and returns a value controller. The type of make_control is therefore 'a -> 'a value controller. Given a value, this function returns a record of the form above. Your job when writing this function is to figure out how to implement each of those functions.

Think carefully about what 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

Using the helper function defined above, you will implement a generic checkbox widget. Although you will use the checkbox for thickness, we want the checkbox widget to be a general purpose component that could be used in any GUI application. Therefore, it has 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 the initial string for the label of the checkbox. Along with the checkbox widget itself, this function returns 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 actually store state of the checkbox; instead the value_controller stores this!

Remember, a widget is also a record of functions. A widget can draw itself, handle events, and knows it size. Some of this functionality may depend on or need to interact with the state of the checkbox.

The aesthetics of your checkbox are a matter of preference; simply make sure that your checkbox has a label, and that it clearly displays whether or not it is toggled. For example, our own implementation draws an X within itself when checked.

We have provided several test cases for checkbox for you in widgetTest.ml.

Important Note About the Relationship Between Checkboxes and Buttons.

Checkboxes function similar to buttons, but the addition of a piece of local state that must be maintained and updated between clicks. While understanding how our given button widget works will give some insight into how to complete this task, we strongly advise you not to simply over the code and tweak random things until it "just works". Take the time to understand how the differences in how these widgets are specified will translate to implementation differences between them!

What is lightbulb...

We provided you an adaptation of the lightbulb code presented in lecture ( lightbulb.ml) that uses the checkbox widget you implement for this task. There are two versions of the lightbulb within the given code:

  1. STATE LIGHT: this lightbulb directly uses the get_value function of the bool value_controller to determine whether it should be turned on or not.
  2. LISTENER LIGHT: this lightbulb 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 check (pun intended) if 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 that you get these examples working in order if you feel overwhelmed by both tasks at first.

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, so toggling the thickness checkbox should do the same for the thickness of past shapes!

Task 6: A Cool New Widget!

For this task you should develop a cool new widget that is useful for your paint application. We have two suggestions for this new widget: Sliders or Radio Buttons, described below.

However, these are not the only possible options for your cool new widget! If you have an alternate idea, feel free to run it by us using a private Piazza post. Remember that any suggestions should involve creating a new type of widget, and should be of reasonable difficulty; if you think it would be substantially easier than our suggested widgets, it is probably too easy.

Sliders

Create a slider widget for your widget library, which lets the user select from a range of values, and then use it in your paint application. If you do this, make sure you 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).
  • As with your checkboxes, these sliders must be general-purpose -- you must provide some means 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.
  • For your slider, 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.
  • Once your slider is completed, use it to control some value within your paint program. Our solution uses three sliders to provide a color picker to the user.
  • Our implementation has a different color for each slider, but this is not required; we did this just for fun.
  • Your slider may not break any other features in your paint program or vice versa. In previous semesters, students have added a slider for line thickness for example, but this means it must work in some reasonable way with your (required) thickness checkbox. For example, the thickness level set by the slider might only be applied to shapes when the checkbox is checked. Similarly, pressing the color buttons should not hinder the performance of color sliders.

Radio Buttons

Create a radio button group widget. Radio buttons function like a series of interconnected checkboxes; checking one member of a radio button group should uncheck all other members of the group.

Radio Buttons and Checkboxes.
Although conceptually we compare a group of radio buttons to a group of checkboxes, we advise against actually implementing this way. If you choose to implement radio buttons, think carefully about why using a group of checkboxes might not be a good approach.
  • This 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.
  • As with checkboxes, there should be some visible indicator for which radio button is selected.
  • Our solution uses a radio group of three buttons for controlling paint.mode and displaying which one is selected.
  • You might notice that the given mode buttons in the initial paint program already seem to behave 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.


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 to you 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), you should keep in mind that you should be commenting your code appropriately for this assignment, especially for Task 6.

Submission Instructions

You will need to 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.