Refactor rendering system: Introduced RenderObject abstraction, replaced RenderBoundary with ObserverListener, and updated slot calculation for inventory types.

Signed-off-by: Chaoscaot <max@maxsp.de>
This commit is contained in:
2026-06-12 12:00:04 +02:00
parent 997292d58e
commit 70c8678c60
8 changed files with 100 additions and 33 deletions
@@ -19,16 +19,46 @@
package de.steamwar.kotlin.ui package de.steamwar.kotlin.ui
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
class Observer<T>(private var value: T): ReadWriteProperty<Any?, T> { abstract class Observable<T> {
private val listeners = mutableSetOf<RenderBoundary>() private val listeners = mutableSetOf<ObserverListener>()
fun removeListener(render: RenderBoundary) { fun removeListener(render: ObserverListener) {
listeners.remove(render) listeners.remove(render)
} }
internal fun notifyListeners() {
listeners.forEach { it.update() }
}
context(render: ObserverListener)
fun listen(): Observable<T> {
listeners.add(render)
render.observers.add(this)
return this
}
fun listen(callback: (T) -> Unit): () -> Unit {
val listener = object: ObserverListener() {
override fun update() = callback(get())
}
listeners.add(listener)
return { removeListener(listener) }
}
abstract fun get(): T
fun <R> map(mapper: (T) -> R): ReadOnlyProperty<Any?, R> = Delegate(mapper(get()))
class Delegate<T>(val value: T): ReadOnlyProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
}
}
class Observer<T>(private var value: T): ReadWriteProperty<Any?, T>, Observable<T>() {
override fun getValue(thisRef: Any?, property: KProperty<*>): T { override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value return value
} }
@@ -43,21 +73,10 @@ class Observer<T>(private var value: T): ReadWriteProperty<Any?, T> {
notifyListeners() notifyListeners()
} }
fun get() = value override fun get() = value
fun update(update: (T) -> T) { fun update(update: (T) -> T) {
this.value = update(value) this.value = update(value)
notifyListeners() notifyListeners()
} }
private fun notifyListeners() {
listeners.forEach { it.render() }
}
context(render: RenderBoundary)
fun listen(): Observer<T> {
listeners.add(render)
render.observers.add(this)
return this
}
} }
@@ -19,10 +19,10 @@
package de.steamwar.kotlin.ui package de.steamwar.kotlin.ui
abstract class RenderBoundary { abstract class ObserverListener {
val observers = mutableSetOf<Observer<*>>() val observers = mutableSetOf<Observable<*>>()
abstract fun render() abstract fun update()
open fun destroy() { open fun destroy() {
observers.forEach { it.removeListener(this) } observers.forEach { it.removeListener(this) }
@@ -0,0 +1,26 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.kotlin.ui
interface RenderObject {
fun destroy()
fun render()
}
@@ -23,7 +23,7 @@ import de.steamwar.kotlin.ui.context.WindowContext
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryType import org.bukkit.event.inventory.InventoryType
abstract class UIInventory(val player: Player): RenderBoundary() { abstract class UIInventory(val player: Player): ObserverListener() {
var window: UIWindow? = null var window: UIWindow? = null
abstract fun view() abstract fun view()
@@ -37,7 +37,7 @@ abstract class UIInventory(val player: Player): RenderBoundary() {
window!!.open() window!!.open()
} }
override fun render() { fun render() {
window?.onClose() window?.onClose()
window = null window = null
open() open()
@@ -46,7 +46,8 @@ class UIWindow(val player: Player, val render: WindowContext.() -> Unit): Invent
} }
constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) { constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) {
bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title)); assert(type != InventoryType.CHEST) { "Chest inventories should use the constructor with size" }
bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title))
} }
fun open() { fun open() {
@@ -19,8 +19,9 @@
package de.steamwar.kotlin.ui.components package de.steamwar.kotlin.ui.components
import de.steamwar.kotlin.ui.RenderBoundary import de.steamwar.kotlin.ui.ObserverListener
import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.RenderObject
import de.steamwar.kotlin.ui.context.GroupContext import de.steamwar.kotlin.ui.context.GroupContext
import de.steamwar.kotlin.ui.context.RenderParent import de.steamwar.kotlin.ui.context.RenderParent
import org.bukkit.Material import org.bukkit.Material
@@ -28,17 +29,25 @@ import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderBoundary() { class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, ObserverListener() {
override fun render() { override fun update() {
val oldX = x val oldX = x
val oldY = y val oldY = y
renderFunc(this) render()
parent.renderItem(x, y, item, onClick)
if (oldX != x || oldY != y) { if (oldX != x || oldY != y) {
parent.resetSlot(oldX, oldY) parent.resetSlot(oldX, oldY)
} }
} }
override fun destroy() {
parent.resetSlot(x, y)
}
override fun render() {
renderFunc(this)
parent.renderItem(x, y, item, onClick)
}
init { init {
render() render()
} }
@@ -19,16 +19,19 @@
package de.steamwar.kotlin.ui.context package de.steamwar.kotlin.ui.context
import de.steamwar.kotlin.ui.RenderBoundary import de.steamwar.kotlin.ui.ObserverListener
import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.RenderObject
import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): RenderBoundary(), RenderParent { open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): ObserverListener(), RenderObject, RenderParent {
val children = mutableListOf<RenderBoundary>() val children = mutableListOf<RenderObject>()
val updatedSlots = mutableSetOf<Pair<Int, Int>>() val updatedSlots = mutableSetOf<Pair<Int, Int>>()
override fun update() = render()
override fun destroy() { override fun destroy() {
children.forEach { it.destroy() } children.forEach { it.destroy() }
super.destroy() super.destroy()
@@ -23,18 +23,27 @@ import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.UIWindow import de.steamwar.kotlin.ui.UIWindow
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.InventoryType
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
class WindowContext(val window: UIWindow): GroupContext(null, {}) { class WindowContext(val window: UIWindow): GroupContext(null, {}) {
override fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) { override fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) {
window.bukkitInv.setItem(x + y * 9, item) val slot = calculateSlot(x, y)
window.onClicks[x + y * 9] = onClick window.bukkitInv.setItem(slot, item)
window.onClicks[slot] = onClick
} }
override fun resetSlot(x: Int, y: Int) { override fun resetSlot(x: Int, y: Int) {
window.bukkitInv.setItem(x + y * 9, ItemStack.of(Material.AIR)) val slot = calculateSlot(x, y)
window.onClicks.remove(x + y * 9) window.bukkitInv.setItem(slot, ItemStack.of(Material.AIR))
window.onClicks.remove(slot)
}
private fun calculateSlot(x: Int, y: Int) = when (window.inventory.type) {
InventoryType.DROPPER, InventoryType.DISPENSER -> x + y * 3
InventoryType.HOPPER -> x
else -> x + y * 9
} }
fun outsideClick(click: (event: InventoryClickEvent) -> Unit) { fun outsideClick(click: (event: InventoryClickEvent) -> Unit) {