Sparrow WebGL Devlog 20: TreeView UI Element
A new TreeView UI element, Scrollbar changes, and a 2D object inspector
13.05.2024 - 18:06After updating all major 2D features of my WebGL engine in the last devlog, I wanted to create a new editor menu that shows all existing 2D objects. However, unlike other lists in the editor, 2D objects can have parent-child relationships, which makes a linear list impractical. Instead, I needed a new UI element to represent this recursive data structure: A TreeView element.
A TreeView element consists of a list of nodes. When you click on a node, it expands and reveals its children below it - often with some indentation to indicate the depth. The best example of this is folders and files in a file system - Windows uses a TreeView element on the left side of the explorer.
The TreeView element was more complicated than other UI elements and required major changes to existing features, but ultimately, I’m happy with my implementation.
TreeView Element
Before starting with the TreeView element, I struggled to come up with a good solution that would work with my existing UI systems. I had a few ideas, but all of them had some obvious downsides along with potentially unseen consequences. Sometimes it’s hard to start implementing something when you aren’t completely confident about your approach, but most of the time a suboptimal solution is better than no solution, so after a day of indecisiveness, I sat down and started to work.
One of the questions I struggled with was whether to have an internal data structure in the TreeView element or attach the necessary variables to the data structure it’s displaying. While this is easily possible in JavaScript, it felt like bad code design, so I implemented another recursive data structure inside of the TreeView element class.
The UI elements themselves are already in a recursive data structure since they can be in parent-child relationships. However, I didn't use this existing structure because it would have been poorly optimized. The whole point of a TreeView element is that most nodes are hidden most of the time. If I had used the existing UI elements as the data structure, it would have meant creating all of the UI resources including WebGL buffers and textures for all nodes, most of them would never be expanded and seen.
Instead, I created a completely separate data structure with a simple node class that only stores a name and an object reference. Each node also has a UI element, but it starts as null and only gets initialized when the node is expanded. Right now, it stays allocated when the node is hidden again, but removing the UI elements of hidden nodes could be another optimization in the future.
The remaining question was how to position and render the nodes depending on their expansion state. I'm very happy with my solution to this problem: All node UI elements are direct children of the main TreeView element independent of their depth in the tree. Additionally, the TreeView element has a list of active nodes, which are all currently visible nodes. When a node is clicked, the active nodes are cleared and repopulated with a simple depth first tree traversal. This automatically puts the UI elements in the correct order and they only have to be positioned based on their active node list index and tree depth.
With the basic functionality done, it was time for some polishing. TreeView elements often have little triangles in front of each node that indicate whether the node has children and whether it's expanded or not. Since JavaScript and my WebGL text rendering support Unicode, I just prefixed the name of the node with Unicode triangle symbols: A right-facing one if the node has children, a down-facing one if it’s expanded, and a simple dash if it has no children.
Finally, I added a hover quad that’s rendered behind the nodes to indicate which node the mouse is currently over.
Scrollbar Changes
However, the biggest challenge was yet to come: Putting the TreeView element inside of a scrollable container. I don't know whether it's just me, but scrollbars always cause the most trouble when implementing anything UI-related. The first test looked promising: The TreeView element was scrollable and the nodes were clickable and expandable. However, every node click caused the scrollbar to scroll back to the top, which felt very unintuitive. I tried a few different solutions, but ultimately, I realized that I had to rework how my UI system handles scrolling from the ground up.
Previously, the scrollbar changed the positions of the elements directly. When adding a new element into an already scrolled area (like the TreeView element does when expanding a node), this caused problems because there was no easy way to figure out the default position of the new element. Instead, I decided to split the position of UI elements into two parts: A normal position and a scroll offset.
This meant changing the fundamental way how UI element positions are calculated and I had to go through all existing UI classes and make sure they worked with the new system. While doing this, I also took the opportunity and changed how the UI scaling is applied because it was a bit inconsistent before.
With the changes to the scrollbar behavior, the TreeView element now works correctly inside scrollable containers:
Object Inspector
After implementing the TreeView element and refactoring the complete positioning system of all UI elements, I was finally ready to add the object inspector. Luckily, this was very easy with all of the preparations. However, there’s one big caveat: The TreeView element doesn't update automatically and requires the user to manually hit the refresh button. Tracking changes somewhere inside of a complex tree and syncing them to the TreeView element seems quite complicated and I think the refresh button is good enough for now, especially since it's only an editor feature.
I also added a checkbox toggle that can be used to include engine-internal objects in the list. It's not necessary for normal engine use, but I'm sure it will be useful for me when developing and debugging the engine itself.
Finally, I added a new window that opens when you click on an object. It shows the object's transformations and allows you to change its position, rotation, and scale. Right now, it only works with the spinboxes, but I want to add little widgets in the future that can be used to move, rotate, and scale the object directly in the editor.
Overall, I'm very happy with the new TreeView element. It works like you would expect and looks decent too. And even though redoing the fundamental positioning system of all UI elements was a pain, the new version with an explicit per-element scroll offset is much better in my opinion.
by Christian - 13.05.2024 - 18:06
Comments
Comments are disabled