Making a DSL for building a UI component tree?

I’m new to Julia. I’m experimenting with using it for a native macOS app (wrapping the native AppKit classes). Lately I’ve used both Apple’s SwiftUI and Android’s Jetpack Compose. They have DSLs for UIs that are very similar, and they do a nice job defining the tree structure of a UI in a clean and declarative way. Here’s some sample pseudocode to give a sense of what it looks like:

MyPanel {
    Button("Foo") { print("clicked") }
    List(data: items) { item -> 
        HStack {
            Text(item.name, font: .large)
            Image(item.thumbnail)
        }
    }
}

Most of those elements would map to objects (NSView subclasses on macOS) that form the nodes of a tree.

The languages (Swift, or Kotlin) have different ways to translate this syntax, but the overall look is similar. How could I do something similar in Julia?

I see Julia has macros (Lisp style, real macros), so I could do that. In that case, would the curly braces above end up as beginend. That seems a bit verbose. Is there way to create something that looks like the above – minimal keywords and boilerplate, and clearly showing the nested tree structure?

I know this is partly a subjective issue. I just like the look of the SwiftUI/Compose style. I’ve been using curly brace languages most of my life :slight_smile: (C, C++, Java, Swift, etc.)

The input to a macro needs to be acceptable to the Julia parser. The parser will accept curly braces at least as deprecated vector syntax or for specifying type parameters. This seems to be acceptable:

julia> macro dummy(args...)
         println(typeof(args)); println(args)
         return :()
         end

julia> @dummy Panel {
         Button("Foo")
         }
Tuple{Symbol, Expr}
(:Panel, :({Button("Foo")}))
()

Your example text failed for me at .large:

julia> @dummy MyPanel {
         Button("Foo") { print("clicked") }
         List(data: items) { item ->
           HStack {
             Text(item.name, font: .large)
ERROR: syntax: invalid identifier name "."
Stacktrace:
 [1] top-level scope
   @ none:1

With macros you can make the DSL look any way you want.

@mydsl MyPanel(
    Button("Foo")( print("clicked"))
    List(items)( item -> 
        HStack(
            Text(item.name, font = :large)
            Image(item.thumbnail)
        )
    )
)
@MyPanel(
    @Button("Foo")  print("clicked") 
    @List(items)( item -> 
        @HStack(
            @Text(item.name, font = :large)
            @Image(item.thumbnail)
        )
    )
)

The only thing that’s actually making use of macros there is the print("clicked"), since that would evaluate eagerly.

If you want it to be plain non-macro julia, a few options come to mind off hand using a declarative style.

# curried parameters
MyPanel(
    Button("Foo")(Print("clicked"))
    List(data=items)( item -> 
        HStack(
            Text(item.name, font = :large)
            Image(item.thumbnail)
        )
    )
)
# uncurried parameters
MyPanel(
    Button("Foo", Print("clicked")),
    List(items, item -> 
        HStack(
            Text(item.name, font = :large),
            Image(item.thumbnail),
        )
    )
)
# swapped parameters
MyPanel() do 
  Button("Foo") do print("clicked") end
  List(items) do item
    HStack() do
        [Text(item.name, font=:large)
         Image(item.thumbnail)]
    end    
  end      
end
3 Likes

Thanks, that gives me a lot to experiment with.

Take a look at CImGui.jl: https://github.com/Gnimuc/CImGui.jl/discussions/39

1 Like