[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm

This commit is contained in:
Edwin Jakobs
2021-06-27 21:32:24 +02:00
parent 5814acef8f
commit 874d49779f
159 changed files with 22 additions and 21 deletions

View 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)
![DemoHorizontalLayout01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-panel/images/DemoHorizontalLayout01Kt.png)
### DemoVerticalLayout01
[source code](src/demo/kotlin/DemoVerticalLayout01.kt)
![DemoVerticalLayout01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-panel/images/DemoVerticalLayout01Kt.png)
### DemoWatchDiv01
[source code](src/demo/kotlin/DemoWatchDiv01.kt)
![DemoWatchDiv01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-panel/images/DemoWatchDiv01Kt.png)
### DemoWatchObjectDiv01
[source code](src/demo/kotlin/DemoWatchObjectDiv01.kt)
![DemoWatchObjectDiv01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-panel/images/DemoWatchObjectDiv01Kt.png)

View 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)
}

View 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)
}
}
}

View 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 {
}
}
}

View 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)
}
}

View 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)
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -0,0 +1,5 @@
package org.openrndr.panel.elements
import org.openrndr.panel.ControlManager
class Body(val controlManager: ControlManager) : Element(ElementType("Body"))

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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})"
}
}

View File

@@ -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()
}
}
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}
)

View File

@@ -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
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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())
//
//
// }
//
// }
//
//})
//

View File

@@ -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
// }
//})