2019-06-22 19:02:17 +00:00
|
|
|
package net.shadowfacts.cacao.view
|
2019-06-21 20:22:23 +00:00
|
|
|
|
|
|
|
import net.shadowfacts.kiwidsl.dsl
|
2019-06-22 19:02:17 +00:00
|
|
|
import net.shadowfacts.cacao.LayoutVariable
|
2019-06-23 03:03:23 +00:00
|
|
|
import net.shadowfacts.cacao.geometry.*
|
2019-06-23 20:53:25 +00:00
|
|
|
import net.shadowfacts.cacao.util.*
|
2019-06-22 14:21:29 +00:00
|
|
|
import no.birkett.kiwi.Constraint
|
2019-06-21 20:22:23 +00:00
|
|
|
import no.birkett.kiwi.Solver
|
2019-06-22 20:08:00 +00:00
|
|
|
import java.util.*
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-22 14:59:18 +00:00
|
|
|
/**
|
2019-06-22 20:08:00 +00:00
|
|
|
* 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.
|
|
|
|
*
|
2019-06-22 14:59:18 +00:00
|
|
|
* @author shadowfacts
|
|
|
|
*/
|
2019-06-23 20:53:25 +00:00
|
|
|
open class View() {
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
lateinit var solver: Solver
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the left edge of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val leftAnchor = LayoutVariable(this, "left")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the right edge of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val rightAnchor = LayoutVariable(this, "right")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the top edge of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val topAnchor = LayoutVariable(this, "top")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the bottom edge of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val bottomAnchor = LayoutVariable(this, "bottom")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the width of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val widthAnchor = LayoutVariable(this, "width")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the height of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val heightAnchor = LayoutVariable(this, "height")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the center X position of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val centerXAnchor = LayoutVariable(this, "centerX")
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Layout anchor for the center Y position of this view in the window's coordinate system.
|
|
|
|
*/
|
2019-06-21 20:22:23 +00:00
|
|
|
val centerYAnchor = LayoutVariable(this, "centerY")
|
|
|
|
|
2019-06-23 20:53:25 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
|
2019-06-23 20:53:25 +00:00
|
|
|
* 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].
|
2019-06-22 20:08:00 +00:00
|
|
|
*/
|
2019-06-23 20:53:25 +00:00
|
|
|
var frame: Rect by ObservableLateInitProperty { this.bounds = Rect(Point.ORIGIN, it.size) }
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* The rectangle for this view in its own coordinate system.
|
2019-06-23 20:53:25 +00:00
|
|
|
* If using constraint based layout, this property is not initialized until [didLayout] called.
|
|
|
|
* Otherwise, this will be initialized when [frame] is set.
|
2019-06-22 20:08:00 +00:00
|
|
|
*/
|
2019-06-21 23:02:08 +00:00
|
|
|
lateinit var bounds: Rect
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2019-06-22 14:21:29 +00:00
|
|
|
var intrinsicContentSize: Size? = null
|
|
|
|
set(value) {
|
|
|
|
updateIntrinsicContentSizeConstraints(intrinsicContentSize, value)
|
|
|
|
field = value
|
|
|
|
}
|
|
|
|
private var intrinsicContentSizeWidthConstraint: Constraint? = null
|
|
|
|
private var intrinsicContentSizeHeightConstraint: Constraint? = null
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* The background color of this view.
|
|
|
|
*/
|
2019-06-22 14:56:12 +00:00
|
|
|
var backgroundColor = Color.CLEAR
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
|
2019-06-23 20:53:25 +00:00
|
|
|
constructor(frame: Rect): this() {
|
|
|
|
this.usesConstraintBasedLayout = false
|
|
|
|
this.frame = frame
|
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Helper method for retrieve the anchor for a specific position on the given axis.
|
|
|
|
*/
|
2019-06-22 18:59:37 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2019-06-21 20:22:23 +00:00
|
|
|
view.solver = solver
|
|
|
|
|
|
|
|
view.wasAdded()
|
2019-06-21 23:02:08 +00:00
|
|
|
|
|
|
|
return view
|
2019-06-21 20:22:23 +00:00
|
|
|
}
|
|
|
|
|
2019-06-23 15:41:32 +00:00
|
|
|
/**
|
|
|
|
* Attempts to find a subview which contains the given point.
|
|
|
|
* If multiple subviews contain the given point, which one this method returns is undefined.
|
|
|
|
*
|
|
|
|
* @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 }
|
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Called when this view was added to a view hierarchy.
|
|
|
|
* If overridden, the super-class method must be called.
|
|
|
|
*/
|
2019-06-22 14:56:12 +00:00
|
|
|
open fun wasAdded() {
|
2019-06-21 20:22:23 +00:00
|
|
|
createInternalConstraints()
|
2019-06-22 18:59:37 +00:00
|
|
|
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
|
2019-06-21 20:22:23 +00:00
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2019-06-22 14:56:12 +00:00
|
|
|
open fun createInternalConstraints() {
|
2019-06-23 20:53:25 +00:00
|
|
|
if (!usesConstraintBasedLayout) return
|
|
|
|
|
2019-06-21 20:22:23 +00:00
|
|
|
solver.dsl {
|
|
|
|
rightAnchor equalTo (leftAnchor + widthAnchor)
|
|
|
|
bottomAnchor equalTo (topAnchor + heightAnchor)
|
|
|
|
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
|
|
|
|
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-22 14:21:29 +00:00
|
|
|
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
|
2019-06-23 20:53:25 +00:00
|
|
|
if (!usesConstraintBasedLayout || !this::solver.isInitialized) return
|
2019-06-22 18:59:37 +00:00
|
|
|
|
2019-06-22 14:21:29 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Called after this view has been laid-out.
|
|
|
|
* If overridden, the super-class method must be called.
|
|
|
|
*/
|
2019-06-22 14:56:12 +00:00
|
|
|
open fun didLayout() {
|
2019-06-21 23:02:08 +00:00
|
|
|
subviews.forEach(View::didLayout)
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-23 20:53:25 +00:00
|
|
|
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)
|
|
|
|
}
|
2019-06-21 20:22:23 +00:00
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* Called to draw this view.
|
|
|
|
* This method should not be called directly, it is called by the parent view/window.
|
2019-06-23 21:56:49 +00:00
|
|
|
* This method generally should not be overridden, but it is left open for internal framework use.
|
|
|
|
* Use [drawContent] to draw any custom content.
|
2019-06-23 15:41:32 +00:00
|
|
|
*
|
|
|
|
* @param mouse The position of the mouse in the coordinate system of this view.
|
|
|
|
* @param delta The time since the last frame.
|
2019-06-22 20:08:00 +00:00
|
|
|
*/
|
2019-06-23 21:56:49 +00:00
|
|
|
open fun draw(mouse: Point, delta: Float) {
|
2019-06-23 15:41:32 +00:00
|
|
|
RenderHelper.pushMatrix()
|
2019-06-23 21:56:49 +00:00
|
|
|
RenderHelper.translate(frame.left, frame.top)
|
2019-06-21 20:22:23 +00:00
|
|
|
|
|
|
|
RenderHelper.fill(bounds, backgroundColor)
|
|
|
|
|
2019-06-23 15:41:32 +00:00
|
|
|
drawContent(mouse, delta)
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-23 15:41:32 +00:00
|
|
|
subviews.forEach {
|
|
|
|
val mouseInView = convert(mouse, to = it)
|
|
|
|
it.draw(mouseInView, delta)
|
|
|
|
}
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-23 15:41:32 +00:00
|
|
|
RenderHelper.popMatrix()
|
2019-06-21 20:22:23 +00:00
|
|
|
}
|
|
|
|
|
2019-06-22 20:08:00 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
2019-06-23 15:41:32 +00:00
|
|
|
*
|
|
|
|
* @param mouse The position of the mouse in the coordinate system of this view.
|
|
|
|
* @param delta The time since the last frame.
|
2019-06-22 20:08:00 +00:00
|
|
|
*/
|
2019-06-23 15:41:32 +00:00
|
|
|
open fun drawContent(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.
|
|
|
|
*/
|
|
|
|
open fun mouseClicked(point: Point, mouseButton: MouseButton) {
|
|
|
|
val view = subviewAtPoint(point)
|
|
|
|
if (view != null) {
|
|
|
|
val pointInView = convert(point, to = view)
|
|
|
|
view.mouseClicked(pointInView, mouseButton)
|
|
|
|
}
|
|
|
|
}
|
2019-06-21 20:22:23 +00:00
|
|
|
|
2019-06-23 03:03:23 +00:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
}
|
|
|
|
|
2019-06-21 20:22:23 +00:00
|
|
|
}
|