Updating panel elements during runtime

Hey folks!

How exactly should one go about updating the UI (org.openrndr.panel) during runtime?
For example, given the following:

extend(ControlManager()) {
  layout {
    p { "Shapes on screen: ${shapes.length}" }
  }
}

I can’t seem to find a way to force a re-draw that actually re-computes the text in the p element.

My apologies if this is something super straight forward that I missed :')

It seems this isn’t directly possible supported. The init block for textElement (and probably most others) doesn’t get saved:

inline fun <reified T : TextElement> Element.textElement(classes: Array<out String>, init: T.() -> String): T {
    val te = T::class.java.newInstance()
    te.classes.addAll(classes.map { ElementClass(it) })
    te.text(te.init()) // init being used, but not saved anywhere
    append(te)
    return te
}

Which I believe to be a design decision, as I also found the following methods on TextElement:

TextElement.replaceText(text: String)
TextElement.bind(property: KMutableProperty0<String>)

Bind just calls replaceText whenever the provided property updates, and replaceText casts its first child to a TextNode and sets the text directly, as can be seen below:

(children.first() as? TextNode)?.text = text
requestRedraw()

Unfortunately, you can’t bind on the spot, since the underlying function needs to access program to bind to updates, resulting in a UninitializedPropertyAccessException.

Which is kind of a funny, as if you set the property yourself- everything works fine:

var temp = "Hello World!"

fun main() = application {
  program {
    extend(ControlManager()) {
      program = this@program
      layout {
        p {
          bind(::temp) // works fine
          temp
        }
      }
    }
  }
}

As such, with a little bit of Kotlin magic, and taking some bits from the existing openrndr source:

context(Program)
inline fun <reified T : TextElement> Element.textElement(classes: Array<out String>, crossinline init: T.() -> String): T {
    val te = T::class.java.newInstance()
    te.classes.addAll(classes.map { ElementClass(it) })
    var currentText = te.init()
    te.text(currentText)
    append(te)

    launch {
        while(true) {
            val newText = te.init()
            if(currentText != newText) {
                currentText = newText
                te.replaceText(newText)
            }
            yield()
        }
    }

    return te
}

context(Program)
fun Element.p(vararg classes: String, init: P.() -> String): P = textElement(classes, init)

Tada! Which allows you to do things like this:

fun main() = application {
  program {
    val shapes = mutableListOf<Shape>()

    extend(ControlManager()) {
      layout {
        p { "Shapes on screen: ${shapes.size}" }
      }
    }
  }
}

And the init block will properly update the text.

1 Like