PhysicalConnectivity/src/main/kotlin/net/shadowfacts/cacao/window/Window.kt

276 lines
9.3 KiB
Kotlin

package net.shadowfacts.cacao.window
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import net.shadowfacts.cacao.AbstractCacaoScreen
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Responder
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import no.birkett.kiwi.Variable
import java.lang.RuntimeException
/**
* A Window is the object at the top of a Cacao view hierarchy. It occupies the entirety of the Minecraft screen size
* and provides the base coordinate system for its view hierarchy.
*
* The Window owns the Kiwi [Solver] object used for layout by all of its views.
*
* @author shadowfacts
*
* @param viewController The root view controller for this window.
*/
open class Window(
/**
* The root view controller for this window.
*/
val viewController: ViewController
) {
/**
* The screen that this window belongs to.
* Not initialized until this window is added to a screen, using it before that point will throw a runtime exception.
*/
lateinit var screen: AbstractCacaoScreen
/**
* The constraint solver used by this window and all its views and subviews.
*/
var solver = Solver()
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
val leftAnchor = Variable("left")
/**
* Layout anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = Variable("right")
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
val topAnchor = Variable("top")
/**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = Variable("bottom")
/**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = Variable("width")
/**
* Layout anchor for the height of this view in the window's coordinate system.
*/
val heightAnchor = Variable("height")
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = Variable("centerX")
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
val centerYAnchor = Variable("centerY")
/**
* The first responder of the a window is the first object that receives indirect events (e.g., keypresses).
*
* When an indirect event is received by the window, it is given to the first responder. If the first responder does
* not accept it (i.e. returns `false` from the appropriate method), the event will be passed to that responder's
* [Responder.nextResponder], and so on.
*
* The following is the order of events when setting this property:
* 1. The old first responder (if any) has [Responder.didResignFirstResponder] invoked.
* 2. The value of the field is updated.
* 3. The new value (if any) has [Responder.didBecomeFirstResponder] invoked.
*/
var firstResponder: Responder? = null
set(value) {
field?.didResignFirstResponder()
field = value
field?.didBecomeFirstResponder()
}
// internal constraints that specify the window size based on the MC screen size
// stored so that they can be removed when the screen is resized
private var widthConstraint: Constraint? = null
private var heightConstraint: Constraint? = null
private var currentDragReceiver: View? = null
private var currentDeferredTooltip: List<Text>? = null
init {
createInternalConstraints()
}
fun wasAdded() {
viewController.window = this
viewController.loadViewIfNeeded()
viewController.view.window = this
viewController.view.solver = solver
viewController.view.wasAdded()
viewController.createConstraints {
viewController.view.leftAnchor equalTo leftAnchor
viewController.view.rightAnchor equalTo rightAnchor
viewController.view.topAnchor equalTo topAnchor
viewController.view.bottomAnchor equalTo bottomAnchor
}
viewController.viewDidLoad()
layout()
}
/**
* Creates the internal constraints used by the window.
* If overridden, the super-class method must be called.
*/
protected fun createInternalConstraints() {
solver.dsl {
leftAnchor equalTo 0
topAnchor equalTo 0
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
/**
* Called by the window's [screen] when the Minecraft screen is resized.
* Used to update the window's width and height constraints and re-layout views.
*/
internal fun resize(width: Int, height: Int) {
if (widthConstraint != null) solver.removeConstraint(widthConstraint)
if (heightConstraint != null) solver.removeConstraint(heightConstraint)
solver.dsl {
widthConstraint = (widthAnchor equalTo width)
heightConstraint = (heightAnchor equalTo height)
}
layout()
}
/**
* Convenience method that removes this window from its [screen].
*/
fun removeFromScreen() {
viewController.viewWillDisappear()
screen.removeWindow(this)
viewController.viewDidDisappear()
}
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
*/
fun layout() {
viewController.viewWillLayoutSubviews()
solver.updateVariables()
viewController.viewDidLayoutSubviews()
}
/**
* Draws this window and all of its views.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param mouse The point in the coordinate system of the window.
* @param delta The time elapsed since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
currentDeferredTooltip = null
val mouseInView = Point(mouse.x - viewController.view.frame.left, mouse.y - viewController.view.frame.top)
viewController.view.draw(matrixStack, mouseInView, delta)
if (currentDeferredTooltip != null) {
RenderHelper.drawTooltip(matrixStack, currentDeferredTooltip!!, mouse)
}
}
/**
* Draw a tooltip containing the given lines at the mouse pointer location.
*
* Implementation note: the tooltip is not drawn immediately, it is done after the window is done drawing all of its
* views. This is done to prevent other views from being drawn in front of the tooltip. Additionally, more than one
* tooltip cannot be drawn in a frame as they would appear at the same position.
*/
fun drawTooltip(text: List<Text>) {
if (currentDeferredTooltip != null) {
throw RuntimeException("Deferred tooltip already registered for current frame")
}
currentDeferredTooltip = text
}
/**
* Called when a mouse button is clicked and this is the active window.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param point The point in the window of the click.
* @param mouseButton The mouse button that was used to click.
* @return Whether the mouse click was handled by a view.
*/
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
// todo: isn't this always true?
if (point in viewController.view.frame) {
val mouseInView = Point(point.x - viewController.view.frame.left, point.y - viewController.view.frame.top)
return viewController.view.mouseClicked(mouseInView, mouseButton)
} else {
// remove the window from the screen when the mouse clicks outside the window and this is not the primary window
if (screen.windows.size > 1) {
removeFromScreen()
}
}
return false
}
fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
val pointInView = viewController.view.convert(startPoint, to = currentlyDraggedView)
return currentlyDraggedView.mouseDragged(pointInView, delta, mouseButton)
} else if (startPoint in viewController.view.frame) {
val startInView =
Point(startPoint.x - viewController.view.frame.left, startPoint.y - viewController.view.frame.top)
var prevView: View? = null
var view = viewController.view.subviewsAtPoint(startInView).maxByOrNull(View::zIndex)
while (view != null && !view.respondsToDragging) {
prevView = view
val pointInView = viewController.view.convert(startInView, to = view)
view = view.subviewsAtPoint(pointInView).maxByOrNull(View::zIndex)
}
this.currentDragReceiver = view ?: prevView
return if (this.currentDragReceiver != null) {
val pointInView = viewController.view.convert(startPoint, to = this.currentDragReceiver!!)
this.currentDragReceiver!!.mouseDragged(pointInView, delta, mouseButton)
} else {
false
}
}
return false
}
fun mouseReleased(point: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
val pointInView = viewController.view.convert(point, to = currentlyDraggedView)
currentlyDraggedView.mouseDragEnded(pointInView, mouseButton)
this.currentDragReceiver = null
return true
}
return false
}
fun mouseScrolled(point: Point, amount: Double): Boolean {
return viewController.view.mouseScrolled(point, amount)
}
}