Despite working on this project months ago, I still haven’t finished fully cataloging all of 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.
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 shapefiles
- 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.
- Angled Primitive Types
- MPV Integration
- Statistics Library
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
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
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
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
and the desired dimensions, and from there is able to make as many objects to
draw things as it wants.
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
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 offest 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 verticies. See more
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 the 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
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
- The final
my_graph.init()function should be responsible for essentially all of 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
GraphWidget, both of which will be discussed later.
LineGraphWidget is now a subclass of
GraphWidget, which in turn is
a subclass of
SuperWidget2D. Doing this allows for the graph to create it’s
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
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:
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,
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
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.
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 sub classes off of
SuperWidget2D, which has a pointer to the
Canvas, allowing it to create it’s own
It’s 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
As of now, the graphs in Grail follow a hierarchical relationship of
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.