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 StyledMultiShape2D
s or MultiText
s 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 aMultiText
. 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 finalinit()
. - 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
AxisWidget
s 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 StyledMultiShape2D
s and MultiText
s. 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 GraphWidget
s 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