ASMR/src/main/kotlin/net/shadowfacts/cacao/view/View.kt

217 lines
6.8 KiB
Kotlin

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.Axis
import net.shadowfacts.cacao.geometry.AxisPosition
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
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<View>()
/**
* 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<View> = _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 <T: View> 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() {}
}