package net.shadowfacts.cacao.view import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.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.* /** * 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 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. */ var window: Window? = null /** * 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") /** * 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 /** * 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() private var subviewsSortedByZIndex: List = 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 = _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 addSubview(view: T): T { _subviews.add(view) subviewsSortedByZIndex = subviews.sortedBy(View::zIndex) view.superview = this 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") } solver.constraints.filter { constraint -> constraint.getVariables().any { it is LayoutVariable && it.owner == view } }.forEach(solver::removeConstraint) _subviews.remove(view) subviewsSortedByZIndex = subviews.sortedBy(View::zIndex) view.superview = null // 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 { 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 || !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) 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) } } /** * 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(mouse: Point, delta: Float) { RenderHelper.pushMatrix() RenderHelper.translate(frame.left, frame.top) RenderHelper.fill(bounds, backgroundColor) drawContent(mouse, delta) subviewsSortedByZIndex.forEach { val mouseInView = convert(mouse, to = it) it.draw(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(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 view = subviewsAtPoint(point).maxBy(View::zIndex) if (view != null) { val pointInView = convert(point, to = view) return view.mouseClicked(pointInView, 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) } }