An example of the GUI in use. |
There are plenty of existing GUIs -- why write my own?
Two technical reasons:
- To have a rendering back-end which is compatible with my game.
- Limiting dependencies to SDL and OpenGL, which are widely supported.
For me, the most important reason, and not a technical one, is that whenever I look at GUI APIs my brain wants to jump out of my skull. Mind you, my own mess here might have the same effect if I were to encounter it afresh. Maybe let me know what your outside perspective is?
Oh, oh, Gooey -- or is it OO GUI?
I once believed the standard claim that OOP is natural for GUIs -- the one thing OOP might really be suited to. My initial take on a GUI in OCaml was based on objects: defining widget classes and layering complexity with inheritance. There were some issues... composition of functionality rarely works well with objects because they're wedded to internal state, and that is frustrating. But there was another pitfall lurking in the shadows. Initially a lot of details were hard-coded: font, colors, borders, etc. The day came when I wanted to control these details... A default font for everything... maybe setting another font for some group of things, and overriding that in specific cases... well ain't this a mess!
Update: Leo White, in the comments, has clued me in to using OCaml objects with the mixin technique -- which seems to offer similar composability and inheritance, but retaining the power of the type-system. I'll make a future post comparing the approaches. An early impression/summary: nearly identical expressiveness, while the differences are similar to the trade-offs between static and dynamic typing.
I'm Destined to Use Databases for Everything
(to atone for disparaging remarks about DB programmers in my past)It didn't take too long before I realized that I want properties: arbitrarily aggregating and inheritable properties... a.k.a. components. (The component system I'm using is described here.) Well then, what if all of my "widgets" were just IDs with associated components? What would it look like?
Here's the "default font" I wanted...
let normal_font = new_id () |> Font.s fnt |> Fontgradient.s `Crisp
Now "normal_font" is something to inherit from: a convenient bundle of properties. If I set the Font to something else during runtime, everything which inherits and doesn't otherwise override Font will get the change.
I'll set a bunch of global defaults called "gui_base"...
let gui_base = new_id () |> Anchor.s Vec2.null |> Color.s (0.5,0.5,0.5) |> Shape.s rounded_rectangle |> Gradient.s `FramedUnderlay |> Underlay.s Default |> Border.s 6.
So far, these properties are just data -- mostly related to rendering options.
Next we'll use inheritance, and add an event handler. This sets up properties for a particular class of buttons:
let hover_button = new_child_of [ gui_base ] |> Fontsize.s 18. |> Fontcolor.s (1.,0.95,0.9) |> Shadow.s (`Shadow,black,0.5,1.5,2.) |> Event.s [ Event.Child (hover_fade (0.8,0.7,0.5)) ]
Now anything inheriting from this, and placed on-screen, will respond to pointer "hover" with an animated color-fade. Event handlers aren't masked, so I usually keep them near "leaf" nodes of inheritance. As a silly test, I made a right-click menu for color-changing in gui_base... so everything could have it's color changed. It worked, if a bit heavy handed. Still, something like that could be useful to create a configuration mode.
You might realize by now that there are no specific button, label, or textbox widgets. Any GUI element is defined by its cumulative properties. In practice, there are convenient "widgets" which I build functions for, like this color_button:
let color_button color name = new_child_of [ hover_button; normal_font ] |> Color.s color |> Text.s_string name
I'm using components with multiple inheritance, and this function creates a button by inheriting both hover_button and normal_font, as well as adding a given color and name.
Well this looks promising to me. Properties, via components, provide a reasonable way to build GUI elements, ample support for hierarchical styles, and there's no need for optional parameters on every creation function -- a free-form chain of components serves as the optional parameters for any/all creation. For example, I can use the "color_button" above, but override the font, almost like a labeled parameter:
color_button red "LAUNCH" |> Font.s dangerfont
Furthermore, when new components are created to implement features... there's no need to update functions with new parameters. I've often been running competing components in parallel, until one is deprecated. Purely modular bliss.
Model-View-Controller... or something else?
GUIs which separate declaration, control, and behavior all over the place drive me nuts. This would be the typical implementation of a Model-View-Controller pattern which is so beloved.
With MVC, and the numerous other GUI-pattern varieties (MVVM, MVP, etc), the principle notion is separation of concerns. Each particular pattern identifies different concerns and separations. I'm usually rather keen on separation of concerns -- modular, composable, understandable, no stepping on toes. I find with GUIs that I desire tighter coupling. An extreme in this regard is ImGUI, which goes too far for my usual preferences -- with it, view is stateless and coupled to execution-flow.
What I want, is to declare everything in a stream. We can break things down into sub-parts, as usual with programming. What I don't want, are code and declarations scattered in different places, files, or even languages... held together by message-spaghetti. Using the color_button again, here's a larger GUI object:
let framed_colors = let blue = color_button (0.1,0.1,1.0) "Blue" in let red = color_button (1.0,0.0,0.0) "Red" in let green = color_button (0.1,0.9,0.1) "Green" in let yellow = color_button (0.9,0.9,0.0) "Yellow" in let black = color_button (0.0,0.0,0.0) "Black" in new_child_of [ gui_base ] |> Pos.s (Vec2.make 40. 20.) |> Dim.s (Vec2.make 240. 120.) |> Pad.(s (frame (Layout.gap 4))) |> Border.s 12. |> Layout.(s (vspread [ hcenter (Id blue); hbox [ I green; G (gap 2); I yellow; G (fill 1); I black ]; hcenter (Id red) ])) |> Event.s [ Event.Child (drag_size RIGHT); Event.Child (drag_move LEFT) ]
Layout uses a "boxes and glue" approach. |
All in one, an object is declared with nested elements. It has a defined (yet flexible) layout, and responds to move and resize operations; the buttons have their own event handlers. The event handlers (as I'll get into in another post) are just functions. The point is that the composition of this is as for any typical code: functions. No out-of-band stuff... no layout in some other file keyed on string-names, no messages/signals/slots. There are events, but event-generation is from outside (SDL), so the only part we're concerned with is right here -- the event handlers.
I guess what I'm trying to minimize, is spooky-action-at-a-distance. To jump ahead a bit, when I call a menu function, it returns a result based on the user's selection (pausing this execution path while awaiting user response, via coroutines), rather than changing a state variable or firing off a message. Functions returning values.
Parts of a GUI
- Renderer
- Description/Layout
- Event Handling
I'll go into more depth in future posts, leaving off with just a note about each...
The renderer is a thin layer leveraging the game's renderer. It mows over the GUI scenegraph, rendering relevant components it encounters. It's entirely possible for the renderer to have alternate interpretations of the data -- and, in fact, I do this: to render for "picking". Effectively I render the "Event" view, including pass-through or "transparent" events. Some details about render layers and stenciling will be their own topic.
Declaration of elements leverages "components" as in the examples given above. For layout, I've always had a fondness for TeX's boxes and glue -- I ride Knuth's coat-tails in matters of typesetting. A hint of this is in the Layout component for the box of color_buttons. I'll probably cover layout or line-splitting in another post.
Event Handling. One of my favorite parts, and a separate post: GUI Event Handling with a Functional Hierarchical State Machine.
There's something missing still...
4. The UI Code
What I mean is the flow of code which triggers GUI element creation, awaits user responses, and does things with them. This could be done as a state machine, but I prefer keeping the flow continuous (as if the user always has an immediate answer), using coroutines. This is yet another topic for another day.
I haven't made a complex UI, so there might be a point beyond which I have to rely on messages -- really, I keep anticipating this, but it hasn't happened... yet.
Did you consider using a mixin style approach (like the examples in https://realworldocaml.org/v1/en/html/classes.html#mixins)? It's pretty similar to components.
ReplyDeleteThank-you for pointing that out. I hadn't considered mixins. The examples certainly look nice, and I'm tempted to get sidetracked into making comparable examples to see what the differences are.
DeleteIf you do I'd be interested to see the results. (full disclosure -- I wrote that section of Real World OCaml). I've enjoyed your blog posts so far, keep up the good work.
DeleteThat was an interesting experiment! I was trying to mimic your shapes.ml, and it was very easy -- like translating a difference in surface syntax of the same underlying language. I've updated this post with a brief note... I'll have to write another going into more detail. Any code combining/building a GUI element is essentially the same, except forward-pipe versus "inherit". Using member values is simpler and faster than modifying component values. And retaining the power of the type-system is a big plus -- with components, if I require a particular component to exist and it doesn't, this only shows up at runtime.
DeleteI'm curious what differences arise with scale, or if it's as easy to mimic some of my existing UI code. I'm probably too invested to change course, but I'll keep the technique in mind for other uses, and continue experimenting.
Thanks for the tip, Leo! That's a well made chapter. A variety of features are built up and then combined to make an impressive example. It was fun making a parallel version, though mine is upside down. OpenGL really makes it a slicker demonstration -- there should be Core-GL! ;)
I look forward to a follow-up post. I think your "early impression/summary" in the note is probably a good way of thinking about components vs. mixins: they are basically the same approach with one typed dynamically and the other typed statically.
DeleteThis comment has been removed by the author.
ReplyDelete