diff --git a/build.gradle b/build.gradle index a2bbb91b..3d3c4d1b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'org.jetbrains.dokka' project.ext { openrndrVersion = "0.3.40-rc.5" - panelVersion = "0.3.22-rc.3" kotlinVersion = "1.3.70" spekVersion = "2.0.10" libfreenectVersion = "0.5.7-1.5.2" diff --git a/orx-gui/build.gradle b/orx-gui/build.gradle index c6fddf2a..78e1c41b 100644 --- a/orx-gui/build.gradle +++ b/orx-gui/build.gradle @@ -1,6 +1,6 @@ dependencies { api project(":orx-parameters") - api "org.openrndr.panel:openrndr-panel:$panelVersion" + api project(":orx-panel") implementation "org.openrndr:openrndr-dialogs:$openrndrVersion" implementation "com.google.code.gson:gson:$gsonVersion" } \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/ControlManager.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/ControlManager.kt new file mode 100644 index 00000000..ba3aa934 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/ControlManager.kt @@ -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() +// +// 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() + + 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>() + 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() + 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() + 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) { + 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) { + 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 +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/FontManager.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/FontManager.kt new file mode 100644 index 00000000..a04ec4ae --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/FontManager.kt @@ -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 = 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 + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/collections/ObservableCopyOnWriteArrayList.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/collections/ObservableCopyOnWriteArrayList.kt new file mode 100644 index 00000000..017d2ac0 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/collections/ObservableCopyOnWriteArrayList.kt @@ -0,0 +1,31 @@ +package org.openrndr.panel.collections + +import org.openrndr.events.Event +import java.util.concurrent.CopyOnWriteArrayList + +class ObservableCopyOnWriteArrayList : CopyOnWriteArrayList() { + + val changed = Event>() + 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) + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/collections/ObservableHashSet.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/collections/ObservableHashSet.kt new file mode 100644 index 00000000..bcb093c1 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/collections/ObservableHashSet.kt @@ -0,0 +1,36 @@ +package org.openrndr.panel.collections + +import org.openrndr.events.Event +import java.util.* + +class ObservableHashSet : HashSet() { + + class ChangeEvent(val source: ObservableHashSet, val added: Set, val removed: Set) + + val changed = Event>() + + 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)) + } + +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Body.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Body.kt new file mode 100644 index 00000000..6f53b6be --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Body.kt @@ -0,0 +1,5 @@ +package org.openrndr.panel.elements + +import org.openrndr.panel.ControlManager + +class Body(val controlManager: ControlManager) : Element(ElementType("Body")) \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Button.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Button.kt new file mode 100644 index 00000000..eea989fd --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Button.kt @@ -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 = 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() + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Canvas.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Canvas.kt new file mode 100644 index 00000000..e758dbb0 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Canvas.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Colorpicker.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Colorpicker.kt new file mode 100644 index 00000000..461080af --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Colorpicker.kt @@ -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() + } + + 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) + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/ColorpickerButton.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/ColorpickerButton.kt new file mode 100644 index 00000000..754fb3d8 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/ColorpickerButton.kt @@ -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() + } + + 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 = 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) + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Div.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Div.kt new file mode 100644 index 00000000..d545e54a --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Div.kt @@ -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})" + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/DropdownButton.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/DropdownButton.kt new file mode 100644 index 00000000..766b879e --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/DropdownButton.kt @@ -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() + } + + 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() + } + + 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) ?: ""}" + 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 = children.filterIsInstance().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) ?: "" + + 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() + + 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 > DropdownButton.bind(property: KMutableProperty0, map: Map) { + val options = mutableMapOf() + 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() + } + } +} diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Element.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Element.kt new file mode 100644 index 00000000..2fd85f3a --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Element.kt @@ -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("element-mouse-clicked") + val doubleClicked = Event("element-mouse-double-clicked") + val entered = Event("element-mouse-entered") + val exited = Event("element-mouse-exited") + val dragged = Event("element-mouse-dragged") + val moved = Event("element-mouse-moved") + val scrolled = Event("element-mouse-scrolled") + val pressed = Event("element-mouse-pressed") + } + + class DropObserverables { + val dropped = Event("element-dropped") + } + + val drop = DropObserverables() + val mouse = MouseObservables() + + class KeyboardObservables { + val pressed = Event("element-keyboard-pressed") + val released = Event("element-keyboard-released") + val repeated = Event("element-keyboard-repeated") + val character = Event("element-keyboard-character") + val focusGained = Event("element-keyboard-focus-gained") + val focusLost = Event("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("element-class-added") + val classRemoved = Event("element-class-removed") + } + + val classEvents = ClassObserverables() + + + var id: String? = null + val classes: ObservableHashSet = ObservableHashSet() + val pseudoClasses: ObservableHashSet = ObservableHashSet() + + var parent: Element? = null + val children: ObservableCopyOnWriteArrayList = 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 { + val result = ArrayList() + val stack = Stack() + + 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 { + val result = ArrayList() + val stack = Stack() + + 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 { + var c = this + val result = ArrayList() + + 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 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) } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/EnvelopeButton.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/EnvelopeButton.kt new file mode 100644 index 00000000..3a6637b4 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/EnvelopeButton.kt @@ -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 = 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) + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/EnvelopeEditor.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/EnvelopeEditor.kt new file mode 100644 index 00000000..1994a701 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/EnvelopeEditor.kt @@ -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("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) + } + } +} diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/LayoutBuilder.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/LayoutBuilder.kt new file mode 100644 index 00000000..4b2e9042 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/LayoutBuilder.kt @@ -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 Element.initElement(classes: Array, 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 Element.textElement(classes: Array, 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) + diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/SequenceEditor.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/SequenceEditor.kt new file mode 100644 index 00000000..db998aaf --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/SequenceEditor.kt @@ -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, + val newValue: List) + + class Events { + val valueChanged = Event("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) + } +} diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Slider.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Slider.kt new file mode 100644 index 00000000..465bc9e7 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Slider.kt @@ -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("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) { + 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() + } + } +} diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/TextElements.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/TextElements.kt new file mode 100644 index 00000000..6138ca02 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/TextElements.kt @@ -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) { + 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() + } + } +} diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Textfield.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Textfield.kt new file mode 100644 index 00000000..29fd8d34 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Textfield.kt @@ -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("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) { + 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() + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Toggle.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Toggle.kt new file mode 100644 index 00000000..4305724a --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/Toggle.kt @@ -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("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) { + 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() + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/elements/XYPad.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/XYPad.kt new file mode 100644 index 00000000..a94b8ae9 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/elements/XYPad.kt @@ -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("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) { + 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 +} diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/layout/Layouter.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/layout/Layouter.kt new file mode 100644 index 00000000..37755393 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/layout/Layouter.kt @@ -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() + 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) } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/style/DefaultStyles.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/style/DefaultStyles.kt new file mode 100644 index 00000000..629b5ec1 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/style/DefaultStyles.kt @@ -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) + } + } +) diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/style/Matcher.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/style/Matcher.kt new file mode 100644 index 00000000..4ef51d31 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/style/Matcher.kt @@ -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 + } + } + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/style/Selector.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/style/Selector.kt new file mode 100644 index 00000000..1cdc6c5e --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/style/Selector.kt @@ -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? + var selectors: MutableList + + constructor() { + previous = null + selectors = mutableListOf() + } + + constructor(previous: Pair?, selectors: List) { + 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 +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/style/StyleSheet.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/style/StyleSheet.kt new file mode 100644 index 00000000..7c6dd3c5 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/style/StyleSheet.kt @@ -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() +} + +class PropertyHandler( + 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() + val properties = HashMap() + + 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("width", RESET, LinearDimension.Auto) +var StyleSheet.height by PropertyHandler("height", RESET, LinearDimension.Auto) +var StyleSheet.top by PropertyHandler("top", RESET, 0.px) // css default is auto +var StyleSheet.left by PropertyHandler("left", RESET, 0.px) // css default is auto + +var StyleSheet.marginTop by PropertyHandler("margin-top", RESET, 0.px) +var StyleSheet.marginBottom by PropertyHandler("margin-bottom", RESET, 0.px) +var StyleSheet.marginLeft by PropertyHandler("margin-left", RESET, 0.px) +var StyleSheet.marginRight by PropertyHandler("margin-right", RESET, 0.px) + + +var StyleSheet.paddingTop by PropertyHandler("padding-top", RESET, 0.px) +var StyleSheet.paddingBottom by PropertyHandler("padding-bottom", RESET, 0.px) +var StyleSheet.paddingLeft by PropertyHandler("padding-left", RESET, 0.px) +var StyleSheet.paddingRight by PropertyHandler("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("flex-direction", RESET, FlexDirection.Row) +var StyleSheet.flexGrow by PropertyHandler("flex-grow", RESET, FlexGrow.Ratio(0.0)) + + +var StyleSheet.background by PropertyHandler("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", 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("font-size", INHERIT, 14.px) +var StyleSheet.fontFamily by PropertyHandler("font-family", INHERIT, "default") +var StyleSheet.overflow by PropertyHandler("overflow", RESET, Overflow.Visible) +var StyleSheet.zIndex by PropertyHandler("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 { + return listOf(this) + children.flatMap { it.flatten() } +} + +fun styleSheet(selector: CompoundSelector = CompoundSelector.DUMMY, init: StyleSheet.() -> Unit): StyleSheet { + return StyleSheet(selector).apply { + init() + } +} \ No newline at end of file diff --git a/orx-panel/src/main/kotlin/org/openrndr/panel/tools/Tooltip.kt b/orx-panel/src/main/kotlin/org/openrndr/panel/tools/Tooltip.kt new file mode 100644 index 00000000..9c5ec6b9 --- /dev/null +++ b/orx-panel/src/main/kotlin/org/openrndr/panel/tools/Tooltip.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/orx-panel/src/main/resources/fonts/Roboto-Medium.ttf b/orx-panel/src/main/resources/fonts/Roboto-Medium.ttf new file mode 100644 index 00000000..39c63d74 Binary files /dev/null and b/orx-panel/src/main/resources/fonts/Roboto-Medium.ttf differ diff --git a/orx-panel/src/main/resources/fonts/Roboto-Regular.ttf b/orx-panel/src/main/resources/fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..8c082c8d Binary files /dev/null and b/orx-panel/src/main/resources/fonts/Roboto-Regular.ttf differ diff --git a/orx-panel/src/test/java/org/openrndr/panel/test/SelectorBuilderTest.kt b/orx-panel/src/test/java/org/openrndr/panel/test/SelectorBuilderTest.kt new file mode 100644 index 00000000..1d260a05 --- /dev/null +++ b/orx-panel/src/test/java/org/openrndr/panel/test/SelectorBuilderTest.kt @@ -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()) +// +// +// } +// +// } +// +//}) +// diff --git a/orx-panel/src/test/java/org/openrndr/panel/test/StyleSheetTest.kt b/orx-panel/src/test/java/org/openrndr/panel/test/StyleSheetTest.kt new file mode 100644 index 00000000..f01aded4 --- /dev/null +++ b/orx-panel/src/test/java/org/openrndr/panel/test/StyleSheetTest.kt @@ -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 +// } +//}) diff --git a/settings.gradle b/settings.gradle index 005d1930..0235c1ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include 'orx-camera', 'orx-olive', 'orx-osc', 'orx-palette', + 'orx-panel', 'orx-poisson-fill', 'orx-runway', 'orx-shader-phrases',