prototype of new shelf UI

This commit is contained in:
りき萌 2023-05-05 11:03:31 +02:00
parent 8324bff154
commit 766a9e10ee
19 changed files with 537 additions and 129 deletions

Binary file not shown.

View file

@ -5,7 +5,7 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking
import net.liquidev.dawd3.common.*
import net.liquidev.dawd3.item.PatchCableItem
import net.liquidev.dawd3.net.DisconnectPort
import net.liquidev.dawd3.ui.Rack
import net.liquidev.dawd3.ui.RackScreen
import net.minecraft.block.*
import net.minecraft.block.entity.BlockEntity
import net.minecraft.client.MinecraftClient
@ -98,7 +98,7 @@ class DeviceBlock(private val descriptor: AnyDeviceBlockDescriptor) :
if (usedPortName == null && !player.isSneaking) {
// We have to do a little dance such that the server executes this code too and doesn't
// try to perform the item action if the UI is opened.
val adjacentDevices = Rack.collectAdjacentDevices(world, pos)
val adjacentDevices = RackScreen.collectAdjacentDevices(world, pos)
val shouldOpenUI = adjacentDevices.any { blockPos ->
val blockEntityAtPosition =
world.getBlockEntity(blockPos) as? DeviceBlockEntity ?: return@any false
@ -106,7 +106,7 @@ class DeviceBlock(private val descriptor: AnyDeviceBlockDescriptor) :
}
if (shouldOpenUI) {
if (world is ClientWorld) {
MinecraftClient.getInstance().setScreen(Rack(world, adjacentDevices))
MinecraftClient.getInstance().setScreen(RackScreen(world, adjacentDevices))
}
return ActionResult.success(world.isClient)
}

View file

@ -5,7 +5,7 @@ import net.liquidev.dawd3.audio.device.ControlSet
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.block.device.descriptor.FaceTextures
import net.liquidev.dawd3.common.Cuboids
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.block.AbstractBlock
import net.minecraft.block.Material
import net.minecraft.client.world.ClientWorld
@ -41,7 +41,7 @@ interface DeviceBlockDescriptor<out CS : DeviceBlockDescriptor.ClientState, out
}
interface UI<out Controls : ControlSet> {
fun open(controls: @UnsafeVariance Controls, x: Float, y: Float): Widget
fun open(controls: @UnsafeVariance Controls, x: Float, y: Float): Window
}
val ui: UI<Controls>?

View file

@ -8,7 +8,6 @@ import net.liquidev.dawd3.block.device.PhysicalPort
import net.liquidev.dawd3.block.device.descriptor.FaceTextures
import net.liquidev.dawd3.common.Cuboids
import net.liquidev.dawd3.ui.widget.Knob
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.world.ClientWorld
import net.minecraft.text.Text
@ -39,7 +38,7 @@ object KnobBlockDescriptor : DeviceBlockDescriptor<KnobBlockDescriptor.ClientSta
ClientState(controls)
override val ui = object : DeviceBlockDescriptor.UI<ConstantDevice.Controls> {
override fun open(controls: ConstantDevice.Controls, x: Float, y: Float): Widget =
override fun open(controls: ConstantDevice.Controls, x: Float, y: Float) =
Window(x, y, 48f, 48f, Text.translatable("block.dawd3.knob")).apply {
children.add(
Knob(

View file

@ -8,7 +8,6 @@ import net.liquidev.dawd3.block.device.PhysicalPort
import net.liquidev.dawd3.ui.units.AmplitudeValue
import net.liquidev.dawd3.ui.units.PercentageValue
import net.liquidev.dawd3.ui.widget.Knob
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.world.ClientWorld
import net.minecraft.text.Text
@ -45,7 +44,7 @@ object AmplifierBlockDescriptor : DeviceBlockDescriptor<AmplifierBlockDescriptor
ClientState(controls)
override val ui = object : DeviceBlockDescriptor.UI<AmplifierDevice.Controls> {
override fun open(controls: AmplifierDevice.Controls, x: Float, y: Float): Widget =
override fun open(controls: AmplifierDevice.Controls, x: Float, y: Float) =
Window(
x,
y,

View file

@ -8,7 +8,6 @@ import net.liquidev.dawd3.block.device.PhysicalPort
import net.liquidev.dawd3.block.device.descriptor.FaceTextures
import net.liquidev.dawd3.common.Cuboids
import net.liquidev.dawd3.ui.widget.Knob
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.world.ClientWorld
import net.minecraft.text.Text
@ -43,7 +42,7 @@ object ModulatorBlockDescriptor : DeviceBlockDescriptor<ModulatorBlockDescriptor
ClientState(controls)
override val ui = object : DeviceBlockDescriptor.UI<FmaDevice.Controls> {
override fun open(controls: FmaDevice.Controls, x: Float, y: Float): Widget =
override fun open(controls: FmaDevice.Controls, x: Float, y: Float) =
Window(x, y, 78f, 48f, Text.translatable("block.dawd3.modulator")).apply {
children.add(
Knob(

View file

@ -364,4 +364,12 @@ object Render {
val x = centerX - width / 2
tooltip(matrices, x, y, text, atlas, tooltip)
}
inline fun colorized(r: Float, g: Float, b: Float, a: Float, then: () -> Unit) {
val (oldR, oldG, oldB, oldA) = RenderSystem.getShaderColor()
RenderSystem.setShaderColor(r, g, b, a)
RenderSystem.enableBlend() // sigh
then()
RenderSystem.setShaderColor(oldR, oldG, oldB, oldA)
}
}

View file

@ -1,11 +1,11 @@
package net.liquidev.dawd3.ui
import com.mojang.blaze3d.systems.RenderSystem
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.liquidev.dawd3.render.Atlas
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.Sprite
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.rack.Rack
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.util.math.MatrixStack
@ -15,86 +15,39 @@ import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import kotlin.math.max
class Rack(
class RackScreen(
val world: ClientWorld,
shownDevices: Iterable<BlockPos>,
) : Screen(Text.translatable("screen.dawd3.rack.title")) {
private data class OpenWidget(
val blockPosition: BlockPos,
val widget: Widget,
)
private var openWidgets =
ArrayList(shownDevices.mapNotNull { blockPosition ->
val blockEntity = world.getBlockEntity(blockPosition) as DeviceBlockEntity
blockEntity.descriptor.ui
?.open(blockEntity.controls, 0f, 0f)
?.let { widget -> OpenWidget(blockPosition, widget) }
})
private fun layoutWindows() {
var x = 16f
var y = 16f
var rowHeight = 0f
for (openWidget in openWidgets) {
if (x + openWidget.widget.width >= width - 16) {
x = 16f
y += rowHeight + 8f
rowHeight = 0f
}
openWidget.widget.x = x
openWidget.widget.y = y
x += openWidget.widget.width + 8f
rowHeight = max(rowHeight, openWidget.widget.height)
}
}
private val rootWidget = Rack(width.toFloat(), height.toFloat(), world, shownDevices)
override fun init() {
super.init()
layoutWindows()
rootWidget.width = width.toFloat()
rootWidget.height = height.toFloat()
rootWidget.reflow()
}
override fun resize(client: MinecraftClient?, width: Int, height: Int) {
super.resize(client, width, height)
layoutWindows()
rootWidget.width = width.toFloat()
rootWidget.height = height.toFloat()
rootWidget.reflow()
}
override fun render(matrices: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrices)
// Handle blocks being destroyed behind our back.
openWidgets.removeIf { world.getBlockEntity(it.blockPosition) !is DeviceBlockEntity }
if (openWidgets.isEmpty()) {
close()
}
for (openWidget in openWidgets) {
openWidget.widget.draw(matrices, mouseX.toFloat(), mouseY.toFloat(), delta)
}
Render.sprite(
matrices,
8f,
height - badge.height - 8,
badge.width * 2,
badge.height * 2,
atlas,
badge
)
// For some silly reason blending is disabled by default, so let's enable it for rendering
// widgets.
RenderSystem.enableBlend()
rootWidget.draw(matrices, mouseX.toFloat(), mouseY.toFloat(), delta)
RenderSystem.disableBlend()
}
private fun propagateEvent(event: Event): Boolean {
for (openWidget in openWidgets) {
if (
openWidget.widget.event(
EventContext(world, openWidget.blockPosition),
event.relativeTo(openWidget.widget.x, openWidget.widget.y)
)
) return true
}
return false
return rootWidget.event(Unit, event).eventConsumed
}
override fun mouseMoved(mouseX: Double, mouseY: Double) {
@ -129,10 +82,6 @@ class Rack(
val smallFont = Identifier(Mod.id, "altopixel")
val smallText = Style.EMPTY.withFont(smallFont)!!
// TODO: This "adjacent device" system is kind of janky but it allows for pretty nice
// organization of your devices into multiple racks of sorts. Maybe in the future we can
// think of collecting non-adjacent devices as well.
fun collectAdjacentDevices(world: World, position: BlockPos): HashSet<BlockPos> {
val result = hashSetOf<BlockPos>()
collectAdjacentDevicesRec(result, world, position)

View file

@ -45,7 +45,14 @@ data class MouseMove(
MouseMove(mouseX - x, mouseY - y, absoluteMouseX, absoluteMouseY)
}
class EventContext(
class DeviceEventContext(
val world: ClientWorld,
val blockPosition: BlockPos,
)
open class Message(val eventConsumed: Boolean) {
companion object {
val eventIgnored = Message(eventConsumed = false)
val eventUsed = Message(eventConsumed = true)
}
}

View file

@ -1,11 +1,11 @@
package net.liquidev.dawd3.ui.widget
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.EventContext
import net.liquidev.dawd3.ui.Message
import net.minecraft.client.util.math.MatrixStack
abstract class Container(x: Float, y: Float) : Widget(x, y) {
abstract val children: List<Widget>
abstract class Container<in C>(x: Float, y: Float) : Widget<C, Message>(x, y) {
abstract val children: List<Widget<C, Message>>
override fun drawContent(
matrices: MatrixStack,
@ -18,7 +18,7 @@ abstract class Container(x: Float, y: Float) : Widget(x, y) {
}
}
override fun event(context: EventContext, event: Event): Boolean {
override fun event(context: C, event: Event): Message {
return propagateEvent(context, event, children.reversed())
}
}

View file

@ -35,7 +35,7 @@ class Knob(
val unit: FloatUnit = RawValue,
// Pick a default sensitivity such that for pitch ranges we move by steps of 0.25.
val sensitivity: Float = (max - min) * 0.25f / 96f,
) : Widget(x, y) {
) : DeviceWidget(x, y) {
override val width = size
override val height = size + 4f
@ -44,7 +44,7 @@ class Knob(
private var draggingInfo: DraggingInfo? = null
private val controlLabel =
Text.translatable(control.descriptor.name.toTranslationKey()).setStyle(Rack.smallText)
Text.translatable(control.descriptor.name.toTranslationKey()).setStyle(RackScreen.smallText)
override fun drawContent(
matrices: MatrixStack,
@ -72,7 +72,7 @@ class Knob(
Radians(min(valueAngle, zeroAngle)),
Radians(max(valueAngle, zeroAngle)),
vertexCount = 16,
Rack.atlas,
RackScreen.atlas,
coloredStrip(color)
)
Render.arcOutline(
@ -84,7 +84,7 @@ class Knob(
startAngle,
endAngle,
vertexCount = 16,
Rack.atlas,
RackScreen.atlas,
blackStrip
)
Render.line(
@ -94,7 +94,7 @@ class Knob(
centerX + cos(valueAngle) * radius,
centerY + sin(valueAngle) * radius,
lineThickness / 2,
Rack.atlas,
RackScreen.atlas,
blackStrip
)
@ -112,13 +112,13 @@ class Knob(
centerX,
height - 4,
Text.literal(unit.display(control.value)),
Rack.atlas,
RackScreen.atlas,
Tooltip.dark
)
}
}
override fun event(context: EventContext, event: Event): Boolean {
override fun event(context: DeviceEventContext, event: Event): Message {
val client = MinecraftClient.getInstance()
when (event) {
is MouseButton -> if (event.button == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
@ -138,7 +138,7 @@ class Knob(
event.absoluteMouseX * guiScale,
event.absoluteMouseY * guiScale,
)
return true
return Message.eventUsed
}
Action.Up -> {
val draggingInfo = draggingInfo
@ -174,12 +174,12 @@ class Knob(
draggingInfo.previousMouseY = event.absoluteMouseY
// Consume the events so that other controls don't get triggered while the knob
// is being dragged.
return true
return Message.eventUsed
}
}
}
return false
return Message.eventIgnored
}
private fun isFineTuning(): Boolean =

View file

@ -1,10 +1,11 @@
package net.liquidev.dawd3.ui.widget
import net.liquidev.dawd3.ui.DeviceEventContext
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.EventContext
import net.liquidev.dawd3.ui.Message
import net.minecraft.client.util.math.MatrixStack
abstract class Widget(var x: Float, var y: Float) {
abstract class Widget<in C, out M : Message>(var x: Float, var y: Float) {
abstract val width: Float
abstract val height: Float
@ -15,37 +16,44 @@ abstract class Widget(var x: Float, var y: Float) {
deltaTime: Float,
)
/** Returns false to propagate the event, or true to consume it. */
abstract fun event(context: EventContext, event: Event): Boolean
abstract fun event(context: C, event: Event): M
fun draw(matrices: MatrixStack, mouseX: Float, mouseY: Float, deltaTime: Float) {
inline fun drawInside(matrices: MatrixStack, draw: () -> Unit) {
matrices.push()
matrices.translate(x.toDouble(), y.toDouble(), 0.0)
drawContent(matrices, mouseX - x, mouseY - y, deltaTime)
draw()
matrices.pop()
}
fun draw(matrices: MatrixStack, mouseX: Float, mouseY: Float, deltaTime: Float) {
drawInside(matrices) { drawContent(matrices, mouseX - x, mouseY - y, deltaTime) }
}
/** Can be overridden to reflow the children's layout. */
open fun reflow() {}
fun containsRelativePoint(x: Float, y: Float) =
x >= 0 && y >= 0 && x <= width && y <= height
companion object {
/** Propagates the event through the given iterable, returns whether it was consumed in the end. */
fun propagateEvent(
context: EventContext,
fun <C> propagateEvent(
context: C,
event: Event,
through: Iterable<Widget>,
): Boolean {
through: Iterable<Widget<C, Message>>,
): Message {
for (widget in through) {
if (
widget.event(
context,
event.relativeTo(widget.x, widget.y)
)
) {
return true
val message = widget.event(
context,
event.relativeTo(widget.x, widget.y)
)
if (message.eventConsumed) {
return message
}
}
return false
return Message.eventIgnored
}
}
}
}
typealias DeviceWidget = Widget<DeviceEventContext, Message>

View file

@ -2,9 +2,10 @@ package net.liquidev.dawd3.ui.widget
import net.liquidev.dawd3.render.NinePatch
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.ui.Rack
import net.liquidev.dawd3.ui.*
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import org.lwjgl.glfw.GLFW
class Window(
x: Float,
@ -12,9 +13,9 @@ class Window(
override val width: Float,
override val height: Float,
val title: Text,
) : Container(x, y) {
) : Container<DeviceEventContext>(x, y) {
override val children = mutableListOf<Widget>()
override val children = mutableListOf<DeviceWidget>()
override fun drawContent(
matrices: MatrixStack,
@ -22,8 +23,8 @@ class Window(
mouseY: Float,
deltaTime: Float,
) {
Render.ninePatch(matrices, 2f, 2f, width, height, Rack.atlas, windowShadow)
Render.ninePatch(matrices, 0f, 0f, width, height, Rack.atlas, windowBackground)
Render.ninePatch(matrices, 2f, 2f, width, height, RackScreen.atlas, windowShadow)
Render.ninePatch(matrices, 0f, 0f, width, height, RackScreen.atlas, windowBackground)
Render.textCentered(
matrices,
@ -36,6 +37,27 @@ class Window(
super.drawContent(matrices, mouseX, mouseY, deltaTime)
}
override fun event(context: DeviceEventContext, event: Event): Message {
val childrenMessage = super.event(context, event)
if (childrenMessage.eventConsumed) {
return childrenMessage
}
if (event is MouseButton && event.button == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
if (containsRelativePoint(event.mouseX, event.mouseY) && event.action == Action.Down) {
return BeginDrag(this, event.mouseX, event.mouseY)
}
}
return Message.eventIgnored
}
data class BeginDrag(
val window: Window,
val x: Float,
val y: Float,
) : Message(eventConsumed = true)
companion object {
val windowBackground = NinePatch(
u = 0f, v = 0f,

View file

@ -3,10 +3,11 @@ package net.liquidev.dawd3.ui.widget.keyboard
import net.liquidev.dawd3.audio.device.FloatControl
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.Sprite
import net.liquidev.dawd3.ui.DeviceEventContext
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.EventContext
import net.liquidev.dawd3.ui.Rack
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.Message
import net.liquidev.dawd3.ui.RackScreen
import net.liquidev.dawd3.ui.widget.DeviceWidget
import net.minecraft.client.util.math.MatrixStack
class Key(
@ -15,7 +16,7 @@ class Key(
val type: Type,
val note: Float,
val pitchControl: FloatControl,
) : Widget(x, y) {
) : DeviceWidget(x, y) {
enum class Type(
val width: Float,
@ -55,7 +56,7 @@ class Key(
matrices,
x = 0f,
y = 0f,
Rack.atlas,
RackScreen.atlas,
if (isPressed) type.pressedSprite else type.idleSprite
)
if (pitchControl.value == note) {
@ -63,13 +64,13 @@ class Key(
matrices,
x = width / 2f - type.currentPitchSprite.width / 2f,
y = height - type.currentPitchSprite.height - 2,
Rack.atlas,
RackScreen.atlas,
type.currentPitchSprite
)
}
}
override fun event(context: EventContext, event: Event) = false
override fun event(context: DeviceEventContext, event: Event) = Message.eventIgnored
internal fun triggerDown() {
isPressed = true

View file

@ -12,7 +12,7 @@ class Keyboard(
firstNote: Int,
lastNote: Int,
private val controls: KeyboardDevice.Controls,
) : Container(x, y) {
) : Container<DeviceEventContext>(x, y) {
override val children = run {
var whiteX = 0f
@ -37,7 +37,7 @@ class Keyboard(
private var pressedKey: Key? = null
override fun event(context: EventContext, event: Event): Boolean {
override fun event(context: DeviceEventContext, event: Event): Message {
if (event is MouseButton && event.button == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
mouseIsDown = event.action == Action.Down && containsRelativePoint(
event.mouseX,
@ -48,20 +48,20 @@ class Keyboard(
} else {
setPressedKey(context, null)
}
return mouseIsDown && pressedKey != null
return if (mouseIsDown && pressedKey != null) Message.eventUsed else Message.eventIgnored
}
if (event is MouseMove && mouseIsDown) {
setPressedKey(context, findPressedKey(event.mouseX.toInt(), event.mouseY.toInt()))
}
return false
return Message.eventIgnored
}
private fun findPressedKey(mouseX: Int, mouseY: Int): Key? =
children.findLast { it.containsRelativePoint(mouseX - it.x, mouseY - it.y) }
private fun setPressedKey(context: EventContext, newKey: Key?) {
private fun setPressedKey(context: DeviceEventContext, newKey: Key?) {
val previousKey = pressedKey
pressedKey = newKey
if (newKey != previousKey) {

View file

@ -0,0 +1,275 @@
package net.liquidev.dawd3.ui.widget.rack
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.liquidev.dawd3.render.NinePatch
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.TextureStrip
import net.liquidev.dawd3.ui.*
import net.liquidev.dawd3.ui.widget.DeviceWidget
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.math.BlockPos
import org.lwjgl.glfw.GLFW
class Rack(
override var width: Float,
override var height: Float,
private val world: ClientWorld,
shownDevices: Iterable<BlockPos>,
) : Widget<Unit, Message>(0f, 0f) {
private val sidebar = Sidebar(0f, 0f, 160f, 0f)
private val blockPositionsByWidget = hashMapOf<DeviceWidget, BlockPos>()
private val shelves = MutableList(10) { Shelf(0f, 0f, width) }
enum class DragPlace {
Sidebar,
Shelf,
}
data class DraggedWindow(
val window: Window,
val source: DragPlace,
val byX: Float,
val byY: Float,
val sourceShelfIndex: Int = 0, // Only relevant when source == DragPlace.Shelf
)
private var draggedWindow: DraggedWindow? = null
init {
for (blockPosition in shownDevices) {
val blockEntity = world.getBlockEntity(blockPosition) as? DeviceBlockEntity ?: continue
val widget = blockEntity.descriptor.ui?.open(blockEntity.controls, x = 0f, y = 0f)
if (widget != null) {
sidebar.windows.add(widget)
blockPositionsByWidget[widget] = blockPosition
}
}
}
private fun <W : DeviceWidget> removeStaleWidgetsFromContainer(container: MutableList<W>) {
container.removeAll { world.getBlockEntity(blockPositionsByWidget[it]) !is DeviceBlockEntity }
}
override fun drawContent(
matrices: MatrixStack,
mouseX: Float,
mouseY: Float,
deltaTime: Float,
) {
removeStaleWidgetsFromContainer(sidebar.windows)
for (shelf in shelves) {
shelf.draw(matrices, mouseX, mouseY, deltaTime)
}
sidebar.draw(matrices, mouseX, mouseY, deltaTime)
val draggedWindow = draggedWindow
if (draggedWindow != null) {
Render.ninePatch(
matrices,
mouseX - draggedWindow.byX,
mouseY - draggedWindow.byY,
draggedWindow.window.width,
draggedWindow.window.height,
RackScreen.atlas,
windowDraggingPreview,
)
val destination = findDragDestination(mouseX, mouseY)
println(destination)
when (destination?.place) {
DragPlace.Sidebar -> {
sidebar.drawInside(matrices) {
val y = if (sidebar.windows.size == 0) {
Sidebar.padding
} else if (destination.indexInPlace == sidebar.windows.size) {
val window = sidebar.windows.last()
window.y + window.height - Sidebar.spacingBetweenWindows / 2f
} else {
val window = sidebar.windows[destination.indexInPlace]
window.y - Sidebar.spacingBetweenWindows / 2f
}
Render.line(
matrices,
Sidebar.padding,
y,
sidebar.width - Sidebar.padding,
y,
thickness = 0.5f,
RackScreen.atlas,
windowDestinationPreview
)
}
}
DragPlace.Shelf -> {
val shelf = shelves[destination.shelfIndex]
shelf.drawInside(matrices) {
val x = if (shelf.windows.size == 0) {
Shelf.padding
} else if (destination.indexInPlace == shelf.windows.size) {
val window = shelf.windows.last()
window.x + window.width + Shelf.spacingBetweenWindows / 2f
} else {
val window = shelf.windows[destination.indexInPlace]
window.x - Shelf.spacingBetweenWindows / 2f
}
Render.line(
matrices,
x,
Shelf.padding,
x,
shelf.height - Shelf.padding,
thickness = 0.5f,
RackScreen.atlas,
windowDestinationPreview
)
}
}
null -> {}
}
}
}
private fun sendEventToWindows(
windows: List<Window>,
event: Event,
source: DragPlace,
sourceShelfIndex: Int = 0,
): Message {
for (window in windows) {
val blockPosition = blockPositionsByWidget[window]!!
val windowRelativeEvent = event.relativeTo(window.x, window.y)
val windowMessage =
window.event(DeviceEventContext(world, blockPosition), windowRelativeEvent)
if (windowMessage is Window.BeginDrag) {
draggedWindow =
DraggedWindow(
window,
source,
windowMessage.x,
windowMessage.y,
sourceShelfIndex
)
}
if (windowMessage.eventConsumed) return Message.eventUsed
}
return Message.eventIgnored
}
override fun event(context: Unit, event: Event): Message {
val sidebarWindowMessage =
sendEventToWindows(
sidebar.windows,
event.relativeTo(sidebar.x, sidebar.y),
DragPlace.Sidebar
)
if (sidebarWindowMessage.eventConsumed) {
return sidebarWindowMessage
}
shelves.forEachIndexed { index, shelf ->
val shelfWindowMessage =
sendEventToWindows(
shelf.windows,
event.relativeTo(shelf.x, shelf.y),
DragPlace.Shelf,
index,
)
if (shelfWindowMessage.eventConsumed) {
return shelfWindowMessage
}
}
if (event is MouseButton && event.button == GLFW.GLFW_MOUSE_BUTTON_LEFT && event.action == Action.Up) {
val draggedWindow = draggedWindow
if (draggedWindow != null) {
val destination = findDragDestination(event.mouseX, event.mouseY)
if (destination != null) {
removeWindowFromSource(
draggedWindow.source,
draggedWindow.sourceShelfIndex,
draggedWindow.window,
)
when (destination.place) {
DragPlace.Sidebar -> sidebar.windows.add(
destination.indexInPlace,
draggedWindow.window
)
DragPlace.Shelf -> shelves[destination.shelfIndex].windows.add(
destination.indexInPlace,
draggedWindow.window
)
}
reflow()
}
}
this.draggedWindow = null
}
return Message.eventIgnored
}
internal data class DragDestination(
val place: DragPlace,
val indexInPlace: Int,
val shelfIndex: Int = 0,
)
private fun findDragDestination(mouseX: Float, mouseY: Float): DragDestination? {
val indexInSidebar = sidebar.findDragDestination(mouseX - sidebar.x, mouseY - sidebar.y)
if (indexInSidebar != null) {
return DragDestination(DragPlace.Sidebar, indexInSidebar)
}
shelves.forEachIndexed { shelfIndex, shelf ->
val indexInShelf = shelf.findDragDestination(mouseX - shelf.x, mouseY - shelf.y)
if (indexInShelf != null) {
return DragDestination(DragPlace.Shelf, indexInShelf, shelfIndex)
}
}
return null
}
private fun removeWindowFromSource(
source: DragPlace,
indexOfSource: Int,
window: Window,
) {
when (source) {
DragPlace.Sidebar -> {
sidebar.windows.remove(window)
}
DragPlace.Shelf -> {
shelves[indexOfSource].windows.remove(window)
}
}
}
override fun reflow() {
sidebar.x = width - sidebar.width
sidebar.height = height
sidebar.reflow()
var dy = 0f
for (shelf in shelves) {
shelf.reflow()
shelf.y = dy
shelf.width = width
dy += shelf.height
}
}
companion object {
private val windowDraggingPreview =
NinePatch(u = 48f, v = 0f, width = 8f, height = 8f, border = 2f)
private val windowDestinationPreview = TextureStrip(25f, 16f, 25f, 32f)
}
}

View file

@ -0,0 +1,76 @@
package net.liquidev.dawd3.ui.widget.rack
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.TextureStrip
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.Message
import net.liquidev.dawd3.ui.RackScreen
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.util.math.MatrixStack
import kotlin.math.max
class Shelf(x: Float, y: Float, override var width: Float) : Widget<Nothing, Message>(x, y) {
override var height = 128f
private set
val windows = mutableListOf<Window>()
override fun drawContent(
matrices: MatrixStack,
mouseX: Float,
mouseY: Float,
deltaTime: Float,
) {
for (window in windows) {
window.draw(matrices, mouseX, mouseY, deltaTime)
}
Render.colorized(1f, 1f, 1f, 0.1f) {
Render.line(
matrices,
0f,
height,
width,
height,
thickness = 0.5f,
RackScreen.atlas,
spacer
)
}
}
// NOTE: Events here are not handled directly by the Shelf, but rather by the parent rack
// screen which has extra context that needs to be passed down to windows. Hence this cannot
// be called (context is Nothing.)
override fun event(context: Nothing, event: Event): Message {
return Message.eventIgnored
}
override fun reflow() {
height = padding * 2f + (windows.maxOfOrNull { it.height } ?: 0f)
height = max(64f, height)
var dx = 0f
for (window in windows) {
window.x = padding + dx
window.y = padding
dx += window.width + spacingBetweenWindows
}
}
internal fun findDragDestination(mouseX: Float, mouseY: Float): Int? {
return if (containsRelativePoint(mouseX, mouseY)) {
val maybeIndex = windows.indexOfFirst { mouseX < it.x + it.width / 2f }
if (maybeIndex == -1) windows.size else maybeIndex
} else {
null
}
}
companion object {
const val padding = 8f
const val spacingBetweenWindows = 8f
private val spacer = TextureStrip(25f, 16f, 25f, 32f)
}
}

View file

@ -0,0 +1,65 @@
package net.liquidev.dawd3.ui.widget.rack
import net.liquidev.dawd3.render.NinePatch
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.Message
import net.liquidev.dawd3.ui.RackScreen
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.util.math.MatrixStack
class Sidebar(
x: Float,
y: Float,
override val width: Float,
override var height: Float,
) : Widget<Nothing, Message>(x, y) {
val windows = mutableListOf<Window>()
override fun drawContent(
matrices: MatrixStack,
mouseX: Float,
mouseY: Float,
deltaTime: Float,
) {
Render.ninePatch(matrices, 0f, 0f, width, height, RackScreen.atlas, background)
for (window in windows) {
window.draw(matrices, mouseX, mouseY, deltaTime)
}
}
// NOTE: Events here are not handled directly by the Sidebar, but rather by the parent rack
// screen which has extra context that needs to be passed down to windows. Hence this cannot
// be called (context is Nothing.)
override fun event(context: Nothing, event: Event): Message {
return Message.eventIgnored
}
override fun reflow() {
var dy = 0f
for (window in windows) {
window.x = padding
window.y = padding + dy
dy += window.height + spacingBetweenWindows
}
}
internal fun findDragDestination(mouseX: Float, mouseY: Float): Int? {
return if (containsRelativePoint(mouseX, mouseY)) {
val maybeIndex = windows.indexOfFirst { mouseY < it.y + it.height / 2f }
if (maybeIndex == -1) windows.size else maybeIndex
} else {
null
}
}
companion object {
val background = NinePatch(u = 40f, v = 0f, width = 8f, height = 8f, border = 2f)
const val padding = 8f
const val spacingBetweenWindows = 8f
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

After

Width:  |  Height:  |  Size: 551 B

Before After
Before After