Move Panel into orx-panel
This commit is contained in:
@@ -16,7 +16,6 @@ apply plugin: 'org.jetbrains.dokka'
|
|||||||
|
|
||||||
project.ext {
|
project.ext {
|
||||||
openrndrVersion = "0.3.40-rc.5"
|
openrndrVersion = "0.3.40-rc.5"
|
||||||
panelVersion = "0.3.22-rc.3"
|
|
||||||
kotlinVersion = "1.3.70"
|
kotlinVersion = "1.3.70"
|
||||||
spekVersion = "2.0.10"
|
spekVersion = "2.0.10"
|
||||||
libfreenectVersion = "0.5.7-1.5.2"
|
libfreenectVersion = "0.5.7-1.5.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
api project(":orx-parameters")
|
api project(":orx-parameters")
|
||||||
api "org.openrndr.panel:openrndr-panel:$panelVersion"
|
api project(":orx-panel")
|
||||||
implementation "org.openrndr:openrndr-dialogs:$openrndrVersion"
|
implementation "org.openrndr:openrndr-dialogs:$openrndrVersion"
|
||||||
implementation "com.google.code.gson:gson:$gsonVersion"
|
implementation "com.google.code.gson:gson:$gsonVersion"
|
||||||
}
|
}
|
||||||
592
orx-panel/src/main/kotlin/org/openrndr/panel/ControlManager.kt
Normal file
592
orx-panel/src/main/kotlin/org/openrndr/panel/ControlManager.kt
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
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.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 {
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: Program.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, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.scale.x
|
||||||
|
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
|
||||||
|
|
||||||
|
body?.draw?.dirty = true
|
||||||
|
|
||||||
|
if (renderTarget.colorBuffers.isNotEmpty()) {
|
||||||
|
renderTarget.colorBuffer(0).destroy()
|
||||||
|
renderTarget.depthBuffer?.destroy()
|
||||||
|
renderTarget.detachColorBuffers()
|
||||||
|
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.background(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.background(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.background(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.background(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
|
||||||
|
}
|
||||||
24
orx-panel/src/main/kotlin/org/openrndr/panel/FontManager.kt
Normal file
24
orx-panel/src/main/kotlin/org/openrndr/panel/FontManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(name: String, url: String) {
|
||||||
|
registry[name] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.openrndr.panel.collections
|
||||||
|
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
class ObservableCopyOnWriteArrayList<E> : CopyOnWriteArrayList<E>() {
|
||||||
|
|
||||||
|
val changed = Event<ObservableCopyOnWriteArrayList<E>>()
|
||||||
|
override fun add(element: E): Boolean {
|
||||||
|
return if (super.add(element)) {
|
||||||
|
changed.trigger(this)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(element: E): Boolean {
|
||||||
|
return if (super.remove(element)) {
|
||||||
|
changed.trigger(this)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
super.clear()
|
||||||
|
changed.trigger(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.openrndr.panel.collections
|
||||||
|
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ObservableHashSet<E> : HashSet<E>() {
|
||||||
|
|
||||||
|
class ChangeEvent<E>(val source: ObservableHashSet<E>, val added: Set<E>, val removed: Set<E>)
|
||||||
|
|
||||||
|
val changed = Event<ChangeEvent<E>>()
|
||||||
|
|
||||||
|
override fun add(element: E): Boolean {
|
||||||
|
return if (super.add(element)) {
|
||||||
|
changed.trigger(ChangeEvent(this, setOf(element), emptySet()))
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(element: E): Boolean {
|
||||||
|
return if (super.remove(element)) {
|
||||||
|
changed.trigger(ChangeEvent(this, emptySet(), setOf(element)))
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
val old = this.toSet()
|
||||||
|
super.clear()
|
||||||
|
changed.trigger(ChangeEvent(this, emptySet(), old))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.panel.ControlManager
|
||||||
|
|
||||||
|
class Body(val controlManager: ControlManager) : Element(ElementType("Body"))
|
||||||
100
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Button.kt
Normal file
100
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Button.kt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.draw.FontImageMap
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import org.openrndr.panel.style.*
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
import org.openrndr.text.Writer
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
|
||||||
|
class Button : Element(ElementType("button")) {
|
||||||
|
|
||||||
|
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.stroke = null
|
||||||
|
drawer.strokeWeight = 0.0
|
||||||
|
|
||||||
|
drawer.rectangle(0.0, 0.0, layout.screenWidth, layout.screenHeight)
|
||||||
|
|
||||||
|
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||||
|
val font = it.font(computedStyle)
|
||||||
|
val writer = Writer(drawer)
|
||||||
|
drawer.fontMap = (font)
|
||||||
|
val textWidth = writer.textWidth(label)
|
||||||
|
val textHeight = font.ascenderLength
|
||||||
|
|
||||||
|
val offset = round((layout.screenWidth - textWidth) / 2.0)
|
||||||
|
val yOffset = round((layout.screenHeight / 2) + textHeight / 2.0 - 2.0) * 1.0
|
||||||
|
|
||||||
|
drawer.fill = ((computedStyle.color as? Color.RGBa)?.color ?: ColorRGBa.WHITE).opacify(
|
||||||
|
if (disabled in pseudoClasses) 0.25 else 1.0
|
||||||
|
)
|
||||||
|
drawer.text(label, 0.0 + offset, 0.0 + yOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer.popStyle()
|
||||||
|
drawer.popTransforms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.*
|
||||||
|
import org.openrndr.math.Matrix44
|
||||||
|
|
||||||
|
class Canvas : Element(ElementType("canvas")) {
|
||||||
|
var userDraw: ((Drawer) -> Unit)? = null
|
||||||
|
private var renderTarget: RenderTarget? = null
|
||||||
|
|
||||||
|
override fun draw(drawer: Drawer) {
|
||||||
|
val width = screenArea.width.toInt()
|
||||||
|
val height = screenArea.height.toInt()
|
||||||
|
|
||||||
|
if (renderTarget != null) {
|
||||||
|
if (renderTarget?.width != width || renderTarget?.height != height) {
|
||||||
|
renderTarget?.colorBuffer(0)?.destroy()
|
||||||
|
renderTarget?.destroy()
|
||||||
|
renderTarget = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenArea.width >= 1 && screenArea.height >= 1) {
|
||||||
|
if (renderTarget == null) {
|
||||||
|
renderTarget = renderTarget(screenArea.width.toInt(), screenArea.height.toInt(), drawer.context.contentScale) {
|
||||||
|
colorBuffer()
|
||||||
|
depthBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTarget?.let { rt ->
|
||||||
|
drawer.isolatedWithTarget(rt) {
|
||||||
|
model = Matrix44.IDENTITY
|
||||||
|
view = Matrix44.IDENTITY
|
||||||
|
background(ColorRGBa.TRANSPARENT)
|
||||||
|
size(screenArea.width.toInt(), screenArea.height.toInt())
|
||||||
|
ortho(rt)
|
||||||
|
userDraw?.invoke(this)
|
||||||
|
}
|
||||||
|
drawer.image(rt.colorBuffer(0), 0.0, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.KEY_BACKSPACE
|
||||||
|
import org.openrndr.KEY_ENTER
|
||||||
|
import org.openrndr.KEY_ESCAPE
|
||||||
|
import org.openrndr.MouseEvent
|
||||||
|
import org.openrndr.color.ColorHSVa
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.ColorBuffer
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.draw.colorBuffer
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import org.openrndr.panel.style.Color
|
||||||
|
import org.openrndr.panel.style.color
|
||||||
|
|
||||||
|
class Colorpicker : Element {
|
||||||
|
|
||||||
|
internal var colorMap: ColorBuffer? = null
|
||||||
|
|
||||||
|
var label: String = "Color"
|
||||||
|
|
||||||
|
var saturation = 0.5
|
||||||
|
var color: ColorRGBa
|
||||||
|
set(value) {
|
||||||
|
realColor = value
|
||||||
|
saturation = color.toHSVa().s
|
||||||
|
generateColorMap()
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
get() {
|
||||||
|
return realColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private var realColor = ColorRGBa.WHITE
|
||||||
|
private var focussed = false
|
||||||
|
|
||||||
|
class ColorChangedEvent(val source: Colorpicker,
|
||||||
|
val oldColor: ColorRGBa,
|
||||||
|
val newColor: ColorRGBa)
|
||||||
|
|
||||||
|
class Events {
|
||||||
|
val colorChanged = Event<ColorChangedEvent>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val events = Events()
|
||||||
|
|
||||||
|
private var keyboardInput = ""
|
||||||
|
private fun pick(e: MouseEvent) {
|
||||||
|
val dx = e.position.x - layout.screenX
|
||||||
|
var dy = e.position.y - layout.screenY
|
||||||
|
|
||||||
|
dy = 50.0 - dy
|
||||||
|
val oldColor = color
|
||||||
|
val hsv = ColorHSVa(360.0 / layout.screenWidth * dx, saturation, dy / 50.0)
|
||||||
|
realColor = hsv.toRGBa()
|
||||||
|
draw.dirty = true
|
||||||
|
events.colorChanged.trigger(ColorChangedEvent(this, oldColor, realColor))
|
||||||
|
e.cancelPropagation()
|
||||||
|
}
|
||||||
|
constructor() : super(ElementType("colorpicker")) {
|
||||||
|
generateColorMap()
|
||||||
|
|
||||||
|
mouse.exited.listen {
|
||||||
|
focussed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse.scrolled.listen {
|
||||||
|
if (colorMap != null) {
|
||||||
|
//if (focussed) {
|
||||||
|
saturation = (saturation - it.rotation.y * 0.01).coerceIn(0.0, 1.0)
|
||||||
|
generateColorMap()
|
||||||
|
colorMap?.shadow?.upload()
|
||||||
|
it.cancelPropagation()
|
||||||
|
pick(it)
|
||||||
|
requestRedraw()
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard.focusLost.listen {
|
||||||
|
keyboardInput = ""
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard.character.listen {
|
||||||
|
keyboardInput += it.character
|
||||||
|
draw.dirty = true
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard.pressed.listen {
|
||||||
|
if (it.key == KEY_BACKSPACE) {
|
||||||
|
if (!keyboardInput.isEmpty()) {
|
||||||
|
keyboardInput = keyboardInput.substring(0, keyboardInput.length - 1)
|
||||||
|
draw.dirty = true
|
||||||
|
|
||||||
|
}
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.key == KEY_ESCAPE) {
|
||||||
|
keyboardInput = ""
|
||||||
|
draw.dirty = true
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (it.key == KEY_ENTER) {
|
||||||
|
val number = if (keyboardInput.length == 6) keyboardInput.toIntOrNull(16) else null
|
||||||
|
|
||||||
|
number?.let {
|
||||||
|
val r = (number shr 16) and 0xff
|
||||||
|
val g = (number shr 8) and 0xff
|
||||||
|
val b = number and 0xff
|
||||||
|
val oldColor = color
|
||||||
|
color = ColorRGBa(r / 255.0, g / 255.0, b / 255.0)
|
||||||
|
events.colorChanged.trigger(ColorChangedEvent(this, oldColor, realColor))
|
||||||
|
keyboardInput = ""
|
||||||
|
draw.dirty = true
|
||||||
|
|
||||||
|
}
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mouse.pressed.listen { it.cancelPropagation(); focussed = true }
|
||||||
|
mouse.clicked.listen { it.cancelPropagation(); pick(it); focussed = true; }
|
||||||
|
mouse.dragged.listen { it.cancelPropagation(); pick(it); focussed = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateColorMap() {
|
||||||
|
colorMap?.shadow?.let {
|
||||||
|
for (y in 0..49) {
|
||||||
|
for (x in 0 until it.colorBuffer.width) {
|
||||||
|
val hsv = ColorHSVa(360.0 / it.colorBuffer.width * x, saturation, (49 - y) / 49.0)
|
||||||
|
it.write(x, y, hsv.toRGBa())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.upload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(drawer: Drawer) {
|
||||||
|
if (colorMap == null) {
|
||||||
|
colorMap = colorBuffer(layout.screenWidth.toInt(), 50, 1.0)
|
||||||
|
generateColorMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer.image(colorMap!!, 0.0, 0.0)
|
||||||
|
drawer.fill = (color)
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.shadeStyle = null
|
||||||
|
drawer.rectangle(0.0, 50.0, layout.screenWidth, 20.0)
|
||||||
|
|
||||||
|
val f = (root() as? Body)?.controlManager?.fontManager?.font(computedStyle)!!
|
||||||
|
drawer.fontMap = f
|
||||||
|
drawer.fill = ((computedStyle.color as Color.RGBa).color)
|
||||||
|
|
||||||
|
if (keyboardInput.isNotBlank()) {
|
||||||
|
drawer.text("input: $keyboardInput", 0.0, layout.screenHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.draw.LineCap
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import org.openrndr.panel.style.*
|
||||||
|
import org.openrndr.text.Writer
|
||||||
|
|
||||||
|
class ColorpickerButton : Element(ElementType("colorpicker-button")) {
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Div.kt
Normal file
44
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Div.kt
Normal 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})"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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 org.openrndr.text.Writer
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
import org.openrndr.KEY_ARROW_DOWN
|
||||||
|
import org.openrndr.KEY_ARROW_UP
|
||||||
|
import org.openrndr.KEY_ENTER
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import org.openrndr.launch
|
||||||
|
import kotlin.math.min
|
||||||
|
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")) {
|
||||||
|
|
||||||
|
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 = Math.round((layout.screenHeight / 2) + textHeight / 2.0) - 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 {
|
||||||
|
itemButtons[it]?.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 {
|
||||||
|
itemButtons[it]?.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 = Math.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 (true) {
|
||||||
|
val cval = property.get()
|
||||||
|
if (cval != currentValue) {
|
||||||
|
currentValue = cval
|
||||||
|
value = options[cval]
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
289
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Element.kt
Normal file
289
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Element.kt
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.DropEvent
|
||||||
|
import org.openrndr.KeyEvent
|
||||||
|
import org.openrndr.MouseEvent
|
||||||
|
import org.openrndr.Program
|
||||||
|
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.StyleSheet
|
||||||
|
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
|
||||||
|
|
||||||
|
open class Element(val type: ElementType) {
|
||||||
|
|
||||||
|
var scrollTop = 0.0
|
||||||
|
open val handlesDoubleClick = false
|
||||||
|
|
||||||
|
open val widthHint: 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<Program.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 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.visit(function: Element.() -> Unit) {
|
||||||
|
this.function()
|
||||||
|
children.forEach { it.visit(function) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.panel.style.*
|
||||||
|
import org.openrndr.text.Cursor
|
||||||
|
import org.openrndr.text.Writer
|
||||||
|
|
||||||
|
class EnvelopeButton : Element(ElementType("envelope-button")) {
|
||||||
|
|
||||||
|
var label = "OK"
|
||||||
|
var envelope = Envelope()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
envelopeSubscription?.let {
|
||||||
|
value.events.envelopeChanged.cancel(it)
|
||||||
|
}
|
||||||
|
envelopeSubscription = value.events.envelopeChanged.listen {
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var envelopeSubscription: ((Envelope.EnvelopeChangedEvent)->Unit)? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
mouse.clicked.listen {
|
||||||
|
append(SlideOut(0.0, screenArea.height, screenArea.width, 200.0, this))
|
||||||
|
}
|
||||||
|
envelopeSubscription = envelope.events.envelopeChanged.listen {
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun append(element: Element) {
|
||||||
|
when (element) {
|
||||||
|
is Item, is SlideOut -> super.append(element)
|
||||||
|
else -> throw RuntimeException("only item and slideout")
|
||||||
|
}
|
||||||
|
super.append(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun items(): List<Item> = children.filter { it is Item }.map { it as Item }
|
||||||
|
|
||||||
|
override fun draw(drawer: Drawer) {
|
||||||
|
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||||
|
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||||
|
|
||||||
|
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||||
|
var chartHeight = 0.0
|
||||||
|
|
||||||
|
(root() as? Body)?.controlManager?.fontManager?.let {
|
||||||
|
val font = it.font(computedStyle)
|
||||||
|
|
||||||
|
val writer = Writer(drawer)
|
||||||
|
drawer.fontMap = (font)
|
||||||
|
drawer.fill = (ColorRGBa.BLACK)
|
||||||
|
writer.cursor = Cursor(0.0,layout.screenHeight - 4.0)
|
||||||
|
chartHeight = writer.cursor.y - font.height-4
|
||||||
|
writer.text("$label")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val w = layout.screenWidth
|
||||||
|
val h = chartHeight
|
||||||
|
val m = envelope.points.map {
|
||||||
|
val v = (Vector2(w, h) * it)
|
||||||
|
Vector2(v.x, h - v.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.size > 1) {
|
||||||
|
drawer.stroke = (ColorRGBa.WHITE)
|
||||||
|
drawer.strokeWeight = (2.0)
|
||||||
|
drawer.lineStrip(m)
|
||||||
|
}
|
||||||
|
if (m.size == 1) {
|
||||||
|
drawer.stroke = (ColorRGBa.WHITE)
|
||||||
|
drawer.strokeWeight = (2.0)
|
||||||
|
drawer.lineSegment(0.0, m[0].y, layout.screenWidth, m[0].y)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer.stroke = (ColorRGBa.BLACK.opacify(0.25))
|
||||||
|
drawer.strokeWeight = (1.0)
|
||||||
|
drawer.lineSegment(envelope.offset * w, 0.0, envelope.offset * w, chartHeight)
|
||||||
|
|
||||||
|
drawer.lineSegment(0.0, 0.0, 3.0, 0.0)
|
||||||
|
drawer.lineSegment(0.0, 0.0, 0.0, chartHeight)
|
||||||
|
drawer.lineSegment(0.0, chartHeight, 3.0, chartHeight)
|
||||||
|
|
||||||
|
drawer.lineSegment(w, 0.0, w-3.0, 0.0)
|
||||||
|
drawer.lineSegment(w, 0.0, w, chartHeight)
|
||||||
|
drawer.lineSegment(w, chartHeight, w-3.0, chartHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SlideOut(val x: Double, val y: Double, val width: Double, val height: Double, parent: EnvelopeButton) : Element(ElementType("envelope-slide-out")) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
mouse.clicked.listen {
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
style = StyleSheet(CompoundSelector.DUMMY).apply {
|
||||||
|
position = Position.ABSOLUTE
|
||||||
|
left = LinearDimension.PX(x)
|
||||||
|
top = LinearDimension.PX(y)
|
||||||
|
width = LinearDimension.PX(this@SlideOut.width)
|
||||||
|
height = LinearDimension.Auto//LinearDimension.PX(this@SlideOut.height)
|
||||||
|
overflow = Overflow.Scroll
|
||||||
|
zIndex = ZIndex.Value(1)
|
||||||
|
background = Color.RGBa(ColorRGBa(0.3, 0.3, 0.3))
|
||||||
|
}
|
||||||
|
|
||||||
|
append(EnvelopeEditor().apply {
|
||||||
|
envelope = parent.envelope
|
||||||
|
})
|
||||||
|
|
||||||
|
append(Button().apply {
|
||||||
|
label = "done"
|
||||||
|
events.clicked.listen {
|
||||||
|
//parent.value = it.source.data as Item
|
||||||
|
//parent.events.valueChanged.onNext(ValueChangedEvent(parent, it.source.data as Item))
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(drawer: Drawer) {
|
||||||
|
drawer.fill = ((computedStyle.background as? Color.RGBa)?.color ?: ColorRGBa.PINK)
|
||||||
|
drawer.rectangle(0.0, 0.0, screenArea.width, screenArea.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
parent?.remove(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import org.openrndr.MouseButton
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.KeyModifier
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
|
||||||
|
class Envelope(constant:Double = 0.5) {
|
||||||
|
|
||||||
|
val points = mutableListOf(Vector2(0.5, constant))
|
||||||
|
var activePoint: Vector2? = null
|
||||||
|
|
||||||
|
var offset:Double = 0.0
|
||||||
|
set(value) { field = value; events.envelopeChanged.trigger(EnvelopeChangedEvent(this))}
|
||||||
|
|
||||||
|
class EnvelopeChangedEvent(val envelope: Envelope)
|
||||||
|
|
||||||
|
class Events {
|
||||||
|
val envelopeChanged = Event<EnvelopeChangedEvent>("envelope-changed")
|
||||||
|
}
|
||||||
|
val events = Events()
|
||||||
|
|
||||||
|
fun insertPoint(v: Vector2) {
|
||||||
|
for (i in 0 until points.size) {
|
||||||
|
if (points[i].x > v.x) {
|
||||||
|
points.add(i, v)
|
||||||
|
activePoint = v
|
||||||
|
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points.add(v)
|
||||||
|
activePoint = v
|
||||||
|
fixBounds()
|
||||||
|
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findNearestPoint(v: Vector2) = points.minBy { (it - v).length }
|
||||||
|
|
||||||
|
fun removePoint(v: Vector2) {
|
||||||
|
points.remove(v)
|
||||||
|
if (v === activePoint) {
|
||||||
|
activePoint = null
|
||||||
|
}
|
||||||
|
fixBounds()
|
||||||
|
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixBounds() {
|
||||||
|
if (points.size >= 2) {
|
||||||
|
if (points[0].x != 0.0) {
|
||||||
|
points[0].copy(x=0.0).let {
|
||||||
|
if (activePoint === points[0]) {
|
||||||
|
activePoint = it
|
||||||
|
}
|
||||||
|
points[0] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (points[points.size-1].x != 1.0) {
|
||||||
|
points[points.size-1].copy(x=1.0).let {
|
||||||
|
if (activePoint === points[points.size-1]) {
|
||||||
|
activePoint = it
|
||||||
|
}
|
||||||
|
points[points.size-1] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePoint(old: Vector2, new: Vector2) {
|
||||||
|
val index = points.indexOf(old)
|
||||||
|
if (index != -1) {
|
||||||
|
points[index] = new
|
||||||
|
}
|
||||||
|
if (old === activePoint) {
|
||||||
|
activePoint = new
|
||||||
|
}
|
||||||
|
points.sortBy { it.x }
|
||||||
|
|
||||||
|
fixBounds()
|
||||||
|
events.envelopeChanged.trigger(EnvelopeChangedEvent(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun value(t: Double): Double {
|
||||||
|
|
||||||
|
val st = t.coerceIn(0.0, 1.0)
|
||||||
|
|
||||||
|
if (points.size == 1) {
|
||||||
|
return points[0].y
|
||||||
|
}
|
||||||
|
else if (points.size == 2) {
|
||||||
|
return points[0].y * (1.0-st) + points[1].y * st
|
||||||
|
} else {
|
||||||
|
if (st == 0.0) {
|
||||||
|
return points[0].y
|
||||||
|
}
|
||||||
|
if (st == 1.0) {
|
||||||
|
return points[points.size-1].y
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until points.size-1) {
|
||||||
|
if (points[i].x <= st && points[i+1].x > st) {
|
||||||
|
val left = points[i]
|
||||||
|
var right = points[i+1]
|
||||||
|
|
||||||
|
val dt = right.x - left.x
|
||||||
|
if (dt > 0.0) {
|
||||||
|
val f = (t - left.x) / dt
|
||||||
|
return left.y * (1.0-f) + right.y * f
|
||||||
|
} else {
|
||||||
|
return left.y
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points[0].y
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --
|
||||||
|
|
||||||
|
class EnvelopeEditor : Element(ElementType("envelope-editor")) {
|
||||||
|
|
||||||
|
var envelope = Envelope()
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
fun query(position: Vector2): Vector2 {
|
||||||
|
val x = (position.x - layout.screenX) / layout.screenWidth
|
||||||
|
val y = 1.0 - ((position.y - layout.screenY) / layout.screenHeight)
|
||||||
|
|
||||||
|
return Vector2(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse.clicked.listen {
|
||||||
|
val query = query(it.position)
|
||||||
|
val nearest = envelope.findNearestPoint(query)
|
||||||
|
val distance = nearest?.let { (it - query).length }
|
||||||
|
|
||||||
|
if (it.button == MouseButton.LEFT && !it.modifiers.contains(KeyModifier.CTRL)) {
|
||||||
|
when {
|
||||||
|
distance == null -> {
|
||||||
|
envelope.insertPoint(query)
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
distance < 0.05 -> {
|
||||||
|
envelope.activePoint = nearest
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
envelope.insertPoint(query)
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (it.button == MouseButton.LEFT) {
|
||||||
|
if (distance != null && distance < 0.1) {
|
||||||
|
envelope.removePoint(nearest)
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse.pressed.listen {
|
||||||
|
val query = query(it.position)
|
||||||
|
val nearest = envelope.findNearestPoint(query)
|
||||||
|
val distance = nearest?.let { it - query }?.length
|
||||||
|
|
||||||
|
if (distance == null) {
|
||||||
|
envelope.activePoint = null
|
||||||
|
draw.dirty = true
|
||||||
|
} else if (distance < 0.1) {
|
||||||
|
envelope.activePoint = nearest
|
||||||
|
} else {
|
||||||
|
envelope.activePoint = null
|
||||||
|
}
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse.dragged.listen {
|
||||||
|
envelope.activePoint?.let { activePoint ->
|
||||||
|
val query = query(it.position)
|
||||||
|
if (!it.modifiers.contains(KeyModifier.SHIFT)) {
|
||||||
|
envelope.updatePoint(activePoint, query)
|
||||||
|
} else {
|
||||||
|
envelope.updatePoint(activePoint, Vector2(activePoint.x, query.y))
|
||||||
|
}
|
||||||
|
draw.dirty = true
|
||||||
|
}
|
||||||
|
it.cancelPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(drawer: Drawer) {
|
||||||
|
val w = layout.screenWidth
|
||||||
|
val h = layout.screenHeight
|
||||||
|
|
||||||
|
val m = envelope.points.map {
|
||||||
|
val v = (it * Vector2(w, h))
|
||||||
|
Vector2(v.x, h - v.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawer.stroke = (ColorRGBa.BLACK.opacify(0.25))
|
||||||
|
drawer.strokeWeight = (1.0)
|
||||||
|
drawer.lineSegment(layout.screenWidth/2.0, 0.0, layout.screenWidth/2.0,layout.screenHeight)
|
||||||
|
drawer.lineSegment(0.0,layout.screenHeight/2.0,layout.screenWidth, layout.screenHeight/2.0)
|
||||||
|
|
||||||
|
if (m.size > 1) {
|
||||||
|
drawer.stroke = (ColorRGBa.WHITE)
|
||||||
|
drawer.strokeWeight = (2.0)
|
||||||
|
drawer.lineStrip(m)
|
||||||
|
drawer.fill = (ColorRGBa.WHITE)
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.circles(m, 4.0)
|
||||||
|
} else if (m.size == 1) {
|
||||||
|
drawer.stroke = (ColorRGBa.WHITE)
|
||||||
|
drawer.strokeWeight = (2.0)
|
||||||
|
drawer.lineSegment(0.0, m[0].y, layout.screenWidth, m[0].y)
|
||||||
|
drawer.fill = (ColorRGBa.WHITE)
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.circle(m[0], 4.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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.textfield(vararg classes: String, init: Textfield.() -> Unit) = initElement(classes, Textfield(), init)
|
||||||
|
|
||||||
|
fun DropdownButton.item(init: Item.() -> Unit): Item {
|
||||||
|
val item = Item().apply(init)
|
||||||
|
|
||||||
|
|
||||||
|
append(item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.div(vararg classes: String, init: Div.() -> Unit): Div {
|
||||||
|
val div = Div()
|
||||||
|
initElement(classes, div, init)
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : TextElement> Element.textElement(classes: Array<out String>, init: T.() -> String): T {
|
||||||
|
val te = T::class.java.newInstance()
|
||||||
|
te.classes.addAll(classes.map { ElementClass(it) })
|
||||||
|
te.text(te.init())
|
||||||
|
append(te)
|
||||||
|
return te
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Element.p(vararg classes: String, init: P.() -> String): P = textElement(classes, init)
|
||||||
|
fun Element.h1(vararg classes: String, init: H1.() -> String): H1 = textElement(classes, init)
|
||||||
|
fun Element.h2(vararg classes: String, init: H2.() -> String): H2 = textElement(classes, init)
|
||||||
|
fun Element.h3(vararg classes: String, init: H3.() -> String): H3 = textElement(classes, init)
|
||||||
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.openrndr.KeyModifier
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.draw.LineCap
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.panel.tools.Tooltip
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.round
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class SequenceEditor : Element(ElementType("sequence-editor")) {
|
||||||
|
|
||||||
|
var value = mutableListOf(0.0)
|
||||||
|
var precision = 2
|
||||||
|
var maximumSequenceLength = 16
|
||||||
|
var minimumSequenceLength = 1
|
||||||
|
|
||||||
|
private var selectedIndex: Int? = null
|
||||||
|
private var tooltip: Tooltip? = null
|
||||||
|
|
||||||
|
class ValueChangedEvent(val source: SequenceEditor,
|
||||||
|
val oldValue: List<Double>,
|
||||||
|
val newValue: List<Double>)
|
||||||
|
|
||||||
|
class Events {
|
||||||
|
val valueChanged = Event<ValueChangedEvent>("sequence-editor-value-changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val events = 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 * 0.5))
|
||||||
|
return Vector2(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse.clicked.listen {
|
||||||
|
it.cancelPropagation()
|
||||||
|
requestRedraw()
|
||||||
|
}
|
||||||
|
mouse.pressed.listen {
|
||||||
|
if (value.isNotEmpty()) {
|
||||||
|
val dx = (layout.screenWidth / (value.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 (value.size > minimumSequenceLength) {
|
||||||
|
val oldValue = value.map { it }
|
||||||
|
value.removeAt(round(index).toInt() - 1)
|
||||||
|
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (KeyModifier.CTRL !in it.modifiers) {
|
||||||
|
if (value.size < maximumSequenceLength) {
|
||||||
|
val q = query(it.position)
|
||||||
|
val oldValue = value.map { it }
|
||||||
|
value.add(index.toInt(), q.y)
|
||||||
|
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 (value.isNotEmpty()) {
|
||||||
|
val dx = (layout.screenWidth / (value.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 < value.size) {
|
||||||
|
val value = String.format("%.0${precision}f", value[readIndex])
|
||||||
|
tooltip = Tooltip(this@SequenceEditor, it.position - Vector2(layout.screenX, layout.screenY), "index: ${index.roundToInt()}, $value")
|
||||||
|
requestRedraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouse.dragged.listen {
|
||||||
|
val q = query(it.position)
|
||||||
|
selectedIndex?.let { index ->
|
||||||
|
val writeIndex = index - 1
|
||||||
|
if (writeIndex >= 0 && writeIndex < value.size) {
|
||||||
|
val oldValue = value.map { it }
|
||||||
|
value[writeIndex] = q.y.coerceIn(-1.0, 1.0)
|
||||||
|
events.valueChanged.trigger(ValueChangedEvent(this, oldValue, value))
|
||||||
|
}
|
||||||
|
requestRedraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(drawer: Drawer) {
|
||||||
|
drawer.stroke = (ColorRGBa.BLACK.opacify(0.25))
|
||||||
|
drawer.strokeWeight = (1.0)
|
||||||
|
drawer.lineSegment(0.0, layout.screenHeight / 2.0, layout.screenWidth, layout.screenHeight / 2.0)
|
||||||
|
|
||||||
|
drawer.strokeWeight = 1.0
|
||||||
|
drawer.stroke = ColorRGBa.WHITE
|
||||||
|
for (i in value.indices) {
|
||||||
|
val dx = layout.screenWidth / (value.size + 1)
|
||||||
|
val height = -value[i] * layout.screenHeight / 2.0
|
||||||
|
|
||||||
|
val x = dx * (i + 1)
|
||||||
|
drawer.lineCap = LineCap.ROUND
|
||||||
|
drawer.lineSegment(x, layout.screenHeight / 2.0, x, layout.screenHeight / 2.0 + height)
|
||||||
|
drawer.circle(x, layout.screenHeight / 2.0 + height, 5.0)
|
||||||
|
}
|
||||||
|
tooltip?.draw(drawer)
|
||||||
|
}
|
||||||
|
}
|
||||||
251
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Slider.kt
Normal file
251
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Slider.kt
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.openrndr.*
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.draw.LineCap
|
||||||
|
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 org.openrndr.text.Cursor
|
||||||
|
import org.openrndr.text.Writer
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
class Slider : Element(ElementType("slider")) {
|
||||||
|
var label = ""
|
||||||
|
var precision = 3
|
||||||
|
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) {
|
||||||
|
val number = NumberFormat.getInstance().parse(keyboardInput).toDouble()
|
||||||
|
if (number != null) {
|
||||||
|
interactiveValue = number.coerceIn(range.min, range.max)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
drawer.stroke = ((computedStyle.color as Color.RGBa).color.opacify(1.0))
|
||||||
|
drawer.lineSegment(margin, 2.0, margin + x, 2.0)
|
||||||
|
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.circle(Vector2(margin + x, 2.0), 5.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)
|
||||||
|
}
|
||||||
|
if (root() as? Body == null) {
|
||||||
|
throw RuntimeException("no body")
|
||||||
|
}
|
||||||
|
(root() as? Body)?.controlManager?.program?.launch {
|
||||||
|
while (true) {
|
||||||
|
if (property.get() != currentValue) {
|
||||||
|
val lcur = property.get()
|
||||||
|
currentValue = lcur
|
||||||
|
value = lcur
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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.launch
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.panel.style.*
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
import org.openrndr.text.Writer
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
fun replaceText(text : String) {
|
||||||
|
if (children.isEmpty()) {
|
||||||
|
text(text)
|
||||||
|
} else {
|
||||||
|
(children.first() as? TextNode)?.text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextElement.bind(property: KMutableProperty0<String>) {
|
||||||
|
var currentValue: Double? = null
|
||||||
|
|
||||||
|
|
||||||
|
if (root() as? Body == null) {
|
||||||
|
throw RuntimeException("no body")
|
||||||
|
}
|
||||||
|
(root() as? Body)?.controlManager?.program?.launch {
|
||||||
|
var lastText = ""
|
||||||
|
while (true) {
|
||||||
|
if (property.get() != lastText) {
|
||||||
|
replaceText(property.get())
|
||||||
|
lastText = property.get()
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
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.events.Event
|
||||||
|
import org.openrndr.launch
|
||||||
|
import org.openrndr.shape.Rectangle
|
||||||
|
import org.openrndr.text.Cursor
|
||||||
|
import org.openrndr.text.writer
|
||||||
|
import kotlin.reflect.KMutableProperty0
|
||||||
|
|
||||||
|
class Textfield : Element(ElementType("textfield")) {
|
||||||
|
|
||||||
|
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
|
||||||
|
//drawer.lineSegment(0.0, yOffset + 4.0, layout.screenWidth, yOffset + 4.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Textfield.bind(property: KMutableProperty0<String>) {
|
||||||
|
var currentValue = property.get()
|
||||||
|
|
||||||
|
events.valueChanged.listen {
|
||||||
|
currentValue = it.newValue
|
||||||
|
property.set(it.newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
(root() as Body).controlManager.program.launch {
|
||||||
|
while (true) {
|
||||||
|
val cval = property.get()
|
||||||
|
if (cval != currentValue) {
|
||||||
|
currentValue = cval
|
||||||
|
value = cval
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Toggle.kt
Normal file
118
orx-panel/src/main/kotlin/org/openrndr/panel/elements/Toggle.kt
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package org.openrndr.panel.elements
|
||||||
|
|
||||||
|
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 org.openrndr.text.Writer
|
||||||
|
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
import org.openrndr.events.Event
|
||||||
|
import org.openrndr.launch
|
||||||
|
import kotlin.reflect.KMutableProperty0
|
||||||
|
|
||||||
|
class Toggle : Element(ElementType("toggle")) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
(root() as Body).controlManager.program.launch {
|
||||||
|
while (true) {
|
||||||
|
val cval = property.get()
|
||||||
|
if (cval != currentValue) {
|
||||||
|
currentValue = cval
|
||||||
|
value = cval
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
250
orx-panel/src/main/kotlin/org/openrndr/panel/elements/XYPad.kt
Normal file
250
orx-panel/src/main/kotlin/org/openrndr/panel/elements/XYPad.kt
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
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.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 org.openrndr.text.Writer
|
||||||
|
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 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 label = "${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(label)
|
||||||
|
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(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")
|
||||||
|
}
|
||||||
|
(root() as? Body)?.controlManager?.program?.launch {
|
||||||
|
while (true) {
|
||||||
|
if (property.get() != currentValue) {
|
||||||
|
val lcur = property.get()
|
||||||
|
currentValue = lcur
|
||||||
|
value = lcur
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Double.round(decimals: Int): Double {
|
||||||
|
var multiplier = 1.0
|
||||||
|
repeat(decimals) { multiplier *= 10 }
|
||||||
|
return round(this * multiplier) / multiplier
|
||||||
|
}
|
||||||
261
orx-panel/src/main/kotlin/org/openrndr/panel/layout/Layouter.kt
Normal file
261
orx-panel/src/main/kotlin/org/openrndr/panel/layout/Layouter.kt
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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): 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 = (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()
|
||||||
|
|
||||||
|
element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.forEach {
|
||||||
|
|
||||||
|
val elementGrow = (it.computedStyle.flexGrow as FlexGrow.Ratio).value
|
||||||
|
val growWidth = if (totalGrow > 0) (elementGrow / totalGrow) * remainder else 0.0
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
it.layout.growWidth = growWidth
|
||||||
|
x += width(it) + growWidth
|
||||||
|
maxHeight = max(height(it), 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 }
|
||||||
|
.sumByDouble { height(it) }
|
||||||
|
val remainder = ((element.layout.screenHeight - verticalPadding) - totalHeight)
|
||||||
|
val totalGrow = element.children
|
||||||
|
.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }
|
||||||
|
.sumByDouble { (it.computedStyle.flexGrow as FlexGrow.Ratio).value }
|
||||||
|
|
||||||
|
element.children.filter { it.computedStyle.display in blockLike && it.computedStyle.position !in manualPosition }.forEach {
|
||||||
|
val elementGrow = (it.computedStyle.flexGrow as FlexGrow.Ratio).value
|
||||||
|
val growHeight = if (totalGrow > 0) (elementGrow / totalGrow) * remainder else 0.0
|
||||||
|
|
||||||
|
it.layout.screenY = ly + ((it.computedStyle.marginTop as? LinearDimension.PX)?.value
|
||||||
|
?: 0.0)
|
||||||
|
it.layout.screenX = lx + ((it.computedStyle.marginLeft as? LinearDimension.PX)?.value
|
||||||
|
?: 0.0)
|
||||||
|
|
||||||
|
it.layout.growHeight = growHeight
|
||||||
|
ly += height(it) + growHeight
|
||||||
|
maxWidth = max(height(it), 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)
|
||||||
|
maxWidth = max(0.0, width(it))
|
||||||
|
y += height(it)
|
||||||
|
} 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, 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 {
|
||||||
|
when (it) {
|
||||||
|
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)
|
||||||
|
positionChildren(element).height + padding
|
||||||
|
}
|
||||||
|
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, 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.Auto -> (element.widthHint ?: positionChildren(element).width) +
|
||||||
|
paddingRight(element) + paddingLeft(element)
|
||||||
|
else -> throw RuntimeException("not supported")
|
||||||
|
}
|
||||||
|
} + 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 }
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
element.layout.screenWidth = width(element, includeMargins = false)
|
||||||
|
element.layout.screenHeight = height(element, includeMargins = false)
|
||||||
|
element.layout.screenWidth += element.layout.growWidth
|
||||||
|
element.layout.screenHeight += element.layout.growHeight
|
||||||
|
positionChildren(element)
|
||||||
|
}
|
||||||
|
element.children.forEach { layout(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
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 "sequence-editor") {
|
||||||
|
height = 60.px
|
||||||
|
width = 100.percent
|
||||||
|
marginTop = 5.px
|
||||||
|
marginBottom = 15.px
|
||||||
|
marginLeft = 5.px
|
||||||
|
marginRight = 5.px
|
||||||
|
},
|
||||||
|
|
||||||
|
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 "selected") {
|
||||||
|
display = Display.BLOCK
|
||||||
|
background = controlActiveColor
|
||||||
|
}
|
||||||
|
and(has state "hover") {
|
||||||
|
display = Display.BLOCK
|
||||||
|
background = Color.RGBa(controlHoverBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
orx-panel/src/main/kotlin/org/openrndr/panel/style/Selector.kt
Normal file
129
orx-panel/src/main/kotlin/org/openrndr/panel/style/Selector.kt
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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 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 CompoundSelector.and(other:CompoundSelector):CompoundSelector {
|
||||||
|
val c = CompoundSelector()
|
||||||
|
c.previous = previous
|
||||||
|
c.selectors.addAll(selectors)
|
||||||
|
c.selectors.addAll(other.selectors)
|
||||||
|
return c
|
||||||
|
}
|
||||||
221
orx-panel/src/main/kotlin/org/openrndr/panel/style/StyleSheet.kt
Normal file
221
orx-panel/src/main/kotlin/org/openrndr/panel/style/StyleSheet.kt
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
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.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
|
||||||
|
|
||||||
|
var StyleSheet.fontSize by PropertyHandler<LinearDimension>("font-size", INHERIT, 14.px)
|
||||||
|
var StyleSheet.fontFamily by PropertyHandler("font-family", INHERIT, "default")
|
||||||
|
var StyleSheet.overflow by PropertyHandler<Overflow>("overflow", RESET, Overflow.Visible)
|
||||||
|
var StyleSheet.zIndex by PropertyHandler<ZIndex>("z-index", RESET, ZIndex.Auto)
|
||||||
|
|
||||||
|
val Number.px: LinearDimension.PX get() = LinearDimension.PX(this.toDouble())
|
||||||
|
val Number.percent: LinearDimension.Percent get() = LinearDimension.Percent(this.toDouble())
|
||||||
|
|
||||||
|
fun StyleSheet.child(selector: CompoundSelector, init: StyleSheet.() -> Unit) {
|
||||||
|
val stylesheet = StyleSheet(selector).apply(init)
|
||||||
|
stylesheet.selector.previous = Pair(Combinator.CHILD, this.selector)
|
||||||
|
children.add(stylesheet)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StyleSheet.descendant(selector: CompoundSelector, init: StyleSheet.() -> Unit) {
|
||||||
|
val stylesheet = StyleSheet(selector).apply(init)
|
||||||
|
stylesheet.selector.previous = Pair(Combinator.DESCENDANT, this.selector)
|
||||||
|
children.add(stylesheet)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StyleSheet.and(selector: CompoundSelector, init: StyleSheet.() -> Unit) {
|
||||||
|
val stylesheet = StyleSheet(this.selector and selector).apply(init)
|
||||||
|
this.children.add(stylesheet)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StyleSheet.flatten(): List<StyleSheet> {
|
||||||
|
return listOf(this) + children.flatMap { it.flatten() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun styleSheet(selector: CompoundSelector = CompoundSelector.DUMMY, init: StyleSheet.() -> Unit): StyleSheet {
|
||||||
|
return StyleSheet(selector).apply {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.openrndr.panel.tools
|
||||||
|
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.draw.Drawer
|
||||||
|
import org.openrndr.draw.FontImageMap
|
||||||
|
import org.openrndr.draw.isolated
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.panel.ControlManager
|
||||||
|
import org.openrndr.panel.elements.Body
|
||||||
|
import org.openrndr.panel.elements.Element
|
||||||
|
import org.openrndr.text.writer
|
||||||
|
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.stroke = null
|
||||||
|
drawer.fill = ColorRGBa.GRAY
|
||||||
|
drawer.rectangle(0.0, 0.0, maxX + 20.0, maxY)
|
||||||
|
drawer.fill = ColorRGBa.BLACK
|
||||||
|
drawer.translate(10.0, 0.0)
|
||||||
|
writer(drawer) {
|
||||||
|
for (line in lines) {
|
||||||
|
newLine()
|
||||||
|
text(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
orx-panel/src/main/resources/fonts/Roboto-Medium.ttf
Normal file
BIN
orx-panel/src/main/resources/fonts/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
orx-panel/src/main/resources/fonts/Roboto-Regular.ttf
Normal file
BIN
orx-panel/src/main/resources/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
@@ -0,0 +1,57 @@
|
|||||||
|
//package org.openrndr.panel.test
|
||||||
|
//
|
||||||
|
//import net.lustlab.panel.elements.Element
|
||||||
|
//import net.lustlab.panel.elements.ElementClass
|
||||||
|
//import net.lustlab.panel.elements.ElementType
|
||||||
|
//import net.lustlab.panel.style.*
|
||||||
|
//import org.jetbrains.spek.api.Spek
|
||||||
|
//import org.jetbrains.spek.api.dsl.describe
|
||||||
|
//import org.jetbrains.spek.api.dsl.it
|
||||||
|
//import kotlin.test.assertEquals
|
||||||
|
//import kotlin.test.assertFalse
|
||||||
|
//import kotlin.test.assertNotNull
|
||||||
|
//import kotlin.test.assertTrue
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Created by voorbeeld on 11/20/16.
|
||||||
|
// */
|
||||||
|
//class SomeTest : Spek({
|
||||||
|
//
|
||||||
|
// describe("a thing") {
|
||||||
|
//
|
||||||
|
// // .panel > button
|
||||||
|
// val cs = selector(class_="panel") withChild selector(type="button", class_="fancy")
|
||||||
|
//
|
||||||
|
// val root = Element(ElementType("body"))
|
||||||
|
// val panel = Element(ElementType("div")).apply {
|
||||||
|
// classes+= ElementClass("panel")
|
||||||
|
// }
|
||||||
|
// val button = Element(ElementType("button"))
|
||||||
|
// val button2 = Element(ElementType("button")).apply {
|
||||||
|
// classes+= ElementClass("fancy")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// root.append(panel)
|
||||||
|
// panel.append(button)
|
||||||
|
// panel.append(button2)
|
||||||
|
//
|
||||||
|
// it("should work") {
|
||||||
|
// assert(cs.selectors.size == 1)
|
||||||
|
// assertTrue(cs.selectors[0] is TypeSelector)
|
||||||
|
// assertNotNull(cs.previous)
|
||||||
|
//
|
||||||
|
// assertFalse(Matcher().matches(cs, button))
|
||||||
|
// assertFalse(Matcher().matches(cs, panel))
|
||||||
|
// assertTrue(Matcher().matches(cs, button2))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// it("should have precedences") {
|
||||||
|
// println(cs.precedence())
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//})
|
||||||
|
//
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//package org.openrndr.panel.test
|
||||||
|
//
|
||||||
|
//import net.lustlab.panel.style.*
|
||||||
|
//import org.jetbrains.spek.api.Spek
|
||||||
|
//import org.jetbrains.spek.api.dsl.describe
|
||||||
|
//
|
||||||
|
//class StyleSheetTest : Spek({
|
||||||
|
//
|
||||||
|
// describe("stylesheet") {
|
||||||
|
// val styleSheet = StyleSheet()
|
||||||
|
// styleSheet.width = 5.px
|
||||||
|
// styleSheet.height = 5.px
|
||||||
|
// styleSheet.left = 10.px
|
||||||
|
// styleSheet.top = 10.percent
|
||||||
|
// styleSheet.position = Position.FIXED
|
||||||
|
// var a = styleSheet.precedence
|
||||||
|
// }
|
||||||
|
//})
|
||||||
@@ -23,6 +23,7 @@ include 'orx-camera',
|
|||||||
'orx-olive',
|
'orx-olive',
|
||||||
'orx-osc',
|
'orx-osc',
|
||||||
'orx-palette',
|
'orx-palette',
|
||||||
|
'orx-panel',
|
||||||
'orx-poisson-fill',
|
'orx-poisson-fill',
|
||||||
'orx-runway',
|
'orx-runway',
|
||||||
'orx-shader-phrases',
|
'orx-shader-phrases',
|
||||||
|
|||||||
Reference in New Issue
Block a user