From 0738366053176dd7ff60e1a6abaa8398b265c58f Mon Sep 17 00:00:00 2001 From: stivais Date: Sat, 27 Apr 2024 20:09:35 +0300 Subject: [PATCH] brought stuff over finally for debugging --- .../main/kotlin/com/github/stivais/ui/UI.kt | 106 +++++++++ .../kotlin/com/github/stivais/ui/UIScreen.kt | 76 ++++++ .../com/github/stivais/ui/UISettings.kt | 12 + .../github/stivais/ui/animation/Animation.kt | 16 ++ .../github/stivais/ui/animation/animations.kt | 26 ++ .../com/github/stivais/ui/color/Color.kt | 169 +++++++++++++ .../stivais/ui/constraints/constraints.kt | 37 +++ .../com/github/stivais/ui/constraints/dsl.kt | 64 +++++ .../ui/constraints/measurements/Animatable.kt | 88 +++++++ .../ui/constraints/measurements/Percent.kt | 18 ++ .../ui/constraints/measurements/Pixel.kt | 24 ++ .../ui/constraints/measurements/Undefined.kt | 11 + .../ui/constraints/operational/Additive.kt | 15 ++ .../ui/constraints/positions/Center.kt | 14 ++ .../ui/constraints/positions/Linked.kt | 14 ++ .../stivais/ui/constraints/sizes/Bounding.kt | 25 ++ .../stivais/ui/constraints/sizes/Copying.kt | 12 + .../com/github/stivais/ui/elements/Element.kt | 222 ++++++++++++++++++ .../com/github/stivais/ui/elements/dsl.kt | 2 + .../github/stivais/ui/elements/impl/Block.kt | 53 +++++ .../github/stivais/ui/elements/impl/Column.kt | 39 +++ .../github/stivais/ui/elements/impl/Group.kt | 13 + .../github/stivais/ui/elements/impl/Image.kt | 17 ++ .../github/stivais/ui/elements/impl/Text.kt | 53 +++++ .../stivais/ui/elements/impl/TextInput.kt | 101 ++++++++ .../github/stivais/ui/events/EventManager.kt | 145 ++++++++++++ .../com/github/stivais/ui/events/dsl.kt | 66 ++++++ .../com/github/stivais/ui/events/events.kt | 113 +++++++++ .../kotlin/com/github/stivais/ui/utils/dsl.kt | 119 ++++++++++ .../com/github/stivais/ui/utils/utils.kt | 33 +++ .../main/kotlin/me/odinmain/font/OdinFont.kt | 2 +- 31 files changed, 1704 insertions(+), 1 deletion(-) create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/UI.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/UIScreen.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/UISettings.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/animation/Animation.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/animation/animations.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/color/Color.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/constraints.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/dsl.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Animatable.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Percent.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Pixel.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Undefined.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/operational/Additive.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Center.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Linked.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Bounding.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Copying.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/Element.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/dsl.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Block.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Column.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Group.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Image.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Text.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/TextInput.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/events/EventManager.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/events/dsl.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/events/events.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/utils/dsl.kt create mode 100644 odinmain/src/main/kotlin/com/github/stivais/ui/utils/utils.kt diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/UI.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/UI.kt new file mode 100644 index 000000000..77dde53a7 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/UI.kt @@ -0,0 +1,106 @@ +package com.github.stivais.ui + +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.px +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.elements.impl.Group +import com.github.stivais.ui.events.EventManager +import me.odinmain.utils.render.TextAlign +import me.odinmain.utils.render.Color as OdinColor +import me.odinmain.utils.render.text +import java.util.logging.Logger + +// TODO: When finished with dsl and inputs, bring to its own window instead of inside of minecraft for benchmarking and reduce all memory usage +class UI( + //val renderer: Renderer2D, + settings: UISettings? = null +) { + + val settings: UISettings = settings ?: UISettings() + + val main: Group = Group(Constraints(0.px, 0.px, 1920.px, 1080.px)).also { + it.initialize(this) + it.position() + } + + constructor(block: Group.() -> Unit) : this() { + main.block() + } + + var eventManager: EventManager? = EventManager(this) + + val mx get() = eventManager!!.mouseX + + val my get() = eventManager!!.mouseY + + fun initialize() { + main.position() + } + + // frametime metrics + private var frames: Int = 0 + private var frameTime: Long = 0 + private var performance: String = "" + + fun render() { + val start = System.nanoTime() +// renderer.beginFrame() + main.position() + main.render() + if (settings.frameMetrics) { + text(performance, main.width, main.height, OdinColor.WHITE, 16f, align = TextAlign.Right) + } +// renderer.endFrame() + if (settings.frameMetrics) { + frames++ + frameTime += System.nanoTime() - start + if (frames > 100) { + performance = + "elements: ${getElementAmount(main, false)}, " + + "elements rendering: ${getElementAmount(main, true)}," + + "frametime avg: ${(frameTime / frames) / 1_000_000.0}ms" + frames = 0 + frameTime = 0 + } + } + frames++ + frameTime += System.nanoTime() - start + } + + fun getElementAmount(element: Element, onlyRender: Boolean): Int { + var amount = 0 + if (!(onlyRender && !element.renders)) { + amount++ + element.elements?.let { + for (i in it) { + amount += getElementAmount(i, onlyRender) + } + } + } + return amount + } + + fun resize(width: Int, height: Int) { + main.constraints.width = width.px + main.constraints.height = height.px + } + + fun focus(element: Element) { + if (eventManager == null) return logger.warning("Event Manager isn't setup, but called focus") + eventManager!!.focus(element) + } + + fun unfocus() { + if (eventManager == null) return logger.warning("Event Manager isn't setup, but called unfocus") + eventManager!!.unfocus() + } + + inline fun settings(block: UISettings.() -> Unit): UI { + settings.apply(block) + return this + } + + companion object { + val logger: Logger = Logger.getLogger("UI") + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/UIScreen.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/UIScreen.kt new file mode 100644 index 000000000..8281e2e8a --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/UIScreen.kt @@ -0,0 +1,76 @@ +package com.github.stivais.ui + +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiScreen +import org.lwjgl.input.Mouse + +class UIScreen(val ui: UI) : GuiScreen() { + + private var previousWidth: Int = 0 + private var previousHeight: Int = 0 + + override fun initGui() { + ui.initialize() + } + + override fun drawScreen(mouseX: Int, mouseY: Int, partialTicks: Float) { + val w = mc.framebuffer.framebufferWidth + val h = mc.framebuffer.framebufferHeight + if (w != previousWidth || h != previousHeight) { + ui.resize(w, h) + previousWidth = w + previousHeight = h + } + ui.render() + } + + override fun mouseClicked(mouseX: Int, mouseY: Int, button: Int) { + ui.eventManager?.onMouseClick(button) + } + + override fun mouseReleased(mouseX: Int, mouseY: Int, button: Int) { + ui.eventManager?.onMouseRelease(button) + } + + fun mouseMoved(mx: Float, my: Float) { + ui.eventManager?.onMouseMove(mx, my) + } + + override fun handleMouseInput() { + ui.eventManager?.apply { + val mx = Mouse.getX().toFloat() + val my = Mouse.getY().toFloat() + + if (mouseX != mx || mouseY != my) { + onMouseMove(mx, my) + } + + val scroll = Mouse.getEventDWheel() + if (scroll != 0) { + onMouseScroll(scroll.toFloat()) + } + } + super.handleMouseInput() + } + + override fun keyTyped(typedChar: Char, keyCode: Int) { + if (ui.eventManager?.onKeyType(typedChar) == true) return + if (ui.eventManager?.onKeycodePressed(keyCode) == true) return + super.keyTyped(typedChar, keyCode) + } + +// no key released because 1.8.9 doesn't have it and I don't want to manually recreate it +// override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { +// if (ui.eventManager?.onKeyReleased(keyCode) == true) { +// return true +// } +// return super.keyPressed(keyCode, scanCode, modifiers) +// } + + override fun onResize(mcIn: Minecraft?, w: Int, h: Int) { + ui.resize(mc.framebuffer.framebufferWidth, mc.framebuffer.framebufferHeight) + super.onResize(mcIn, w, h) + } + + override fun doesGuiPauseGame(): Boolean = false +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/UISettings.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/UISettings.kt new file mode 100644 index 000000000..a45c54b6c --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/UISettings.kt @@ -0,0 +1,12 @@ +@file:Suppress("UNUSED") + +package com.github.stivais.ui + +// add more customizablity +class UISettings { + + var positionOnAdd: Boolean = false + + var frameMetrics: Boolean = true + +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/animation/Animation.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/animation/Animation.kt new file mode 100644 index 000000000..47e63dd67 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/animation/Animation.kt @@ -0,0 +1,16 @@ +package com.github.stivais.ui.animation + +class Animation(private var duration: Float, val type: Animations, var from: Float = 0f, var to: Float = 1f) { + + private var time: Long = System.nanoTime() + + var finished: Boolean = false + + fun get(): Float { + val percent = ((System.nanoTime() - time) / duration) + finished = percent >= 1f + return (if (finished) to else from + (to - from) * type.getValue(percent)) + } + + override fun toString(): String = "Animation(duration=$duration)" +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/animation/animations.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/animation/animations.kt new file mode 100644 index 000000000..3a0059e55 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/animation/animations.kt @@ -0,0 +1,26 @@ +package com.github.stivais.ui.animation + +import kotlin.math.pow + +private interface Strategy { + fun getValue(percent: Float): Float +} + +// todo: add more +enum class Animations : Strategy { + Linear { + override fun getValue(percent: Float): Float = percent + }, + EaseInQuint { + override fun getValue(percent: Float): Float = percent * percent * percent * percent * percent + }, + EaseOutQuint { + override fun getValue(percent: Float): Float = 1 - (1 - percent).pow(5f) + }, + EaseInOutQuint { + override fun getValue(percent: Float): Float { + return if (percent < 0.5f) 16f * percent * percent * percent * percent * percent + else 1 - (-2 * percent + 2).pow(5f) / 2f + } + }; +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/color/Color.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/color/Color.kt new file mode 100644 index 000000000..6c084ba37 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/color/Color.kt @@ -0,0 +1,169 @@ +package com.github.stivais.ui.color + +import com.github.stivais.ui.animation.Animation +import com.github.stivais.ui.animation.Animations +import com.github.stivais.ui.utils.getRGBA +import java.awt.Color as JColor + +fun Color.toHSB(): Color.HSB { + return Color.HSB( + JColor.RGBtoHSB( + red, + green, + blue, + FloatArray(size = 3) + ), + a / 255f + ) +} + +inline fun Color(crossinline getter: () -> Int): Color = object : Color { + override val rgba: Int + get() { + return getter() + } +} + +interface Color { + + val rgba: Int + + val red + get() = rgba shr 16 and 0xFF + + val green + get() = rgba shr 8 and 0xFF + + val blue + get() = rgba and 0xFF + + val a + get() = rgba shr 24 and 0xFF + + @JvmInline + value class RGB(override val rgba: Int) : Color { + constructor(red: Int, green: Int, blue: Int, alpha: Float = 1f) : this(getRGBA(red, green, blue, (alpha * 255).toInt())) + } + + open class HSB(hue: Float, saturation: Float, brightness: Float, alpha: Float = 1f) : Color { + + constructor(hsb: FloatArray, alpha: Float = 1f) : this(hsb[0], hsb[1], hsb[2], alpha) + + var hue = hue + set(value) { + field = value + needsUpdate = true + } + + var saturation = saturation + set(value) { + field = value + needsUpdate = true + } + + var brightness = brightness + set(value) { + field = value + needsUpdate = true + } + + var alpha = alpha + set(value) { + field = value + needsUpdate = true + } + + private var needsUpdate: Boolean = true + + override var rgba: Int = 0 + get() { + if (needsUpdate) { + field = (JColor.HSBtoRGB(hue, saturation, brightness) and 0X00FFFFFF) or ((alpha * 255).toInt() shl 24) + needsUpdate = false + } + return field + } + } + + class Animated(from: Color, to: Color) : Color { + + constructor(from: Color, to: Color, swapIf: Boolean) : this(from, to) { + if (swapIf) { + swap() + current = color1.rgba + } + } + + private var color1: Color = from + private var color2: Color = to + + private var animation: Animation? = null + + var current: Int = color1.rgba + var from: Int = color1.rgba + + override val rgba: Int + get() { + if (animation != null) { + val progress = animation!!.get() + val to = color2.rgba + current = getRGBA( + (from.red + (to.red - from.red) * progress).toInt(), + (from.green + (to.green - from.green) * progress).toInt(), + (from.blue + (to.blue - from.blue) * progress).toInt(), + (from.alpha + (to.alpha - from.alpha) * progress).toInt() + ) + if (animation!!.finished) { + animation = null + swap() + } + return current + } + return color1.rgba + } + + fun animate(duration: Float = 0f, type: Animations) { + if (duration == 0f) { + swap() + current = color1.rgba // here so it updates if you swap a color and want to animate it later + } else { + if (animation != null) { + swap() + animation = Animation(duration * animation!!.get(), type) + } else { + animation = Animation(duration, type) + } + from = current + } + } + + private fun swap() { + val temp = color2 + color2 = color1 + color1 = temp + } + } + + companion object { + @JvmField + val TRANSPARENT = RGB(0, 0, 0, 0f) + + @JvmField + val WHITE = RGB(255, 255, 255) + + @JvmField + val BLACK = RGB(0, 0, 0) + } +} + +inline val Int.red + get() = this shr 16 and 0xFF + +inline val Int.green + get() = this shr 8 and 0xFF + +inline val Int.blue + get() = this and 0xFF + +inline val Int.alpha + get() = this shr 24 and 0xFF \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/constraints.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/constraints.kt new file mode 100644 index 000000000..5b95fce31 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/constraints.kt @@ -0,0 +1,37 @@ +package com.github.stivais.ui.constraints + +import com.github.stivais.ui.elements.Element + +class Constraints(var x: Position, var y: Position, var width: Size, var height: Size) + +// todo: reduce interface usages (however im not sure if its possible) +interface Constraint { + fun get(element: Element, type: Type): Float + + fun reliesOnChild() = false + + companion object { + const val HORIZONTAL: Int = 0 + const val VERTICAL: Int = 1 + } +} + +interface Position : Constraint + +interface Size : Constraint + +interface Measurement : Position, Size + +enum class Type { + X, Y, W, H; + + inline val axis: Int + get() = when (this) { + X, W -> 0 + Y, H -> 1 + } + + + inline val isPosition: Boolean + get() = ordinal < 2 +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/dsl.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/dsl.kt new file mode 100644 index 000000000..1303355b0 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/dsl.kt @@ -0,0 +1,64 @@ +package com.github.stivais.ui.constraints + +import com.github.stivais.ui.constraints.measurements.LeftPixel +import com.github.stivais.ui.constraints.measurements.Percent +import com.github.stivais.ui.constraints.measurements.Pixel +import com.github.stivais.ui.constraints.measurements.Undefined +import com.github.stivais.ui.constraints.operational.Additive +import com.github.stivais.ui.constraints.operational.Subtractive +import com.github.stivais.ui.constraints.positions.Center + +// A lot of options depending on preferences + +fun constrain(x: Position, y: Position, w: Size, h: Size) = Constraints(x, y, w, h) + +fun c(x: Position, y: Position, w: Size, h: Size) = Constraints(x, y, w, h) + +fun at(x: Position, y: Position) = Constraints(x, y, Undefined, Undefined) + +fun size(w: Size, h: Size) = Constraints(Undefined, Undefined, w, h) + +fun x(x: Position) = at(x, Undefined) + +fun Position.toX() = x(this) + +fun y(y: Position) = at(Undefined, y) + +fun Position.toY() = y(this) + +fun width(w: Size) = size(w, Undefined) + +fun w(w: Size) = width(w) + +fun Size.toWidth() = width(this) + +fun height(h: Size) = size(Undefined, h) + +fun h(h: Size) = height(h) + +fun Size.toHeight() = height(this) + + +val Number.px: Pixel + get() { + val value = this.toFloat() + return if (value < 0) LeftPixel(value) else Pixel(value) + } + +val Number.percent: Percent + get() { + val value = this.toFloat() / 100f + return Percent(value) + } + +fun center(): Constraints = Constraints(Center, Center, Undefined, Undefined) + +// todo: check if indent is 0 and make it uses copying() and also make it an object to reduce amount of initialized classes +fun copyParent(indent: Number = 0f): Constraints { + val px = indent.px + return Constraints(px, px, -px, -px) +} + +operator fun Constraint.plus(other: Constraint) = Additive(this, other) + +operator fun Constraint.minus(other: Constraint) = Subtractive(this, other) diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Animatable.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Animatable.kt new file mode 100644 index 000000000..e4ba9a5cf --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Animatable.kt @@ -0,0 +1,88 @@ +package com.github.stivais.ui.constraints.measurements + +import com.github.stivais.ui.animation.Animation +import com.github.stivais.ui.animation.Animations +import com.github.stivais.ui.constraints.Constraint +import com.github.stivais.ui.constraints.Measurement +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +class Animatable(var from: Constraint, var to: Constraint): Measurement { + + constructor(from: Constraint, to: Constraint, swapIf: Boolean) : this(from, to) { + if (swapIf) { + swap() + } + } + + private var animation: Animation? = null + + var current: Float = 0f + + var before: Float? = null // is null + + override fun get(element: Element, type: Type): Float { + if (animation != null) { + val progress = animation!!.get() + val from = before ?: from.get(element, type) + current = from + (to.get(element, type) - from) * progress + + if (animation!!.finished) { + animation = null + before = null + swap() + } + return current + } + return from.get(element, type) + } + + fun animate(duration: Float, type: Animations) { + if (duration == 0f) { + swap() + } else { + if (animation != null) { + before = current + swap() + animation = Animation(duration * animation!!.get(), type) + } else { + animation = Animation(duration, type) + } + } + } + + private fun swap() { + val temp = to + to = from + from = temp + } + + override fun reliesOnChild(): Boolean { + return from.reliesOnChild() || to.reliesOnChild() + } + + class Raw(start: Float) : Measurement { + + private var current: Float = start + + private var animation: Animation? = null + + fun animate(to: Float, duration: Float, type: Animations = Animations.Linear) { + if (duration != 0f) animation = Animation(duration, type, animation?.get() ?: current, to) else current = to + } + + fun to(to: Float) = if (animation != null) animation!!.to = to else current = to + + override fun get(element: Element, type: Type): Float { + if (animation != null) { + val result = animation!!.get() + if (animation!!.finished) { + animation = null + current = result + } + return result + } + return current + } + } +} diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Percent.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Percent.kt new file mode 100644 index 000000000..dfbd3b081 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Percent.kt @@ -0,0 +1,18 @@ +package com.github.stivais.ui.constraints.measurements + +import com.github.stivais.ui.constraints.Constraint.Companion.HORIZONTAL +import com.github.stivais.ui.constraints.Measurement +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +class Percent(percent: Float) : Measurement { + + private var percent = percent + set(value) { + field = value.coerceIn(0f, 1f) + } + + override fun get(element: Element, type: Type): Float { + return (if (type.axis == HORIZONTAL) element.parent!!.width else element.parent!!.height) * percent + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Pixel.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Pixel.kt new file mode 100644 index 000000000..56c1f5603 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Pixel.kt @@ -0,0 +1,24 @@ +package com.github.stivais.ui.constraints.measurements + +import com.github.stivais.ui.constraints.Constraint.Companion.HORIZONTAL +import com.github.stivais.ui.constraints.Measurement +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +open class Pixel(var pixels: Float): Measurement { + + override fun get(element: Element, type: Type): Float = pixels + + operator fun unaryMinus(): LeftPixel = LeftPixel(pixels) +} + +// todo: find a better name +class LeftPixel(pixels: Float): Pixel(pixels) { + override fun get(element: Element, type: Type): Float { + return if (type.axis == HORIZONTAL) { + (element.parent?.width ?: 0f) - (if (type.isPosition) element.width else element.internalX) - pixels + } else { + (element.parent?.height ?: 0f) - (if (type.isPosition) element.height else element.internalY) - pixels + } + } +} diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Undefined.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Undefined.kt new file mode 100644 index 000000000..bfe928e58 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/measurements/Undefined.kt @@ -0,0 +1,11 @@ +package com.github.stivais.ui.constraints.measurements + +import com.github.stivais.ui.constraints.Measurement +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +data object Undefined : Measurement { + override fun get(element: Element, type: Type): Float { + return 0f + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/operational/Additive.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/operational/Additive.kt new file mode 100644 index 000000000..ce4b33954 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/operational/Additive.kt @@ -0,0 +1,15 @@ +package com.github.stivais.ui.constraints.operational + +import com.github.stivais.ui.constraints.Constraint +import com.github.stivais.ui.constraints.Measurement +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +class Additive(val first: Constraint, val second: Constraint) : Measurement { + override fun get(element: Element, type: Type): Float = first.get(element, type) + second.get(element, type) + +} + +class Subtractive(val first: Constraint, val second: Constraint) : Measurement { + override fun get(element: Element, type: Type): Float = first.get(element, type) - second.get(element, type) +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Center.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Center.kt new file mode 100644 index 000000000..993f4af5d --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Center.kt @@ -0,0 +1,14 @@ +package com.github.stivais.ui.constraints.positions + +import com.github.stivais.ui.constraints.Constraint.Companion.HORIZONTAL +import com.github.stivais.ui.constraints.Position +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +object Center : Position { + override fun get(element: Element, type: Type): Float { + val axis = type.axis + return if (axis == HORIZONTAL) (element.parent?.width ?: 0f) / 2f - element.width / 2f + else (element.parent?.height ?: 0f) / 2f - element.height / 2f + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Linked.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Linked.kt new file mode 100644 index 000000000..98d2d8d08 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/positions/Linked.kt @@ -0,0 +1,14 @@ +package com.github.stivais.ui.constraints.positions + +import com.github.stivais.ui.constraints.Constraint.Companion.HORIZONTAL +import com.github.stivais.ui.constraints.Position +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +class Linked(private val link: Element?) : Position { + override fun get(element: Element, type: Type): Float { + if (link == null) return 0f + return if (type.axis == HORIZONTAL) link.internalX + link.width else link.internalY + link.height + } +} +// column position gets \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Bounding.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Bounding.kt new file mode 100644 index 000000000..d25eec9b2 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Bounding.kt @@ -0,0 +1,25 @@ +package com.github.stivais.ui.constraints.sizes + +import com.github.stivais.ui.constraints.Constraint.Companion.HORIZONTAL +import com.github.stivais.ui.constraints.Size +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.utils.forLoop + +object Bounding : Size { + + override fun get(element: Element, type: Type): Float { + if (element.elements == null) return 0f + var value = 0f + element.elements!!.forLoop { child -> + if (!child.enabled) return@forLoop + val new = if (type.axis == HORIZONTAL) child.internalX + child.width else child.internalY + child.height + if (new > value) value = new + } + return value + } + + override fun reliesOnChild(): Boolean { + return true + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Copying.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Copying.kt new file mode 100644 index 000000000..fa99f87ca --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/constraints/sizes/Copying.kt @@ -0,0 +1,12 @@ +package com.github.stivais.ui.constraints.sizes + +import com.github.stivais.ui.constraints.Constraint.Companion.HORIZONTAL +import com.github.stivais.ui.constraints.Size +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.elements.Element + +object Copying : Size { + override fun get(element: Element, type: Type): Float { + return if (type.axis == HORIZONTAL) element.parent!!.width else element.parent!!.height + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/Element.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/Element.kt new file mode 100644 index 000000000..e56480084 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/Element.kt @@ -0,0 +1,222 @@ +package com.github.stivais.ui.elements + +import com.github.stivais.ui.UI +import com.github.stivais.ui.UI.Companion.logger +import com.github.stivais.ui.color.Color +import com.github.stivais.ui.constraints.Constraint +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.constraints.measurements.Undefined +import com.github.stivais.ui.constraints.positions.Center +import com.github.stivais.ui.events.Event +import com.github.stivais.ui.events.Mouse +import com.github.stivais.ui.utils.forLoop + + +// Positioning (X or Y) should be: defined with a measurement or aligned by left/top, center, right/bottom (with some padding) +// or undefined where it gets set by the parent's element (by default best option is to center) +// +// Sizing (Width or Height) should be: defined with a measurement, copy the parents size, or wrap around elements children, +// if left undefined, it is up to the element to set what it's default value should be +// +// Required: +// Measurements (All): +// Pixels: Pixels on the screen +// Animatable: Based between 2 constraints values +// Percents: Based on parent elements position (Maybe?) +// +// Positions (X or Y): +// Linked: Gets position based on where the linked element ends, (Used in column) +// Aligned: Left/Middle/Right Aligning with padding (padding doesn't apply to middle) +// +// Sizing (Width or Height): +// Bounding: Wraps around all children element, so they all fit +// Copying: Copies the parent's sizing +// +// Goal: to avoid assigning positions and sizes as much as possible/mainly avoid magic numbers + +abstract class Element(constraints: Constraints?) { + + // todo: maybe bring all values into here? + val constraints: Constraints = constraints ?: Constraints(Undefined, Undefined, Undefined, Undefined) + + lateinit var ui: UI + +// val renderer get() = ui.renderer + + var parent: Element? = null + + var elements: ArrayList? = null + + open var events: HashMap Boolean>>? = null + + private var initializationTasks: ArrayList<() -> Unit>? = null + + var x: Float = 0f + var y: Float = 0f + var width: Float = 0f + var height: Float = 0f + + var internalX: Float = 0f + set(value) { + field = value + x = value + (parent?.x ?: 0f) + } + + var internalY: Float = 0f + set(value) { + field = value + y = value + (parent?.y ?: 0f) + } + + var color: Color? = null + + var isHovered = false + set(value) { + if (value) { + accept(Mouse.Entered) + } else { + accept(Mouse.Exited) + } + field = value + } + + var enabled: Boolean = true + + var renders: Boolean = true + get() = enabled && field + + abstract fun draw() + + // position needs a rework, make it only reposition if it needs, + // make it parent place child so it can be customized allowing for wrapping columns/rows + open fun position() { + if (!enabled) return + if (!constraints.width.reliesOnChild()) width = constraints.width.get(this, Type.W) + if (!constraints.height.reliesOnChild()) height = constraints.height.get(this, Type.H) + internalX = constraints.x.get(this, Type.X) + internalY = constraints.y.get(this, Type.Y) + + elements?.forLoop { element -> + element.position() + element.renders = element.intersects(this.x, this.y, width, height) + } + if (constraints.width.reliesOnChild()) width = constraints.width.get(this, Type.W) + if (constraints.height.reliesOnChild()) height = constraints.height.get(this, Type.H) + } + + fun render() { + if (!renders) return + draw() + elements?.forLoop { element -> + element.render() + } + } + + open fun accept(event: Event): Boolean { + if (events != null) { + events?.get(event)?.let { actions -> actions.forLoop { if (it(event)) return true } } + } + return false + } + + fun registerEvent(event: Event, block: Event.() -> Boolean) { + if (events == null) events = HashMap() + events!!.getOrPut(event) { arrayListOf() }.add(block) + + } + + fun onInitialization(action: () -> Unit) { + if (::ui.isInitialized) return logger.warning("Tried calling \"onInitialization\" after init has already been done") + if (initializationTasks == null) initializationTasks = arrayListOf() + initializationTasks!!.add(action) + } + + fun addElement(element: Element) { + if (elements == null) elements = arrayListOf() + elements!!.add(element) + element.parent = this + element.initialize(ui) + onElementAdded(element) + if (ui.settings.positionOnAdd) element.position() + } + + fun initialize(ui: UI) { + this.ui = ui + if (initializationTasks != null) { + initializationTasks!!.forLoop { it() } + initializationTasks!!.clear() + initializationTasks = null + } + } + + // sets up position if element being added has an undefined position + open fun onElementAdded(element: Element) { + element.apply { + if (constraints.x is Undefined) constraints.x = Center + if (constraints.y is Undefined) constraints.y = Center + } + } + + fun isInside(x: Float, y: Float): Boolean { + val tx = this.x + val ty = this.y + return x in tx..tx + width && y in ty..ty + height + } + + fun intersects(x: Float, y: Float, width: Float, height: Float): Boolean { + val tx = this.x + val ty = this.y + val tw = this.width + val th = this.height + return (x < tx + tw && tx < x + width) && (y < ty + th && ty < y + height) + } + + fun focused(): Boolean = ui.eventManager?.focused == this + + // todo: dsl, maybe move out of this class? + fun toggle(value: Boolean = !enabled) { + enabled = value + } + + // todo: dsl, maybe move out of this class? + fun width(): Constraint { + return constraints.width + } + + // todo: dsl, maybe move out of this class? + fun height(): Constraint { + return constraints.height + } + + // todo: dsl, maybe move out of this class? + fun sibling(distance: Int = 1): Element? { + if (parent != null) { + val currIndex = parent!!.elements!!.indexOf(this) + return parent!!.elements!!.getOrNull(currIndex + distance) + } + return null + } + + // todo: dsl, maybe move out of this class? + fun sendEventTo(event: Event, target: Element): Boolean { + return target.accept(event) + } + + // todo: dsl, maybe move out of this class? + fun sendEventTo(target: Element): Event.() -> Boolean { + return { target.accept(this) } + } + + fun takeEvents(from: Element) { + if (from.events == null) return logger.warning("Tried to take event from an element that doesn't have events") + if (events != null) { + events!!.putAll(from.events!!) + } else { + events = from.events + } + from.events = null + } + + operator fun invoke(action: Element.() -> Unit) = action() +} diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/dsl.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/dsl.kt new file mode 100644 index 000000000..215e191d4 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/dsl.kt @@ -0,0 +1,2 @@ +package com.github.stivais.ui.elements + diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Block.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Block.kt new file mode 100644 index 000000000..3f99743f5 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Block.kt @@ -0,0 +1,53 @@ +package com.github.stivais.ui.elements.impl + +import com.github.stivais.ui.color.Color +import com.github.stivais.ui.color.alpha +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.sizes.Copying +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.utils.replaceUndefined +import me.odinmain.utils.render.rectangleOutline +import me.odinmain.utils.render.roundedRectangle + +open class Block(constraints: Constraints?, color: Color) : Element(constraints?.replaceUndefined(w = Copying, h = Copying)) { + + var outlineColor: Color? = null + + init { + this.color = color + } + + override fun draw() { + if (color!!.rgba.alpha != 0) { + roundedRectangle(x, y, width, height, me.odinmain.utils.render.Color(color!!.rgba)) +// renderer.rect(x, y, width, height, color!!.rgba) + } + if (outlineColor != null && outlineColor!!.rgba.alpha != 0) { + rectangleOutline(x, y, width, height, me.odinmain.utils.render.Color(outlineColor!!.rgba), 0f, 1f) +// renderer.border(x, y, width, height, 1f, null, outlineColor!!.rgba) + } + } + + // Maybe add width + fun outline(color: Color): Block { + outlineColor = color + return this + } +} + +class RoundedBlock(constraints: Constraints?, color: Color, private val radii: FloatArray) : Block(constraints, color) { + + init { + require(radii.size == 4) { "Radii FloatArray for RoundedBlock must only have 4 values." } + } + + override fun draw() { + if (color!!.rgba.alpha != 0) { + val clr = me.odinmain.utils.render.Color(color!!.rgba) + roundedRectangle(x, y, width, height, clr, clr, clr, 0f, radii[0], radii[1], radii[2], radii[3], 0.5f) + } + if (outlineColor != null && outlineColor!!.rgba.alpha != 0) { + rectangleOutline(x, y, width, height, me.odinmain.utils.render.Color(outlineColor!!.rgba), 0f, 1f) + } + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Column.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Column.kt new file mode 100644 index 000000000..6cbe5c568 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Column.kt @@ -0,0 +1,39 @@ +package com.github.stivais.ui.elements.impl + +import com.github.stivais.ui.color.Color +import com.github.stivais.ui.color.alpha +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.Type +import com.github.stivais.ui.constraints.measurements.Pixel +import com.github.stivais.ui.constraints.measurements.Undefined +import com.github.stivais.ui.constraints.positions.Linked +import com.github.stivais.ui.constraints.sizes.Bounding +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.utils.replaceUndefined +import me.odinmain.utils.render.roundedRectangle + +// todo: rework it so it works horiziontally, and if a width is defined, it wraps etc +class Column(constraints: Constraints?, var padding: Float = 0f) : Element(constraints.replaceUndefined(w = Bounding, h = Bounding)) { + + override fun draw() { + if (color != null && color!!.rgba.alpha != 0) { + roundedRectangle(x, y, width, height, me.odinmain.utils.render.Color(color!!.rgba)) +// renderer.rect(x, y, width, height, color!!.rgba) + } + } + + override fun onElementAdded(element: Element) { + if (element.constraints.x is Undefined) element.constraints.x = Pixel(0f) + if (element.constraints.y is Undefined) { + val last = elements?.lastOrNull { it.constraints.y is Linked } + element.constraints.y = Linked(last) + element.y = element.constraints.y.get(element, Type.Y) + height = constraints.height.get(element, Type.H) + } + } + + fun background(color: Color): Column { + this.color = color + return this + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Group.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Group.kt new file mode 100644 index 000000000..5b59b0cb2 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Group.kt @@ -0,0 +1,13 @@ +package com.github.stivais.ui.elements.impl + +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.sizes.Bounding +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.utils.replaceUndefined + + +class Group(constraints: Constraints?) : Element(constraints.replaceUndefined(w = Bounding, h = Bounding)) { + override fun draw() { +// renderer.border(x, y, width, height, 1f, java.awt.Color.WHITE.rgb) + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Image.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Image.kt new file mode 100644 index 000000000..59721dd2d --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Image.kt @@ -0,0 +1,17 @@ +package com.github.stivais.ui.elements.impl + +//class Image( +// val image: Image, +// constraints: Constraints?, +// private val radius: Float, +// private val imagePos: FloatArray? +//) : Element(constraints) { +// +// override fun draw() { +// if (imagePos != null) { +// renderer.roundedImage(image, x, y, width, height, radius, imagePos[0], imagePos[1], imagePos[2], imagePos[3]) +// } else { +// renderer.roundedImage(image, x, y, width, height, radius) +// } +// } +//} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Text.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Text.kt new file mode 100644 index 000000000..9a9c5e61a --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/Text.kt @@ -0,0 +1,53 @@ +package com.github.stivais.ui.elements.impl + +import com.github.stivais.ui.color.Color +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.Measurement +import com.github.stivais.ui.constraints.measurements.Pixel +import com.github.stivais.ui.constraints.px +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.utils.replaceUndefined +import me.odinmain.font.OdinFont +import kotlin.reflect.KProperty + +class Text( + text: String, + textColor: Color, + constraints: Constraints?, + val size: Measurement +) : Element(constraints.replaceUndefined(w = 0.px, h = size)) { + + var text: String = text + set(value) { + if (field == value) return + field = value + (constraints.width as Pixel).pixels = OdinFont.getTextWidth(text, height)//renderer.textWidth(value, height) + + } + + init { + this.color = textColor + + onInitialization { + // needs size to be set + parent?.position() + val width = OdinFont.getTextWidth(text, height) +// val width = renderer.textWidth(text, height) + (this.constraints.width as Pixel).pixels = width + this.width = width + } + } + + override fun draw() { + OdinFont.text(text, x, y, me.odinmain.utils.render.Color(color!!.rgba), height) +// renderer.text(text, x, y, color!!.rgba, height) + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): String { + return text + } + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { + text = value + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/TextInput.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/TextInput.kt new file mode 100644 index 000000000..892bc2b82 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/elements/impl/TextInput.kt @@ -0,0 +1,101 @@ +package com.github.stivais.ui.elements.impl + +// Unfinished +//class TextInput(var text: String, constraints: Constraints?) : Element(constraints) { +// +// private var caret: Int = text.length +// set(value) { +// field = value.coerceIn(0, text.length) +// } +// +// //var dragg +// +// var selected = 0f +// +// var cx: Float = 0f +// +// var cy: Float = 0f +// +// override fun draw() { +// if (focused()) { +// renderer.rect(cx, cy, 1f, 16f, Color.WHITE.rgb) +// } +// renderer.text(text, x, y, Color.WHITE.rgb, 16f) +// } +// +// +// init { +// registerEvent(Key.Typed(' ')) { +// insert((this as Key.Typed).char) +// positionCaret() +// true +// } +// onKeycodePressed { +// when (code) { +// GLFW.GLFW_KEY_RIGHT -> right(1) +// +// GLFW.GLFW_KEY_LEFT -> left(1) +// +// GLFW.GLFW_KEY_ENTER -> insert('\n') +// +// GLFW.GLFW_KEY_BACKSPACE -> remove(1) +// +// +// GLFW.GLFW_KEY_ESCAPE -> { +// ui.unfocus() +// } +// } +// positionCaret() +// true +// } +// focuses() +// } +// +// fun left(amount: Int) { +// caret -= amount +// } +// +// fun right(amount: Int) { +// caret += amount +// } +// +// private fun insert(string: Char) { +// val before = caret +// text = text.substring(0, caret) + string + text.substring(caret) +// if (text.length != before) caret++ +// } +// +// private fun insert(string: String) { +// text = text.substring(0, caret) + string + text.substring(caret) +// } +// +// private fun remove(amount: Int) { +// if (caret - amount < 0) return +// text = text.substring(0, caret - amount) + text.substring(caret) +// caret -= amount +// } +// +// fun positionCaret() { +// val currLine = getCurrentLine() +// cx = renderer.textWidth(currLine.first, fontSize = 16f) + x +// cy = y + currLine.second * 16f +// } +// +// fun getCurrentLine(): Pair { +// var i = 0 +// var ls = 0 +// var line = 0 +// +// for (chr in text) { +// i++ +// if (chr == '\n') { +// ls = i +// line++ +// } +// if (i == caret) { +// return text.substring(ls, caret).substringBefore('\n') to line +// } +// } +// return "" to 0 +// } +//} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/events/EventManager.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/events/EventManager.kt new file mode 100644 index 000000000..06f6b0ab4 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/events/EventManager.kt @@ -0,0 +1,145 @@ +package com.github.stivais.ui.events + +import com.github.stivais.ui.UI +import com.github.stivais.ui.elements.Element + + +class EventManager(private val ui: UI) { + + var mouseX: Float = 0f + + var mouseY: Float = 0f + + var focused: Element? = null + private set + + var elementHovered: Element? = null + private set(value) { + if (field === value) return + field?.isHovered = false + value?.isHovered = true + field = value + } + + // + // Mouse Input + // + + fun onMouseMove(x: Float, y: Float) { + mouseX = x + mouseY = y + elementHovered = getHovered(x, y, ui.main) + dispatchToAll(Mouse.Moved, ui.main) + } + + fun onMouseClick(button: Int) { + val event = Mouse.Clicked(button) + if (focused != null) { + if (!dispatchFocused(focused, event) && !focused!!.isInside(mouseX, mouseY)) { + unfocus() + } + return + } + dispatch(event) + } + + fun onMouseRelease(button: Int) { + val event = Mouse.Released(button) + dispatchToAll(event, ui.main) + } + + fun onMouseScroll(amount: Float) { + val event = Mouse.Scrolled(amount) + dispatch(event) + } + + // + // Keyboard Input + // + + fun onKeyType(char: Char): Boolean { + val event = Key.Typed(char) + if (focused != null) { + return focused!!.accept(event) + } + return false + } + + fun onKeycodePressed(code: Int): Boolean { + val event = Key.CodePressed(code, true) + if (focused != null) { + return focused!!.accept(event) + } + return false + } + + fun onKeyReleased(code: Int): Boolean { + val event = Key.CodePressed(code, false) + if (focused != null) { + return focused!!.accept(event) + } + return false + } + + // + // + // + + fun focus(element: Element) { + focused?.accept(Focused.Lost) + focused = element + element.accept(Focused.Gained) + } + + fun unfocus() { + focused?.accept(Focused.Lost) + focused = null + } + + private fun getHovered(x: Float, y: Float, element: Element): Element? { + var result: Element? = null + if (element.renders && element.isInside(x, y)) { + if (element.events != null) result = element // checks if even accepts any input/events + if (element.elements != null) { + for (child in element.elements!!) { + getHovered(x, y, child)?.let { result = it } + } + } + } + return result + } + + private fun dispatch(event: Event, element: Element? = elementHovered): Boolean { + var current = element + while (current != null) { + if (current.accept(event)) return true + current = current.parent + } + return false + } + + private fun dispatchToAll(event: Event, element: Element) { + element.accept(event) + if (element.elements != null) { + for (child in element.elements!!) { + dispatchToAll(event, child) + } + } + } + + // todo: maybe find a better way or clean this up? im not quite happy with it + private fun dispatchFocused(element: Element?, event: Event): Boolean { // could cause unwanted execution of certain inputs + element?.let { + for (entry in it.events?.entries ?: return true) { + if (entry.key::class.java == event::class.java) { // check if java class is the same + if (!entry.key.isFocused()) continue + for (function in entry.value) { + if (function(event)) return true + } + return true + } + } + } + return false + } +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/events/dsl.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/events/dsl.kt new file mode 100644 index 000000000..c846de76c --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/events/dsl.kt @@ -0,0 +1,66 @@ +@file:Suppress("UNCHECKED_CAST") + +package com.github.stivais.ui.events + +import com.github.stivais.ui.elements.Element + + +fun E.onClick(button: Int?, block: Mouse.Clicked.() -> Boolean): E { + registerEvent(Mouse.Clicked(button), block as Event.() -> Boolean) + return this +} + +fun E.onRelease(button: Int, block: Mouse.Released.() -> Unit): E { + registerEvent(Mouse.Released(button)) { + (this as Mouse.Released).block() + true + } + return this +} + +fun E.onMouseEnter(block: Mouse.Entered.() -> Boolean): E { + registerEvent(Mouse.Entered, block as Event.() -> Boolean) + return this +} + +fun E.onMouseExit(block: Mouse.Exited.() -> Boolean): E { + registerEvent(Mouse.Exited, block as Event.() -> Boolean) + return this +} + +fun E.onMouseEnterExit(block: Event.() -> Boolean): E { + registerEvent(Mouse.Entered, block) + registerEvent(Mouse.Exited, block) + return this +} + +fun E.onMouseMove(block: Mouse.Moved.() -> Boolean): E { + registerEvent(Mouse.Moved, block as Event.() -> Boolean) + return this +} + +fun E.onKeycodePressed(keycode: Int = -1, block: Key.CodePressed.() -> Boolean): E { + registerEvent(Key.CodePressed(keycode, true), block as Event.() -> Boolean) + return this +} + +//fun E.onKeyRelease(keycode: Int? = null, block: Key.CodeReleased.() -> Boolean): E { +// registerEvent(Key.CodeReleased(keycode), false, block as Event.() -> Boolean) +// return this +//} + +fun E.onFocusGain(block: Focused.Gained.() -> Unit): E { + registerEvent(Focused.Gained) { + (this as Focused.Gained).block() + true + } + return this +} + +fun E.onFocusLost(block: Focused.Lost.() -> Unit): E { + registerEvent(Focused.Lost) { + Focused.Lost.block() + true + } + return this +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/events/events.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/events/events.kt new file mode 100644 index 000000000..3683fd9ae --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/events/events.kt @@ -0,0 +1,113 @@ +package com.github.stivais.ui.events + +interface Event { + // todo: maybe better way? + fun isFocused() = false +} + +interface Mouse : Event { + + class Clicked(val button: Int?) : Mouse { + + override fun isFocused(): Boolean = button == null + + override fun equals(other: Any?): Boolean { // needs to be overridden, so it is recognized in the events Map + if (this === other) return true + if (other !is Clicked) return false + return button == other.button + } + + override fun hashCode(): Int = 31 * ((button ?: -1) + 500) + + override fun toString(): String = "MouseClicked(button=$button)" + } + + class Released(val button: Int) : Mouse { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Released) return false + return button == other.button + } + + override fun hashCode(): Int = 31 * (button + 250) + + override fun toString(): String = "MouseReleased(button=$button)" + } + + class Scrolled(val amount: Float) : Mouse { + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Scrolled + } + + override fun hashCode(): Int = 28629151 // 31^5 + } + + data object Entered : Mouse + + data object Exited : Mouse + + data object Moved: Mouse +} + +// todo: implement cleaner way? +// todo: implement key mods (i.e indicator for if ctrl and or shift is down) +interface Key : Event { + + class Typed(val char: Char) : Key { + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Typed + //if (other !is Typed) return false + //return char == other.char + } + + override fun hashCode(): Int { + return 31 + } + } + + + class CodePressed(val code: Int, private val down: Boolean) : Key { + + override fun isFocused(): Boolean = true + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CodePressed) return false + return down == other.down + + } + + override fun hashCode(): Int { + return 29791 + down.hashCode()// 31^3 + } + } + +// class CodeReleased(val code: Int?) : Key { +// +// override fun isFocused(): Boolean = true +// +// override fun equals(other: Any?): Boolean { +// if (this === other) return true +// if (other !is CodeReleased) return false +// return other.code == null || code == other.code +// } +// +// override fun hashCode(): Int { +// return 923521 // 31^4 +// } +// } +} + +interface Focused : Event { + + data object Gained : Focused { + override fun isFocused(): Boolean = true + } + + data object Lost : Focused { + override fun isFocused(): Boolean = true + } + +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/utils/dsl.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/utils/dsl.kt new file mode 100644 index 000000000..6f143154e --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/utils/dsl.kt @@ -0,0 +1,119 @@ +package com.github.stivais.ui.utils + +import com.github.stivais.ui.UI +import com.github.stivais.ui.animation.Animations +import com.github.stivais.ui.color.Color +import com.github.stivais.ui.constraints.Constraint +import com.github.stivais.ui.constraints.measurements.Animatable +import com.github.stivais.ui.constraints.measurements.Pixel +import com.github.stivais.ui.constraints.minus +import com.github.stivais.ui.elements.Element +import com.github.stivais.ui.events.Mouse +import com.github.stivais.ui.events.onClick +import com.github.stivais.ui.events.onMouseMove +import com.github.stivais.ui.events.onRelease + +fun radii(tl: Number = 0f, tr: Number = 0f, bl: Number = 0f, br: Number = 0f) = floatArrayOf(tl.toFloat(), bl.toFloat(), br.toFloat(), tr.toFloat()) + +fun radii(all: Number): FloatArray { + val value = all.toFloat() + return floatArrayOf(value, value, value, value) +} + +/** + * DSL for checking if a [color][IColor] is [animatable][AnimatedColor] and animating it. + * + * @param duration The time it takes to complete the animation (in nanoseconds) + * @param type The type of animation to use. (By default it is Linear) + */ +fun Color.animate(duration: Number, type: Animations = Animations.Linear) { + if (this is Color.Animated) animate(duration.toFloat(), type) +} + +fun Constraint.animate(duration: Number, type: Animations = Animations.Linear) { + if (this is Animatable) animate(duration.toFloat(), type) +} + +// todo: make color functions for all color types +fun color(r: Int, g: Int, b: Int, alpha: Float = 1f): Color.RGB = Color.RGB(r, g, b, alpha) + +fun color(from: Color, to: Color, swap: Boolean = false): Color.Animated = Color.Animated(from, to, swap) + +// +//fun color(r: Int, g: Int, b: Int, alpha: Float = 1f): Color.RGB = Color.RGB(r, g, b, alpha) +// +//fun color(r: Int, g: Int, b: Int, alpha: Float = 1f): Color.RGB = Color.RGB(r, g, b, alpha) + +val Number.seconds + get() = this.toFloat() * 1_000_000_000 + + +// todo: cleanup +fun E.draggable(acceptsEvent: Boolean = true, target: Element = this): E { + var px: Pixel + var py: Pixel + target.constraints.apply { + px = when (x) { + is Pixel -> x as Pixel + else -> { + UI.logger.warning( + "Draggable ${this@draggable::class.java} original X constraint wasn't Pixel, " + + "instead it was ${this::class.simpleName}, this usually leads to unexpected behaviour" + ) + Pixel(0f) + } + } + py = when (y) { + is Pixel -> y as Pixel + else -> { + UI.logger.warning( + "Draggable ${this@draggable::class.java} original Y constraint wasn't Pixel, " + + "instead it was ${this::class.simpleName}, this usually leads to unexpected behaviour" + ) + Pixel(0f) + } + } + } + var pressed = false + var x = 0f + var y = 0f + onClick(0) { + pressed = true + x = ui.mx - this@draggable.x + y = ui.my - this@draggable.y + acceptsEvent + } + onMouseMove { + if (pressed) { + px.pixels = ui.mx - x + py.pixels = ui.my - y + } + acceptsEvent + } + onRelease(0) { + pressed = false + } + return this +} + +fun E.focuses(): E { + onClick(0) { + ui.focus(this@focuses) + true + } + return this +} + +fun E.scrollable(duration: Float, target: Element, min: Float = 0f): E { + var s = 0f + val anim = Animatable.Raw(0f) + target.constraints.apply { + y = (y - anim) + } + registerEvent(Mouse.Scrolled(0f)) { + s = (s - (this as Mouse.Scrolled).amount * 16).coerceIn(min, target.height) + anim.animate(s, duration) + true + } + return this +} \ No newline at end of file diff --git a/odinmain/src/main/kotlin/com/github/stivais/ui/utils/utils.kt b/odinmain/src/main/kotlin/com/github/stivais/ui/utils/utils.kt new file mode 100644 index 000000000..9797e1e75 --- /dev/null +++ b/odinmain/src/main/kotlin/com/github/stivais/ui/utils/utils.kt @@ -0,0 +1,33 @@ +package com.github.stivais.ui.utils + +import com.github.stivais.ui.constraints.Constraints +import com.github.stivais.ui.constraints.Position +import com.github.stivais.ui.constraints.Size +import com.github.stivais.ui.constraints.measurements.Undefined + +fun getRGBA(red: Int, green: Int, blue: Int, alpha: Int): Int { + return ((alpha shl 24) and 0xFF000000.toInt()) or ((red shl 16) and 0x00FF0000) or ((green shl 8) and 0x0000FF00) or (blue and 0x000000FF) +} + +fun Constraints?.replaceUndefined( + x: Position = Undefined, + y: Position = Undefined, + w: Size = Undefined, + h: Size = Undefined +): Constraints { + if (this == null) return Constraints(x, y, w, h) + return this.apply { + if (this.x is Undefined) this.x = x + if (this.y is Undefined) this.y = y + if (this.width is Undefined) this.width = w + if (this.height is Undefined) this.height = h + } +} + +// using this with an arraylist is just as fast as an iterator, but is more memory efficient +inline fun ArrayList.forLoop(block: (E) -> Unit) { + if (this.size == 0) return + for (i in 0..