Druid Postmortem
Hello! I want to share some thoughts about Druid. It started as a small pet project, but over time, Druid’s approach to GUI in Defold has gained popularity. I’ve received a lot of feedback, issues, questions, and suggestions. I’m thrilled to see how Druid is being used in different projects and how it helps make GUI in Defold easier and clearer.
However, Druid still lacks tutorials, especially video tutorials. I hope that with new examples in the future it will become more accessible, and more users will learn it and share their experiences and knowledge with others.
This was almost my first open-source project that I followed from the very beginning. But since the project is widely used, I can’t freely refactor it as I’d like. I have to maintain backward compatibility to avoid breaking existing projects. Right now, I have several ideas that would need to be approached differently.
One of the most frustrating aspects for me was the annotations. When the project started, there weren’t widely adopted standards for annotations, so Druid used LDoc (or LuaDoc) annotations to generate an external API site. Compared to current code annotation practices, LDoc is much harder to read and maintain. I hope to refactor all the annotations in the future to align with the Lua Language Server. The biggest benefit of this change will be easier navigation within the editor, such as using “Go to definition” and “Find all references.” My advice: use the Lua Language Server annotations. They are much easier to read, maintain, and automatically assist with code navigation and validation. Now I annotate code like in this example Panthera file: https://github.com/Insality/panthera/blob/main/panthera/panthera.lua.
Another controversial feature is Druid Styles. They were designed to adjust inner component configurations like scroll speed, animations, and other settings. However, I’ve received a lot of questions about them, and I now believe there are better ways to customize components. Most of the time, I end up overriding the styles directly where they are created, which isn’t perfect, but it gives more control and can be integrated into custom components (https://github.com/Insality/druid/blob/master/example/examples/basic/button/basic_button_hold.lua#L22-L24). This current solution is not very memory-efficient, but in most cases—especially for games, not large UI applications—it’s not a big issue.
About all Druid events: I’m still unsure about the components’ event parameters, especially the self
parameter. Each component’s events say they pass the self
parameter as the first argument, but it’s not always clear which self
. For example, if I attach logic to a hover event in a Button component:
self.button = self.druid:new_button("button", self.on_click)
self.button.hover.on_hover:subscribe(self.on_hover)
---Here self is the button component, not the script. The "self" context of hover is the button component.
---@param self druid.button
function M.on_hover(self, is_hover)
if is_hover then
print("Hovered!")
end
end
I now think the self
context should always be passed as an explicit argument, and callbacks should be simpler, containing only necessary arguments. For example, the on_hover
event should look like this, but making this change would be breaking, so I’m holding off for now:
-- Pass self as an explicit argument
self.button.hover.on_hover:subscribe(self.on_hover, self)
---@param self current_script
---@param is_hover boolean
function M.on_hover(self, is_hover)
if is_hover then
print("Hovered!")
end
end
About custom components. Another small point is how custom components are created. The current solution (in Druid 1.0
, it has become slightly better due the self:get_druid(template, nodes)
intead of directly self:set_template()
and set:set_nodes()
) looks like this:
local component = require("druid.component")
---@class my_amazing_component: druid.base_component
local M = component.create("my_amazing_component")
function M:init(template, nodes)
self.druid = self:get_druid(template, nodes)
-- Now we can use Druid as usual
self.button = self.druid:new_button("button", self.on_click)
end
return M
What I’d prefer to have this approach instead:
---@class my_amazing_component: druid.component
local M = {}
function M:init()
self.button = self.druid:new_button("button", self.on_click)
end
return M
This approach would require refactoring Druid’s base code to manage more on the Druid side, but it would also come with breaking changes. The Druid Editor Scripts, which help create custom components from the GUI scene, make it easier to handle this boilerplate code, but it’s still not perfect. You can read more about editor scripts here: https://github.com/Insality/druid/blob/master/docs_md/02-creating_custom_components.md#create-druid-component-editor-script.
In previous versions, I used a SCHEME
table with a list of required nodes in the component, but over time, it became inconvenient. The current solution is to find all the required nodes in the init
function, like this:
function M:init()
self.button = self:get_node("button") -- instead of self:get_node(SCHEME.BUTTON) like before
self.icon = self:get_node("icon")
self.panel = self:get_node("panel")
-- etc
end
The SCHEME
table has been removed from all examples and custom components. These improvements reduce the amount of code and lines needed to write and maintain.
When developing a GUI with Druid, most of the time you’ll need to pass bindings between components. For example, if a node’s size changes, you may need to update or call functions in related components. For example, we have the scroll:bind_grid(grid)
function to easily manage scroll node size. But I’m currently considering whether Druid components could handle these bindings automatically, using an inner node_property_changed
event bus that reacts in a more declarative way. This is just an idea for now, but it would involve breaking changes and require some time to test.
Another area I care about is performance and memory consumption. When you spawn a component, a bunch of events and data are created, even if you don’t need them. For example, the button component creates events for click, double-click, etc. It’s not a lot of memory (~60 bytes per event), but ideally, these events would only be created if needed.
Well, these are just some ideas and thoughts. I hope if you’re interested in or using Druid, you found this interesting! Have a nice day, and take care!