Babylon.js GUI -> Declarative Fluent GUI library

Fluent-GUI

Last year I did a lot of Yak Shaving, I tried to add Yoga and flex-box to Babylon.js and it kinda worked but it was still a mess. What I realized I really missed for a declarative way to describe the UI. I’ve built apps imperatively in UIKit and and Android Layouts. Its doable but its not a great experience. Apple and Google have moved on to SwiftUI and JetpackCompose and MAN what a difference.

So, I wrapped the Babylon.js GUI objects in a fluent api, added reactive state management and tried to get as close to the SwiftUI/Jetpack experience as I could. It can’t get all the way there, its still a retained mode ui where state is managed in the component and we don’t have the luxury of changing the language to suite the UI system. But with a view model and the fluent api it makes managing complex screens more delightful, at least for me it did.

import {
  DisposeBag,
  FluentBehaviourState,
  FluentContainer,
  FluentRectangle,
  FluentSimpleButton,
  FluentStackPanel,
  FluentTextBlock,
  Ref,
} from "@abraini/fluent-gui"

// Create a dispose bag to manage event listeners
const disposeBag = new DisposeBag()

// Create a reference to a button
const buttonRef = new Ref<Button>()

// Create a state for the headline text
const headlineState = new FluentBehaviourState<string>("Headline")

// Create a container and add controls to it
new FluentContainer("root",
    new FluentStackPanel("stack panel",
        new FluentRectangle("border")
          .width("200px")
          .height("100px")
          .background("green")
          .onPointerClick((eventData, eventState) => {
            console.log("Rectangle clicked!")
          }, disposeBag)
          .onPointerEnter((eventData, eventState) => {
            console.log("Pointer entered!")
          }, disposeBag)
          .modifyControl((c) => (c.thickness = 5)),
        new FluentTextBlock("tb", "Headline")
          .bindState(headlineState, (c, v) =>
            // When headlineState changes I update my text
            c.setText(v)
          ),
        new FluentSimpleButton("simpleButton", "Click Me: 0")
          // Button in stored in a ref so it can be used later
          .storeIn(buttonRef)
          .onPointerClick((eventData, eventState) => {
            console.log("Button clicked!")
          }, disposeBag)
    )
    .setHorizontal()
  )
  .build()

// Update the headline state
headlineState.setValue("Headline 2")
let count = 1
setInterval(() => {
  // Access to the button in the stack panel
  count += 1
  const button = buttonRef.get()
  button.textBlock.text = "Click Me: " + count
}, 1000)

// Dispose of all event listeners when done
disposeBag.dispose()

It let me make complex UIs with lots of interaction, but I’m still no UX designer so it looks like something out of the 80s


5 Likes

This is freaking cool! Love it mate!

1 Like

So nice !!!

1 Like

That’s really nice - I really like the fluent builder APIs - coming from working lots in Java - seems more intuitive. I like that you have bound state changes somehow - looks event driven. I also really like how you capture the references - I think that’s a big downside of loading from JSON. Would be cool if the GUI Editor could save in your format. I feel like JSON and (xmloader) are very portable formats, but there is an impedence mismatch. For example, your bindState() and pointer clicks (especially strongly typed ones) - those aren’t really designed for JSON/XML. Not to knock those approaches, but what you have lines up more with my mental model. Thanks for sharing.

I was curious where your Yoga and flex-box landed - you said it kinda-worked. Would really like to see what you ended up with there. I’ve tried a bit of that in 3d space, but not mapping/adapting from a standardized layout, if that was what you tried.

1 Like

Thanks for the feedback!
With Yoga I was fighting the existing layout code since I was attempting to wrap the GUI.Control and GUI.Container instead of replacing them.
That meant layout had to happen in a couple passes and I would have to wait for the Yoga positions to then set the top and left of the GUI.Control. Managing dirty states to know when the leaf controls had a measured size so I could recalculate the Yoga layout was complicated and unintuitive.

If I started from first principles and made a new GUI library and used Yoga for the layout from the start it would have worked better but I was spending more time on chasing the flex-box dream than building the game at the point of giving up and wanted to move on :slight_smile:

FlexContainer

FlexItem

and it made code that looked like this:

    const root = new FlexContainer("root", this.gui, undefined, FlexDirection.Row)
    root.style.setPadding(Edge.All, 10)
    root.width = 250
    root.height = 475

    const child0 = new FlexContainer("child0")
    child0.style.setFlex(1)
    child0.style.setGap(Gutter.Row, 10)
    root.addControl(child0)

    const child1 = new FlexContainer("child1")
    child1.style.setHeight(60)
    child0.addControl(child1)
    const child2 = new FlexContainer("child2")
    child2.style.setFlex(1)
    child2.style.setMargin(Edge.Start, 10)
    child2.style.setMargin(Edge.End, 10)
    child0.addControl(child2)
    const child3 = new FlexContainer("child3")
    child3.style.setFlex(2)
    child3.style.setMargin(Edge.Start, 10)
    child3.style.setMargin(Edge.End, 10)
    child3.style.setPadding(Edge.All, 10)
    child0.addControl(child3)

ok - that definitely qualifies as yak shaving! Thanks for sharing with the code reference - tracking dirty and waiting on positioning looks pretty gnarly. I often spend too much time in the wrong places where nobody will ever notice - usually it’s pretty fun though. it’s really cool what you tried to do and I think other’s have failed where at least you got it at least kinda working. sometimes it’s takes that kind of learning experience to make something better.

Your Fluent-GUI looks like a hit :smile: