package net.shadowfacts.cacao.view import com.mojang.blaze3d.platform.GlStateManager import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.geometry.* import net.shadowfacts.cacao.util.Color import net.shadowfacts.cacao.util.LowestCommonAncestor import net.shadowfacts.cacao.util.RenderHelper import no.birkett.kiwi.Constraint import no.birkett.kiwi.Solver import java.util.* /** * The base Cacao View class. Provides layout anchors, properties, and helper methods. * Doesn't draw anything itself (unless [backgroundColor] is specified), but may be used for encapsulation/grouping. * * @author shadowfacts */ open class View { /** * The constraint solver used by the [net.shadowfacts.cacao.Window] this view belongs to. * Not initialized until [wasAdded] called, using it before that will throw a runtime exception. */ lateinit var solver: Solver /** * Layout anchor for the left edge of this view in the window's coordinate system. */ val leftAnchor = LayoutVariable(this, "left") /** * Layout anchor for the right edge of this view in the window's coordinate system. */ val rightAnchor = LayoutVariable(this, "right") /** * Layout anchor for the top edge of this view in the window's coordinate system. */ val topAnchor = LayoutVariable(this, "top") /** * Layout anchor for the bottom edge of this view in the window's coordinate system. */ val bottomAnchor = LayoutVariable(this, "bottom") /** * Layout anchor for the width of this view in the window's coordinate system. */ val widthAnchor = LayoutVariable(this, "width") /** * Layout anchor for the height of this view in the window's coordinate system. */ val heightAnchor = LayoutVariable(this, "height") /** * Layout anchor for the center X position of this view in the window's coordinate system. */ val centerXAnchor = LayoutVariable(this, "centerX") /** * Layout anchor for the center Y position of this view in the window's coordinate system. */ val centerYAnchor = LayoutVariable(this, "centerY") /** * The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview). * Not initialized until [didLayout] called. */ lateinit var frame: Rect /** * The rectangle for this view in its own coordinate system. * Not initialized until [didLayout] called. */ lateinit var bounds: Rect /** * The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no * intrinsic size. * * Setting this creates/updates [no.birkett.kiwi.Strength.WEAK] constraints on this view's width/height using * the size. */ var intrinsicContentSize: Size? = null set(value) { updateIntrinsicContentSizeConstraints(intrinsicContentSize, value) field = value } private var intrinsicContentSizeWidthConstraint: Constraint? = null private var intrinsicContentSizeHeightConstraint: Constraint? = null /** * The background color of this view. */ var backgroundColor = Color.CLEAR /** * This view's parent view. If `null`, this view is a top-level view in the [Window]. */ var superview: View? = null // _subviews is the internal, mutable object since we only want it to by mutated by the add/removeSubview methods private val _subviews = LinkedList() /** * The list of all the subviews of this view. * This list should never by mutated directly, only by the [addSubview]/[removeSubview] methods. */ val subviews: List = _subviews /** * Helper method for retrieve the anchor for a specific position on the given axis. */ fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable { return when (axis) { Axis.HORIZONTAL -> when (position) { AxisPosition.LEADING -> leftAnchor AxisPosition.CENTER -> centerXAnchor AxisPosition.TRAILING -> rightAnchor } Axis.VERTICAL -> when (position) { AxisPosition.LEADING -> topAnchor AxisPosition.CENTER -> centerYAnchor AxisPosition.TRAILING -> bottomAnchor } } } /** * Adds the given subview as a child of this view. * * @param view The view to add. * @return The view that was added, as a convenience. */ fun addSubview(view: T): T { _subviews.add(view) view.superview = this view.solver = solver view.wasAdded() return view } /** * Called when this view was added to a view hierarchy. * If overridden, the super-class method must be called. */ open fun wasAdded() { createInternalConstraints() updateIntrinsicContentSizeConstraints(null, intrinsicContentSize) } /** * Called during [wasAdded] to add any constraints to the [solver] that are internal to this view. * If overridden, the super-class method must be called. */ open fun createInternalConstraints() { solver.dsl { rightAnchor equalTo (leftAnchor + widthAnchor) bottomAnchor equalTo (topAnchor + heightAnchor) centerXAnchor equalTo (leftAnchor + widthAnchor / 2) centerYAnchor equalTo (topAnchor + heightAnchor / 2) } } private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) { if (!this::solver.isInitialized) return if (old != null) { solver.removeConstraint(intrinsicContentSizeWidthConstraint!!) solver.removeConstraint(intrinsicContentSizeHeightConstraint!!) } if (new != null) { solver.dsl { this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = WEAK)) this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = WEAK)) } } } /** * Called after this view has been laid-out. * If overridden, the super-class method must be called. */ open fun didLayout() { subviews.forEach(View::didLayout) val superviewLeft = superview?.leftAnchor?.value ?: 0.0 val superviewTop = superview?.topAnchor?.value ?: 0.0 frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value) bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value) } /** * Called to draw this view. * This method should not be called directly, it is called by the parent view/window. * This method may not be overridden, use [drawContent] to draw any custom content. */ fun draw() { GlStateManager.pushMatrix() GlStateManager.translated(frame.left, frame.top, 0.0) RenderHelper.fill(bounds, backgroundColor) drawContent() subviews.forEach(View::draw) GlStateManager.popMatrix() } /** * Called during [draw] to draw content that's part of this view. * During this method, the OpenGL coordinate system has been translated so the origin is at the top left corner * of this view. Be careful not to translate additionally, and not to draw outside the [bounds] of the view. */ open fun drawContent() {} /** * Converts the given point in this view's coordinate system to the coordinate system of another view or the window. * * @param point The point to convert, in the coordinate system of this view. * @param to The view to convert to. If `null`, it will be converted to the window's coordinate system. * @return The point in the coordinate system of the [to] view. */ fun convert(point: Point, to: View?): Point { if (to != null) { val ancestor = LowestCommonAncestor.find(this, to, View::superview) @Suppress("NAME_SHADOWING") var point = point // Convert up to the LCA var view: View? = this while (view != null && view != ancestor) { point = Point(point.x + view.frame.left, point.y + view.frame.top) view = view.superview } // Convert back down to the other view view = to while (view != null && view != ancestor) { point = Point(point.x - view.frame.left, point.y - view.frame.top) view = view.superview } return point } else { return Point(leftAnchor.value + point.x, topAnchor.value + point.y) } } /** * Converts the given rectangle in this view's coordinate system to the coordinate system of another view or the window. * * @param rect The rectangle to convert, in the coordinate system of this view. * @param to The view to convert to. If `null`, it will be converted to the window's coordinate system. * @return The rectangle in the coordinate system of the [to] view. */ fun convert(rect: Rect, to: View?): Rect { return Rect(convert(rect.origin, to), rect.size) } }