Author's Note 02/13/22

Despite working on this project months ago, I still haven't finished fully cataloging all the things I've worked on related to it. It's entirely possible things I comment on here have changed, or something better has superseded the code I've written. If you have any questions about sections that aren't filled in, feel free to bug me and I can answer questions.

General Info

Grail is a research project led by Professor Dov Kruger from Stevens Institute of Technology (more about him here and here), and is best described in his own words from one of the project's README files:

Grail is designed to be the "holy grail" of user interfaces. A single unified GUI that would work across platforms, that is both a web front end and a GUI on a local machine. Why should the two be different after all?

Grail is written in C and C++, and prioritizes speed as well as having a simple API to the user. The main tech used in the project as of now includes:

  • glfw and glad for making a window and loading OpenGL
  • glm for OpenGL math (matrices and vectors)
  • Freetype for all things related to font rendering
  • shapelib for working with ESRI shape files
  • MPV for cross-platform audio and video.

The repository for Grail can is here on GitHub. It is worth noting that this particular repository focuses on the GUI components of Grail and some of the network code, and doesn't include any previous code related to a browser engine.

My Contributions

Table of Contents

SuperWidget2D

Before I joined Grail there was a Widget2D class, which had access to a StyledMultiShape2D (an object that can draw multiple shapes of different colors) and a MultiText (an object that can draw arbitrary text of a particular style at arbitrary coordinates) where the widget could draw whatever components of itself were needed. The primary limitation of a Widget2D is that it can't create it's own StyledMultiShape2Ds or MultiTexts if it wants them, essentially restricting the widget to only one type and color of font and one thickness of lines.

In working with graphs (which I'll go more into later) I wanted to be able to customize individual components of the graph, such as having different font styles for the titles/labels, and different line thicknesses for other bits. As of now, the best solution to do something like this is to have separate StyledMultiShape2D and MultiText objects for each thing I wanted to customize. Asking the user to create and pass these things is not a great design pattern, so the solution was to have graphs able to manage these things themselves.

A widget doesn't have access to the Canvas (an object that holds all of the created Grail primitives such as shapes, text objects, etc), so the SuperWidget2D was born. A SuperWidget2D is passed a pointer to the Canvas and the desired dimensions, and from there is able to make as many objects to draw things as it wants.

Angled Primitive Types

One massive limitation of the MultiText was its inability to draw non-horizontal text, so some sort of solution was needed. What ended up being the quickest solution was having the user optionally pass an angle, x offset, and y offset to a MultiText and using these to generate a matrix that would be multiplied by the projection of the parent Canvas when the MultiText was rendered. What all of that means is that the user can pass their desired parameters to the object, and then when they draw something with it will end up rotated at the specified angle (in radians) and placed at the (x, y) offset specified, assuming the user told the drawing to occur at (0, 0).

While it seems odd to need to specify that the drawing occur at (0, 0), the primary reason is because of how the transformation interacts with the projection. When just the rotational transform is applied to the projection, the coordinate space is rotated at the angle specified, and moving positively in the x and y directions no longer acts the same. As an example, specifying a rotation of 45 degrees counter clock wise would lead to going down in the y direction (which in the case of OpenGL is considered positive) to be moving towards the bottom right-hand corner of the screen.

This interaction makes it incredibly hard to position drawings in this rotated coordinate space in the position where we want them to be in non-transformed space. By pre-placing things within the transform using the x and y offset parameters, drawings in rotated space and up being placed in the location we expect them to be as if we were drawing them in non-rotated space.

StyledMultiShape2D was similarly limited as with MultiText, so the same approach was taken to allow for rotating a group of shapes at the same angle.

As of now, there is an open issue actively being worked on by Dov to try and remove the need for the x and y offset parameters in a MultiText. I believe the current method for accomplishing this is performing a transform on the points before they are pushed back into a vector of vertices. See more here.

Graphs

LineGraphWidget

Line graphs were the first graph that I worked with, as they're relatively simple and one of the most widely used graphs. Initially everything a line graph needed was programmed into it, setting and drawing the title, axes, etc. The design of the API was as follows:

  • The constructor takes almost entirely optional parameters so that the user can create a graph with just pointers to a StyledMultiShape2D and a MultiText. If the user wants they can fill in every value of the massive constructor.
  • The various components of the graph such as the title, x and y data sets, colors for things, etc., are set with my_graph.setThing(thing_type thing). The expectation is that the user calls all of these setter functions before calling a final init().
  • The final my_graph.init() function should be responsible for essentially all the drawing of the graph. Other functions should really only be responsible for setting / processing inputs.

Many of the functions and fields of a line graph could easily be abstracted out to more general classes, which led to the creation of the AxisWidget and GraphWidget, both of which will be discussed later.

Currently, LineGraphWidget is now a subclass of GraphWidget, which in turn is a subclass of SuperWidget2D. Doing this allows for the graph to create its own StyledMultiShape2D and MultiText objects and allow the user to customize as many individual components of the graph as possible. The specifics of GraphWidget, and the abstractions and simplifications it has led to will be discussed later.

AxisWidget

AxisWidgets were designed to be a standalone object that could be used on their own, or as something that could be integrated into other objects (mainly the graphs). There's an interface AxisWidget class, which three children subclass off of: LinearAxisWidget, LogAxisWidget, and TextAxisWidget. The interface isn't purely virtual, and has definitions for a number of common setter functions between each of the different axis types.

The interface defines three virtual functions, setBounds, setTickInterval, and setTickLabels. Depending on whether an axis should be calculating its tick labels from a function, or whether they should be supplied by the user, these functions will be made private or overridden by the subclasses.

For example, the TextAxisWidget has no reason to be setting its bounds or a tick interval, and only needs to be given a list of labels. As such, TextAxisWidget overrides the setBounds and setTickInterval functions to be private to itself and empty. If the user ever tries to call either of these functions, they should get yelled at by the compiler. If by some miracle the compiler allows private functions to be called outside the class, then nothing will actually be run as the functions have been overridden as empty.

GraphWidget

While I worked on LineGraphWidget, another team member worked on creating additional graph types, which had a much different API compared to LineGraphWidget, and did lots of copy and pasting of similar code instead of trying to extract things out into a uniform interface. This is where GraphWidget comes in.

GraphWidget subclasses off of SuperWidget2D, which has a pointer to the Canvas, allowing it to create its own StyledMultiShape2Ds and MultiTexts. Its primary goal was to take the functionality contained within LineGraphWidget that was applicable to theoretically all graph types, and make a general class to subclass off of. Many of the setters from the line graph were brought out, as well as functions to dynamically create the axis objects based on what kind of graph the final product was supposed to be (line graphs shouldn't have a text axis, etc.).

The creation of a GraphWidget has a massive constructor, which gets hidden somewhat with default arguments, but would likely be better suited to using a builder pattern, especially because it would make the setter process much less verbose. There would no longer be a need to do my_graph.setThing() for every setter, and they could instead be strung together similarly to how Rust works with iterators: my_graph.setThing1().setThing2().

As of now, the graphs in Grail follow a hierarchical relationship of inheritance, where LineGraphWidget "is-a" GraphWidget. I come from a more ECS / game development world, where objects / entities follow a "has-a" relationship, and are a sum of their components. I didn't realize it until I started looking into inheritance debates, but doing objects in an ECS style manner is very much a viable option, and often considered better than doing non-interface inheritance. I would like to change GraphWidgets to follow an approach more like this, but that will likely be a massive overhaul.

Prior Graph Conversions

Under Construction

Images

Under Construction

MPV Integration

Under Construction

Audio

Under Construction

Video

Under Construction

Statistics Library

Under Construction