Hello!
I have recently been working on something interesting, a sort of SwiftUI but for Playdate! I guess the best name being: PlaydateUI. It's extremely early still and there isn't much code I'd like to share yet. But I can share snippets of how it's used as well as explain a bit of it and keep track of progress in this thread (if this is the correct place).
One of the first problems that lead to building this was simply making the graphics api easier to work with, especially layout. This was a rather easy problem to solve and relies on exposing Yoga to Lua. Yoga is a quick flex box implementation essentially allowing you to build up a view hierarchy and calculate the layout. From there, we can make clever use of tables and classes to build up a pretty nice UI framework. So instead of writing something like this:
-- very simple example
function playdate.update()
gfx.clear()
gfx.drawText("Hello", 10, 10)
gfx.drawText("World", 10, 20)
end
You can instead write code like this:
View('ContentView')
function ContentView:body()
return VStack(leading) {
Text("Hello"),
Text("World"),
padding = 10
}
end
function playdate.update()
mount(ContentView)
end
Static rendering is pretty simple and works very well now, with most layout modifiers supported, as seen with padding above. There are also the expected layout primitives HStack
, ZStack
, and many other primitives like Circle
, RoundedRectangle
, etc. I have not yet started working on things like color.
Where things get really interesting though is when you start involving state.
View('CountView', { count = 0 })
function CountView:body()
if playdate.buttonJustReleased(playdate.kButtonA) then
self.count = self.count + 1
end
if playdate.buttonJustReleased(playdate.kButtonB) then
self.count = self.count - 1
end
return VStack() {
Text("Count: "..self.count),
Text("Count x2: "..self.count * 2)
}
end
function playdate.update()
mount(CountView)
end
This is the part that has the least polish currently. I plan to build a "focus engine" that will allow for views to be able to be "focused" on with the d-pad and have actions tied directly to the view itself as opposed to listening on the loop as we can see above. One can imagine this would work very similarly to SwiftUI apps for Apple TV.
Probably the more interesting thing though is how the above approach looks sort of like magic. While this portion is far from complete and literally only a prototype of a prototype- you will notice that we must be initializing views in order to compose them, so the logic follows that self.count
should always be reset to 0. But... its not.
The way this bit of magic works is that during the execution phase the renderer walks through the view tree and grabbing any table values (like self.count) that aren't the known functions implemented for views and storing them in a long lived table after a render is completed. Before that, the values are looked up and injected into the view. This lets us "pretend" to have long living state while still having this nice composition we see. You could have multiple CountViews and they each would have their own state. I have not exactly worked out how to achieve this uniqueness yet of course.
This is all very inefficient currently, my understanding of Lua is new (a couple days old) and the entire view tree is recreated on each update and plenty of stuff is only barely working. I hope folks find it interesting and I would love to collaborate on what the design should look like, though I am pretty sure this is NOT idiomatic Lua
As I get closer to something releasable I will share here & on GitHub. Would love to hear thoughts or anything you may want to see.