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() /** * 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 = _views private var viewsSortedByZIndex: List = 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 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 { 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 } }