ASMR/src/main/kotlin/net/shadowfacts/cacao/Window.kt

202 lines
6.1 KiB
Kotlin

package net.shadowfacts.cacao
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import no.birkett.kiwi.Variable
import java.util.*
/**
* 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
*/
class Window {
/**
* 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: CacaoScreen
/**
* 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")
// 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
// _views is the internal, mutable object, since we only want it to be mutated by the add/removeView methods
private val _views = LinkedList<View>()
/**
* The list of top-level views in this window.
* This list should never be modified directly, only by calling the [addView]/[removeView] methods.
*/
val views: List<View> = _views
private var viewsSortedByZIndex: List<View> = listOf()
init {
createInternalConstraints()
}
/**
* Creates the internal constraints used by the window.
* If overridden, the super-class method must be called.
*/
protected open 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() {
screen.removeWindow(this)
}
/**
* Adds the given view as a top-level view in this window.
*
* @param view The view to add.
* @return The same view, as a convenience.
*/
fun <T: View> addView(view: T): T {
_views.add(view)
viewsSortedByZIndex = views.sortedBy(View::zIndex)
view.window = this
view.solver = solver
view.wasAdded()
return view
}
/**
* Finds all views in this window at the given point.
*
* @param point The point to find views at, in the coordinate system of the window.
* @return All views that contain the point.
*/
fun viewsAtPoint(point: Point): List<View> {
return views.filter { point in it.frame }
}
/**
* Attempts to find a top level view of this window that contains the given point.
* If there are multiple overlapping views, which one this method returns is undefined.
* [viewsAtPoint] may be used, and the resulting List sorted by [View.zIndex].
*
* @param point The point to find views at, in the coordinate system of the window.
* @return the Veiw, if any, that contain the given point.
*/
fun viewAtPoint(point: Point): View? {
return views.firstOrNull { point in it.frame }
}
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
*/
fun layout() {
solver.updateVariables()
views.forEach(View::didLayout)
}
/**
* 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.
*/
fun draw(mouse: Point, delta: Float) {
viewsSortedByZIndex.forEach {
val mouseInView = Point(mouse.x - it.frame.left, mouse.y - it.frame.top)
it.draw(mouseInView, delta)
}
}
/**
* 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 {
val view = viewsAtPoint(point).maxBy(View::zIndex)
if (view != null) {
val pointInView = Point(point.x - view.frame.left, point.y - view.frame.top)
return view.mouseClicked(pointInView, mouseButton)
}
return false
}
}