[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm
This commit is contained in:
25
orx-jvm/orx-panel/README.md
Normal file
25
orx-jvm/orx-panel/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# orx-panel
|
||||
|
||||
The OPENRNDR UI toolkit. Provides buttons, sliders, text, a color picker and much more. HTML/CSS-like.
|
||||
|
||||
<!-- __demos__ -->
|
||||
## Demos
|
||||
### DemoHorizontalLayout01
|
||||
[source code](src/demo/kotlin/DemoHorizontalLayout01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoVerticalLayout01
|
||||
[source code](src/demo/kotlin/DemoVerticalLayout01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoWatchDiv01
|
||||
[source code](src/demo/kotlin/DemoWatchDiv01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoWatchObjectDiv01
|
||||
[source code](src/demo/kotlin/DemoWatchObjectDiv01.kt)
|
||||
|
||||

|
||||
19
orx-jvm/orx-panel/build.gradle
Normal file
19
orx-jvm/orx-panel/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
sourceSets {
|
||||
demo {
|
||||
java {
|
||||
srcDirs = ["src/demo/kotlin"]
|
||||
compileClasspath += main.getCompileClasspath()
|
||||
runtimeClasspath += main.getRuntimeClasspath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
||||
demoImplementation("org.openrndr:openrndr-dialogs:$openrndrVersion")
|
||||
demoImplementation("com.google.code.gson:gson:$gsonVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
|
||||
demoImplementation(sourceSets.getByName("main").output)
|
||||
}
|
||||
80
orx-jvm/orx-panel/src/demo/kotlin/DemoHorizontalLayout01.kt
Normal file
80
orx-jvm/orx-panel/src/demo/kotlin/DemoHorizontalLayout01.kt
Normal file
@@ -0,0 +1,80 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.math.Spherical
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.panel.controlManager
|
||||
import org.openrndr.panel.elements.button
|
||||
import org.openrndr.panel.elements.div
|
||||
import org.openrndr.panel.elements.h1
|
||||
import org.openrndr.panel.style.*
|
||||
|
||||
suspend fun main() = application {
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
val cm = controlManager {
|
||||
styleSheet(has class_ "horizontal") {
|
||||
paddingLeft = 10.px
|
||||
paddingTop = 10.px
|
||||
|
||||
// ----------------------------------------------
|
||||
// The next two lines produce a horizontal layout
|
||||
// ----------------------------------------------
|
||||
display = Display.FLEX
|
||||
flexDirection = FlexDirection.Row
|
||||
width = 100.percent
|
||||
}
|
||||
|
||||
styleSheet(has type "h1") {
|
||||
marginTop = 10.px
|
||||
marginLeft = 7.px
|
||||
marginBottom = 10.px
|
||||
}
|
||||
|
||||
layout {
|
||||
val header = h1 { "click a button..." }
|
||||
|
||||
div("horizontal") {
|
||||
// A bunch of names for generating buttons
|
||||
listOf("load", "save", "redo", "stretch", "bounce",
|
||||
"twist", "swim", "roll", "fly", "dance")
|
||||
.forEachIndexed { i, word ->
|
||||
|
||||
// A fun way of generating a set of colors
|
||||
// of similar brightness:
|
||||
// Grab a point on the surface of a sphere
|
||||
// and treat its coordinates as an rgb color.
|
||||
val pos = Vector3.fromSpherical(
|
||||
Spherical(i * 19.0, i * 17.0, 0.4))
|
||||
val rgb = ColorRGBa.fromVector(pos + 0.4)
|
||||
|
||||
button {
|
||||
label = word
|
||||
style = styleSheet {
|
||||
// Use Color.RGBa() to convert a ColorRGBa
|
||||
// color (the standard color datatype)
|
||||
// into "CSS" format:
|
||||
background = Color.RGBa(rgb)
|
||||
}
|
||||
|
||||
// When the button is clicked replace
|
||||
// the header text with the button's label
|
||||
events.clicked.listen {
|
||||
header.replaceText(it.source.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extend(cm)
|
||||
extend {
|
||||
drawer.clear(0.2, 0.18, 0.16, 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
47
orx-jvm/orx-panel/src/demo/kotlin/DemoVerticalLayout01.kt
Normal file
47
orx-jvm/orx-panel/src/demo/kotlin/DemoVerticalLayout01.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.panel.controlManager
|
||||
import org.openrndr.panel.elements.div
|
||||
import org.openrndr.panel.elements.slider
|
||||
import org.openrndr.panel.style.*
|
||||
|
||||
suspend fun main() = application {
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
val cm = controlManager {
|
||||
styleSheet(has class_ "side-bar") {
|
||||
this.height = 100.percent
|
||||
this.width = 200.px
|
||||
this.display = Display.FLEX
|
||||
this.flexDirection = FlexDirection.Column
|
||||
this.paddingLeft = 10.px
|
||||
this.paddingRight = 10.px
|
||||
this.background = Color.RGBa(ColorRGBa.GRAY)
|
||||
}
|
||||
styleSheet(has type "slider") {
|
||||
this.marginTop = 25.px
|
||||
this.marginBottom = 25.px
|
||||
}
|
||||
layout {
|
||||
div("side-bar") {
|
||||
slider {
|
||||
label = "Slider 1"
|
||||
}
|
||||
slider {
|
||||
label = "Slider 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extend(cm)
|
||||
extend {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
130
orx-jvm/orx-panel/src/demo/kotlin/DemoWatchDiv01.kt
Normal file
130
orx-jvm/orx-panel/src/demo/kotlin/DemoWatchDiv01.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
import com.google.gson.Gson
|
||||
import org.openrndr.application
|
||||
import org.openrndr.dialogs.openFileDialog
|
||||
import org.openrndr.dialogs.saveFileDialog
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.panel.controlManager
|
||||
import org.openrndr.panel.elements.*
|
||||
import org.openrndr.panel.style.*
|
||||
import java.io.File
|
||||
|
||||
// -- these have to be top-level classes or Gson will silently fail.
|
||||
private class ConfigItem {
|
||||
var value: Double = 0.0
|
||||
}
|
||||
|
||||
private class ProgramState {
|
||||
var rows = 1
|
||||
var columns = 1
|
||||
val matrix = mutableListOf(mutableListOf(ConfigItem()))
|
||||
|
||||
fun copyTo(programState: ProgramState) {
|
||||
programState.rows = rows
|
||||
programState.columns = columns
|
||||
programState.matrix.clear()
|
||||
programState.matrix.addAll(matrix)
|
||||
}
|
||||
|
||||
fun save(file: File) {
|
||||
file.writeText(Gson().toJson(this))
|
||||
}
|
||||
|
||||
fun load(file: File) {
|
||||
Gson().fromJson(file.readText(), ProgramState::class.java).copyTo(this)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun main() = application {
|
||||
configure {
|
||||
width = 900
|
||||
height = 720
|
||||
}
|
||||
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
val programState = ProgramState()
|
||||
val cm = controlManager {
|
||||
layout {
|
||||
styleSheet(has class_ "matrix") {
|
||||
this.width = 100.percent
|
||||
}
|
||||
|
||||
styleSheet(has class_ "row") {
|
||||
this.display = Display.FLEX
|
||||
this.flexDirection = FlexDirection.Row
|
||||
this.width = 100.percent
|
||||
|
||||
child(has type "slider") {
|
||||
this.width = 80.px
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
label = "save"
|
||||
clicked {
|
||||
saveFileDialog(supportedExtensions = listOf("json")) {
|
||||
programState.save(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
label = "load"
|
||||
clicked {
|
||||
openFileDialog(supportedExtensions = listOf("json")) {
|
||||
programState.load(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slider {
|
||||
label = "rows"
|
||||
precision = 0
|
||||
bind(programState::rows)
|
||||
|
||||
events.valueChanged.listen {
|
||||
while (programState.matrix.size > programState.rows) {
|
||||
programState.matrix.removeAt(programState.matrix.size - 1)
|
||||
}
|
||||
while (programState.matrix.size < programState.rows) {
|
||||
programState.matrix.add(MutableList(programState.columns) { ConfigItem() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slider {
|
||||
label = "columns"
|
||||
precision = 0
|
||||
bind(programState::columns)
|
||||
events.valueChanged.listen {
|
||||
for (row in programState.matrix) {
|
||||
while (row.size > programState.columns) {
|
||||
row.removeAt(row.size - 1)
|
||||
}
|
||||
while (row.size < programState.columns) {
|
||||
row.add(ConfigItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchListDiv("matrix", watchList = programState.matrix) { row ->
|
||||
watchListDiv("row", watchList = row) { item ->
|
||||
this.id = "some-row"
|
||||
slider {
|
||||
label = "value"
|
||||
bind(item::value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extend(cm)
|
||||
}
|
||||
}
|
||||
|
||||
75
orx-jvm/orx-panel/src/demo/kotlin/DemoWatchObjectDiv01.kt
Normal file
75
orx-jvm/orx-panel/src/demo/kotlin/DemoWatchObjectDiv01.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.panel.controlManager
|
||||
import org.openrndr.panel.elements.*
|
||||
import org.openrndr.panel.style.*
|
||||
|
||||
|
||||
suspend fun main() = application {
|
||||
configure {
|
||||
width = 900
|
||||
height = 720
|
||||
}
|
||||
// A very simple state
|
||||
class State {
|
||||
var x = 0
|
||||
var y = 0
|
||||
var z = 0
|
||||
}
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
val programState = State()
|
||||
val cm = controlManager {
|
||||
layout {
|
||||
styleSheet(has class_ "matrix") {
|
||||
this.width = 100.percent
|
||||
}
|
||||
|
||||
styleSheet(has class_ "row") {
|
||||
this.display = Display.FLEX
|
||||
this.flexDirection = FlexDirection.Row
|
||||
this.width = 100.percent
|
||||
|
||||
child(has type "slider") {
|
||||
this.width = 80.px
|
||||
}
|
||||
}
|
||||
|
||||
slider {
|
||||
label = "x"
|
||||
precision = 0
|
||||
bind(programState::x)
|
||||
}
|
||||
|
||||
slider {
|
||||
label = "y"
|
||||
precision = 0
|
||||
bind(programState::y)
|
||||
}
|
||||
|
||||
watchObjectDiv("matrix", watchObject = object {
|
||||
// for primitive types we have to use property references
|
||||
val x = programState::x
|
||||
val y = programState::y
|
||||
}) {
|
||||
for (y in 0 until watchObject.y.get()) {
|
||||
div("row") {
|
||||
for (x in 0 until watchObject.x.get()) {
|
||||
button() {
|
||||
label = "$x, $y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extend(cm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
package org.openrndr.panel
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.openrndr.*
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.mod_
|
||||
import org.openrndr.panel.elements.*
|
||||
import org.openrndr.panel.layout.Layouter
|
||||
import org.openrndr.panel.style.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
import org.openrndr.shape.intersects
|
||||
|
||||
//class SurfaceCache(val width: Int, val height: Int, val contentScale:Double) {
|
||||
// val cache = renderTarget(width, height, contentScale) {
|
||||
// colorMap()
|
||||
// depthBuffer()
|
||||
// }
|
||||
//
|
||||
// private val atlas = mutableMapOf<Element, IntRectangle>()
|
||||
//
|
||||
// private var root: PackNode = PackNode(IntRectangle(0, 0, width, height))
|
||||
// val packer = IntPacker()
|
||||
//
|
||||
// fun flush() {
|
||||
// atlas.clear()
|
||||
// root = PackNode(IntRectangle(0, 0, width, height))
|
||||
// }
|
||||
//
|
||||
// fun drawCached(drawer: Drawer, element: Element, f: () -> Unit): IntRectangle {
|
||||
// val rectangle = atlas.getOrPut(element) {
|
||||
// val r = packer.insert(root, IntRectangle(0, 0, Math.ceil(element.layout.screenWidth).toInt(),
|
||||
// Math.ceil(element.layout.screenHeight).toInt()))?.area?:throw RuntimeException("bork")
|
||||
// draw(drawer, r, f)
|
||||
// r
|
||||
// }
|
||||
// if (element.draw.dirty) {
|
||||
// draw(drawer, rectangle, f)
|
||||
// }
|
||||
// drawer.ortho()
|
||||
// drawer.isolated {
|
||||
// drawer.model = Matrix44.IDENTITY
|
||||
// drawer.image(cache.colorMap(0), Rectangle(rectangle.corner.x * 1.0, rectangle.corner.y * 1.0, rectangle.width * 1.0, rectangle.height * 1.0),
|
||||
// element.screenArea)
|
||||
// }
|
||||
// return rectangle
|
||||
// }
|
||||
//
|
||||
// fun draw(drawer: Drawer, rectangle: IntRectangle, f: () -> Unit) {
|
||||
// drawer.isolatedWithTarget(cache) {
|
||||
// drawer.ortho(cache)
|
||||
// drawer.drawStyle.blendMode = BlendMode.REPLACE
|
||||
// drawer.drawStyle.fill = ColorRGBa.BLACK.opacify(0.0)
|
||||
// drawer.drawStyle.stroke = null
|
||||
// drawer.view = Matrix44.IDENTITY
|
||||
// drawer.model = Matrix44.IDENTITY
|
||||
// drawer.rectangle(Rectangle(rectangle.x * 1.0, rectangle.y * 1.0, rectangle.width * 1.0, rectangle.height * 1.0))
|
||||
//
|
||||
// drawer.drawStyle.blendMode = BlendMode.OVER
|
||||
// drawer.drawStyle.clip = Rectangle(rectangle.x * 1.0, rectangle.y * 1.0, rectangle.width * 1.0, rectangle.height * 1.0)
|
||||
// drawer.view = Matrix44.IDENTITY
|
||||
// drawer.model = org.openrndr.math.transforms.translate(rectangle.x * 1.0, rectangle.y * 1.0, 0.0)
|
||||
// f()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class ControlManager : Extension {
|
||||
var body: Element? = null
|
||||
val layouter = Layouter()
|
||||
val fontManager = FontManager()
|
||||
lateinit var window: Program.Window
|
||||
private val renderTargetCache = HashMap<Element, RenderTarget>()
|
||||
|
||||
lateinit var program: Program
|
||||
override var enabled: Boolean = true
|
||||
|
||||
var contentScale = 1.0
|
||||
lateinit var renderTarget: RenderTarget
|
||||
|
||||
init {
|
||||
fontManager.register("default", resourceUrl("/fonts/Roboto-Regular.ttf"))
|
||||
layouter.styleSheets.addAll(defaultStyles().flatMap { it.flatten() })
|
||||
}
|
||||
|
||||
inner class DropInput {
|
||||
var target: Element? = null
|
||||
fun drop(event: DropEvent) {
|
||||
target?.drop?.dropped?.trigger(event)
|
||||
}
|
||||
}
|
||||
|
||||
val dropInput = DropInput()
|
||||
|
||||
|
||||
|
||||
|
||||
inner class KeyboardInput {
|
||||
private var lastTarget: Element? = null
|
||||
var target: Element? = null
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field?.pseudoClasses?.remove(ElementPseudoClass("active"))
|
||||
field?.keyboard?.focusLost?.trigger(FocusEvent())
|
||||
value?.keyboard?.focusGained?.trigger(FocusEvent())
|
||||
field = value
|
||||
field?.pseudoClasses?.add(ElementPseudoClass("active"))
|
||||
value?.let {
|
||||
lastTarget = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun press(event: KeyEvent) {
|
||||
target?.let {
|
||||
var current: Element? = it
|
||||
while (current != null) {
|
||||
if (!event.propagationCancelled) {
|
||||
current.keyboard.pressed.trigger(event)
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
checkForManualRedraw()
|
||||
}
|
||||
|
||||
if (!event.propagationCancelled) {
|
||||
if (event.key == KEY_TAB) {
|
||||
val focusableControls = body?.findAllVisible { it.handlesKeyboardFocus } ?: emptyList()
|
||||
|
||||
val index = target?.let { focusableControls.indexOf(it) } ?: lastTarget?.let { focusableControls.indexOf(it) } ?: -1
|
||||
if (focusableControls.isNotEmpty()) {
|
||||
|
||||
target = if (target != null) {
|
||||
if (KeyModifier.SHIFT in event.modifiers) {
|
||||
focusableControls[(index - 1).mod_(focusableControls.size)]
|
||||
} else {
|
||||
focusableControls[(index + 1).mod_(focusableControls.size)]
|
||||
}
|
||||
} else {
|
||||
lastTarget ?: focusableControls[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun release(event: KeyEvent) {
|
||||
target?.keyboard?.released?.trigger(event)
|
||||
if (target != null) {
|
||||
checkForManualRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun repeat(event: KeyEvent) {
|
||||
target?.keyboard?.repeated?.trigger(event)
|
||||
if (target != null) {
|
||||
checkForManualRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun character(event: CharacterEvent) {
|
||||
target?.keyboard?.character?.trigger(event)
|
||||
if (target != null) {
|
||||
checkForManualRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestFocus(element: Element) {
|
||||
target = element
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardInput = KeyboardInput()
|
||||
|
||||
inner class MouseInput {
|
||||
var dragTarget: Element? = null
|
||||
var clickTarget: Element? = null
|
||||
var lastClick = System.currentTimeMillis()
|
||||
|
||||
fun scroll(event: MouseEvent) {
|
||||
fun traverse(element: Element) {
|
||||
element.children.forEach(::traverse)
|
||||
if (!event.propagationCancelled) {
|
||||
if (event.position in element.screenArea && element.computedStyle.display != Display.NONE) {
|
||||
element.mouse.scrolled.trigger(event)
|
||||
if (event.propagationCancelled) {
|
||||
keyboardInput.target = element
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
body?.let(::traverse)
|
||||
checkForManualRedraw()
|
||||
}
|
||||
|
||||
fun click(event: MouseEvent) {
|
||||
logger.debug { "click event: $event" }
|
||||
dragTarget = null
|
||||
val ct = System.currentTimeMillis()
|
||||
logger.debug { "click target: $clickTarget" }
|
||||
|
||||
clickTarget?.let {
|
||||
if (it.handlesDoubleClick) {
|
||||
if (ct - lastClick > 500) {
|
||||
logger.debug { "normal click on $clickTarget" }
|
||||
it.mouse.clicked.trigger(event)
|
||||
} else {
|
||||
if (clickTarget != null) {
|
||||
logger.debug { "double-click on $clickTarget" }
|
||||
it.mouse.doubleClicked.trigger(event)
|
||||
}
|
||||
}
|
||||
lastClick = ct
|
||||
} else {
|
||||
logger.debug { "normal click on $clickTarget" }
|
||||
it.mouse.clicked.trigger(event)
|
||||
}
|
||||
}
|
||||
checkForManualRedraw()
|
||||
}
|
||||
|
||||
fun press(event: MouseEvent) {
|
||||
logger.debug { "press event: $event" }
|
||||
val candidates = mutableListOf<Pair<Element, Int>>()
|
||||
fun traverse(element: Element, depth: Int = 0) {
|
||||
|
||||
if (element.computedStyle.overflow == Overflow.Scroll) {
|
||||
if (event.position !in element.screenArea) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (element.computedStyle.display != Display.NONE) {
|
||||
element.children.forEach { traverse(it, depth + 1) }
|
||||
}
|
||||
|
||||
if (!event.propagationCancelled && event.position in element.screenArea && element.computedStyle.display != Display.NONE) {
|
||||
candidates.add(Pair(element, depth))
|
||||
}
|
||||
}
|
||||
|
||||
body?.let { traverse(it) }
|
||||
//candidates.sortByDescending { it.second }
|
||||
clickTarget = null
|
||||
candidates.sortWith(compareBy({ -it.first.layout.zIndex }, { -it.second }))
|
||||
for (c in candidates) {
|
||||
if (!event.propagationCancelled) {
|
||||
c.first.mouse.pressed.trigger(event)
|
||||
if (event.propagationCancelled) {
|
||||
logger.debug { "propagation cancelled by ${c.first}" }
|
||||
dragTarget = c.first
|
||||
clickTarget = c.first
|
||||
keyboardInput.target = c.first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clickTarget == null) {
|
||||
dragTarget = null
|
||||
keyboardInput.target = null
|
||||
}
|
||||
|
||||
checkForManualRedraw()
|
||||
}
|
||||
|
||||
fun drag(event: MouseEvent) {
|
||||
logger.debug { "drag event $event" }
|
||||
dragTarget?.mouse?.dragged?.trigger(event)
|
||||
if (event.propagationCancelled) {
|
||||
logger.debug { "propagation cancelled by $dragTarget setting clickTarget to null" }
|
||||
clickTarget = null
|
||||
}
|
||||
checkForManualRedraw()
|
||||
}
|
||||
|
||||
val insideElements = mutableSetOf<Element>()
|
||||
fun move(event: MouseEvent) {
|
||||
val hover = ElementPseudoClass("hover")
|
||||
val toRemove = insideElements.filter { (event.position !in it.screenArea) }
|
||||
|
||||
toRemove.forEach {
|
||||
it.mouse.exited.trigger(MouseEvent(event.position, Vector2.ZERO, Vector2.ZERO, MouseEventType.MOVED, MouseButton.NONE, event.modifiers))
|
||||
}
|
||||
|
||||
insideElements.removeAll(toRemove)
|
||||
|
||||
fun traverse(element: Element) {
|
||||
if (event.position in element.screenArea) {
|
||||
if (element !in insideElements) {
|
||||
element.mouse.entered.trigger(event)
|
||||
}
|
||||
insideElements.add(element)
|
||||
if (hover !in element.pseudoClasses) {
|
||||
element.pseudoClasses.add(hover)
|
||||
}
|
||||
element.mouse.moved.trigger(event)
|
||||
} else {
|
||||
if (hover in element.pseudoClasses) {
|
||||
element.pseudoClasses.remove(hover)
|
||||
}
|
||||
}
|
||||
element.children.forEach(::traverse)
|
||||
}
|
||||
body?.let(::traverse)
|
||||
checkForManualRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForManualRedraw() {
|
||||
if (window.presentationMode == PresentationMode.MANUAL) {
|
||||
val redraw = body?.any {
|
||||
it.draw.dirty
|
||||
} ?: false
|
||||
if (redraw) {
|
||||
window.requestDraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mouseInput = MouseInput()
|
||||
override fun setup(program: Program) {
|
||||
this.program = program
|
||||
|
||||
contentScale = program.window.contentScale
|
||||
window = program.window
|
||||
|
||||
//surfaceCache = SurfaceCache(4096, 4096, contentScale)
|
||||
fontManager.contentScale = contentScale
|
||||
// program.mouse.buttonUp.listen { mouseInput.release(it) }
|
||||
program.mouse.buttonUp.listen { mouseInput.click(it) }
|
||||
program.mouse.moved.listen { mouseInput.move(it) }
|
||||
program.mouse.scrolled.listen { mouseInput.scroll(it) }
|
||||
program.mouse.dragged.listen { mouseInput.drag(it) }
|
||||
program.mouse.buttonDown.listen { mouseInput.press(it) }
|
||||
|
||||
program.keyboard.keyDown.listen { keyboardInput.press(it) }
|
||||
program.keyboard.keyUp.listen { keyboardInput.release(it) }
|
||||
program.keyboard.keyRepeat.listen { keyboardInput.repeat(it) }
|
||||
program.keyboard.character.listen { keyboardInput.character(it) }
|
||||
|
||||
program.window.drop.listen { dropInput.drop(it) }
|
||||
program.window.sized.listen { resize(program, it.size.x.toInt(), it.size.y.toInt()) }
|
||||
|
||||
width = program.width
|
||||
height = program.height
|
||||
renderTarget = renderTarget(program.width, program.height, contentScale) {
|
||||
colorBuffer()
|
||||
}
|
||||
|
||||
body?.draw?.dirty = true
|
||||
}
|
||||
|
||||
var width: Int = 0
|
||||
var height: Int = 0
|
||||
|
||||
private fun resize(program: Program, width: Int, height: Int) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
|
||||
// check if user did not minimize window
|
||||
if (width > 0 && height > 0) {
|
||||
body?.draw?.dirty = true
|
||||
if (renderTarget.colorAttachments.isNotEmpty()) {
|
||||
renderTarget.colorBuffer(0).destroy()
|
||||
renderTarget.depthBuffer?.destroy()
|
||||
renderTarget.detachColorAttachments()
|
||||
renderTarget.detachDepthBuffer()
|
||||
renderTarget.destroy()
|
||||
} else {
|
||||
logger.error { "that is strange. no color buffers" }
|
||||
}
|
||||
|
||||
renderTarget = renderTarget(program.width, program.height, contentScale) {
|
||||
colorBuffer()
|
||||
depthBuffer()
|
||||
}
|
||||
|
||||
renderTarget.bind()
|
||||
program.drawer.clear(ColorRGBa.BLACK.opacify(0.0))
|
||||
renderTarget.unbind()
|
||||
|
||||
renderTargetCache.forEach { (_, u) -> u.destroy() }
|
||||
renderTargetCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawElement(element: Element, drawer: Drawer, zIndex: Int, zComp: Int) {
|
||||
val newZComp =
|
||||
element.computedStyle.zIndex.let {
|
||||
when (it) {
|
||||
is ZIndex.Value -> it.value
|
||||
else -> zComp
|
||||
}
|
||||
}
|
||||
|
||||
if (element.computedStyle.display != Display.NONE) {
|
||||
if (element.computedStyle.overflow == Overflow.Visible) {
|
||||
drawer.isolated {
|
||||
drawer.translate(element.screenPosition)
|
||||
if (newZComp == zIndex) {
|
||||
element.draw(drawer)
|
||||
}
|
||||
// if (newZComp == zIndex) {
|
||||
// surfaceCache.drawCached(drawer, element) {
|
||||
// element.draw(drawer)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
}
|
||||
element.children.forEach {
|
||||
drawElement(it, drawer, zIndex, newZComp)
|
||||
}
|
||||
} else {
|
||||
val area = element.screenArea
|
||||
val rt = renderTargetCache.computeIfAbsent(element) {
|
||||
renderTarget(width, height, contentScale) {
|
||||
colorBuffer()
|
||||
depthBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
rt.bind()
|
||||
drawer.clear(ColorRGBa.BLACK.opacify(0.0))
|
||||
|
||||
drawer.pushProjection()
|
||||
drawer.ortho(rt)
|
||||
element.children.forEach {
|
||||
drawElement(it, drawer, zIndex, newZComp)
|
||||
}
|
||||
rt.unbind()
|
||||
drawer.popProjection()
|
||||
|
||||
drawer.pushTransforms()
|
||||
drawer.pushStyle()
|
||||
drawer.translate(element.screenPosition)
|
||||
|
||||
if (newZComp == zIndex) {
|
||||
element.draw(drawer)
|
||||
}
|
||||
drawer.popStyle()
|
||||
drawer.popTransforms()
|
||||
|
||||
drawer.drawStyle.blendMode = BlendMode.OVER
|
||||
//drawer.image(rt.colorMap(0))
|
||||
drawer.image(rt.colorBuffer(0), Rectangle(Vector2(area.x, area.y), area.width, area.height),
|
||||
Rectangle(Vector2(area.x, area.y), area.width, area.height))
|
||||
}
|
||||
}
|
||||
element.draw.dirty = false
|
||||
|
||||
}
|
||||
|
||||
class ProfileData(var hits: Int = 0, var time: Long = 0)
|
||||
|
||||
private val profiles = mutableMapOf<String, ProfileData>()
|
||||
private fun profile(name: String, f: () -> Unit) {
|
||||
val start = System.currentTimeMillis()
|
||||
f()
|
||||
val end = System.currentTimeMillis()
|
||||
val pd = profiles.getOrPut(name) { ProfileData(0, 0L) }
|
||||
pd.hits++
|
||||
pd.time += (end - start)
|
||||
|
||||
if (pd.hits == 100) {
|
||||
//println("name: $name, avg: ${pd.time / pd.hits}ms, ${pd.hits}")
|
||||
pd.hits = 0
|
||||
pd.time = 0
|
||||
}
|
||||
}
|
||||
|
||||
var drawCount = 0
|
||||
override fun afterDraw(drawer: Drawer, program: Program) {
|
||||
if (program.width > 0 && program.height > 0) {
|
||||
profile("after draw") {
|
||||
|
||||
if (program.width != renderTarget.width || program.height != renderTarget.height) {
|
||||
profile("resize target") {
|
||||
body?.draw?.dirty = true
|
||||
|
||||
renderTarget.colorBuffer(0).destroy()
|
||||
renderTarget.destroy()
|
||||
renderTarget = renderTarget(program.width, program.height, contentScale) {
|
||||
colorBuffer()
|
||||
}
|
||||
|
||||
renderTarget.bind()
|
||||
program.drawer.clear(ColorRGBa.BLACK.opacify(0.0))
|
||||
renderTarget.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
val redraw = body?.any {
|
||||
it.draw.dirty
|
||||
} ?: false
|
||||
|
||||
if (redraw) {
|
||||
drawer.ortho()
|
||||
drawer.view = Matrix44.IDENTITY
|
||||
drawer.defaults()
|
||||
|
||||
profile("redraw") {
|
||||
renderTarget.bind()
|
||||
body?.style = StyleSheet(CompoundSelector())
|
||||
body?.style?.width = program.width.px
|
||||
body?.style?.height = program.height.px
|
||||
|
||||
body?.let {
|
||||
program.drawer.clear(ColorRGBa.BLACK.opacify(0.0))
|
||||
layouter.computeStyles(it)
|
||||
layouter.layout(it)
|
||||
drawElement(it, program.drawer, 0, 0)
|
||||
drawElement(it, program.drawer, 1, 0)
|
||||
drawElement(it, program.drawer, 1000, 0)
|
||||
}
|
||||
renderTarget.unbind()
|
||||
}
|
||||
}
|
||||
body?.visit {
|
||||
draw.dirty = false
|
||||
}
|
||||
|
||||
profile("draw image") {
|
||||
drawer.size(program.width, program.height)
|
||||
drawer.ortho()
|
||||
drawer.view = Matrix44.IDENTITY
|
||||
drawer.defaults()
|
||||
program.drawer.image(renderTarget.colorBuffer(0), 0.0, 0.0)
|
||||
}
|
||||
drawCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("use new style extend")
|
||||
class ControlManagerBuilder(val controlManager: ControlManager) {
|
||||
fun styleSheet(selector: CompoundSelector, init: StyleSheet.() -> Unit): StyleSheet {
|
||||
val styleSheet = StyleSheet(selector).apply { init() }
|
||||
controlManager.layouter.styleSheets.addAll(styleSheet.flatten())
|
||||
return styleSheet
|
||||
}
|
||||
|
||||
fun styleSheets(styleSheets: List<StyleSheet>) {
|
||||
controlManager.layouter.styleSheets.addAll(styleSheets.flatMap { it.flatten() })
|
||||
}
|
||||
|
||||
fun layout(init: Body.() -> Unit) {
|
||||
val body = Body(controlManager)
|
||||
body.init()
|
||||
controlManager.body = body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ControlManager.styleSheet(selector: CompoundSelector, init: StyleSheet.() -> Unit): StyleSheet {
|
||||
val styleSheet = StyleSheet(selector).apply { init() }
|
||||
layouter.styleSheets.addAll(styleSheet.flatten())
|
||||
return styleSheet
|
||||
}
|
||||
|
||||
fun ControlManager.styleSheets(styleSheets: List<StyleSheet>) {
|
||||
layouter.styleSheets.addAll(styleSheets.flatMap { it.flatten() })
|
||||
}
|
||||
|
||||
fun ControlManager.layout(init: Body.() -> Unit) {
|
||||
val body = Body(this)
|
||||
body.init()
|
||||
this.body = body
|
||||
}
|
||||
|
||||
@Deprecated("use Program.controlManager")
|
||||
fun controlManager(builder: ControlManagerBuilder.() -> Unit): ControlManager {
|
||||
val cm = ControlManager()
|
||||
cm.fontManager.register("default", resourceUrl("/fonts/Roboto-Regular.ttf"))
|
||||
cm.layouter.styleSheets.addAll(defaultStyles().flatMap { it.flatten() })
|
||||
val cmb = ControlManagerBuilder(cm)
|
||||
cmb.builder()
|
||||
return cm
|
||||
}
|
||||
|
||||
fun Program.controlManager(builder: ControlManagerBuilder.() -> Unit): ControlManager {
|
||||
val cm = ControlManager()
|
||||
cm.program = this
|
||||
cm.fontManager.register("default", resourceUrl("/fonts/Roboto-Regular.ttf"))
|
||||
cm.layouter.styleSheets.addAll(defaultStyles().flatMap { it.flatten() })
|
||||
val cmb = ControlManagerBuilder(cm)
|
||||
cmb.builder()
|
||||
return cm
|
||||
}
|
||||
|
||||
private fun Element.any(function: (Element) -> Boolean): Boolean {
|
||||
if (function(this)) {
|
||||
return true
|
||||
} else {
|
||||
children.forEach {
|
||||
if (it.any(function)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.anyVisible(function: (Element) -> Boolean): Boolean {
|
||||
if (computedStyle.display != Display.NONE && function(this)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (computedStyle.display != Display.NONE) {
|
||||
children.forEach {
|
||||
if (it.anyVisible(function)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.openrndr.panel
|
||||
|
||||
import org.openrndr.draw.FontImageMap
|
||||
import org.openrndr.panel.style.LinearDimension
|
||||
import org.openrndr.panel.style.StyleSheet
|
||||
import org.openrndr.panel.style.fontFamily
|
||||
import org.openrndr.panel.style.fontSize
|
||||
|
||||
class FontManager {
|
||||
val registry: MutableMap<String, String> = mutableMapOf()
|
||||
var contentScale: Double = 1.0
|
||||
|
||||
fun resolve(name: String): String? = registry[name]
|
||||
|
||||
fun font(cs: StyleSheet): FontImageMap {
|
||||
val fontUrl = resolve(cs.fontFamily) ?: "cp:fonts/Roboto-Medium.ttf"
|
||||
val fontSize = (cs.fontSize as? LinearDimension.PX)?.value ?: 16.0
|
||||
return FontImageMap.fromUrl(fontUrl, fontSize, contentScale = contentScale)
|
||||
}
|
||||
|
||||
fun register(name: String, url: String) {
|
||||
registry[name] = url
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.openrndr.panel.collections
|
||||
|
||||
import org.openrndr.events.Event
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class ObservableCopyOnWriteArrayList<E> : CopyOnWriteArrayList<E>() {
|
||||
|
||||
val changed = Event<ObservableCopyOnWriteArrayList<E>>()
|
||||
override fun add(element: E): Boolean {
|
||||
return if (super.add(element)) {
|
||||
changed.trigger(this)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(element: E): Boolean {
|
||||
return if (super.remove(element)) {
|
||||
changed.trigger(this)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
super.clear()
|
||||
changed.trigger(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.openrndr.panel.collections
|
||||
|
||||
import org.openrndr.events.Event
|
||||
import java.util.*
|
||||
|
||||
class ObservableHashSet<E> : HashSet<E>() {
|
||||
|
||||
class ChangeEvent<E>(val source: ObservableHashSet<E>, val added: Set<E>, val removed: Set<E>)
|
||||
|
||||
val changed = Event<ChangeEvent<E>>()
|
||||
|
||||
override fun add(element: E): Boolean {
|
||||
return if (super.add(element)) {
|
||||
changed.trigger(ChangeEvent(this, setOf(element), emptySet()))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(element: E): Boolean {
|
||||
return if (super.remove(element)) {
|
||||
changed.trigger(ChangeEvent(this, emptySet(), setOf(element)))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
val old = this.toSet()
|
||||
super.clear()
|
||||
changed.trigger(ChangeEvent(this, emptySet(), old))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.panel.ControlManager
|
||||
|
||||
class Body(val controlManager: ControlManager) : Element(ElementType("Body"))
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.FontImageMap
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.panel.style.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
|
||||
import kotlin.math.round
|
||||
|
||||
|
||||
class Button : Element(ElementType("button")) {
|
||||
|
||||
override val handlesKeyboardFocus = true
|
||||
var label: String = "OK"
|
||||
|
||||
class ButtonEvent(val source: Button)
|
||||
class Events(val clicked: Event<ButtonEvent> = Event())
|
||||
|
||||
var data: Any? = null
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.clicked.listen {
|
||||
if (disabled !in pseudoClasses) {
|
||||
events.clicked.trigger(ButtonEvent(this))
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
keyboard.pressed.listen {
|
||||
if (it.key == 32) {
|
||||
it.cancelPropagation()
|
||||
if (disabled !in pseudoClasses) {
|
||||
events.clicked.trigger(ButtonEvent(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val widthHint: Double
|
||||
get() {
|
||||
computedStyle.let { style ->
|
||||
val fontUrl = (root() as? Body)?.controlManager?.fontManager?.resolve(style.fontFamily) ?: "broken"
|
||||
val fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 14.0
|
||||
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
|
||||
|
||||
val writer = Writer(null)
|
||||
|
||||
writer.box = Rectangle(0.0,
|
||||
0.0,
|
||||
Double.POSITIVE_INFINITY,
|
||||
Double.POSITIVE_INFINITY)
|
||||
|
||||
writer.drawStyle.fontMap = fontMap
|
||||
writer.newLine()
|
||||
writer.text(label, visible = false)
|
||||
|
||||
return writer.cursor.x
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
|
||||
computedStyle.let {
|
||||
|
||||
drawer.pushTransforms()
|
||||
drawer.pushStyle()
|
||||
drawer.fill = ((it.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
|
||||
drawer.isolated {
|
||||
drawer.stroke = computedStyle.effectiveBorderColor
|
||||
drawer.strokeWeight = computedStyle.effectiveBorderWidth
|
||||
drawer.rectangle(0.0, 0.0, layout.screenWidth, layout.screenHeight)
|
||||
}
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
val textWidth = writer.textWidth(label)
|
||||
val textHeight = font.ascenderLength
|
||||
|
||||
val offset = round((layout.screenWidth - textWidth) / 2.0)
|
||||
val yOffset = round((layout.screenHeight / 2) + textHeight / 2.0 - 2.0) * 1.0
|
||||
|
||||
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE).opacify(
|
||||
if (disabled in pseudoClasses) 0.25 else 1.0
|
||||
)
|
||||
drawer.text(label, 0.0 + offset, 0.0 + yOffset)
|
||||
}
|
||||
|
||||
drawer.popStyle()
|
||||
drawer.popTransforms()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.math.Matrix44
|
||||
|
||||
class Canvas : Element(ElementType("canvas")) {
|
||||
var userDraw: ((Drawer) -> Unit)? = null
|
||||
private var renderTarget: RenderTarget? = null
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
val width = screenArea.width.toInt()
|
||||
val height = screenArea.height.toInt()
|
||||
|
||||
if (renderTarget != null) {
|
||||
if (renderTarget?.width != width || renderTarget?.height != height) {
|
||||
renderTarget?.colorBuffer(0)?.destroy()
|
||||
renderTarget?.destroy()
|
||||
renderTarget = null
|
||||
}
|
||||
}
|
||||
|
||||
if (screenArea.width >= 1 && screenArea.height >= 1) {
|
||||
if (renderTarget == null) {
|
||||
renderTarget = renderTarget(screenArea.width.toInt(), screenArea.height.toInt(), drawer.context.contentScale) {
|
||||
colorBuffer()
|
||||
depthBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
renderTarget?.let { rt ->
|
||||
drawer.isolatedWithTarget(rt) {
|
||||
model = Matrix44.IDENTITY
|
||||
view = Matrix44.IDENTITY
|
||||
clear(ColorRGBa.TRANSPARENT)
|
||||
size(screenArea.width.toInt(), screenArea.height.toInt())
|
||||
ortho(rt)
|
||||
userDraw?.invoke(this)
|
||||
}
|
||||
drawer.image(rt.colorBuffer(0), 0.0, 0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.KEY_BACKSPACE
|
||||
import org.openrndr.KEY_ENTER
|
||||
import org.openrndr.KEY_ESCAPE
|
||||
import org.openrndr.MouseEvent
|
||||
import org.openrndr.color.ColorHSVa
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.ColorBuffer
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.colorBuffer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.panel.style.Color
|
||||
import org.openrndr.panel.style.color
|
||||
|
||||
class Colorpicker : Element {
|
||||
|
||||
internal var colorMap: ColorBuffer? = null
|
||||
|
||||
var label: String = "Color"
|
||||
|
||||
var saturation = 0.5
|
||||
var color: ColorRGBa
|
||||
set(value) {
|
||||
realColor = value
|
||||
saturation = color.toHSVa().s
|
||||
generateColorMap()
|
||||
draw.dirty = true
|
||||
}
|
||||
get() {
|
||||
return realColor
|
||||
}
|
||||
|
||||
private var realColor = ColorRGBa.WHITE
|
||||
private var focussed = false
|
||||
|
||||
class ColorChangedEvent(val source: Colorpicker,
|
||||
val oldColor: ColorRGBa,
|
||||
val newColor: ColorRGBa)
|
||||
|
||||
class Events {
|
||||
val colorChanged = Event<ColorChangedEvent>()
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
private var keyboardInput = ""
|
||||
private fun pick(e: MouseEvent) {
|
||||
val dx = e.position.x - layout.screenX
|
||||
var dy = e.position.y - layout.screenY
|
||||
|
||||
dy = 50.0 - dy
|
||||
val oldColor = color
|
||||
val hsv = ColorHSVa(360.0 / layout.screenWidth * dx, saturation, dy / 50.0)
|
||||
realColor = hsv.toRGBa()
|
||||
draw.dirty = true
|
||||
events.colorChanged.trigger(ColorChangedEvent(this, oldColor, realColor))
|
||||
e.cancelPropagation()
|
||||
}
|
||||
constructor() : super(ElementType("colorpicker")) {
|
||||
generateColorMap()
|
||||
|
||||
mouse.exited.listen {
|
||||
focussed = false
|
||||
}
|
||||
|
||||
mouse.scrolled.listen {
|
||||
if (colorMap != null) {
|
||||
//if (focussed) {
|
||||
saturation = (saturation - it.rotation.y * 0.01).coerceIn(0.0, 1.0)
|
||||
generateColorMap()
|
||||
colorMap?.shadow?.upload()
|
||||
it.cancelPropagation()
|
||||
pick(it)
|
||||
requestRedraw()
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
keyboard.focusLost.listen {
|
||||
keyboardInput = ""
|
||||
draw.dirty = true
|
||||
}
|
||||
|
||||
keyboard.character.listen {
|
||||
keyboardInput += it.character
|
||||
draw.dirty = true
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
keyboard.pressed.listen {
|
||||
if (it.key == KEY_BACKSPACE) {
|
||||
if (!keyboardInput.isEmpty()) {
|
||||
keyboardInput = keyboardInput.substring(0, keyboardInput.length - 1)
|
||||
draw.dirty = true
|
||||
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_ESCAPE) {
|
||||
keyboardInput = ""
|
||||
draw.dirty = true
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
|
||||
if (it.key == KEY_ENTER) {
|
||||
val number = if (keyboardInput.length == 6) keyboardInput.toIntOrNull(16) else null
|
||||
|
||||
number?.let {
|
||||
val r = (number shr 16) and 0xff
|
||||
val g = (number shr 8) and 0xff
|
||||
val b = number and 0xff
|
||||
val oldColor = color
|
||||
color = ColorRGBa(r / 255.0, g / 255.0, b / 255.0)
|
||||
events.colorChanged.trigger(ColorChangedEvent(this, oldColor, realColor))
|
||||
keyboardInput = ""
|
||||
draw.dirty = true
|
||||
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mouse.pressed.listen { it.cancelPropagation(); focussed = true }
|
||||
mouse.clicked.listen { it.cancelPropagation(); pick(it); focussed = true; }
|
||||
mouse.dragged.listen { it.cancelPropagation(); pick(it); focussed = true; }
|
||||
}
|
||||
|
||||
private fun generateColorMap() {
|
||||
colorMap?.shadow?.let {
|
||||
for (y in 0..49) {
|
||||
for (x in 0 until it.colorBuffer.width) {
|
||||
val hsv = ColorHSVa(360.0 / it.colorBuffer.width * x, saturation, (49 - y) / 49.0)
|
||||
it.write(x, y, hsv.toRGBa())
|
||||
}
|
||||
}
|
||||
it.upload()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
if (colorMap == null) {
|
||||
colorMap = colorBuffer(layout.screenWidth.toInt(), 50, 1.0)
|
||||
generateColorMap()
|
||||
}
|
||||
|
||||
drawer.image(colorMap!!, 0.0, 0.0)
|
||||
drawer.fill = (color)
|
||||
drawer.stroke = null
|
||||
drawer.shadeStyle = null
|
||||
drawer.rectangle(0.0, 50.0, layout.screenWidth, 20.0)
|
||||
|
||||
val f = (root() as? Body)?.controlManager?.fontManager?.font(computedStyle)!!
|
||||
drawer.fontMap = f
|
||||
drawer.fill = ((computedStyle.color as Color.RGBa).color)
|
||||
|
||||
if (keyboardInput.isNotBlank()) {
|
||||
drawer.text("input: $keyboardInput", 0.0, layout.screenHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.LineCap
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.launch
|
||||
import org.openrndr.panel.style.*
|
||||
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class ColorpickerButton : Element(ElementType("colorpicker-button")), DisposableElement {
|
||||
override var disposed: Boolean = false
|
||||
|
||||
var label: String = "OK"
|
||||
var color: ColorRGBa = ColorRGBa(0.5, 0.5, 0.5)
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
events.valueChanged.trigger(ColorChangedEvent(this, value))
|
||||
}
|
||||
}
|
||||
|
||||
class ColorChangedEvent(val source: ColorpickerButton, val color: ColorRGBa)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ColorChangedEvent>()
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
mouse.clicked.listen {
|
||||
append(SlideOut(0.0, screenArea.height, screenArea.width, 200.0, color, this))
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun append(element: Element) {
|
||||
when (element) {
|
||||
is Item, is SlideOut -> super.append(element)
|
||||
else -> throw RuntimeException("only item and slideout")
|
||||
}
|
||||
super.append(element)
|
||||
}
|
||||
|
||||
fun items(): List<Item> = children.filter { it is Item }.map { it as Item }
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
|
||||
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
drawer.stroke = null
|
||||
drawer.strokeWeight = 0.0
|
||||
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
|
||||
val text = "$label"
|
||||
|
||||
val textWidth = writer.textWidth(text)
|
||||
val textHeight = font.ascenderLength
|
||||
|
||||
val offset = Math.round((layout.screenWidth - textWidth) / 2.0)
|
||||
val yOffset = Math.round((layout.screenHeight / 2) + textHeight / 2.0) - 2.0
|
||||
|
||||
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE)
|
||||
drawer.fontMap = font
|
||||
drawer.text(text, 0.0 + offset, 0.0 + yOffset)
|
||||
drawer.stroke = (color)
|
||||
drawer.pushStyle()
|
||||
drawer.strokeWeight = (4.0)
|
||||
drawer.lineCap = (LineCap.ROUND)
|
||||
drawer.lineSegment(2.0, layout.screenHeight - 2.0, layout.screenWidth - 2.0, layout.screenHeight - 2.0)
|
||||
drawer.popStyle()
|
||||
}
|
||||
}
|
||||
|
||||
class SlideOut(val x: Double, val y: Double, val width: Double, val height: Double, color: ColorRGBa, parent: Element) : Element(ElementType("slide-out")) {
|
||||
|
||||
init {
|
||||
style = StyleSheet(CompoundSelector.DUMMY).apply {
|
||||
position = Position.ABSOLUTE
|
||||
left = LinearDimension.PX(x)
|
||||
top = LinearDimension.PX(y)
|
||||
width = LinearDimension.PX(this@SlideOut.width)
|
||||
height = LinearDimension.Auto//LinearDimension.PX(this@SlideOut.height)
|
||||
overflow = Overflow.Scroll
|
||||
zIndex = ZIndex.Value(1000)
|
||||
background = Color.RGBa(ColorRGBa(0.3, 0.3, 0.3))
|
||||
}
|
||||
|
||||
val colorPicker = Colorpicker().apply {
|
||||
this.color = color
|
||||
label = (parent as ColorpickerButton).label
|
||||
events.colorChanged.listen {
|
||||
parent.color = it.newColor
|
||||
parent.events.valueChanged.trigger(ColorChangedEvent(parent, parent.color))
|
||||
}
|
||||
}
|
||||
append(colorPicker)
|
||||
|
||||
mouse.exited.listen {
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
(root() as Body).controlManager.keyboardInput.requestFocus(children[0])
|
||||
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
parent?.remove(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ColorpickerButton.bind(property: KMutableProperty0<ColorRGBa>) {
|
||||
var currentValue: ColorRGBa? = null
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = color
|
||||
property.set(it.color)
|
||||
}
|
||||
if (root() as? Body == null) {
|
||||
throw RuntimeException("no body")
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
color = lcur
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.panel.style.Color
|
||||
import org.openrndr.panel.style.Overflow
|
||||
import org.openrndr.panel.style.background
|
||||
import org.openrndr.panel.style.overflow
|
||||
import kotlin.math.max
|
||||
|
||||
open class Div : TextElement(ElementType("div")) {
|
||||
init {
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
mouse.scrolled.listen {
|
||||
computedStyle.let { cs ->
|
||||
if (cs.overflow != Overflow.Visible) {
|
||||
scrollTop -= it.rotation.y * 10
|
||||
scrollTop = max(0.0, scrollTop)
|
||||
draw.dirty = true
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
computedStyle.let { style ->
|
||||
style.background.let {
|
||||
drawer.fill = ((it as? Color.RGBa)?.color ?: ColorRGBa.BLACK)
|
||||
drawer.stroke = null
|
||||
drawer.strokeWeight = 0.0
|
||||
//drawer.smooth(false)
|
||||
drawer.rectangle(0.0, 0.0, layout.screenWidth, layout.screenHeight)
|
||||
//drawer.smooth(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Div(id=${id})"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.FontImageMap
|
||||
import org.openrndr.panel.style.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.KEY_ARROW_DOWN
|
||||
import org.openrndr.KEY_ARROW_UP
|
||||
import org.openrndr.KEY_ENTER
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.launch
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class Item : Element(ElementType("item")) {
|
||||
var label: String = ""
|
||||
var data: Any? = null
|
||||
|
||||
class PickedEvent(val source: Item)
|
||||
|
||||
class Events {
|
||||
val picked = Event<PickedEvent>()
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
fun picked() {
|
||||
events.picked.trigger(PickedEvent(this))
|
||||
}
|
||||
}
|
||||
|
||||
class DropdownButton : Element(ElementType("dropdown-button")), DisposableElement {
|
||||
override var disposed = false
|
||||
|
||||
var label: String = "OK"
|
||||
var value: Item? = null
|
||||
|
||||
class ValueChangedEvent(val source: DropdownButton, val value: Item)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>()
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.clicked.listen {
|
||||
val itemCount = items().size
|
||||
|
||||
if (children.none { it is SlideOut }) {
|
||||
val height = min(240.0, itemCount * 24.0)
|
||||
if (screenPosition.y < root().layout.screenHeight - height) {
|
||||
val so = SlideOut(0.0, screenArea.height, screenArea.width, height, this, value)
|
||||
append(so)
|
||||
(root() as Body).controlManager.keyboardInput.requestFocus(so)
|
||||
} else {
|
||||
val so = SlideOut(0.0, screenArea.height - height, screenArea.width, height, this, value)
|
||||
append(so)
|
||||
(root() as Body).controlManager.keyboardInput.requestFocus(so)
|
||||
}
|
||||
} else {
|
||||
(children.first { it is SlideOut } as SlideOut?)?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val widthHint: Double?
|
||||
get() {
|
||||
computedStyle.let { style ->
|
||||
val fontUrl = (root() as? Body)?.controlManager?.fontManager?.resolve(style.fontFamily) ?: "broken"
|
||||
val fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 16.0
|
||||
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
|
||||
val writer = Writer(null)
|
||||
|
||||
writer.box = Rectangle(0.0,
|
||||
0.0,
|
||||
Double.POSITIVE_INFINITY,
|
||||
Double.POSITIVE_INFINITY)
|
||||
|
||||
val text = "$label ${(value?.label) ?: "<choose>"}"
|
||||
writer.drawStyle.fontMap = fontMap
|
||||
writer.newLine()
|
||||
writer.text(text, visible = false)
|
||||
|
||||
return writer.cursor.x + 10.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun append(element: Element) {
|
||||
when (element) {
|
||||
is Item, is SlideOut -> super.append(element)
|
||||
else -> throw RuntimeException("only item and slideout")
|
||||
}
|
||||
super.append(element)
|
||||
}
|
||||
|
||||
fun items(): List<Item> = children.filterIsInstance<Item>().map { it }
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
|
||||
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
drawer.stroke = null
|
||||
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
|
||||
val text = (value?.label) ?: "<choose>"
|
||||
|
||||
val textWidth = writer.textWidth(text)
|
||||
val textHeight = font.ascenderLength
|
||||
|
||||
val offset = Math.round((layout.screenWidth - textWidth))
|
||||
val yOffset = ((layout.screenHeight / 2) + textHeight / 2.0).roundToInt() - 2.0
|
||||
|
||||
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE)
|
||||
|
||||
drawer.text(label, 5.0, 0.0 + yOffset)
|
||||
drawer.text(text, -5.0 + offset, 0.0 + yOffset)
|
||||
}
|
||||
}
|
||||
|
||||
class SlideOut(val x: Double, val y: Double, val width: Double, val height: Double, parent: Element, active: Item?) : Element(ElementType("slide-out")) {
|
||||
init {
|
||||
|
||||
val itemButtons = mutableMapOf<Item, Button>()
|
||||
|
||||
var activeIndex =
|
||||
if (active != null) {
|
||||
(parent as DropdownButton).items().indexOf(active)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
keyboard.pressed.listen {
|
||||
|
||||
if (it.key == KEY_ENTER) {
|
||||
it.cancelPropagation()
|
||||
dispose()
|
||||
}
|
||||
|
||||
if (it.key == KEY_ARROW_DOWN) {
|
||||
activeIndex = (activeIndex + 1).coerceAtMost((parent as DropdownButton).items().size - 1)
|
||||
it.cancelPropagation()
|
||||
val newValue = parent.items()[activeIndex]
|
||||
|
||||
parent.value?.let { item ->
|
||||
itemButtons[item]?.pseudoClasses?.remove(ElementPseudoClass("selected"))
|
||||
}
|
||||
parent.value?.let {
|
||||
itemButtons[newValue]?.pseudoClasses?.add(ElementPseudoClass("selected"))
|
||||
}
|
||||
|
||||
parent.value = newValue
|
||||
parent.events.valueChanged.trigger(ValueChangedEvent(parent, newValue))
|
||||
newValue.picked()
|
||||
draw.dirty = true
|
||||
|
||||
val ypos = 24.0 * activeIndex
|
||||
if (ypos >= scrollTop + 10 * 24.0) {
|
||||
scrollTop += 24.0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (it.key == KEY_ARROW_UP) {
|
||||
activeIndex = (activeIndex - 1).coerceAtLeast(0)
|
||||
|
||||
|
||||
val newValue = (parent as DropdownButton).items()[activeIndex]
|
||||
|
||||
val ypos = 24.0 * activeIndex
|
||||
if (ypos < scrollTop) {
|
||||
scrollTop -= 24.0
|
||||
}
|
||||
|
||||
parent.value?.let { item ->
|
||||
itemButtons[item]?.pseudoClasses?.remove(ElementPseudoClass("selected"))
|
||||
}
|
||||
parent.value?.let {
|
||||
itemButtons[newValue]?.pseudoClasses?.add(ElementPseudoClass("selected"))
|
||||
}
|
||||
|
||||
parent.value = newValue
|
||||
parent.events.valueChanged.trigger(ValueChangedEvent(parent, newValue))
|
||||
newValue.picked()
|
||||
draw.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
mouse.scrolled.listen {
|
||||
scrollTop -= it.rotation.y
|
||||
scrollTop = max(0.0, scrollTop)
|
||||
draw.dirty = true
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.exited.listen {
|
||||
it.cancelPropagation()
|
||||
dispose()
|
||||
}
|
||||
|
||||
style = StyleSheet(CompoundSelector.DUMMY).apply {
|
||||
position = Position.ABSOLUTE
|
||||
left = LinearDimension.PX(x)
|
||||
top = LinearDimension.PX(y)
|
||||
width = LinearDimension.PX(this@SlideOut.width)
|
||||
height = LinearDimension.PX(this@SlideOut.height)
|
||||
overflow = Overflow.Scroll
|
||||
zIndex = ZIndex.Value(1000)
|
||||
background = Color.Inherit
|
||||
}
|
||||
|
||||
(parent as DropdownButton).items().forEach {
|
||||
append(Button().apply {
|
||||
data = it
|
||||
label = it.label
|
||||
itemButtons[it] = this
|
||||
events.clicked.listen {
|
||||
parent.value = it.source.data as Item
|
||||
parent.events.valueChanged.trigger(ValueChangedEvent(parent, it.source.data as Item))
|
||||
(data as Item).picked()
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
active?.let {
|
||||
itemButtons[active]?.pseudoClasses?.add(ElementPseudoClass("selected"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
drawer.stroke = null
|
||||
drawer.strokeWeight = 0.0
|
||||
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||
drawer.strokeWeight = 1.0
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
parent?.remove(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <E : Enum<E>> DropdownButton.bind(property: KMutableProperty0<E>, map: Map<E, String>) {
|
||||
val options = mutableMapOf<E, Item>()
|
||||
map.forEach { (k, v) ->
|
||||
options[k] = item {
|
||||
label = v
|
||||
events.picked.listen {
|
||||
property.set(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
var currentValue = property.get()
|
||||
value = options[currentValue]
|
||||
draw.dirty = true
|
||||
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
val cval = property.get()
|
||||
if (cval != currentValue) {
|
||||
currentValue = cval
|
||||
value = options[cval]
|
||||
draw.dirty = true
|
||||
}
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.*
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.panel.collections.ObservableCopyOnWriteArrayList
|
||||
import org.openrndr.panel.collections.ObservableHashSet
|
||||
import org.openrndr.panel.style.CompoundSelector
|
||||
import org.openrndr.panel.style.Display
|
||||
import org.openrndr.panel.style.StyleSheet
|
||||
import org.openrndr.panel.style.display
|
||||
import org.openrndr.shape.Rectangle
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class ElementClass(val name: String)
|
||||
data class ElementPseudoClass(val name: String)
|
||||
data class ElementType(val name: String)
|
||||
|
||||
val disabled = ElementPseudoClass("disabled")
|
||||
|
||||
class FocusEvent
|
||||
|
||||
interface DisposableElement {
|
||||
var disposed: Boolean
|
||||
|
||||
fun dispose() {
|
||||
disposed = true
|
||||
}
|
||||
}
|
||||
|
||||
open class Element(val type: ElementType) {
|
||||
|
||||
var scrollTop = 0.0
|
||||
open val handlesDoubleClick = false
|
||||
open val handlesKeyboardFocus = false
|
||||
|
||||
open val widthHint: Double?
|
||||
get() {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
open val heightHint: Double?
|
||||
get() {
|
||||
return null
|
||||
}
|
||||
|
||||
class MouseObservables {
|
||||
val clicked = Event<MouseEvent>("element-mouse-clicked")
|
||||
val doubleClicked = Event<MouseEvent>("element-mouse-double-clicked")
|
||||
val entered = Event<MouseEvent>("element-mouse-entered")
|
||||
val exited = Event<MouseEvent>("element-mouse-exited")
|
||||
val dragged = Event<MouseEvent>("element-mouse-dragged")
|
||||
val moved = Event<MouseEvent>("element-mouse-moved")
|
||||
val scrolled = Event<MouseEvent>("element-mouse-scrolled")
|
||||
val pressed = Event<MouseEvent>("element-mouse-pressed")
|
||||
}
|
||||
|
||||
class DropObserverables {
|
||||
val dropped = Event<DropEvent>("element-dropped")
|
||||
}
|
||||
|
||||
val drop = DropObserverables()
|
||||
val mouse = MouseObservables()
|
||||
|
||||
class KeyboardObservables {
|
||||
val pressed = Event<KeyEvent>("element-keyboard-pressed")
|
||||
val released = Event<KeyEvent>("element-keyboard-released")
|
||||
val repeated = Event<KeyEvent>("element-keyboard-repeated")
|
||||
val character = Event<CharacterEvent>("element-keyboard-character")
|
||||
val focusGained = Event<FocusEvent>("element-keyboard-focus-gained")
|
||||
val focusLost = Event<FocusEvent>("element-keyboard-focus-lost")
|
||||
}
|
||||
|
||||
val keyboard = KeyboardObservables()
|
||||
|
||||
class Layout {
|
||||
var zIndex = 0
|
||||
var screenX = 0.0
|
||||
var screenY = 0.0
|
||||
var screenWidth = 0.0
|
||||
var screenHeight = 0.0
|
||||
var growWidth = 0.0
|
||||
var growHeight = 0.0
|
||||
override fun toString(): String {
|
||||
return "Layout(screenX=$screenX, screenY=$screenY, screenWidth=$screenWidth, screenHeight=$screenHeight, growWidth=$growWidth, growHeight=$growHeight)"
|
||||
}
|
||||
}
|
||||
|
||||
class Draw {
|
||||
var dirty = true
|
||||
}
|
||||
|
||||
val draw = Draw()
|
||||
val layout = Layout()
|
||||
|
||||
class ClassEvent(val source: Element, val `class`: ElementClass)
|
||||
class ClassObserverables {
|
||||
val classAdded = Event<ClassEvent>("element-class-added")
|
||||
val classRemoved = Event<ClassEvent>("element-class-removed")
|
||||
}
|
||||
|
||||
val classEvents = ClassObserverables()
|
||||
|
||||
|
||||
var id: String? = null
|
||||
val classes: ObservableHashSet<ElementClass> = ObservableHashSet()
|
||||
val pseudoClasses: ObservableHashSet<ElementPseudoClass> = ObservableHashSet()
|
||||
|
||||
var parent: Element? = null
|
||||
val children: ObservableCopyOnWriteArrayList<Element> = ObservableCopyOnWriteArrayList()
|
||||
get() = field
|
||||
|
||||
var computedStyle: StyleSheet = StyleSheet(CompoundSelector.DUMMY)
|
||||
var style: StyleSheet? = null
|
||||
|
||||
init {
|
||||
pseudoClasses.changed.listen {
|
||||
draw.dirty = true
|
||||
}
|
||||
classes.changed.listen {
|
||||
draw.dirty = true
|
||||
it.added.forEach {
|
||||
classEvents.classAdded.trigger(ClassEvent(this, it))
|
||||
}
|
||||
it.removed.forEach {
|
||||
classEvents.classRemoved.trigger(ClassEvent(this, it))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
children.changed.listen {
|
||||
draw.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun root(): Element {
|
||||
return parent?.root() ?: this
|
||||
}
|
||||
|
||||
open fun append(element: Element) {
|
||||
if (element !in children) {
|
||||
element.parent = this
|
||||
children.add(element)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(element: Element) {
|
||||
if (element in children) {
|
||||
element.parent = null
|
||||
children.remove(element)
|
||||
}
|
||||
}
|
||||
|
||||
open fun draw(drawer: Drawer) {
|
||||
|
||||
}
|
||||
|
||||
fun filter(f: (Element) -> Boolean): List<Element> {
|
||||
val result = ArrayList<Element>()
|
||||
val stack = Stack<Element>()
|
||||
|
||||
stack.add(this)
|
||||
while (!stack.isEmpty()) {
|
||||
val node = stack.pop()
|
||||
if (f(node)) {
|
||||
result.add(node)
|
||||
stack.addAll(node.children)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun flatten(): List<Element> {
|
||||
val result = ArrayList<Element>()
|
||||
val stack = Stack<Element>()
|
||||
|
||||
stack.add(this)
|
||||
while (!stack.isEmpty()) {
|
||||
val node = stack.pop()
|
||||
|
||||
result.add(node)
|
||||
stack.addAll(node.children)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun previousSibling(): Element? {
|
||||
parent?.let { p ->
|
||||
p.childIndex(this)?.let {
|
||||
if (it > 0) {
|
||||
return p.children[it - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun childIndex(element: Element): Int? {
|
||||
if (element in children) {
|
||||
return children.indexOf(element)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun ancestors(): List<Element> {
|
||||
var c = this
|
||||
val result = ArrayList<Element>()
|
||||
|
||||
while (c.parent != null) {
|
||||
c.parent?.let {
|
||||
result.add(it)
|
||||
c = it
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun previous(): Element? {
|
||||
return parent?.let { p ->
|
||||
val index = p.children.indexOf(this)
|
||||
when (index) {
|
||||
-1, 0 -> null
|
||||
else -> p.children[index - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun next(): Element? {
|
||||
return parent?.let { p ->
|
||||
when (val index = p.children.indexOf(this)) {
|
||||
-1, p.children.size - 1 -> null
|
||||
else -> p.children[index + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findNext(premise: (Element) -> Boolean): Element? {
|
||||
return parent?.let { p ->
|
||||
val index = p.children.indexOf(this)
|
||||
val siblingCount = p.children.size
|
||||
for (i in index + 1 until siblingCount) {
|
||||
if (premise(p.children[i])) {
|
||||
return p.children[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun findPrevious(premise: (Element) -> Boolean): Element? {
|
||||
return parent?.let { p ->
|
||||
val index = p.children.indexOf(this)
|
||||
for (i in index - 1 downTo 0) {
|
||||
if (premise(p.children[i])) {
|
||||
return p.children[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun move(steps: Int) {
|
||||
parent?.let { p ->
|
||||
if (steps != 0) {
|
||||
val index = p.children.indexOf(this)
|
||||
p.children.add(index + steps, this)
|
||||
if (steps > 0) {
|
||||
p.children.removeAt(index)
|
||||
} else {
|
||||
p.children.removeAt(index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findFirst(element: Element, matches: (Element) -> Boolean): Element? {
|
||||
if (matches.invoke(element)) {
|
||||
return element
|
||||
} else {
|
||||
element.children.forEach { c ->
|
||||
findFirst(c, matches)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> elementWithId(id: String): T? {
|
||||
return findFirst(this) { e -> e.id == id && e is T } as T
|
||||
}
|
||||
|
||||
val screenPosition: Vector2
|
||||
get() = Vector2(layout.screenX, layout.screenY)
|
||||
|
||||
val screenArea: Rectangle
|
||||
get() = Rectangle(Vector2(layout.screenX,
|
||||
layout.screenY),
|
||||
layout.screenWidth,
|
||||
layout.screenHeight)
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun Element.requestRedraw() {
|
||||
draw.dirty = true
|
||||
}
|
||||
|
||||
fun Element.disable() {
|
||||
pseudoClasses.add(disabled)
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
fun Element.enable() {
|
||||
pseudoClasses.remove(disabled)
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
fun Element.isDisabled(): Boolean = disabled in pseudoClasses
|
||||
|
||||
fun Element.findAll(predicate: (Element) -> Boolean): List<Element> {
|
||||
val results = mutableListOf<Element>()
|
||||
visit {
|
||||
if (predicate(this)) {
|
||||
results.add(this)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
fun Element.findAllVisible(predicate: (Element) -> Boolean): List<Element> {
|
||||
val results = mutableListOf<Element>()
|
||||
visitVisible {
|
||||
if (predicate(this)) {
|
||||
results.add(this)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
fun Element.visit(function: Element.() -> Unit) {
|
||||
this.function()
|
||||
children.forEach { it.visit(function) }
|
||||
}
|
||||
|
||||
fun Element.visitVisible(function: Element.() -> Unit) {
|
||||
if (this.computedStyle.display != Display.NONE) {
|
||||
this.function()
|
||||
children.forEach { it.visitVisible(function) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Cursor
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.panel.style.*
|
||||
|
||||
class EnvelopeButton : Element(ElementType("envelope-button")) {
|
||||
|
||||
var label = "OK"
|
||||
var envelope = Envelope()
|
||||
set(value) {
|
||||
field = value
|
||||
envelopeSubscription?.let {
|
||||
value.events.envelopeChanged.cancel(it)
|
||||
}
|
||||
envelopeSubscription = value.events.envelopeChanged.listen {
|
||||
draw.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var envelopeSubscription: ((Envelope.EnvelopeChangedEvent)->Unit)? = null
|
||||
|
||||
init {
|
||||
mouse.clicked.listen {
|
||||
append(SlideOut(0.0, screenArea.height, screenArea.width, 200.0, this))
|
||||
}
|
||||
envelopeSubscription = envelope.events.envelopeChanged.listen {
|
||||
draw.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun append(element: Element) {
|
||||
when (element) {
|
||||
is Item, is SlideOut -> super.append(element)
|
||||
else -> throw RuntimeException("only item and slideout")
|
||||
}
|
||||
super.append(element)
|
||||
}
|
||||
|
||||
fun items(): List<Item> = children.filter { it is Item }.map { it as Item }
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
var chartHeight = 0.0
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
drawer.fill = (ColorRGBa.BLACK)
|
||||
writer.cursor = Cursor(0.0,layout.screenHeight - 4.0)
|
||||
chartHeight = writer.cursor.y - font.height-4
|
||||
writer.text("$label")
|
||||
}
|
||||
|
||||
|
||||
val w = layout.screenWidth
|
||||
val h = chartHeight
|
||||
val m = envelope.points.map {
|
||||
val v = (Vector2(w, h) * it)
|
||||
Vector2(v.x, h - v.y)
|
||||
}
|
||||
|
||||
if (m.size > 1) {
|
||||
drawer.stroke = (ColorRGBa.WHITE)
|
||||
drawer.strokeWeight = (2.0)
|
||||
drawer.lineStrip(m)
|
||||
}
|
||||
if (m.size == 1) {
|
||||
drawer.stroke = (ColorRGBa.WHITE)
|
||||
drawer.strokeWeight = (2.0)
|
||||
drawer.lineSegment(0.0, m[0].y, layout.screenWidth, m[0].y)
|
||||
}
|
||||
|
||||
drawer.stroke = (ColorRGBa.BLACK.opacify(0.25))
|
||||
drawer.strokeWeight = (1.0)
|
||||
drawer.lineSegment(envelope.offset * w, 0.0, envelope.offset * w, chartHeight)
|
||||
|
||||
drawer.lineSegment(0.0, 0.0, 3.0, 0.0)
|
||||
drawer.lineSegment(0.0, 0.0, 0.0, chartHeight)
|
||||
drawer.lineSegment(0.0, chartHeight, 3.0, chartHeight)
|
||||
|
||||
drawer.lineSegment(w, 0.0, w-3.0, 0.0)
|
||||
drawer.lineSegment(w, 0.0, w, chartHeight)
|
||||
drawer.lineSegment(w, chartHeight, w-3.0, chartHeight)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SlideOut(val x: Double, val y: Double, val width: Double, val height: Double, parent: EnvelopeButton) : Element(ElementType("envelope-slide-out")) {
|
||||
|
||||
init {
|
||||
|
||||
mouse.clicked.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
style = StyleSheet(CompoundSelector.DUMMY).apply {
|
||||
position = Position.ABSOLUTE
|
||||
left = LinearDimension.PX(x)
|
||||
top = LinearDimension.PX(y)
|
||||
width = LinearDimension.PX(this@SlideOut.width)
|
||||
height = LinearDimension.Auto//LinearDimension.PX(this@SlideOut.height)
|
||||
overflow = Overflow.Scroll
|
||||
zIndex = ZIndex.Value(1)
|
||||
background = Color.RGBa(ColorRGBa(0.3, 0.3, 0.3))
|
||||
}
|
||||
|
||||
append(EnvelopeEditor().apply {
|
||||
envelope = parent.envelope
|
||||
})
|
||||
|
||||
append(Button().apply {
|
||||
label = "done"
|
||||
events.clicked.listen {
|
||||
//parent.value = it.source.data as Item
|
||||
//parent.events.valueChanged.onNext(ValueChangedEvent(parent, it.source.data as Item))
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
parent?.remove(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.MouseButton
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.KeyModifier
|
||||
import org.openrndr.events.Event
|
||||
|
||||
class Envelope(constant:Double = 0.5) {
|
||||
|
||||
val points = mutableListOf(Vector2(0.5, constant))
|
||||
var activePoint: Vector2? = null
|
||||
|
||||
var offset:Double = 0.0
|
||||
set(value) { field = value; events.envelopeChanged.trigger(EnvelopeChangedEvent(this))}
|
||||
|
||||
class EnvelopeChangedEvent(val envelope: Envelope)
|
||||
|
||||
class Events {
|
||||
val envelopeChanged = Event<EnvelopeChangedEvent>("envelope-changed")
|
||||
}
|
||||
val events = Events()
|
||||
|
||||
fun insertPoint(v: Vector2) {
|
||||
for (i in 0 until points.size) {
|
||||
if (points[i].x > v.x) {
|
||||
points.add(i, v)
|
||||
activePoint = v
|
||||
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||
return
|
||||
}
|
||||
}
|
||||
points.add(v)
|
||||
activePoint = v
|
||||
fixBounds()
|
||||
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||
}
|
||||
|
||||
fun findNearestPoint(v: Vector2) = points.minByOrNull { (it - v).length }
|
||||
|
||||
fun removePoint(v: Vector2) {
|
||||
points.remove(v)
|
||||
if (v === activePoint) {
|
||||
activePoint = null
|
||||
}
|
||||
fixBounds()
|
||||
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||
}
|
||||
|
||||
private fun fixBounds() {
|
||||
if (points.size >= 2) {
|
||||
if (points[0].x != 0.0) {
|
||||
points[0].copy(x=0.0).let {
|
||||
if (activePoint === points[0]) {
|
||||
activePoint = it
|
||||
}
|
||||
points[0] = it
|
||||
}
|
||||
}
|
||||
if (points[points.size-1].x != 1.0) {
|
||||
points[points.size-1].copy(x=1.0).let {
|
||||
if (activePoint === points[points.size-1]) {
|
||||
activePoint = it
|
||||
}
|
||||
points[points.size-1] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePoint(old: Vector2, new: Vector2) {
|
||||
val index = points.indexOf(old)
|
||||
if (index != -1) {
|
||||
points[index] = new
|
||||
}
|
||||
if (old === activePoint) {
|
||||
activePoint = new
|
||||
}
|
||||
points.sortBy { it.x }
|
||||
|
||||
fixBounds()
|
||||
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||
}
|
||||
|
||||
fun value(t: Double): Double {
|
||||
|
||||
val st = t.coerceIn(0.0, 1.0)
|
||||
|
||||
if (points.size == 1) {
|
||||
return points[0].y
|
||||
}
|
||||
else if (points.size == 2) {
|
||||
return points[0].y * (1.0-st) + points[1].y * st
|
||||
} else {
|
||||
if (st == 0.0) {
|
||||
return points[0].y
|
||||
}
|
||||
if (st == 1.0) {
|
||||
return points[points.size-1].y
|
||||
}
|
||||
|
||||
for (i in 0 until points.size-1) {
|
||||
if (points[i].x <= st && points[i+1].x > st) {
|
||||
val left = points[i]
|
||||
var right = points[i+1]
|
||||
|
||||
val dt = right.x - left.x
|
||||
if (dt > 0.0) {
|
||||
val f = (t - left.x) / dt
|
||||
return left.y * (1.0-f) + right.y * f
|
||||
} else {
|
||||
return left.y
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return points[0].y
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
class EnvelopeEditor : Element(ElementType("envelope-editor")) {
|
||||
|
||||
var envelope = Envelope()
|
||||
|
||||
init {
|
||||
|
||||
fun query(position: Vector2): Vector2 {
|
||||
val x = (position.x - layout.screenX) / layout.screenWidth
|
||||
val y = 1.0 - ((position.y - layout.screenY) / layout.screenHeight)
|
||||
|
||||
return Vector2(x, y)
|
||||
}
|
||||
|
||||
mouse.clicked.listen {
|
||||
val query = query(it.position)
|
||||
val nearest = envelope.findNearestPoint(query)
|
||||
val distance = nearest?.let { (it - query).length }
|
||||
|
||||
if (it.button == MouseButton.LEFT && !it.modifiers.contains(KeyModifier.CTRL)) {
|
||||
when {
|
||||
distance == null -> {
|
||||
envelope.insertPoint(query)
|
||||
draw.dirty = true
|
||||
}
|
||||
distance < 0.05 -> {
|
||||
envelope.activePoint = nearest
|
||||
}
|
||||
else -> {
|
||||
envelope.insertPoint(query)
|
||||
draw.dirty = true
|
||||
}
|
||||
}
|
||||
} else if (it.button == MouseButton.LEFT) {
|
||||
if (distance != null && distance < 0.1) {
|
||||
envelope.removePoint(nearest)
|
||||
draw.dirty = true
|
||||
}
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.pressed.listen {
|
||||
val query = query(it.position)
|
||||
val nearest = envelope.findNearestPoint(query)
|
||||
val distance = nearest?.let { it - query }?.length
|
||||
|
||||
if (distance == null) {
|
||||
envelope.activePoint = null
|
||||
draw.dirty = true
|
||||
} else if (distance < 0.1) {
|
||||
envelope.activePoint = nearest
|
||||
} else {
|
||||
envelope.activePoint = null
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.dragged.listen {
|
||||
envelope.activePoint?.let { activePoint ->
|
||||
val query = query(it.position)
|
||||
if (!it.modifiers.contains(KeyModifier.SHIFT)) {
|
||||
envelope.updatePoint(activePoint, query)
|
||||
} else {
|
||||
envelope.updatePoint(activePoint, Vector2(activePoint.x, query.y))
|
||||
}
|
||||
draw.dirty = true
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
val w = layout.screenWidth
|
||||
val h = layout.screenHeight
|
||||
|
||||
val m = envelope.points.map {
|
||||
val v = (it * Vector2(w, h))
|
||||
Vector2(v.x, h - v.y)
|
||||
}
|
||||
|
||||
drawer.stroke = (ColorRGBa.BLACK.opacify(0.25))
|
||||
drawer.strokeWeight = (1.0)
|
||||
drawer.lineSegment(layout.screenWidth/2.0, 0.0, layout.screenWidth/2.0,layout.screenHeight)
|
||||
drawer.lineSegment(0.0,layout.screenHeight/2.0,layout.screenWidth, layout.screenHeight/2.0)
|
||||
|
||||
if (m.size > 1) {
|
||||
drawer.stroke = (ColorRGBa.WHITE)
|
||||
drawer.strokeWeight = (2.0)
|
||||
drawer.lineStrip(m)
|
||||
drawer.fill = (ColorRGBa.WHITE)
|
||||
drawer.stroke = null
|
||||
drawer.circles(m, 4.0)
|
||||
} else if (m.size == 1) {
|
||||
drawer.stroke = (ColorRGBa.WHITE)
|
||||
drawer.strokeWeight = (2.0)
|
||||
drawer.lineSegment(0.0, m[0].y, layout.screenWidth, m[0].y)
|
||||
drawer.fill = (ColorRGBa.WHITE)
|
||||
drawer.stroke = null
|
||||
drawer.circle(m[0], 4.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.panel.ControlManager
|
||||
|
||||
fun Element.layout(init: Element.() -> Unit) {
|
||||
init()
|
||||
}
|
||||
|
||||
fun layout(controlManager: ControlManager, init: Body.() -> Unit): Body {
|
||||
val body = Body(controlManager)
|
||||
body.init()
|
||||
return body
|
||||
}
|
||||
|
||||
fun <T : Element> Element.initElement(classes: Array<out String>, element: T, init: T.() -> Unit): Element {
|
||||
append(element)
|
||||
element.classes.addAll(classes.map { ElementClass(it) })
|
||||
element.init()
|
||||
return element
|
||||
}
|
||||
|
||||
fun Element.button(vararg classes: String, label: String = "button", init: Button.() -> Unit): Button {
|
||||
val button = Button().apply {
|
||||
this.classes.addAll(classes.map { ElementClass(it) })
|
||||
this.id = id
|
||||
this.label = label
|
||||
}
|
||||
initElement(classes, button, init)
|
||||
return button
|
||||
}
|
||||
|
||||
fun Button.clicked(listener: (Button.ButtonEvent) -> Unit) {
|
||||
events.clicked.listen(listener)
|
||||
}
|
||||
|
||||
fun Element.slider(vararg classes: String, init: Slider.() -> Unit) = initElement(classes, Slider(), init) as Slider
|
||||
fun Element.toggle(vararg classes: String, init: Toggle.() -> Unit) = initElement(classes, Toggle(), init) as Toggle
|
||||
|
||||
fun Element.colorpicker(vararg classes: String, init: Colorpicker.() -> Unit) = initElement(classes, Colorpicker(), init)
|
||||
fun Element.colorpickerButton(vararg classes: String, init: ColorpickerButton.() -> Unit) = initElement(classes, ColorpickerButton(), init)
|
||||
|
||||
fun Element.xyPad(vararg classes: String, init: XYPad.() -> Unit) = initElement(classes, XYPad(), init) as XYPad
|
||||
|
||||
fun Canvas.draw(f: (Drawer) -> Unit) {
|
||||
this.userDraw = f
|
||||
}
|
||||
|
||||
fun Element.canvas(vararg classes: String, init: Canvas.() -> Unit) {
|
||||
val canvas = Canvas()
|
||||
classes.forEach { canvas.classes.add(ElementClass(it)) }
|
||||
canvas.init()
|
||||
append(canvas)
|
||||
}
|
||||
|
||||
fun Element.dropdownButton(vararg classes: String, id: String? = null, label: String = "button", init: DropdownButton.() -> Unit) = initElement(classes, DropdownButton().apply {
|
||||
this.id = id
|
||||
this.label = label
|
||||
}, init)
|
||||
|
||||
fun Element.envelopeButton(vararg classes: String, init: EnvelopeButton.() -> Unit) = initElement(classes, EnvelopeButton().apply {}, init)
|
||||
fun Element.envelopeEditor(vararg classes: String, init: EnvelopeEditor.() -> Unit) = initElement(classes, EnvelopeEditor().apply {}, init)
|
||||
|
||||
fun Element.sequenceEditor(vararg classes: String, init: SequenceEditor.() -> Unit) = initElement(classes, SequenceEditor().apply {}, init)
|
||||
|
||||
fun Element.slidersVector2(vararg classes: String, init: SlidersVector2.() -> Unit) = initElement(classes, SlidersVector2().apply {}, init)
|
||||
fun Element.slidersVector3(vararg classes: String, init: SlidersVector3.() -> Unit) = initElement(classes, SlidersVector3().apply {}, init)
|
||||
fun Element.slidersVector4(vararg classes: String, init: SlidersVector4.() -> Unit) = initElement(classes, SlidersVector4().apply {}, init)
|
||||
|
||||
|
||||
|
||||
|
||||
fun Element.textfield(vararg classes: String, init: Textfield.() -> Unit) = initElement(classes, Textfield(), init)
|
||||
|
||||
fun DropdownButton.item(init: Item.() -> Unit): Item {
|
||||
val item = Item().apply(init)
|
||||
|
||||
|
||||
append(item)
|
||||
return item
|
||||
}
|
||||
|
||||
fun Element.div(vararg classes: String, init: Div.() -> Unit): Div {
|
||||
val div = Div()
|
||||
initElement(classes, div, init)
|
||||
return div
|
||||
}
|
||||
|
||||
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())
|
||||
append(te)
|
||||
return te
|
||||
}
|
||||
|
||||
fun Element.p(vararg classes: String, init: P.() -> String): P = textElement(classes, init)
|
||||
fun Element.h1(vararg classes: String, init: H1.() -> String): H1 = textElement(classes, init)
|
||||
fun Element.h2(vararg classes: String, init: H2.() -> String): H2 = textElement(classes, init)
|
||||
fun Element.h3(vararg classes: String, init: H3.() -> String): H3 = textElement(classes, init)
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.openrndr.KeyModifier
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.map
|
||||
import org.openrndr.panel.style.effectiveColor
|
||||
import org.openrndr.panel.tools.Tooltip
|
||||
import org.openrndr.shape.Rectangle
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SequenceEditor : SequenceEditorBase("sequence-editor") {
|
||||
var value
|
||||
get() = baseValue
|
||||
set(value) {
|
||||
baseValue = value
|
||||
}
|
||||
|
||||
public override var maximumSequenceLength = 16
|
||||
public override var minimumSequenceLength = 1
|
||||
|
||||
class ValueChangedEvent(val source: SequenceEditorBase,
|
||||
val oldValue: List<Double>,
|
||||
val newValue: List<Double>)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("sequence-editor-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
baseEvents.valueChanged.listen {
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, it.oldValue, it.newValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class SequenceEditorBase(type: String = "sequence-editor-base") : Element(ElementType(type)), DisposableElement {
|
||||
override var disposed = false
|
||||
|
||||
internal var baseValue = mutableListOf(0.0)
|
||||
var label = "sequence"
|
||||
var precision = 2
|
||||
internal open var maximumSequenceLength = 16
|
||||
internal open var minimumSequenceLength = 1
|
||||
var range: ClosedRange<Double> = -1.0..1.0
|
||||
|
||||
private var selectedIndex: Int? = null
|
||||
private var tooltip: Tooltip? = null
|
||||
|
||||
private val footerHeight = 20.0
|
||||
|
||||
internal class ValueChangedEvent(val source: SequenceEditorBase,
|
||||
val oldValue: List<Double>,
|
||||
val newValue: List<Double>)
|
||||
|
||||
internal class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("sequence-editor-base-value-changed")
|
||||
}
|
||||
|
||||
internal val baseEvents = Events()
|
||||
|
||||
init {
|
||||
fun query(position: Vector2): Vector2 {
|
||||
val x = (position.x - layout.screenX) / layout.screenWidth
|
||||
val y = 1.0 - ((position.y - layout.screenY) / ((layout.screenHeight - footerHeight) * 0.5))
|
||||
return Vector2(x, y)
|
||||
}
|
||||
|
||||
mouse.clicked.listen {
|
||||
it.cancelPropagation()
|
||||
requestRedraw()
|
||||
}
|
||||
mouse.pressed.listen {
|
||||
if (baseValue.isNotEmpty()) {
|
||||
val dx = (layout.screenWidth / (baseValue.size + 1))
|
||||
val index = (it.position.x - layout.screenX) / dx
|
||||
|
||||
val d = index - round(index)
|
||||
val dp = d * dx
|
||||
val dpa = abs(dp)
|
||||
|
||||
if (dpa < 10.0) {
|
||||
selectedIndex = if (KeyModifier.CTRL !in it.modifiers) {
|
||||
round(index).toInt()
|
||||
} else {
|
||||
if (baseValue.size > minimumSequenceLength) {
|
||||
val oldValue = baseValue.map { it }
|
||||
baseValue.removeAt(round(index).toInt() - 1)
|
||||
baseEvents.valueChanged.trigger(ValueChangedEvent(this, oldValue, baseValue))
|
||||
}
|
||||
null
|
||||
}
|
||||
} else {
|
||||
if (KeyModifier.CTRL !in it.modifiers) {
|
||||
if (baseValue.size < maximumSequenceLength) {
|
||||
val q = query(it.position)
|
||||
val oldValue = baseValue.map { it }
|
||||
baseValue.add(index.toInt(), q.y.map(-1.0, 1.0, range.start, range.endInclusive))
|
||||
baseEvents.valueChanged.trigger(ValueChangedEvent(this, oldValue, baseValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
var hoverJob: Job? = null
|
||||
|
||||
mouse.exited.listen {
|
||||
hoverJob?.cancel()
|
||||
if (tooltip != null) {
|
||||
tooltip = null
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
mouse.moved.listen {
|
||||
hoverJob?.let { job ->
|
||||
job.cancel()
|
||||
}
|
||||
if (tooltip != null) {
|
||||
tooltip = null
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
if (baseValue.isNotEmpty()) {
|
||||
val dx = (layout.screenWidth / (baseValue.size + 1))
|
||||
val index = (it.position.x - layout.screenX) / dx
|
||||
val d = index - round(index)
|
||||
val dp = d * dx
|
||||
val dpa = abs(dp)
|
||||
|
||||
if (dpa < 10.0) {
|
||||
hoverJob = GlobalScope.launch {
|
||||
val readIndex = index.roundToInt() - 1
|
||||
if (readIndex >= 0 && readIndex < baseValue.size) {
|
||||
val value = String.format("%.0${precision}f", baseValue[readIndex])
|
||||
tooltip = Tooltip(this@SequenceEditorBase, it.position - Vector2(layout.screenX, layout.screenY), "$value")
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mouse.dragged.listen {
|
||||
val q = query(it.position)
|
||||
selectedIndex?.let { index ->
|
||||
val writeIndex = index - 1
|
||||
if (writeIndex >= 0 && writeIndex < baseValue.size) {
|
||||
val oldValue = baseValue.map { it }
|
||||
baseValue[writeIndex] = q.y.coerceIn(-1.0, 1.0).map(-1.0, 1.0, range.start, range.endInclusive)
|
||||
baseEvents.valueChanged.trigger(ValueChangedEvent(this, oldValue, baseValue))
|
||||
}
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
val controlArea = Rectangle(0.0, 0.0, layout.screenWidth, layout.screenHeight - footerHeight)
|
||||
|
||||
drawer.stroke = computedStyle.effectiveColor?.opacify(0.25)
|
||||
drawer.strokeWeight = (1.0)
|
||||
|
||||
|
||||
val zeroHeight = 0.0.map(range.start, range.endInclusive, -1.0, 1.0).coerceIn(-1.0, 1.0) * controlArea.height / -2.0
|
||||
drawer.lineSegment(0.0, controlArea.height / 2.0 + zeroHeight, layout.screenWidth, controlArea.height / 2.0 + zeroHeight)
|
||||
|
||||
drawer.strokeWeight = 7.0
|
||||
drawer.fill = computedStyle.effectiveColor
|
||||
|
||||
for (i in baseValue.indices) {
|
||||
val dx = layout.screenWidth / (baseValue.size + 1)
|
||||
val height = -baseValue[i].map(range.start, range.endInclusive, -1.0, 1.0).coerceIn(-1.0, 1.0) * controlArea.height / 2.0
|
||||
|
||||
val x = dx * (i + 1)
|
||||
drawer.lineCap = LineCap.ROUND
|
||||
drawer.stroke = computedStyle.effectiveColor
|
||||
drawer.lineSegment(x, controlArea.height / 2.0 + zeroHeight, x, controlArea.height / 2.0 + height)
|
||||
|
||||
drawer.stroke = computedStyle.effectiveColor?.shade(1.1)
|
||||
drawer.fill = ColorRGBa.PINK
|
||||
drawer.circle(x, controlArea.height / 2.0 + height, 7.0)
|
||||
}
|
||||
|
||||
drawer.isolated {
|
||||
drawer.translate(0.0, controlArea.height)
|
||||
drawer.fill = computedStyle.effectiveColor
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
drawer.fill = computedStyle.effectiveColor
|
||||
writer.cursor = Cursor(0.0, 4.0)
|
||||
writer.box = Rectangle(0.0, 4.0, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)
|
||||
writer.newLine()
|
||||
writer.text(label)
|
||||
}
|
||||
}
|
||||
|
||||
tooltip?.draw(drawer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import mu.KotlinLogging
|
||||
import org.openrndr.*
|
||||
import org.openrndr.draw.Cursor
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.LineCap
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.panel.style.Color
|
||||
import org.openrndr.panel.style.color
|
||||
import org.openrndr.panel.style.effectiveColor
|
||||
import org.openrndr.shape.Rectangle
|
||||
import java.text.NumberFormat
|
||||
import java.text.ParseException
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
data class Range(val min: Double, val max: Double) {
|
||||
val span: Double get() = max - min
|
||||
}
|
||||
|
||||
enum class SliderMode {
|
||||
RANGE,
|
||||
POINT,
|
||||
SEGMENT
|
||||
}
|
||||
|
||||
class Slider : Element(ElementType("slider")), DisposableElement {
|
||||
override var disposed = false
|
||||
override val handlesKeyboardFocus = true
|
||||
|
||||
var label = ""
|
||||
var precision = 3
|
||||
var mode = SliderMode.RANGE
|
||||
|
||||
var value: Double
|
||||
set(v) {
|
||||
val oldV = realValue
|
||||
realValue = clean(v)
|
||||
if (realValue != oldV) {
|
||||
draw.dirty = true
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, false, oldV, realValue))
|
||||
}
|
||||
}
|
||||
get() = realValue
|
||||
|
||||
private var interactiveValue: Double
|
||||
set(v) {
|
||||
val oldV = realValue
|
||||
realValue = clean(v)
|
||||
if (realValue != oldV) {
|
||||
draw.dirty = true
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, true, oldV, realValue))
|
||||
}
|
||||
}
|
||||
get() = realValue
|
||||
|
||||
|
||||
var range = Range(0.0, 10.0)
|
||||
set(value) {
|
||||
field = value
|
||||
this.value = this.value
|
||||
}
|
||||
private var realValue = 0.0
|
||||
|
||||
fun clean(value: Double): Double {
|
||||
val cleanV = value.coerceIn(range.min, range.max)
|
||||
val quantized = String.format("%.0${precision}f", cleanV).replace(",", ".").toDouble()
|
||||
return quantized
|
||||
}
|
||||
|
||||
class ValueChangedEvent(val source: Slider,
|
||||
val interactive: Boolean,
|
||||
val oldValue: Double,
|
||||
val newValue: Double)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("slider-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
private val margin = 7.0
|
||||
private var keyboardInput = ""
|
||||
|
||||
init {
|
||||
mouse.pressed.listen {
|
||||
val t = (it.position.x - layout.screenX - margin) / (layout.screenWidth - 2.0 * margin)
|
||||
interactiveValue = t * range.span + range.min
|
||||
it.cancelPropagation()
|
||||
}
|
||||
mouse.clicked.listen {
|
||||
val t = (it.position.x - layout.screenX - margin) / (layout.screenWidth - 2.0 * margin)
|
||||
interactiveValue = t * range.span + range.min
|
||||
it.cancelPropagation()
|
||||
}
|
||||
mouse.dragged.listen {
|
||||
val t = (it.position.x - layout.screenX - margin) / (layout.screenWidth - 2.0 * margin)
|
||||
interactiveValue = t * range.span + range.min
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.scrolled.listen {
|
||||
if (Math.abs(it.rotation.y) < 0.001) {
|
||||
interactiveValue += range.span * 0.001 * it.rotation.x
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
keyboard.focusLost.listen {
|
||||
keyboardInput = ""
|
||||
draw.dirty = true
|
||||
}
|
||||
|
||||
keyboard.character.listen {
|
||||
if (it.character in setOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ',', '-')) {
|
||||
try {
|
||||
val candidate = keyboardInput + it.character.toString()
|
||||
if (candidate.length > 1) {
|
||||
NumberFormat.getInstance().parse(candidate).toDouble()
|
||||
}
|
||||
keyboardInput = candidate
|
||||
requestRedraw()
|
||||
} catch (e: ParseException) {
|
||||
}
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
|
||||
keyboard.repeated.listen {
|
||||
val delta = Math.pow(10.0, -(precision - 0.0))
|
||||
if (it.key == KEY_ARROW_RIGHT) {
|
||||
interactiveValue += delta
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_ARROW_LEFT) {
|
||||
interactiveValue -= delta
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
}
|
||||
keyboard.pressed.listen {
|
||||
val delta = Math.pow(10.0, -(precision - 0.0))
|
||||
|
||||
if (it.key == KEY_ARROW_RIGHT) {
|
||||
interactiveValue += delta
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_ARROW_LEFT) {
|
||||
interactiveValue -= delta
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_BACKSPACE) {
|
||||
if (!keyboardInput.isEmpty()) {
|
||||
keyboardInput = keyboardInput.substring(0, keyboardInput.length - 1)
|
||||
draw.dirty = true
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_ESCAPE) {
|
||||
keyboardInput = ""
|
||||
draw.dirty = true
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_ENTER) {
|
||||
try {
|
||||
val number = NumberFormat.getInstance().parse(keyboardInput).toDouble()
|
||||
interactiveValue = number.coerceIn(range.min, range.max)
|
||||
} catch (e: ParseException) {
|
||||
// -- silently (but safely) ignore the exception
|
||||
}
|
||||
keyboardInput = ""
|
||||
draw.dirty = true
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_HOME) {
|
||||
interactiveValue = range.min
|
||||
keyboardInput = ""
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
if (it.key == KEY_END) {
|
||||
interactiveValue = range.max
|
||||
keyboardInput = ""
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
val f = (root() as? Body)?.controlManager?.fontManager?.font(computedStyle)!!
|
||||
drawer.translate(0.0, (layout.screenHeight - (10.0 + f.height)) / 2)
|
||||
|
||||
drawer.fill = ((computedStyle.color as Color.RGBa).color)
|
||||
drawer.stroke = ((computedStyle.color as Color.RGBa).color)
|
||||
drawer.strokeWeight = (8.0)
|
||||
drawer.lineCap = (LineCap.ROUND)
|
||||
val x = ((value - range.min) / range.span) * (layout.screenWidth - 2 * margin)
|
||||
|
||||
drawer.stroke = ((computedStyle.color as Color.RGBa).color.opacify(0.25))
|
||||
drawer.lineSegment(margin + 0.0, 2.0, margin + layout.screenWidth - 2 * margin, 2.0)
|
||||
|
||||
if (mode == SliderMode.RANGE) {
|
||||
drawer.stroke = ((computedStyle.color as Color.RGBa).color.opacify(1.0))
|
||||
drawer.lineSegment(margin, 2.0, margin + x, 2.0)
|
||||
|
||||
drawer.fill = ((computedStyle.color as Color.RGBa).color.opacify(1.0))
|
||||
drawer.stroke = null
|
||||
drawer.strokeWeight = 0.0
|
||||
drawer.circle(margin + x, 2.0, 5.0)
|
||||
}
|
||||
|
||||
if (mode == SliderMode.POINT && precision == 0) {
|
||||
val lineSegments = mutableListOf<Vector2>()
|
||||
for (i in range.min.toInt()..range.max.toInt()) {
|
||||
val lx = ((i - range.min) / range.span) * (layout.screenWidth - 2 * margin)
|
||||
drawer.strokeWeight = 1.0
|
||||
drawer.stroke = ((computedStyle.color as Color.RGBa).color.opacify(0.5))
|
||||
lineSegments.add(Vector2(margin + lx, -2.0))
|
||||
lineSegments.add(Vector2(margin + lx, 4.0))
|
||||
}
|
||||
drawer.lineSegments(lineSegments)
|
||||
}
|
||||
|
||||
if (mode == SliderMode.SEGMENT) {
|
||||
drawer.stroke = ((computedStyle.color as Color.RGBa).color.opacify(1.0))
|
||||
|
||||
val sx = ((value - range.min) / (range.span+1.0)) * (layout.screenWidth - 2 * margin) + margin
|
||||
val ex = (((value+1) - range.min) / (range.span+1.0)) * (layout.screenWidth - 2 * margin) + margin
|
||||
|
||||
drawer.strokeWeight = 8.0
|
||||
drawer.lineSegment(sx, 2.0, ex, 2.0)
|
||||
|
||||
drawer.stroke = null
|
||||
drawer.strokeWeight = 0.0
|
||||
|
||||
|
||||
val lineSegments = mutableListOf<Vector2>()
|
||||
for (i in range.min.toInt()..(range.max.toInt()+1)) {
|
||||
val lx = ((i - range.min) / (range.span+1.0)) * (layout.screenWidth - 2 * margin)
|
||||
drawer.strokeWeight = 1.0
|
||||
drawer.stroke = ((computedStyle.color as Color.RGBa).color.opacify(0.5))
|
||||
lineSegments.add(Vector2(margin + lx, -2.0))
|
||||
lineSegments.add(Vector2(margin + lx, 4.0))
|
||||
}
|
||||
drawer.lineSegments(lineSegments)
|
||||
}
|
||||
|
||||
|
||||
if (mode == SliderMode.POINT) {
|
||||
drawer.fill = ((computedStyle.color as Color.RGBa).color.opacify(1.0))
|
||||
drawer.stroke = null
|
||||
drawer.circle(margin + x, 2.0, 8.0)
|
||||
}
|
||||
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
drawer.fill = computedStyle.effectiveColor
|
||||
writer.cursor = Cursor(0.0, 8.0)
|
||||
writer.box = Rectangle(0.0, 8.0, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)
|
||||
writer.newLine()
|
||||
writer.text(label)
|
||||
|
||||
if (keyboardInput.isEmpty()) {
|
||||
val valueFormatted = String.format("%.0${precision}f", value)
|
||||
val tw = writer.textWidth(valueFormatted)
|
||||
writer.cursor.x = (layout.screenWidth - tw)
|
||||
writer.text(valueFormatted)
|
||||
} else {
|
||||
val tw = writer.textWidth(keyboardInput)
|
||||
writer.cursor.x = (layout.screenWidth - tw)
|
||||
writer.text(keyboardInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Slider.bind(property: KMutableProperty0<Double>) {
|
||||
var currentValue: Double? = null
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = it.newValue
|
||||
property.set(it.newValue)
|
||||
}
|
||||
GlobalScope.launch {
|
||||
while(!disposed) {
|
||||
val body = (root() as? Body)
|
||||
if (body != null) {
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur.toDouble()
|
||||
}
|
||||
}
|
||||
update()
|
||||
body.controlManager.program.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("bindInt")
|
||||
fun Slider.bind(property: KMutableProperty0<Int>) {
|
||||
var currentValue: Int? = null
|
||||
events.valueChanged.listen {
|
||||
currentValue = it.newValue.toInt()
|
||||
property.set(it.newValue.toInt())
|
||||
}
|
||||
GlobalScope.launch {
|
||||
while(!disposed) {
|
||||
val body = (root() as? Body)
|
||||
if (body != null) {
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur.toDouble()
|
||||
}
|
||||
}
|
||||
update()
|
||||
body.controlManager.program.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.launch
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.Vector4
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class SlidersVector2 : SequenceEditorBase("sliders-vector2") {
|
||||
var value : Vector2
|
||||
get() {
|
||||
return Vector2(baseValue[0], baseValue[1])
|
||||
}
|
||||
set(value) {
|
||||
baseValue[0] = value.x
|
||||
baseValue[1] = value.y
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
class ValueChangedEvent(val source: SequenceEditorBase,
|
||||
val oldValue: Vector2,
|
||||
val newValue: Vector2)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("sequence-editor-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
baseValue = mutableListOf(0.0, 0.0)
|
||||
minimumSequenceLength = 2
|
||||
maximumSequenceLength = 2
|
||||
baseEvents.valueChanged.listen {
|
||||
events.valueChanged.trigger(ValueChangedEvent(this,
|
||||
Vector2(it.oldValue[0], it.oldValue[1]),
|
||||
Vector2(it.newValue[0], it.newValue[1]))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SlidersVector2.bind(property: KMutableProperty0<Vector2>) {
|
||||
var currentValue: Vector2? = null
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = value
|
||||
property.set(it.newValue)
|
||||
}
|
||||
if (root() as? Body == null) {
|
||||
throw RuntimeException("no body")
|
||||
}
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SlidersVector3 : SequenceEditorBase("sliders-vector3") {
|
||||
var value : Vector3
|
||||
get() {
|
||||
return Vector3(baseValue[0], baseValue[1], baseValue[2])
|
||||
}
|
||||
set(value) {
|
||||
baseValue[0] = value.x
|
||||
baseValue[1] = value.y
|
||||
baseValue[2] = value.z
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
class ValueChangedEvent(val source: SequenceEditorBase,
|
||||
val oldValue: Vector3,
|
||||
val newValue: Vector3)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("sliders-vector3-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
baseValue = mutableListOf(0.0, 0.0, 0.0)
|
||||
minimumSequenceLength = 3
|
||||
maximumSequenceLength = 3
|
||||
baseEvents.valueChanged.listen {
|
||||
events.valueChanged.trigger(ValueChangedEvent(this,
|
||||
Vector3(it.oldValue[0], it.oldValue[1], it.oldValue[2]),
|
||||
Vector3(it.newValue[0], it.newValue[1], it.newValue[2]))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SlidersVector3.bind(property: KMutableProperty0<Vector3>) {
|
||||
var currentValue: Vector3? = null
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = value
|
||||
property.set(it.newValue)
|
||||
}
|
||||
if (root() as? Body == null) {
|
||||
throw RuntimeException("no body")
|
||||
}
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SlidersVector4 : SequenceEditorBase("sliders-vector4") {
|
||||
var value : Vector4
|
||||
get() {
|
||||
return Vector4(baseValue[0], baseValue[1], baseValue[2], baseValue[3])
|
||||
}
|
||||
set(value) {
|
||||
baseValue[0] = value.x
|
||||
baseValue[1] = value.y
|
||||
baseValue[2] = value.z
|
||||
baseValue[3] = value.w
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
class ValueChangedEvent(val source: SequenceEditorBase,
|
||||
val oldValue: Vector4,
|
||||
val newValue: Vector4)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("sliders-vector4-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
baseValue = mutableListOf(0.0, 0.0, 0.0, 0.0)
|
||||
minimumSequenceLength = 4
|
||||
maximumSequenceLength = 4
|
||||
baseEvents.valueChanged.listen {
|
||||
events.valueChanged.trigger(ValueChangedEvent(this,
|
||||
Vector4(it.oldValue[0], it.oldValue[1], it.oldValue[2], it.oldValue[3]),
|
||||
Vector4(it.newValue[0], it.newValue[1], it.newValue[2], it.newValue[3]))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SlidersVector4.bind(property: KMutableProperty0<Vector4>) {
|
||||
var currentValue: Vector4? = null
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = value
|
||||
property.set(it.newValue)
|
||||
}
|
||||
if (root() as? Body == null) {
|
||||
throw RuntimeException("no body")
|
||||
}
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.FontImageMap
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.launch
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.panel.style.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class TextNode(var text: String) : Element(ElementType("text")) {
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
computedStyle.let { style ->
|
||||
style.color.let {
|
||||
val fill = (it as? Color.RGBa)?.color ?: ColorRGBa.WHITE
|
||||
drawer.fill = (fill)
|
||||
}
|
||||
val fontMap = (root() as Body).controlManager.fontManager.font(computedStyle)
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (fontMap)
|
||||
|
||||
writer.box = Rectangle(Vector2(layout.screenX * 0.0, layout.screenY * 0.0), layout.screenWidth, layout.screenHeight)
|
||||
writer.newLine()
|
||||
writer.text(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun sizeHint(): Rectangle {
|
||||
computedStyle.let { style ->
|
||||
val fontUrl = (root() as? Body)?.controlManager?.fontManager?.resolve(style.fontFamily)?:"broken"
|
||||
val fontSize = (style.fontSize as? LinearDimension.PX)?.value?: 14.0
|
||||
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
|
||||
|
||||
val writer = Writer(null)
|
||||
|
||||
writer.box = Rectangle(layout.screenX,
|
||||
layout.screenY,
|
||||
layout.screenWidth,
|
||||
layout.screenHeight)
|
||||
|
||||
writer.drawStyle.fontMap = fontMap
|
||||
writer.newLine()
|
||||
writer.text(text, visible = false)
|
||||
|
||||
return Rectangle(layout.screenX,
|
||||
layout.screenY,
|
||||
layout.screenWidth,
|
||||
(writer.cursor.y - layout.screenY) - fontMap.descenderLength*2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "TextNode(id='$id',text='$text')"
|
||||
}
|
||||
}
|
||||
|
||||
class H1 : TextElement(ElementType("h1"))
|
||||
class H2 : TextElement(ElementType("h2"))
|
||||
class H3 : TextElement(ElementType("h3"))
|
||||
class H4 : TextElement(ElementType("h4"))
|
||||
class H5 : TextElement(ElementType("h5"))
|
||||
|
||||
class P : TextElement(ElementType("p"))
|
||||
|
||||
abstract class TextElement(et: ElementType) : Element(et) {
|
||||
fun text(text: String) {
|
||||
append(TextNode(text))
|
||||
requestRedraw()
|
||||
}
|
||||
fun replaceText(text : String) {
|
||||
if (children.isEmpty()) {
|
||||
text(text)
|
||||
} else {
|
||||
(children.first() as? TextNode)?.text = text
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TextElement.bind(property: KMutableProperty0<String>) {
|
||||
if (root() as? Body == null) {
|
||||
throw RuntimeException("no body")
|
||||
}
|
||||
var lastText = ""
|
||||
fun update() {
|
||||
if (property.get() != lastText) {
|
||||
replaceText(property.get())
|
||||
lastText = property.get()
|
||||
}
|
||||
}
|
||||
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
update()
|
||||
while (true) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.openrndr.KEY_BACKSPACE
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.LineCap
|
||||
import org.openrndr.panel.style.*
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.KeyModifier
|
||||
import org.openrndr.draw.Cursor
|
||||
import org.openrndr.draw.writer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.launch
|
||||
import org.openrndr.shape.Rectangle
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class Textfield : Element(ElementType("textfield")), DisposableElement {
|
||||
|
||||
var value: String = ""
|
||||
var label: String = "label"
|
||||
|
||||
class ValueChangedEvent(val source: Textfield, val oldValue: String, val newValue: String)
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("textfield-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
init {
|
||||
keyboard.repeated.listen {
|
||||
if (it.key == KEY_BACKSPACE) {
|
||||
if (value.isNotEmpty()) {
|
||||
val oldValue = value
|
||||
value = value.substring(0, value.length - 1)
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
keyboard.pressed.listen {
|
||||
if (KeyModifier.CTRL in it.modifiers || KeyModifier.SUPER in it.modifiers) {
|
||||
if (it.name == "v") {
|
||||
val oldValue = value
|
||||
(root() as Body).controlManager.program.clipboard.contents?.let {
|
||||
value += it
|
||||
|
||||
}
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
if (it.key == KEY_BACKSPACE) {
|
||||
if (value.isNotEmpty()) {
|
||||
val oldValue = value
|
||||
value = value.substring(0, value.length - 1)
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||
}
|
||||
}
|
||||
requestRedraw()
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
keyboard.character.listen {
|
||||
val oldValue = value
|
||||
value += it.character
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
mouse.clicked.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
drawer.fill = computedStyle.effectiveBackground
|
||||
drawer.stroke = null
|
||||
drawer.rectangle(0.0, 0.0, layout.screenWidth, layout.screenHeight)
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
|
||||
drawer.fontMap = (font)
|
||||
val textHeight = font.ascenderLength
|
||||
|
||||
val offset = 5.0
|
||||
val yOffset = Math.round((layout.screenHeight / 2) + textHeight / 2.0 - 2.0) * 1.0
|
||||
|
||||
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE)
|
||||
drawer.text(label, 0.0 + offset, 0.0 + yOffset - textHeight * 1.5)
|
||||
|
||||
drawer.fill = (((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE).opacify(0.05))
|
||||
drawer.rectangle(0.0 + offset, 0.0 + yOffset - (textHeight + 2), layout.screenWidth - 10.0, textHeight + 8.0)
|
||||
|
||||
drawer.drawStyle.clip = Rectangle(screenPosition.x + offset, screenPosition.y + yOffset - (textHeight + 2), layout.screenWidth - 10.0, textHeight + 8.0)
|
||||
|
||||
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE)
|
||||
|
||||
var cursorX = 0.0
|
||||
writer(drawer) {
|
||||
val emWidth = textWidth("m") * 2
|
||||
cursor = Cursor(offset, yOffset)
|
||||
text(value, visible = false)
|
||||
val width = cursor.x - offset
|
||||
val scroll =
|
||||
if (width > screenArea.width - emWidth) {
|
||||
screenArea.width - emWidth - width
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
cursor = Cursor(offset + scroll, yOffset)
|
||||
text(value)
|
||||
cursorX = cursor.x
|
||||
}
|
||||
|
||||
if (ElementPseudoClass("active") in pseudoClasses) {
|
||||
drawer.stroke = ColorRGBa.WHITE
|
||||
drawer.lineSegment(cursorX + 1.0, yOffset, cursorX + 1.0, yOffset - textHeight)
|
||||
}
|
||||
drawer.drawStyle.clip = null
|
||||
|
||||
drawer.stroke = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE)
|
||||
drawer.strokeWeight = 1.0
|
||||
|
||||
drawer.stroke = computedStyle.effectiveColor?.shade(0.25)
|
||||
drawer.lineCap = LineCap.ROUND
|
||||
}
|
||||
}
|
||||
|
||||
override var disposed: Boolean = false
|
||||
}
|
||||
|
||||
fun Textfield.bind(property: KMutableProperty0<String>) {
|
||||
GlobalScope.launch {
|
||||
install@ while (!disposed) {
|
||||
val body = (root() as? Body)
|
||||
if (body != null) {
|
||||
events.valueChanged.listen {
|
||||
property.set(it.newValue)
|
||||
}
|
||||
fun update() {
|
||||
val propertyValue = property.get()
|
||||
if (propertyValue != value) {
|
||||
value = propertyValue
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as Body).controlManager.program.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
break@install
|
||||
}
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.FontImageMap
|
||||
import org.openrndr.draw.LineCap
|
||||
import org.openrndr.panel.style.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.launch
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class Toggle : Element(ElementType("toggle")), DisposableElement {
|
||||
override var disposed = false
|
||||
|
||||
var label = ""
|
||||
var value = false
|
||||
|
||||
class ValueChangedEvent(val source: Toggle,
|
||||
val oldValue: Boolean,
|
||||
val newValue: Boolean)
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("toggle-value-changed")
|
||||
}
|
||||
|
||||
val events = Events()
|
||||
|
||||
override val widthHint: Double?
|
||||
get() {
|
||||
computedStyle.let { style ->
|
||||
val fontUrl = (root() as? Body)?.controlManager?.fontManager?.resolve(style.fontFamily) ?: "broken"
|
||||
val fontSize = (style.fontSize as? LinearDimension.PX)?.value ?: 14.0
|
||||
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
|
||||
|
||||
val writer = Writer(null)
|
||||
|
||||
writer.box = Rectangle(0.0,
|
||||
0.0,
|
||||
Double.POSITIVE_INFINITY,
|
||||
Double.POSITIVE_INFINITY)
|
||||
|
||||
writer.drawStyle.fontMap = fontMap
|
||||
writer.newLine()
|
||||
writer.text(label, visible = false)
|
||||
|
||||
return writer.cursor.x + (computedStyle.height as LinearDimension.PX).value - 8.0 + 5.0
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
mouse.clicked.listen {
|
||||
value = !value
|
||||
draw.dirty = true
|
||||
events.valueChanged.trigger(Toggle.ValueChangedEvent(this, !value, value))
|
||||
it.cancelPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the current value through the valueChanged event
|
||||
*/
|
||||
fun emit() {
|
||||
events.valueChanged.trigger(Toggle.ValueChangedEvent(this, value, value))
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
drawer.pushModel()
|
||||
val checkBoxSize = layout.screenHeight - 8.0
|
||||
drawer.translate(0.0, (layout.screenHeight - checkBoxSize) / 2.0)
|
||||
drawer.strokeWeight = 1.0
|
||||
drawer.stroke = computedStyle.effectiveColor
|
||||
drawer.fill = null
|
||||
drawer.rectangle(0.0, 0.0, checkBoxSize, checkBoxSize)
|
||||
|
||||
if (value) {
|
||||
drawer.strokeWeight = 2.0
|
||||
drawer.stroke = computedStyle.effectiveColor
|
||||
drawer.fill = null
|
||||
drawer.lineCap = LineCap.ROUND
|
||||
drawer.lineSegment(5.0, 5.0, checkBoxSize / 2.0 - 2.0, checkBoxSize / 2.0 - 2.0)
|
||||
drawer.lineSegment(checkBoxSize / 2.0 + 2.0, checkBoxSize / 2.0 + 2.0, checkBoxSize - 5.0, checkBoxSize - 5.0)
|
||||
drawer.lineSegment(checkBoxSize - 5.0, 5.0, checkBoxSize / 2.0 + 2.0, checkBoxSize / 2.0 - 2.0)
|
||||
drawer.lineSegment(checkBoxSize / 2.0 - 2.0, checkBoxSize / 2.0 + 2.0, 5.0, checkBoxSize - 5.0)
|
||||
}
|
||||
|
||||
drawer.popModel()
|
||||
drawer.fontMap = (root() as? Body)?.controlManager?.fontManager?.font(computedStyle)!!
|
||||
drawer.translate(5.0 + checkBoxSize, (layout.screenHeight / 2.0) + drawer.fontMap!!.height / 2.0)
|
||||
drawer.stroke = null
|
||||
drawer.fill = computedStyle.effectiveColor
|
||||
drawer.text(label, 0.0, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fun Toggle.bind(property: KMutableProperty0<Boolean>) {
|
||||
var currentValue = property.get()
|
||||
value = currentValue
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = it.newValue
|
||||
property.set(it.newValue)
|
||||
}
|
||||
GlobalScope.launch {
|
||||
while (!disposed) {
|
||||
val body = (root() as? Body)
|
||||
if (body != null) {
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as Body).controlManager.program.launch {
|
||||
while (!disposed) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.launch
|
||||
|
||||
class WatchListDiv<T : Any>(private val watchList: List<T>, private val builder: WatchListDiv<T>.(T) -> Unit) : Div(), DisposableElement {
|
||||
override var disposed: Boolean = false
|
||||
private var listState = emptyList<T>()
|
||||
private var watchJob: Job? = null
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
for (child in children) {
|
||||
child.parent = null
|
||||
(child as? DisposableElement)?.dispose()
|
||||
}
|
||||
children.clear()
|
||||
}
|
||||
|
||||
fun regenerate() {
|
||||
var regenerate = false
|
||||
if (listState.size != watchList.size) {
|
||||
regenerate = true
|
||||
}
|
||||
if (!regenerate) {
|
||||
for (i in watchList.indices) {
|
||||
if (watchList[i] !== listState[i]) {
|
||||
regenerate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (regenerate) {
|
||||
for (child in children) {
|
||||
child.parent = null
|
||||
(child as? DisposableElement)?.dispose()
|
||||
}
|
||||
children.clear()
|
||||
listState = watchList.map { it }
|
||||
for (i in watchList) {
|
||||
builder(i)
|
||||
}
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkJob() {
|
||||
if (watchJob == null) {
|
||||
watchJob = (root() as Body).controlManager.program.launch {
|
||||
while (!disposed) {
|
||||
regenerate()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun draw(drawer: Drawer) {
|
||||
checkJob()
|
||||
super.draw(drawer)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> Element.watchListDiv(vararg classes: String, watchList: List<T>, builder: WatchListDiv<T>.(T) -> Unit) {
|
||||
val wd = WatchListDiv(watchList, builder)
|
||||
wd.classes.addAll(classes.map { ElementClass(it) })
|
||||
this.append(wd)
|
||||
wd.regenerate()
|
||||
wd.checkJob()
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.launch
|
||||
import org.openrndr.panel.elements.*
|
||||
import org.openrndr.panel.hash.watchHash
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class WatchObjectDiv<T:Any>(
|
||||
val watchObject: T,
|
||||
private val builder: WatchObjectDiv<T>.(T) -> Unit
|
||||
) : Div(),
|
||||
DisposableElement {
|
||||
override var disposed: Boolean = false
|
||||
private var objectStateHash = watchHash(watchObject)
|
||||
private var watchJob: Job? = null
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
for (child in children) {
|
||||
child.parent = null
|
||||
(child as? DisposableElement)?.dispose()
|
||||
}
|
||||
children.clear()
|
||||
}
|
||||
|
||||
fun regenerate(force: Boolean = false) {
|
||||
var regenerate = force
|
||||
if (watchHash(watchObject) != objectStateHash) {
|
||||
regenerate = true
|
||||
}
|
||||
|
||||
if (regenerate) {
|
||||
for (child in children) {
|
||||
child.parent = null
|
||||
(child as? DisposableElement)?.dispose()
|
||||
}
|
||||
objectStateHash = watchHash(watchObject)
|
||||
children.clear()
|
||||
builder(watchObject)
|
||||
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkJob() {
|
||||
if (watchJob == null) {
|
||||
watchJob = (root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
regenerate()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
checkJob()
|
||||
super.draw(drawer)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> Element.watchObjectDiv(
|
||||
vararg classes: String,
|
||||
watchObject: T,
|
||||
builder: WatchObjectDiv<T>.(T) -> Unit
|
||||
) : WatchObjectDiv<T> {
|
||||
val wd = WatchObjectDiv(watchObject, builder)
|
||||
wd.classes.addAll(classes.map { ElementClass(it) })
|
||||
this.append(wd)
|
||||
wd.regenerate(true)
|
||||
wd.checkJob()
|
||||
return wd
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.launch
|
||||
import org.openrndr.panel.elements.*
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
class WatchPropertyDiv<T : Any>(
|
||||
private val watchProperty: KMutableProperty0<T>,
|
||||
private val builder: WatchPropertyDiv<T>.(T) -> Unit
|
||||
) : Div(),
|
||||
DisposableElement {
|
||||
override var disposed: Boolean = false
|
||||
private var propertyState = watchProperty.get()
|
||||
private var watchJob: Job? = null
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
for (child in children) {
|
||||
child.parent = null
|
||||
(child as? DisposableElement)?.dispose()
|
||||
}
|
||||
children.clear()
|
||||
}
|
||||
|
||||
fun regenerate(force: Boolean = false) {
|
||||
var regenerate = force
|
||||
if (watchProperty.get() != propertyState) {
|
||||
regenerate = true
|
||||
}
|
||||
|
||||
if (regenerate) {
|
||||
for (child in children) {
|
||||
child.parent = null
|
||||
(child as? DisposableElement)?.dispose()
|
||||
}
|
||||
propertyState = watchProperty.get()
|
||||
children.clear()
|
||||
builder(propertyState)
|
||||
|
||||
requestRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkJob() {
|
||||
if (watchJob == null) {
|
||||
watchJob = (root() as? Body)?.controlManager?.program?.launch {
|
||||
while (!disposed) {
|
||||
regenerate()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
checkJob()
|
||||
super.draw(drawer)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> Element.watchPropertyDiv(
|
||||
vararg classes: String,
|
||||
watchProperty: KMutableProperty0<T>,
|
||||
builder: WatchPropertyDiv<T>.(T) -> Unit
|
||||
) {
|
||||
val wd = WatchPropertyDiv(watchProperty, builder)
|
||||
wd.classes.addAll(classes.map { ElementClass(it) })
|
||||
this.append(wd)
|
||||
wd.regenerate(true)
|
||||
wd.checkJob()
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package org.openrndr.panel.elements
|
||||
|
||||
import kotlinx.coroutines.yield
|
||||
import org.openrndr.*
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.Writer
|
||||
import org.openrndr.events.Event
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.clamp
|
||||
import org.openrndr.math.map
|
||||
import org.openrndr.panel.style.Color
|
||||
import org.openrndr.panel.style.color
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.round
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
|
||||
class XYPad : Element(ElementType("xy-pad")) {
|
||||
var minX = -1.0
|
||||
var minY = -1.0
|
||||
var maxX = 1.0
|
||||
var maxY = 1.0
|
||||
|
||||
/**
|
||||
* The label
|
||||
*/
|
||||
var label = ""
|
||||
|
||||
/**
|
||||
* The precision of the control, default is 2
|
||||
*/
|
||||
var precision = 2
|
||||
|
||||
/**
|
||||
* Should the control visualize the value as a vector?, default is false
|
||||
*/
|
||||
var showVector = false
|
||||
|
||||
/**
|
||||
* Should the control invert the Y-axis?, default is true
|
||||
*/
|
||||
var invertY = true
|
||||
|
||||
// The value is derived from the normalized value...
|
||||
var normalizedValue = Vector2(0.0, 0.0)
|
||||
|
||||
var value: Vector2
|
||||
get() = Vector2(
|
||||
map(-1.0, 1.0, minX, maxX, normalizedValue.x).round(precision),
|
||||
map(-1.0, 1.0, minY, maxY, normalizedValue.y).round(precision)
|
||||
)
|
||||
set(newValue) {
|
||||
normalizedValue = Vector2(
|
||||
clamp(map(minX, maxX, -1.0, 1.0, newValue.x), -1.0, 1.0),
|
||||
clamp(map(minY, maxY, -1.0, 1.0, newValue.y), -1.0, 1.0)
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
mouse.clicked.listen {
|
||||
it.cancelPropagation()
|
||||
pick(it)
|
||||
}
|
||||
|
||||
mouse.dragged.listen {
|
||||
it.cancelPropagation()
|
||||
pick(it)
|
||||
}
|
||||
|
||||
mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
keyboard.pressed.listen { handleKeyEvent(it) }
|
||||
keyboard.repeated.listen { handleKeyEvent(it) }
|
||||
}
|
||||
|
||||
class ValueChangedEvent(val source: XYPad,
|
||||
val oldValue: Vector2,
|
||||
val newValue: Vector2)
|
||||
|
||||
|
||||
val events = Events()
|
||||
|
||||
class Events {
|
||||
val valueChanged = Event<ValueChangedEvent>("xypad-value-changed")
|
||||
}
|
||||
|
||||
|
||||
private fun handleKeyEvent(keyEvent: KeyEvent) {
|
||||
val keyboardIncrementX = if (KeyModifier.SHIFT in keyEvent.modifiers) {
|
||||
(maxX - minX) / 10.0
|
||||
} else {
|
||||
10.0.pow(-(precision - 0.0))
|
||||
}
|
||||
|
||||
val keyboardIncrementY = if (KeyModifier.SHIFT in keyEvent.modifiers) {
|
||||
(maxY - minY) / 10.0
|
||||
} else {
|
||||
10.0.pow(-(precision - 0.0))
|
||||
}
|
||||
|
||||
val old = value
|
||||
|
||||
if (keyEvent.key == KEY_ARROW_RIGHT) {
|
||||
value = Vector2(value.x + keyboardIncrementX, value.y)
|
||||
}
|
||||
|
||||
if (keyEvent.key == KEY_ARROW_LEFT) {
|
||||
value = Vector2(value.x - keyboardIncrementX, value.y)
|
||||
}
|
||||
|
||||
if (keyEvent.key == KEY_ARROW_UP) {
|
||||
value = Vector2(value.x, value.y - keyboardIncrementY * if (invertY) -1.0 else 1.0)
|
||||
}
|
||||
|
||||
if (keyEvent.key == KEY_ARROW_DOWN) {
|
||||
value = Vector2(value.x, value.y + keyboardIncrementY * if (invertY) -1.0 else 1.0)
|
||||
}
|
||||
|
||||
requestRedraw()
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, old, value))
|
||||
keyEvent.cancelPropagation()
|
||||
}
|
||||
|
||||
private fun pick(e: MouseEvent) {
|
||||
val old = value
|
||||
|
||||
// Difference
|
||||
val dx = e.position.x - layout.screenX
|
||||
val dy = e.position.y - layout.screenY
|
||||
|
||||
// Normalize to -1 - 1
|
||||
val nx = clamp(dx / layout.screenWidth * 2.0 - 1.0, -1.0, 1.0)
|
||||
val ny = clamp(dy / layout.screenHeight * 2.0 - 1.0, -1.0, 1.0) * if (invertY) -1.0 else 1.0
|
||||
|
||||
normalizedValue = Vector2(nx, ny)
|
||||
|
||||
events.valueChanged.trigger(ValueChangedEvent(this, old, value))
|
||||
requestRedraw()
|
||||
}
|
||||
|
||||
override val widthHint: Double?
|
||||
get() = 200.0
|
||||
|
||||
|
||||
private val ballPosition: Vector2
|
||||
get() = Vector2(
|
||||
map(-1.0, 1.0, 0.0, layout.screenWidth, normalizedValue.x),
|
||||
if (invertY) {
|
||||
map(1.0, -1.0, 0.0, layout.screenHeight, normalizedValue.y)
|
||||
} else {
|
||||
map(-1.0, 1.0, 0.0, layout.screenHeight, normalizedValue.y)
|
||||
}
|
||||
)
|
||||
|
||||
override fun draw(drawer: Drawer) {
|
||||
computedStyle.let {
|
||||
drawer.pushTransforms()
|
||||
drawer.pushStyle()
|
||||
drawer.fill = ColorRGBa.GRAY
|
||||
drawer.stroke = null
|
||||
drawer.strokeWeight = 0.0
|
||||
|
||||
drawer.rectangle(0.0, 0.0, layout.screenWidth, layout.screenHeight)
|
||||
|
||||
|
||||
// lines grid
|
||||
drawer.stroke = ColorRGBa.GRAY.shade(1.2)
|
||||
drawer.strokeWeight = 1.0
|
||||
|
||||
for (y in 0 until 21) {
|
||||
drawer.lineSegment(
|
||||
0.0,
|
||||
layout.screenHeight / 20 * y,
|
||||
layout.screenWidth - 1.0,
|
||||
layout.screenHeight / 20 * y
|
||||
)
|
||||
}
|
||||
|
||||
for (x in 0 until 21) {
|
||||
drawer.lineSegment(
|
||||
layout.screenWidth / 20 * x,
|
||||
0.0,
|
||||
layout.screenWidth / 20 * x,
|
||||
layout.screenHeight - 1.0
|
||||
)
|
||||
}
|
||||
|
||||
// cross
|
||||
drawer.stroke = ColorRGBa.GRAY.shade(1.6)
|
||||
// drawer.lineSegment(0.0, layout.screenHeight / 2.0, layout.screenWidth, layout.screenHeight / 2.0)
|
||||
// drawer.lineSegment(layout.screenWidth / 2.0, 0.0, layout.screenWidth / 2.0, layout.screenHeight)
|
||||
|
||||
// angle line from center
|
||||
if (showVector) {
|
||||
drawer.lineSegment(Vector2(layout.screenHeight / 2.0, layout.screenWidth / 2.0), ballPosition)
|
||||
}
|
||||
|
||||
// ball
|
||||
drawer.fill = ColorRGBa.PINK
|
||||
drawer.stroke = ColorRGBa.WHITE
|
||||
drawer.circle(ballPosition, 8.0)
|
||||
|
||||
val valueLabel = "${String.format("%.0${precision}f", value.x)}, ${String.format("%.0${precision}f", value.y)}"
|
||||
|
||||
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||
val font = it.font(computedStyle)
|
||||
val writer = Writer(drawer)
|
||||
drawer.fontMap = (font)
|
||||
val textWidth = writer.textWidth(valueLabel)
|
||||
val textHeight = font.ascenderLength
|
||||
|
||||
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE).opacify(
|
||||
if (disabled in pseudoClasses) 0.25 else 1.0
|
||||
)
|
||||
|
||||
|
||||
drawer.text(label, Vector2(4.0, 14.0))
|
||||
drawer.text(valueLabel, Vector2(layout.screenWidth - textWidth - 4.0, layout.screenHeight - textHeight + 6.0))
|
||||
}
|
||||
|
||||
drawer.popStyle()
|
||||
drawer.popTransforms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun XYPad.bind(property: KMutableProperty0<Vector2>) {
|
||||
var currentValue: Vector2? = null
|
||||
|
||||
events.valueChanged.listen {
|
||||
currentValue = it.newValue
|
||||
property.set(it.newValue)
|
||||
}
|
||||
if (root() as? Body == null) {
|
||||
throw RuntimeException("no body")
|
||||
}
|
||||
fun update() {
|
||||
if (property.get() != currentValue) {
|
||||
val lcur = property.get()
|
||||
currentValue = lcur
|
||||
value = lcur
|
||||
}
|
||||
}
|
||||
update()
|
||||
(root() as? Body)?.controlManager?.program?.launch {
|
||||
while (true) {
|
||||
update()
|
||||
yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Double.round(decimals: Int): Double {
|
||||
val multiplier = 10.0.pow(decimals)
|
||||
return round(this * multiplier) / multiplier
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.openrndr.panel.hash
|
||||
|
||||
import kotlin.reflect.KProperty0
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
|
||||
fun watchHash(toHash: Any): Int {
|
||||
var hash = 0
|
||||
for (property in toHash::class.declaredMemberProperties) {
|
||||
val v = ((property as KProperty1<Any, Any?>).getter).invoke(toHash)
|
||||
if (v is KProperty0<*>) {
|
||||
val pv = v.get()
|
||||
hash = 31 * hash + (pv?.hashCode() ?: 0)
|
||||
} else {
|
||||
hash = 31 * hash + (v?.hashCode() ?: 0)
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
package org.openrndr.panel.layout
|
||||
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.panel.elements.Element
|
||||
import org.openrndr.panel.elements.TextNode
|
||||
import org.openrndr.panel.style.*
|
||||
import org.openrndr.shape.Rectangle
|
||||
import java.util.*
|
||||
import kotlin.comparisons.compareBy
|
||||
import kotlin.math.max
|
||||
|
||||
class Layouter {
|
||||
val styleSheets = ArrayList<StyleSheet>()
|
||||
val blockLike = setOf(Display.BLOCK, Display.FLEX)
|
||||
val manualPosition = setOf(Position.FIXED, Position.ABSOLUTE)
|
||||
|
||||
fun positionChildren(element: Element, knownWidth:Double? = null): Rectangle {
|
||||
|
||||
return element.computedStyle.let { cs ->
|
||||
var y = element.layout.screenY - element.scrollTop + element.computedStyle.effectivePaddingTop
|
||||
|
||||
when (cs.display) {
|
||||
Display.FLEX -> {
|
||||
when (cs.flexDirection) {
|
||||
FlexDirection.Row -> {
|
||||
var maxHeight = 0.0
|
||||
var x = element.layout.screenX + element.computedStyle.effectivePaddingLeft
|
||||
|
||||
val totalWidth = element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.map { width(it) }.sum()
|
||||
val remainder = (knownWidth?: element.layout.screenWidth) - totalWidth
|
||||
val totalGrow = element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.map { (it.computedStyle.flexGrow as FlexGrow.Ratio).value }.sum()
|
||||
val totalShrink = element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.map { (it.computedStyle.flexShrink as FlexGrow.Ratio).value }.sum()
|
||||
|
||||
|
||||
element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.forEach { child ->
|
||||
val elementGrow = (child.computedStyle.flexGrow as FlexGrow.Ratio).value
|
||||
val elementShrink = (child.computedStyle.flexShrink as FlexGrow.Ratio).value
|
||||
val growWidth = if (totalGrow > 0) (elementGrow / totalGrow) * remainder else 0.0
|
||||
val shrinkWidth = if (totalShrink > 0) (elementShrink / totalShrink) * remainder else 0.0
|
||||
|
||||
child.layout.screenY = y + ((child.computedStyle.marginTop as? LinearDimension.PX)?.value
|
||||
?: 0.0)
|
||||
child.layout.screenX = x + ((child.computedStyle.marginLeft as? LinearDimension.PX)?.value
|
||||
?: 0.0)
|
||||
|
||||
child.layout.growWidth = if (remainder > 0) growWidth else shrinkWidth
|
||||
|
||||
val effectiveWidth = width(child) + (if (remainder > 0) growWidth else shrinkWidth)
|
||||
x += effectiveWidth
|
||||
maxHeight = max(height(child, effectiveWidth), maxHeight)
|
||||
}
|
||||
Rectangle(Vector2(x, y), x - element.layout.screenX, maxHeight)
|
||||
}
|
||||
FlexDirection.Column -> {
|
||||
var maxWidth = 0.0
|
||||
var ly = element.layout.screenY + element.computedStyle.effectivePaddingTop
|
||||
val lx = element.layout.screenX + element.computedStyle.effectivePaddingLeft
|
||||
|
||||
val verticalPadding = element.computedStyle.effectivePaddingTop + element.computedStyle.effectivePaddingBottom
|
||||
val totalHeight = element.children
|
||||
.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }
|
||||
.sumOf { height(it, width(it)) }
|
||||
val remainder = ((element.layout.screenHeight - verticalPadding) - totalHeight)
|
||||
val totalGrow = element.children
|
||||
.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }
|
||||
.sumOf { (it.computedStyle.flexGrow as FlexGrow.Ratio).value }
|
||||
|
||||
element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.forEach { child ->
|
||||
val elementGrow = (child.computedStyle.flexGrow as FlexGrow.Ratio).value
|
||||
val growHeight = if (totalGrow > 0) (elementGrow / totalGrow) * remainder else 0.0
|
||||
|
||||
child.layout.screenY = ly + ((child.computedStyle.marginTop as? LinearDimension.PX)?.value
|
||||
?: 0.0)
|
||||
child.layout.screenX = lx + ((child.computedStyle.marginLeft as? LinearDimension.PX)?.value
|
||||
?: 0.0)
|
||||
|
||||
child.layout.growHeight = growHeight
|
||||
|
||||
val effectHeight = height(child) + growHeight
|
||||
ly += effectHeight
|
||||
maxWidth = max(width(child), maxWidth)
|
||||
}
|
||||
|
||||
Rectangle(Vector2(lx, ly), maxWidth, ly - element.layout.screenY)
|
||||
}
|
||||
else -> Rectangle(Vector2(element.layout.screenX, element.layout.screenY), 0.0, 0.0)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val x = element.layout.screenX + element.computedStyle.effectivePaddingLeft
|
||||
var maxWidth = 0.0
|
||||
element.children.forEach {
|
||||
if (it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition) {
|
||||
it.layout.screenY = y + ((it.computedStyle.marginTop as? LinearDimension.PX)?.value ?: 0.0)
|
||||
it.layout.screenX = x + ((it.computedStyle.marginLeft as? LinearDimension.PX)?.value ?: 0.0)
|
||||
val effectiveWidth = width(it)
|
||||
maxWidth = max(effectiveWidth, maxWidth)
|
||||
y += height(it, effectiveWidth)
|
||||
} else if (it.computedStyle.position == Position.ABSOLUTE) {
|
||||
it.layout.screenX = element.layout.screenX + ((it.computedStyle.left as? LinearDimension.PX)?.value
|
||||
?: 0.0)
|
||||
it.layout.screenY = element.layout.screenY + ((it.computedStyle.top as? LinearDimension.PX)?.value
|
||||
?: 0.0)
|
||||
}
|
||||
}
|
||||
Rectangle(Vector2(element.layout.screenX, element.layout.screenY), maxWidth, y - element.layout.screenY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun computeStyles(element: Element) {
|
||||
val matcher = Matcher()
|
||||
|
||||
if (element is TextNode) {
|
||||
// TODO: figure out why this is needed
|
||||
element.computedStyle = element.parent?.computedStyle?.cascadeOnto(StyleSheet(CompoundSelector.DUMMY))
|
||||
?: StyleSheet(CompoundSelector.DUMMY)
|
||||
} else {
|
||||
element.computedStyle =
|
||||
styleSheets
|
||||
.filter {
|
||||
it.selector.let {
|
||||
matcher.matches(it, element)
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy({ it.precedence.component1() },
|
||||
{ it.precedence.component2() },
|
||||
{ it.precedence.component3() },
|
||||
{ it.precedence.component4() }))
|
||||
.reversed()
|
||||
.fold(StyleSheet(CompoundSelector.DUMMY), { a, b -> a.cascadeOnto(b) })
|
||||
|
||||
element.style?.let {
|
||||
element.computedStyle = it.cascadeOnto(element.computedStyle)
|
||||
}
|
||||
}
|
||||
element.computedStyle.let { cs ->
|
||||
|
||||
element.parent?.let { p ->
|
||||
cs.properties.forEach { (k, v) ->
|
||||
if ((v.value as? PropertyValue)?.inherit == true) {
|
||||
cs.properties[k] = p.computedStyle.getProperty(k) ?: v
|
||||
}
|
||||
}
|
||||
PropertyBehaviours.behaviours.forEach { (k, v) ->
|
||||
if (v.inheritance == PropertyInheritance.INHERIT && k !in cs.properties) {
|
||||
if (k in p.computedStyle.properties) {
|
||||
cs.properties[k] = p.computedStyle.getProperty(k)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.children.forEach { computeStyles(it) }
|
||||
}
|
||||
|
||||
fun margin(element: Element, f: (StyleSheet) -> LinearDimension): Double {
|
||||
val value = f(element.computedStyle)
|
||||
return when (value) {
|
||||
is LinearDimension.PX -> value.value
|
||||
else -> 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fun padding(element: Element?, f: (StyleSheet) -> LinearDimension): Double {
|
||||
return if (element != null) {
|
||||
val value = f(element.computedStyle)
|
||||
when (value) {
|
||||
is LinearDimension.PX -> value.value
|
||||
else -> 0.0
|
||||
}
|
||||
} else 0.0
|
||||
}
|
||||
|
||||
fun marginTop(element: Element) = margin(element, StyleSheet::marginTop)
|
||||
fun marginBottom(element: Element) = margin(element, StyleSheet::marginBottom)
|
||||
fun marginLeft(element: Element) = margin(element, StyleSheet::marginLeft)
|
||||
fun marginRight(element: Element) = margin(element, StyleSheet::marginRight)
|
||||
|
||||
fun paddingTop(element: Element?) = padding(element, StyleSheet::paddingTop)
|
||||
fun paddingBottom(element: Element?) = padding(element, StyleSheet::paddingBottom)
|
||||
fun paddingLeft(element: Element?) = padding(element, StyleSheet::paddingLeft)
|
||||
fun paddingRight(element: Element?) = padding(element, StyleSheet::paddingRight)
|
||||
|
||||
fun height(element: Element, width: Double? = null, includeMargins: Boolean = true): Double {
|
||||
if (element.computedStyle.display == Display.NONE) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
if (element is TextNode) {
|
||||
return element.sizeHint().height + if (includeMargins) marginBottom(element) + marginTop(element) else 0.0
|
||||
}
|
||||
|
||||
return element.computedStyle.let {
|
||||
it.height.let { ld ->
|
||||
when (val it = ld) {
|
||||
is LinearDimension.PX -> it.value
|
||||
is LinearDimension.Percent -> {
|
||||
val parentHeight = element.parent?.layout?.screenHeight ?: 0.0
|
||||
val parentPadding = element.parent?.computedStyle?.effectivePaddingHeight ?: 0.0
|
||||
val margins = marginTop(element) + marginBottom(element)
|
||||
val effectiveHeight = (parentHeight - parentPadding) * (it.value / 100.0) - margins
|
||||
effectiveHeight
|
||||
}
|
||||
is LinearDimension.Auto -> {
|
||||
val padding = paddingTop(element) + paddingBottom(element)
|
||||
(element.heightHint ?: positionChildren(element, width).height) + padding
|
||||
}
|
||||
is LinearDimension.Calculate -> {
|
||||
val context = CalculateContext(width, null)
|
||||
it.function(context)
|
||||
|
||||
}
|
||||
else -> throw RuntimeException("not supported")
|
||||
}
|
||||
} + if (includeMargins) ((it.marginTop as? LinearDimension.PX)?.value
|
||||
?: 0.0) + ((it.marginBottom as? LinearDimension.PX)?.value ?: 0.0) else 0.0
|
||||
}
|
||||
}
|
||||
|
||||
fun width(element: Element, height: Double? = null, includeMargins: Boolean = true): Double = element.computedStyle.let {
|
||||
if (element.computedStyle.display == Display.NONE) {
|
||||
return 0.0
|
||||
}
|
||||
val result =
|
||||
it.width.let {
|
||||
when (it) {
|
||||
is LinearDimension.PX -> it.value
|
||||
is LinearDimension.Percent -> {
|
||||
val parentWidth = element.parent?.layout?.screenWidth ?: 0.0
|
||||
val parentPadding = element.parent?.computedStyle?.effectivePaddingWidth ?: 0.0
|
||||
val margins = marginLeft(element) + marginRight(element)
|
||||
val effectiveWidth = (parentWidth - parentPadding) * (it.value / 100.0) - margins
|
||||
effectiveWidth
|
||||
}
|
||||
// is LinearDimension.Calculate -> {
|
||||
// val context = CalculateContext(null, height)
|
||||
// it.function(context)
|
||||
//
|
||||
// }
|
||||
is LinearDimension.Auto -> (element.widthHint ?: positionChildren(element).width) +
|
||||
paddingRight(element) + paddingLeft(element)
|
||||
else -> throw RuntimeException("not supported")
|
||||
}
|
||||
} + if (includeMargins) marginLeft(element) + marginRight(element) else 0.0
|
||||
|
||||
// TODO: find out why this hack is needed, I added this because somewhere in the layout process
|
||||
// this information is lost
|
||||
element.layout.screenWidth = result - if (includeMargins) marginLeft(element) + marginRight(element) else 0.0
|
||||
result
|
||||
}
|
||||
|
||||
fun layout(element: Element) {
|
||||
element.computedStyle.also { cs ->
|
||||
cs.display.let { if (it == Display.NONE) return }
|
||||
element.layout.screenWidth = width(element, includeMargins = false)
|
||||
element.layout.screenWidth += element.layout.growWidth
|
||||
element.layout.screenHeight = height(element, element.layout.screenWidth, includeMargins = false)
|
||||
element.layout.screenHeight += element.layout.growHeight
|
||||
|
||||
when (cs.position) {
|
||||
Position.FIXED -> {
|
||||
element.layout.screenX = (cs.left as? LinearDimension.PX)?.value ?: 0.0
|
||||
element.layout.screenY = (cs.top as? LinearDimension.PX)?.value ?: 0.0
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
val lzi = cs.zIndex
|
||||
element.layout.zIndex = when (lzi) {
|
||||
is ZIndex.Value -> lzi.value
|
||||
is ZIndex.Auto -> element.parent?.layout?.zIndex ?: 0
|
||||
is ZIndex.Inherit -> element.parent?.layout?.zIndex ?: 0
|
||||
}
|
||||
val result = positionChildren(element)
|
||||
}
|
||||
element.children.forEach { layout(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package org.openrndr.panel.style
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
|
||||
fun defaultStyles(
|
||||
controlBackground: ColorRGBa = ColorRGBa(0.5, 0.5, 0.5),
|
||||
controlHoverBackground: ColorRGBa = controlBackground.shade(1.5),
|
||||
controlTextColor: Color = Color.RGBa(ColorRGBa.WHITE.shade(0.8)),
|
||||
controlActiveColor : Color = Color.RGBa(ColorRGBa.fromHex(0xf88379 )),
|
||||
controlFontSize: Double = 14.0
|
||||
) = listOf(
|
||||
styleSheet(has type "item") {
|
||||
display = Display.NONE
|
||||
},
|
||||
|
||||
styleSheet(has type "textfield") {
|
||||
width = 100.percent
|
||||
height = 64.px
|
||||
and(has state "active") {
|
||||
color = controlActiveColor
|
||||
}
|
||||
},
|
||||
|
||||
styleSheet(has type "dropdown-button") {
|
||||
width = LinearDimension.Auto
|
||||
height = 32.px
|
||||
background = Color.RGBa(controlBackground)
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
fontSize = controlFontSize.px
|
||||
|
||||
and(has state "hover") {
|
||||
background = Color.RGBa(controlHoverBackground)
|
||||
}
|
||||
|
||||
descendant(has type "button") {
|
||||
width = 100.percent
|
||||
height = 24.px
|
||||
marginBottom = 0.px
|
||||
marginTop = 0.px
|
||||
marginLeft = 0.px
|
||||
marginRight = 0.px
|
||||
}
|
||||
},
|
||||
|
||||
styleSheet(has type "colorpicker-button") {
|
||||
width = 100.px
|
||||
height = 32.px
|
||||
background = Color.RGBa(controlBackground)
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
|
||||
and(has state "hover") {
|
||||
background = Color.RGBa(controlHoverBackground)
|
||||
}
|
||||
},
|
||||
|
||||
styleSheet(has type "envelope-button") {
|
||||
width = 100.px
|
||||
height = 40.px
|
||||
background = Color.RGBa(controlBackground)
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
},
|
||||
|
||||
styleSheet(has type "body") {
|
||||
fontSize = 18.px
|
||||
fontFamily = "default"
|
||||
},
|
||||
|
||||
styleSheet(has type "slider") {
|
||||
height = 32.px
|
||||
width = 100.percent
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
fontSize = controlFontSize.px
|
||||
color = controlTextColor
|
||||
|
||||
and(has state "active") {
|
||||
color = controlActiveColor
|
||||
}
|
||||
},
|
||||
|
||||
styleSheet(has type "envelope-editor") {
|
||||
height = 60.px
|
||||
width = 100.percent
|
||||
marginTop = 5.px
|
||||
marginBottom = 15.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
},
|
||||
|
||||
styleSheet(has type listOf(
|
||||
"sequence-editor",
|
||||
"sliders-vector2",
|
||||
"sliders-vector3",
|
||||
"sliders-vector4"
|
||||
)) {
|
||||
height = 60.px
|
||||
width = 100.percent
|
||||
marginTop = 5.px
|
||||
marginBottom = 15.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
color = controlTextColor
|
||||
and(has state "active") {
|
||||
color = controlActiveColor
|
||||
}
|
||||
},
|
||||
|
||||
styleSheet(has type "colorpicker") {
|
||||
height = 80.px
|
||||
width = 100.percent
|
||||
marginTop = 5.px
|
||||
marginBottom = 15.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
},
|
||||
|
||||
styleSheet(has type "xy-pad") {
|
||||
display = Display.BLOCK
|
||||
background = Color.RGBa(ColorRGBa.GRAY)
|
||||
width = 175.px
|
||||
height = 175.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
fontFamily = "default"
|
||||
|
||||
and(has state "hover") {
|
||||
display = Display.BLOCK
|
||||
background = Color.RGBa(ColorRGBa.GRAY.shade(1.5))
|
||||
}
|
||||
},
|
||||
|
||||
styleSheet(has type "overlay") {
|
||||
zIndex = ZIndex.Value(1)
|
||||
},
|
||||
|
||||
styleSheet(has type "toggle") {
|
||||
height = 32.px
|
||||
width = LinearDimension.Auto
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
fontSize = controlFontSize.px
|
||||
color = controlTextColor
|
||||
},
|
||||
|
||||
styleSheet(has type "h1") {
|
||||
fontSize = 24.px
|
||||
width = 100.percent
|
||||
height = LinearDimension.Auto
|
||||
display = Display.BLOCK
|
||||
},
|
||||
|
||||
styleSheet(has type "h2") {
|
||||
fontSize = 20.px
|
||||
width = 100.percent
|
||||
height = LinearDimension.Auto
|
||||
display = Display.BLOCK
|
||||
},
|
||||
|
||||
styleSheet(has type "h3") {
|
||||
fontSize = 16.px
|
||||
width = 100.percent
|
||||
height = LinearDimension.Auto
|
||||
display = Display.BLOCK
|
||||
},
|
||||
|
||||
styleSheet(has type "p") {
|
||||
fontSize = 16.px
|
||||
width = 100.percent
|
||||
height = LinearDimension.Auto
|
||||
display = Display.BLOCK
|
||||
},
|
||||
styleSheet(has type "button") {
|
||||
display = Display.BLOCK
|
||||
background = Color.RGBa(controlBackground)
|
||||
width = LinearDimension.Auto
|
||||
height = 32.px
|
||||
paddingLeft = 10.px
|
||||
paddingRight = 10.px
|
||||
marginLeft = 5.px
|
||||
marginRight = 5.px
|
||||
marginTop = 5.px
|
||||
marginBottom = 5.px
|
||||
fontSize = controlFontSize.px
|
||||
|
||||
and(has state "active") {
|
||||
display = Display.BLOCK
|
||||
background = controlActiveColor
|
||||
}
|
||||
and(has state "hover") {
|
||||
display = Display.BLOCK
|
||||
background = Color.RGBa(controlHoverBackground)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.openrndr.panel.style
|
||||
|
||||
import org.openrndr.panel.elements.Element
|
||||
|
||||
class Matcher {
|
||||
enum class MatchingResult {
|
||||
MATCHED, NOT_MATCHED, RESTART_FROM_CLOSEST_DESCENDANT, RESTART_FROM_CLOSEST_LATER_SIBLING
|
||||
}
|
||||
|
||||
fun matches(selector: CompoundSelector, element: Element): Boolean {
|
||||
return matchesCompound(selector, element) == MatchingResult.MATCHED
|
||||
}
|
||||
|
||||
private fun matchesCompound(selector: CompoundSelector, element: Element): MatchingResult {
|
||||
if (selector.selectors.any { !it.accept(element) }) {
|
||||
return MatchingResult.RESTART_FROM_CLOSEST_LATER_SIBLING
|
||||
}
|
||||
|
||||
if (selector.previous == null) {
|
||||
return MatchingResult.MATCHED
|
||||
}
|
||||
|
||||
val (siblings, candidateNotFound) =
|
||||
when (selector.previous?.first) {
|
||||
Combinator.NEXT_SIBLING, Combinator.LATER_SIBLING -> Pair(true, MatchingResult.RESTART_FROM_CLOSEST_DESCENDANT)
|
||||
else -> Pair(false, MatchingResult.NOT_MATCHED)
|
||||
}
|
||||
|
||||
var node = element
|
||||
while (true) {
|
||||
val nextNode = if (siblings) node.previousSibling() else node.parent
|
||||
|
||||
if (nextNode == null) {
|
||||
return candidateNotFound
|
||||
} else {
|
||||
node = nextNode
|
||||
}
|
||||
|
||||
val result = matchesCompound(selector.previous?.second!!, node)
|
||||
|
||||
if (result == MatchingResult.MATCHED || result == MatchingResult.NOT_MATCHED) {
|
||||
return result
|
||||
}
|
||||
|
||||
when (selector.previous?.first) {
|
||||
Combinator.CHILD -> return MatchingResult.RESTART_FROM_CLOSEST_DESCENDANT
|
||||
Combinator.NEXT_SIBLING -> return result
|
||||
Combinator.LATER_SIBLING -> if (result == MatchingResult.RESTART_FROM_CLOSEST_DESCENDANT) {
|
||||
return result
|
||||
}
|
||||
Combinator.DESCENDANT -> {
|
||||
// intentionally do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.openrndr.panel.style
|
||||
|
||||
import org.openrndr.panel.elements.Element
|
||||
import org.openrndr.panel.elements.ElementClass
|
||||
import org.openrndr.panel.elements.ElementPseudoClass
|
||||
import org.openrndr.panel.elements.ElementType
|
||||
|
||||
data class SelectorPrecedence(var inlineStyle: Int = 0, var id: Int = 0, var classOrAttribute: Int = 0, var type: Int = 0)
|
||||
|
||||
abstract class Selector {
|
||||
abstract fun accept(element: Element): Boolean
|
||||
}
|
||||
|
||||
class CompoundSelector {
|
||||
companion object {
|
||||
val DUMMY = CompoundSelector()
|
||||
}
|
||||
|
||||
var previous: Pair<Combinator, CompoundSelector>?
|
||||
var selectors: MutableList<Selector>
|
||||
|
||||
constructor() {
|
||||
previous = null
|
||||
selectors = mutableListOf()
|
||||
}
|
||||
|
||||
constructor(previous: Pair<Combinator, CompoundSelector>?, selectors: List<Selector>) {
|
||||
this.previous = previous
|
||||
this.selectors = ArrayList()
|
||||
selectors.forEach { this.selectors.add(it) }
|
||||
}
|
||||
|
||||
fun precedence(p: SelectorPrecedence = SelectorPrecedence()): SelectorPrecedence {
|
||||
|
||||
selectors.forEach {
|
||||
when (it) {
|
||||
is IdentitySelector -> p.id++
|
||||
is ClassSelector, is PseudoClassSelector -> p.classOrAttribute++
|
||||
is TypeSelector -> p.type++
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
var r = p
|
||||
previous?.let {
|
||||
r = it.second.precedence(p)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CompoundSelector(previous=$previous, selectors=$selectors)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class Combinator {
|
||||
CHILD, DESCENDANT, NEXT_SIBLING, LATER_SIBLING
|
||||
}
|
||||
|
||||
class IdentitySelector(val id: String) : Selector() {
|
||||
override fun accept(element: Element): Boolean = if (element.id != null) {
|
||||
element.id.equals(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "IdentitySelector(id='$id')"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ClassSelector(val c: ElementClass) : Selector() {
|
||||
override fun accept(element: Element): Boolean = c in element.classes
|
||||
override fun toString(): String {
|
||||
return "ClassSelector(c=$c)"
|
||||
}
|
||||
}
|
||||
|
||||
class TypeSelector(val type: ElementType) : Selector() {
|
||||
override fun accept(element: Element): Boolean = element.type == type
|
||||
override fun toString(): String {
|
||||
return "TypeSelector(type=$type)"
|
||||
}
|
||||
}
|
||||
|
||||
class TypesSelector(vararg types: ElementType) : Selector() {
|
||||
private val typeSet = types.toSet()
|
||||
override fun accept(element: Element): Boolean = element.type in typeSet
|
||||
override fun toString(): String {
|
||||
return "TypesSelector(types=$typeSet)"
|
||||
}
|
||||
}
|
||||
|
||||
class PseudoClassSelector(val c: ElementPseudoClass) : Selector() {
|
||||
override fun accept(element: Element): Boolean = c in element.pseudoClasses
|
||||
override fun toString(): String {
|
||||
return "PseudoClassSelector(c=$c)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object has {
|
||||
operator fun invoke(vararg selectors: CompoundSelector): CompoundSelector {
|
||||
val active = CompoundSelector()
|
||||
selectors.forEach {
|
||||
active.selectors.addAll(it.selectors)
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
infix fun state(q: String): CompoundSelector {
|
||||
val active = CompoundSelector()
|
||||
active.selectors.add(PseudoClassSelector(ElementPseudoClass((q))))
|
||||
return active
|
||||
}
|
||||
|
||||
infix fun class_(q: String): CompoundSelector {
|
||||
val active = CompoundSelector()
|
||||
active.selectors.add(ClassSelector(ElementClass(q)))
|
||||
return active
|
||||
}
|
||||
|
||||
infix fun type(q: String): CompoundSelector {
|
||||
val active = CompoundSelector()
|
||||
active.selectors.add(TypeSelector(ElementType(q)))
|
||||
return active
|
||||
}
|
||||
|
||||
infix fun type(qs: Iterable<String>): CompoundSelector {
|
||||
val active = CompoundSelector()
|
||||
val aqs = qs.map { ElementType(it) }.toList().toTypedArray()
|
||||
active.selectors.add(TypesSelector(*aqs))
|
||||
return active
|
||||
}
|
||||
}
|
||||
|
||||
infix fun CompoundSelector.and(other: CompoundSelector): CompoundSelector {
|
||||
val c = CompoundSelector()
|
||||
c.previous = previous
|
||||
c.selectors.addAll(selectors)
|
||||
c.selectors.addAll(other.selectors)
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package org.openrndr.panel.style
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.panel.style.PropertyInheritance.INHERIT
|
||||
import org.openrndr.panel.style.PropertyInheritance.RESET
|
||||
import java.util.*
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
enum class PropertyInheritance {
|
||||
INHERIT,
|
||||
RESET
|
||||
}
|
||||
|
||||
data class Property(val name: String,
|
||||
val value: Any?)
|
||||
|
||||
open class PropertyValue(val inherit: Boolean = false)
|
||||
|
||||
sealed class Color(inherit: Boolean = false) : PropertyValue(inherit) {
|
||||
class RGBa(val color: ColorRGBa) : Color() {
|
||||
override fun toString(): String {
|
||||
return "RGBa(color=$color)"
|
||||
}
|
||||
}
|
||||
|
||||
object Inherit : Color(inherit = true)
|
||||
}
|
||||
|
||||
class CalculateContext(val elementWidth: Double?, val elementHeight: Double?)
|
||||
|
||||
sealed class LinearDimension(inherit: Boolean = false) : PropertyValue(inherit) {
|
||||
class PX(val value: Double) : LinearDimension() {
|
||||
override fun toString(): String {
|
||||
return "PX(value=$value)"
|
||||
}
|
||||
}
|
||||
|
||||
class Percent(val value: Double) : LinearDimension()
|
||||
class Calculate(val function: (CalculateContext) -> Double) : LinearDimension()
|
||||
object Auto : LinearDimension()
|
||||
object Inherit : LinearDimension(inherit = true)
|
||||
}
|
||||
|
||||
|
||||
data class PropertyBehaviour(val inheritance: PropertyInheritance, val intitial: Any)
|
||||
|
||||
object PropertyBehaviours {
|
||||
|
||||
val behaviours = HashMap<String, PropertyBehaviour>()
|
||||
}
|
||||
|
||||
class PropertyHandler<T>(
|
||||
val name: String, val inheritance: PropertyInheritance, val initial: T
|
||||
) {
|
||||
|
||||
init {
|
||||
PropertyBehaviours.behaviours[name] = PropertyBehaviour(inheritance, initial as Any)
|
||||
}
|
||||
|
||||
@Suppress("USELESS_CAST", "UNCHECKED_CAST")
|
||||
operator fun getValue(stylesheet: StyleSheet, property: KProperty<*>): T {
|
||||
val value: T? = stylesheet.getProperty(name)?.value as T?
|
||||
return value ?: PropertyBehaviours.behaviours[name]!!.intitial as T
|
||||
|
||||
}
|
||||
|
||||
operator fun setValue(stylesheet: StyleSheet, property: KProperty<*>, value: T?) {
|
||||
stylesheet.setProperty(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Display {
|
||||
INLINE,
|
||||
BLOCK,
|
||||
FLEX,
|
||||
NONE
|
||||
}
|
||||
|
||||
enum class Position {
|
||||
STATIC,
|
||||
ABSOLUTE,
|
||||
RELATIVE,
|
||||
FIXED,
|
||||
INHERIT
|
||||
}
|
||||
|
||||
sealed class FlexDirection(inherit: Boolean = false) : PropertyValue(inherit) {
|
||||
object Row : FlexDirection()
|
||||
object Column : FlexDirection()
|
||||
object RowReverse : FlexDirection()
|
||||
object ColumnReverse : FlexDirection()
|
||||
object Inherit : FlexDirection(inherit = true)
|
||||
}
|
||||
|
||||
sealed class Overflow(inherit: Boolean = false) : PropertyValue(inherit) {
|
||||
object Visible : Overflow()
|
||||
object Hidden : Overflow()
|
||||
object Scroll : Overflow()
|
||||
object Inherit : Overflow(inherit = true)
|
||||
}
|
||||
|
||||
sealed class ZIndex(inherit: Boolean = false) : PropertyValue(inherit) {
|
||||
object Auto : ZIndex()
|
||||
class Value(val value: Int) : ZIndex()
|
||||
object Inherit : ZIndex(inherit = true)
|
||||
}
|
||||
|
||||
sealed class FlexGrow(inherit: Boolean = false) : PropertyValue(inherit) {
|
||||
class Ratio(val value: Double) : FlexGrow()
|
||||
object Inherit : FlexGrow(inherit = true)
|
||||
}
|
||||
|
||||
private val dummySelector = CompoundSelector()
|
||||
|
||||
class StyleSheet(val selector: CompoundSelector = CompoundSelector.DUMMY) {
|
||||
val children = mutableListOf<StyleSheet>()
|
||||
val properties = HashMap<String, Property>()
|
||||
|
||||
val precedence by lazy {
|
||||
selector.precedence()
|
||||
}
|
||||
|
||||
fun getProperty(name: String) = properties.get(name)
|
||||
|
||||
fun setProperty(name: String, value: Any?) {
|
||||
properties[name] = Property(name, value)
|
||||
}
|
||||
|
||||
fun cascadeOnto(onto: StyleSheet): StyleSheet {
|
||||
val cascaded = StyleSheet(dummySelector)
|
||||
|
||||
cascaded.properties.putAll(onto.properties)
|
||||
cascaded.properties.putAll(properties)
|
||||
return cascaded
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "StyleSheet(properties=$properties)"
|
||||
}
|
||||
}
|
||||
|
||||
var StyleSheet.width by PropertyHandler<LinearDimension>("width", RESET, LinearDimension.Auto)
|
||||
var StyleSheet.height by PropertyHandler<LinearDimension>("height", RESET, LinearDimension.Auto)
|
||||
var StyleSheet.top by PropertyHandler<LinearDimension>("top", RESET, 0.px) // css default is auto
|
||||
var StyleSheet.left by PropertyHandler<LinearDimension>("left", RESET, 0.px) // css default is auto
|
||||
|
||||
var StyleSheet.marginTop by PropertyHandler<LinearDimension>("margin-top", RESET, 0.px)
|
||||
var StyleSheet.marginBottom by PropertyHandler<LinearDimension>("margin-bottom", RESET, 0.px)
|
||||
var StyleSheet.marginLeft by PropertyHandler<LinearDimension>("margin-left", RESET, 0.px)
|
||||
var StyleSheet.marginRight by PropertyHandler<LinearDimension>("margin-right", RESET, 0.px)
|
||||
|
||||
|
||||
var StyleSheet.paddingTop by PropertyHandler<LinearDimension>("padding-top", RESET, 0.px)
|
||||
var StyleSheet.paddingBottom by PropertyHandler<LinearDimension>("padding-bottom", RESET, 0.px)
|
||||
var StyleSheet.paddingLeft by PropertyHandler<LinearDimension>("padding-left", RESET, 0.px)
|
||||
var StyleSheet.paddingRight by PropertyHandler<LinearDimension>("padding-right", RESET, 0.px)
|
||||
|
||||
|
||||
var StyleSheet.position by PropertyHandler("position", RESET, Position.STATIC)
|
||||
var StyleSheet.display by PropertyHandler("display", RESET, Display.BLOCK) // css default is inline
|
||||
|
||||
var StyleSheet.flexDirection by PropertyHandler<FlexDirection>("flex-direction", RESET, FlexDirection.Row)
|
||||
var StyleSheet.flexGrow by PropertyHandler<FlexGrow>("flex-grow", RESET, FlexGrow.Ratio(0.0))
|
||||
var StyleSheet.flexShrink by PropertyHandler<FlexGrow>("flex-shrink", RESET, FlexGrow.Ratio(1.0))
|
||||
|
||||
var StyleSheet.borderWidth by PropertyHandler<LinearDimension>("border-width", RESET, 0.px)
|
||||
var StyleSheet.borderColor by PropertyHandler<Color>("border-color", INHERIT, Color.RGBa(ColorRGBa.TRANSPARENT))
|
||||
|
||||
var StyleSheet.background by PropertyHandler<Color>("background-color", RESET, Color.RGBa(ColorRGBa.BLACK.opacify(0.0)))
|
||||
val StyleSheet.effectiveBackground: ColorRGBa?
|
||||
get() = (background as? Color.RGBa)?.color
|
||||
|
||||
var StyleSheet.color by PropertyHandler<Color>("color", INHERIT, Color.RGBa(ColorRGBa.WHITE))
|
||||
val StyleSheet.effectiveColor: ColorRGBa?
|
||||
get() = (color as? Color.RGBa)?.color
|
||||
|
||||
|
||||
val StyleSheet.effectivePaddingLeft: Double
|
||||
get() = (paddingLeft as? LinearDimension.PX)?.value ?: 0.0
|
||||
|
||||
val StyleSheet.effectivePaddingRight: Double
|
||||
get() = (paddingRight as? LinearDimension.PX)?.value ?: 0.0
|
||||
|
||||
val StyleSheet.effectivePaddingTop: Double
|
||||
get() = (paddingTop as? LinearDimension.PX)?.value ?: 0.0
|
||||
|
||||
val StyleSheet.effectivePaddingBottom: Double
|
||||
get() = (paddingBottom as? LinearDimension.PX)?.value ?: 0.0
|
||||
|
||||
|
||||
val StyleSheet.effectivePaddingHeight: Double
|
||||
get() = effectivePaddingBottom + effectivePaddingTop
|
||||
|
||||
val StyleSheet.effectivePaddingWidth: Double
|
||||
get() = effectivePaddingLeft + effectivePaddingRight
|
||||
|
||||
|
||||
val StyleSheet.effectiveBorderWidth: Double
|
||||
get() = (borderWidth as? LinearDimension.PX)?.value ?: 0.0
|
||||
|
||||
val StyleSheet.effectiveBorderColor: ColorRGBa?
|
||||
get() = (borderColor as? Color.RGBa)?.color
|
||||
|
||||
|
||||
var StyleSheet.fontSize by PropertyHandler<LinearDimension>("font-size", INHERIT, 14.px)
|
||||
var StyleSheet.fontFamily by PropertyHandler("font-family", INHERIT, "default")
|
||||
var StyleSheet.overflow by PropertyHandler<Overflow>("overflow", RESET, Overflow.Visible)
|
||||
var StyleSheet.zIndex by PropertyHandler<ZIndex>("z-index", RESET, ZIndex.Auto)
|
||||
|
||||
val Number.px: LinearDimension.PX get() = LinearDimension.PX(this.toDouble())
|
||||
val Number.percent: LinearDimension.Percent get() = LinearDimension.Percent(this.toDouble())
|
||||
|
||||
fun StyleSheet.child(selector: CompoundSelector, init: StyleSheet.() -> Unit) {
|
||||
val stylesheet = StyleSheet(selector).apply(init)
|
||||
stylesheet.selector.previous = Pair(Combinator.CHILD, this.selector)
|
||||
children.add(stylesheet)
|
||||
}
|
||||
|
||||
fun StyleSheet.descendant(selector: CompoundSelector, init: StyleSheet.() -> Unit) {
|
||||
val stylesheet = StyleSheet(selector).apply(init)
|
||||
stylesheet.selector.previous = Pair(Combinator.DESCENDANT, this.selector)
|
||||
children.add(stylesheet)
|
||||
}
|
||||
|
||||
fun StyleSheet.and(selector: CompoundSelector, init: StyleSheet.() -> Unit) {
|
||||
val stylesheet = StyleSheet(this.selector and selector).apply(init)
|
||||
this.children.add(stylesheet)
|
||||
}
|
||||
|
||||
fun StyleSheet.flatten(): List<StyleSheet> {
|
||||
return listOf(this) + children.flatMap { it.flatten() }
|
||||
}
|
||||
|
||||
fun styleSheet(selector: CompoundSelector = CompoundSelector.DUMMY, init: StyleSheet.() -> Unit): StyleSheet {
|
||||
return StyleSheet(selector).apply {
|
||||
init()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.openrndr.panel.tools
|
||||
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.FontImageMap
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.draw.writer
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.panel.elements.Body
|
||||
import org.openrndr.panel.elements.Element
|
||||
import kotlin.math.max
|
||||
|
||||
class Tooltip(val parent: Element, val position: Vector2, val message: String) {
|
||||
fun draw(drawer: Drawer) {
|
||||
|
||||
val fontUrl = (parent.root() as Body).controlManager.fontManager.resolve("default") ?: error("no font")
|
||||
val fontSize = 14.0
|
||||
val fontMap = FontImageMap.fromUrl(fontUrl, fontSize)
|
||||
val lines = message.split("\n")
|
||||
|
||||
drawer.isolated {
|
||||
drawer.fontMap = fontMap
|
||||
|
||||
var maxX = 0.0
|
||||
var maxY = 0.0
|
||||
writer(drawer) {
|
||||
for (line in lines) {
|
||||
newLine()
|
||||
text(line, false)
|
||||
maxX = max(maxX, cursor.x)
|
||||
maxY = cursor.y
|
||||
}
|
||||
gaplessNewLine()
|
||||
maxY = cursor.y
|
||||
}
|
||||
|
||||
drawer.translate(position)
|
||||
drawer.translate(10.0, 0.0)
|
||||
drawer.strokeWeight = 0.5
|
||||
drawer.stroke = ColorRGBa.WHITE.opacify(0.25)
|
||||
drawer.fill = ColorRGBa.GRAY
|
||||
drawer.rectangle(0.0, 0.0, maxX + 20.0, maxY)
|
||||
drawer.fill = ColorRGBa.BLACK
|
||||
drawer.translate(10.0, 0.0)
|
||||
writer(drawer) {
|
||||
for (line in lines) {
|
||||
newLine()
|
||||
text(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
orx-jvm/orx-panel/src/main/resources/fonts/Roboto-Medium.ttf
Normal file
BIN
orx-jvm/orx-panel/src/main/resources/fonts/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
orx-jvm/orx-panel/src/main/resources/fonts/Roboto-Regular.ttf
Normal file
BIN
orx-jvm/orx-panel/src/main/resources/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
//package org.openrndr.panel.test
|
||||
//
|
||||
//import net.lustlab.panel.elements.Element
|
||||
//import net.lustlab.panel.elements.ElementClass
|
||||
//import net.lustlab.panel.elements.ElementType
|
||||
//import net.lustlab.panel.style.*
|
||||
//import org.jetbrains.spek.api.Spek
|
||||
//import org.jetbrains.spek.api.dsl.describe
|
||||
//import org.jetbrains.spek.api.dsl.it
|
||||
//import kotlin.test.assertEquals
|
||||
//import kotlin.test.assertFalse
|
||||
//import kotlin.test.assertNotNull
|
||||
//import kotlin.test.assertTrue
|
||||
//
|
||||
///**
|
||||
// * Created by voorbeeld on 11/20/16.
|
||||
// */
|
||||
//class SomeTest : Spek({
|
||||
//
|
||||
// describe("a thing") {
|
||||
//
|
||||
// // .panel > button
|
||||
// val cs = selector(class_="panel") withChild selector(type="button", class_="fancy")
|
||||
//
|
||||
// val root = Element(ElementType("body"))
|
||||
// val panel = Element(ElementType("div")).apply {
|
||||
// classes+= ElementClass("panel")
|
||||
// }
|
||||
// val button = Element(ElementType("button"))
|
||||
// val button2 = Element(ElementType("button")).apply {
|
||||
// classes+= ElementClass("fancy")
|
||||
// }
|
||||
//
|
||||
// root.append(panel)
|
||||
// panel.append(button)
|
||||
// panel.append(button2)
|
||||
//
|
||||
// it("should work") {
|
||||
// assert(cs.selectors.size == 1)
|
||||
// assertTrue(cs.selectors[0] is TypeSelector)
|
||||
// assertNotNull(cs.previous)
|
||||
//
|
||||
// assertFalse(Matcher().matches(cs, button))
|
||||
// assertFalse(Matcher().matches(cs, panel))
|
||||
// assertTrue(Matcher().matches(cs, button2))
|
||||
// }
|
||||
//
|
||||
// it("should have precedences") {
|
||||
// println(cs.precedence())
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
//})
|
||||
//
|
||||
@@ -0,0 +1,18 @@
|
||||
//package org.openrndr.panel.test
|
||||
//
|
||||
//import net.lustlab.panel.style.*
|
||||
//import org.jetbrains.spek.api.Spek
|
||||
//import org.jetbrains.spek.api.dsl.describe
|
||||
//
|
||||
//class StyleSheetTest : Spek({
|
||||
//
|
||||
// describe("stylesheet") {
|
||||
// val styleSheet = StyleSheet()
|
||||
// styleSheet.width = 5.px
|
||||
// styleSheet.height = 5.px
|
||||
// styleSheet.left = 10.px
|
||||
// styleSheet.top = 10.percent
|
||||
// styleSheet.position = Position.FIXED
|
||||
// var a = styleSheet.precedence
|
||||
// }
|
||||
//})
|
||||
Reference in New Issue
Block a user