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? = 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) { 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) { return currentlyDraggedView.mouseDragged(startPoint, 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 this.currentDragReceiver?.mouseDragged(startPoint, delta, mouseButton) ?: false } return false } fun mouseReleased(point: Point, mouseButton: MouseButton): Boolean { val currentlyDraggedView = this.currentDragReceiver if (currentlyDraggedView != null) { this.currentDragReceiver = null return true } return false } }