436 lines
14 KiB
Kotlin
436 lines
14 KiB
Kotlin
package net.shadowfacts.cacao.view
|
|
|
|
import net.minecraft.client.util.math.MatrixStack
|
|
import net.shadowfacts.kiwidsl.dsl
|
|
import net.shadowfacts.cacao.LayoutVariable
|
|
import net.shadowfacts.cacao.Responder
|
|
import net.shadowfacts.cacao.window.Window
|
|
import net.shadowfacts.cacao.geometry.*
|
|
import net.shadowfacts.cacao.util.*
|
|
import net.shadowfacts.cacao.util.properties.ObservableLateInitProperty
|
|
import no.birkett.kiwi.Constraint
|
|
import no.birkett.kiwi.Solver
|
|
import java.lang.RuntimeException
|
|
import java.util.*
|
|
import kotlin.collections.HashSet
|
|
|
|
/**
|
|
* 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(): Responder {
|
|
|
|
/**
|
|
* The window whose view hierarchy this view belongs to.
|
|
* Not initialized until the root view in this hierarchy has been added to a hierarchy,
|
|
* using it before that will throw a runtime exception.
|
|
*/
|
|
override var window: Window? = null
|
|
|
|
override val nextResponder: Responder?
|
|
// todo: should the view controller be a Responder?
|
|
get() = superview
|
|
|
|
private val solverDelegate = ObservableLateInitProperty<Solver> {
|
|
for (v in subviews) {
|
|
v.solver = it
|
|
}
|
|
}
|
|
/**
|
|
* 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.
|
|
*/
|
|
var solver: Solver by solverDelegate
|
|
|
|
/**
|
|
* 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")
|
|
|
|
/**
|
|
* Whether this view uses constraint-based layout.
|
|
* If `false`, the view's `frame` must be set manually and the layout anchors may not be used.
|
|
* Note: some views (such as [StackView] require arranged subviews to use constraint based layout.
|
|
*
|
|
* Default is `true`.
|
|
*/
|
|
var usesConstraintBasedLayout = true
|
|
|
|
/**
|
|
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
|
|
* If using constraint based layout, this property is not initialized until [didLayout] called.
|
|
* Otherwise, this must be set manually.
|
|
* Setting this property updates the [bounds].
|
|
*/
|
|
var frame: Rect by ObservableLateInitProperty { this.bounds = Rect(Point.ORIGIN, it.size) }
|
|
/**
|
|
* The rectangle for this view in its own coordinate system.
|
|
* If using constraint based layout, this property is not initialized until [didLayout] called.
|
|
* Otherwise, this will be initialized when [frame] is set.
|
|
*/
|
|
lateinit var bounds: Rect
|
|
|
|
/**
|
|
* The position on the Z-axis of this view.
|
|
* Views are rendered from lowest Z index to highest. Clicks are handled from highest to lowest.
|
|
*/
|
|
var zIndex: Double = 0.0
|
|
|
|
/**
|
|
* 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
|
|
|
|
var respondsToDragging = false
|
|
|
|
/**
|
|
* 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 be mutated by the add/removeSubview methods
|
|
private val _subviews = LinkedList<View>()
|
|
private var subviewsSortedByZIndex: List<View> = listOf()
|
|
|
|
/**
|
|
* 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
|
|
|
|
constructor(frame: Rect): this() {
|
|
this.usesConstraintBasedLayout = false
|
|
this.frame = frame
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
|
|
|
|
view.superview = this
|
|
if (solverDelegate.isInitialized) {
|
|
view.solver = solver
|
|
}
|
|
view.window = window
|
|
|
|
view.wasAdded()
|
|
|
|
return view
|
|
}
|
|
|
|
/**
|
|
* Removes the given view from this view's children and removes all constraints associated with it.
|
|
*
|
|
* @param view The view to removed as a child of this view.
|
|
* @throws RuntimeException If the given [view] is not a subview of this view.
|
|
*/
|
|
fun removeSubview(view: View) {
|
|
if (view.superview !== this) {
|
|
throw RuntimeException("Cannot remove subview whose superview is not this view")
|
|
}
|
|
|
|
|
|
_subviews.remove(view)
|
|
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
|
|
|
|
view.superview = null
|
|
|
|
// we need to remove constraints for this subview that cross the boundary between the subview and ourself
|
|
val constraintsToRemove = solver.constraints.filter { constraint ->
|
|
val variables = constraint.getVariables().mapNotNull { it as? LayoutVariable }
|
|
|
|
for (a in 0 until variables.size - 1) {
|
|
for (b in a + 1 until variables.size) {
|
|
// if the variable views have no common ancestor after the removed view's superview is unset,
|
|
// the constraint crossed the this<->view boundary and should be removed
|
|
val ancestor = LowestCommonAncestor.find(variables[a].owner, variables[b].owner, View::superview)
|
|
if (ancestor == null) {
|
|
return@filter true
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
constraintsToRemove.forEach(solver::removeConstraint)
|
|
|
|
// todo: does this need to be reset
|
|
// view.solver = null
|
|
view.window = null
|
|
|
|
// todo: is this necessary?
|
|
// view.wasRemoved()
|
|
}
|
|
|
|
/**
|
|
* Removes this view from its superview, if it has one.
|
|
*/
|
|
fun removeFromSuperview() {
|
|
superview?.removeSubview(this)
|
|
}
|
|
|
|
/**
|
|
* Finds all subviews that contain the given point.
|
|
*
|
|
* @param point The point to find subviews for, in the coordinate system of this view.
|
|
* @return All views that contain the given point.
|
|
*/
|
|
fun subviewsAtPoint(point: Point): List<View> {
|
|
return subviews.filter { point in it.frame }
|
|
}
|
|
|
|
/**
|
|
* Attempts to find a subview which contains the given point.
|
|
* If multiple subviews contain the given point, which one this method returns is undefined.
|
|
* [subviewsAtPoint] may be used, and the resulting List sorted by [View.zIndex].
|
|
*
|
|
* @param point The point to find a subview for, in the coordinate system of this view.
|
|
* @return The view, if any, that contains the given point.
|
|
*/
|
|
fun subviewAtPoint(point: Point): View? {
|
|
return subviews.firstOrNull { point in it.frame }
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
protected open fun createInternalConstraints() {
|
|
if (!usesConstraintBasedLayout) return
|
|
|
|
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 (!usesConstraintBasedLayout || !solverDelegate.isInitialized) return
|
|
|
|
if (old != null) {
|
|
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
|
|
solver.removeConstraint(intrinsicContentSizeHeightConstraint!!)
|
|
}
|
|
if (new != null) {
|
|
solver.dsl {
|
|
this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = MEDIUM))
|
|
this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = MEDIUM))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called after this view has been laid-out.
|
|
* If overridden, the super-class method must be called.
|
|
*/
|
|
open fun didLayout() {
|
|
if (usesConstraintBasedLayout) {
|
|
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)
|
|
}
|
|
|
|
subviews.forEach(View::didLayout)
|
|
}
|
|
|
|
/**
|
|
* Called to draw this view.
|
|
* This method should not be called directly, it is called by the parent view/window.
|
|
* This method generally should not be overridden, but it is left open for internal framework use.
|
|
* Use [drawContent] to draw any custom content.
|
|
*
|
|
* @param mouse The position of the mouse in the coordinate system of this view.
|
|
* @param delta The time since the last frame.
|
|
*/
|
|
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
|
|
RenderHelper.pushMatrix()
|
|
RenderHelper.translate(frame.left, frame.top)
|
|
|
|
RenderHelper.fill(matrixStack, bounds, backgroundColor)
|
|
|
|
drawContent(matrixStack, mouse, delta)
|
|
|
|
subviewsSortedByZIndex.forEach {
|
|
val mouseInView = convert(mouse, to = it)
|
|
it.draw(matrixStack, mouseInView, delta)
|
|
}
|
|
|
|
RenderHelper.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.
|
|
*
|
|
* @param mouse The position of the mouse in the coordinate system of this view.
|
|
* @param delta The time since the last frame.
|
|
*/
|
|
open fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {}
|
|
|
|
/**
|
|
* Called when this view is clicked. May delegate to [subviews].
|
|
* If overridden, the super-class method does not have to be called. Intentionally not calling it may be used
|
|
* to prevent [subviews] from receiving click events.
|
|
*
|
|
* @param point The point in the coordinate system of this view that the mouse was clicked.
|
|
* @param mouseButton The mouse button used to click.
|
|
* @return Whether the mouse click was handled by this view or any subviews.
|
|
*/
|
|
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
|
|
val (inside, outside) = subviews.partition { point in it.frame }
|
|
val view = inside.maxByOrNull(View::zIndex)
|
|
var result = false
|
|
if (view != null) {
|
|
val pointInView = convert(point, to = view)
|
|
result = view.mouseClicked(pointInView, mouseButton)
|
|
}
|
|
for (v in outside) {
|
|
val pointInV = convert(point, to = v)
|
|
v.mouseClickedOutside(pointInV, mouseButton)
|
|
}
|
|
return result
|
|
}
|
|
|
|
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
|
|
for (view in subviews) {
|
|
val pointInView = convert(point, to = view)
|
|
view.mouseClickedOutside(pointInView, mouseButton)
|
|
}
|
|
}
|
|
|
|
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
|
|
val view = subviewsAtPoint(startPoint).maxByOrNull(View::zIndex)
|
|
if (view != null) {
|
|
val startInView = convert(startPoint, to = view)
|
|
return view.mouseDragged(startInView, delta, mouseButton)
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
}
|
|
|
|
}
|