package net.shadowfacts.cacao.view 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.AxisPosition.* import no.birkett.kiwi.Constraint import java.lang.RuntimeException import java.util.* /** * A view that lays out its children in a stack along either the horizontal for vertical axes. * This view does not have any content of its own. * * Only arranged subviews will be laid out in the stack mode, normal subviews must perform their own layout. * * @author shadowfacts * @param axis The primary axis that this stack lays out its children along. * @param distribution The mode by which this stack lays out its children along the axis perpendicular to the * primary [axis]. * @param spacing The distance between arranged subviews along the primary axis. */ open class StackView( val axis: Axis, val distribution: Distribution = Distribution.FILL, val spacing: Double = 0.0 ): View() { // the internal, mutable list of arranged subviews private val _arrangedSubviews = LinkedList() /** * The list of arranged subviews belonging to this stack view. * This list should never be mutated directly, only be calling the [addArrangedSubview]/[removeArrangedSubview] * methods. */ val arrangedSubviews: List = _arrangedSubviews private var leadingConnection: Constraint? = null private var trailingConnection: Constraint? = null private var arrangedSubviewConnections = mutableListOf() /** * Adds an arranged subview to this view. * Arranged subviews are laid out according to the stack. If you wish to add a subview that is laid out separately, * use the normal [addSubview] method. * * @param view The view to add. * @param index The index in this stack to add the view at. * By default, adds the view to the end of the stack. * @return The view that was added, as a convenience. */ fun addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T { addSubview(view) _arrangedSubviews.add(index, view) addConstraintsForArrangedView(view, index) return view } /** * Removes the given arranged subview from this stack view's arranged subviews. */ fun removeArrangedSubview(view: View) { val index = arrangedSubviews.indexOf(view) if (index < 0) { throw RuntimeException("Cannot remove view that is not arranged subview") } if (index == 0) { solver.removeConstraint(leadingConnection) val next = arrangedSubviews.getOrNull(1) if (next != null) { solver.dsl { leadingConnection = anchor(LEADING) equalTo anchor(LEADING, next) } } else { leadingConnection = null } } if (index == arrangedSubviews.size - 1) { solver.removeConstraint(trailingConnection) val prev = arrangedSubviews.getOrNull(arrangedSubviews.size - 2) if (prev != null) { solver.dsl { trailingConnection = anchor(TRAILING) equalTo anchor(TRAILING, prev) } } else { trailingConnection = null } } // if the removed view is in the middle if (arrangedSubviews.size >= 3 && index > 0 && index < arrangedSubviews.size - 1) { val prev = arrangedSubviews[index - 1] val next = arrangedSubviews[index + 1] solver.dsl { solver.removeConstraint(arrangedSubviewConnections[index - 1]) solver.removeConstraint(arrangedSubviewConnections[index]) // todo: double check me arrangedSubviewConnections[index - 1] = anchor(TRAILING, prev) equalTo anchor(LEADING, next) arrangedSubviewConnections.removeAt(index) } } _arrangedSubviews.remove(view) removeSubview(view) } override fun removeSubview(view: View) { if (arrangedSubviews.contains(view)) { removeArrangedSubview(view) } else { super.removeSubview(view) } } private fun addConstraintsForArrangedView(view: View, index: Int) { if (index == 0) { if (leadingConnection != null) { solver.removeConstraint(leadingConnection) } solver.dsl { leadingConnection = anchor(LEADING) equalTo anchor(LEADING, view) } } if (index == arrangedSubviews.size - 1) { if (trailingConnection != null) { solver.removeConstraint(trailingConnection) } solver.dsl { trailingConnection = anchor(TRAILING, view) equalTo anchor(TRAILING) } } if (arrangedSubviews.size > 1) { solver.dsl { val previous = arrangedSubviews.getOrNull(index - 1) val next = arrangedSubviews.getOrNull(index + 1) if (next != null) { arrangedSubviewConnections.add(index, anchor(TRAILING, view) equalTo (anchor(LEADING, next) + spacing)) } if (previous != null) { arrangedSubviewConnections.add(index - 1, anchor(TRAILING, previous) equalTo (anchor(LEADING, view) - spacing)) } } } solver.dsl { when (distribution) { Distribution.LEADING -> perpAnchor(LEADING) equalTo perpAnchor(LEADING, view) Distribution.TRAILING -> perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view) Distribution.FILL -> { perpAnchor(LEADING) equalTo perpAnchor(LEADING, view) perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view) } Distribution.CENTER -> perpAnchor(CENTER) equalTo perpAnchor(CENTER, view) } } } private fun anchor(position: AxisPosition, view: View = this): LayoutVariable { return view.getAnchor(axis, position) } private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable { return view.getAnchor(axis.perpendicular, position) } /** * Defines the modes of how content is distributed in a stack view along the perpendicular axis (i.e. the * non-primary axis). * * ASCII-art examples are shown below in a stack view with the primary axis [Axis.VERTICAL]. */ enum class Distribution { /** * The leading edges of arranged subviews are pinned to the leading edge of the stack view. * ``` * ┌─────────────────────────────┐ * │┌─────────────┐ │ * ││ │ │ * ││ │ │ * ││ │ │ * │└─────────────┘ │ * │┌─────────┐ │ * ││ │ │ * ││ │ │ * ││ │ │ * │└─────────┘ │ * │┌───────────────────────────┐│ * ││ ││ * ││ ││ * ││ ││ * │└───────────────────────────┘│ * └─────────────────────────────┘ * ``` */ LEADING, /** * The centers of the arranged subviews are pinned to the center of the stack view. * ``` * ┌─────────────────────────────┐ * │ ┌─────────────┐ │ * │ │ │ │ * │ │ │ │ * │ │ │ │ * │ └─────────────┘ │ * │ ┌─────────┐ │ * │ │ │ │ * │ │ │ │ * │ │ │ │ * │ └─────────┘ │ * │┌───────────────────────────┐│ * ││ ││ * ││ ││ * ││ ││ * │└───────────────────────────┘│ * └─────────────────────────────┘ * ``` */ CENTER, /** * The trailing edges of arranged subviews are pinned to the leading edge of the stack view. * ``` * ┌─────────────────────────────┐ * │ ┌─────────────┐│ * │ │ ││ * │ │ ││ * │ │ ││ * │ └─────────────┘│ * │ ┌─────────┐│ * │ │ ││ * │ │ ││ * │ │ ││ * │ └─────────┘│ * │┌───────────────────────────┐│ * ││ ││ * ││ ││ * ││ ││ * │└───────────────────────────┘│ * └─────────────────────────────┘ * ``` */ TRAILING, /** * The arranged subviews fill the perpendicular axis of the stack view. * ``` * ┌─────────────────────────────┐ * │┌───────────────────────────┐│ * ││ ││ * ││ ││ * ││ ││ * │└───────────────────────────┘│ * │┌───────────────────────────┐│ * ││ ││ * ││ ││ * ││ ││ * │└───────────────────────────┘│ * │┌───────────────────────────┐│ * ││ ││ * ││ ││ * ││ ││ * │└───────────────────────────┘│ * └─────────────────────────────┘ * ``` */ FILL } }