Add documentation

This commit is contained in:
Shadowfacts 2019-06-22 16:08:00 -04:00
parent 7bfb18830c
commit 83bd29ecb8
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
16 changed files with 358 additions and 55 deletions

View File

@ -16,7 +16,7 @@ object ASMR: ModInitializer {
CommandRegistry.INSTANCE.register(false) { dispatcher ->
val command = CommandManager.literal("uitest").executes {
try {
MinecraftClient.getInstance().openScreen(TestScreen())
MinecraftClient.getInstance().openScreen(TestCacaoScreen())
} catch (e: Throwable) {
e.printStackTrace()
}

View File

@ -1,7 +1,7 @@
package net.shadowfacts.asmr
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.Screen
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
@ -12,10 +12,10 @@ import net.shadowfacts.cacao.view.StackView
/**
* @author shadowfacts
*/
class TestScreen: Screen() {
class TestCacaoScreen: CacaoScreen() {
init {
windows.add(Window().apply {
addWindow(Window().apply {
val stack = addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER).apply {
backgroundColor = Color.WHITE
})

View File

@ -0,0 +1,43 @@
package net.shadowfacts.cacao
import net.minecraft.client.gui.screen.Screen
import net.minecraft.network.chat.TextComponent
import net.shadowfacts.cacao.geometry.Point
import java.util.*
/**
* This class serves as the bridge between Cacao and a Minecraft [Screen]. It renders Cacao [Window]s in Minecraft and
* sends input events from Minecraft back to Cacao objects.
*
* @author shadowfacts
*/
open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
// _windows is the internal, mutable object, since we only want it to by mutated by the add/removeWindow methods.
private val _windows = LinkedList<Window>()
/**
* The list of windows that belong to this screen.
* This list should never be modified directly, only by using the [addWindow]/[removeWindow] methods.
*/
val windows: List<Window> = _windows
/**
* Adds the given window to this screen's window list.
* By default, the new window is added at the tail of the window list, making it the active window.
* Only the active window will receive input events.
*
* @param window The Window to add to this screen.
* @param index The index to insert the window into the window list at.
*/
fun addWindow(window: Window, index: Int = _windows.size) {
_windows.add(index, window)
}
override fun render(mouseX: Int, mouseY: Int, delta: Float) {
val mouse = Point(mouseX, mouseY)
windows.forEach {
it.draw(mouse, delta)
}
}
}

View File

@ -4,6 +4,9 @@ import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Variable
/**
* A Kiwi variable that belongs to a Cacao view.
* This class generally isn't used directly, but via the anchor *Anchor properties on [View].
*
* @author shadowfacts
*/
class LayoutVariable(val owner: View, val property: String): Variable("LayoutVariable") {

View File

@ -1,29 +0,0 @@
package net.shadowfacts.cacao
import net.minecraft.client.gui.screen.Screen
import net.minecraft.network.chat.TextComponent
import net.shadowfacts.cacao.geometry.Point
/**
* @author shadowfacts
*/
open class Screen: Screen(TextComponent("Screen")) {
val windows = mutableListOf<Window>()
fun addWindow(window: Window, index: Int? = null) {
if (index != null) {
windows.add(index, window)
} else {
windows.add(window)
}
}
override fun render(mouseX: Int, mouseY: Int, delta: Float) {
val mouse = Point(mouseX, mouseY)
windows.forEach {
it.draw(mouse, delta)
}
}
}

View File

@ -3,18 +3,36 @@ package net.shadowfacts.cacao
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Solver
import java.util.*
/**
* A Window is the object at the top of a Cacao view hierarchy. It occupies the entirety of the Minecraft screen size
* and provides the base coordinate system for its view hierarchy.
*
* The Window owns the Kiwi [Solver] object used for layout by all of its views.
*
* @author shadowfacts
*/
class Window {
var solver = Solver()
val views = mutableListOf<View>()
// _views is the internal, mutable object, since we only want it to be mutated by the add/removeView methods
private val _views = LinkedList<View>()
/**
* The list of top-level views in this window.
* This list should never be modified directly, only by calling the [addView]/[removeView] methods.
*/
val views: List<View> = _views
/**
* Adds the given view as a top-level view in this window.
*
* @param view The view to add.
* @return The same view, as a convenience.
*/
fun <T: View> addView(view: T): T {
views.add(view)
_views.add(view)
view.solver = solver
view.wasAdded()
@ -22,11 +40,22 @@ class Window {
return view
}
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
*/
fun layout() {
solver.updateVariables()
views.forEach(View::didLayout)
}
/**
* Draws this window and all of its views.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param mouse The point in the coordinate system of the window.
* @param delta The time elapsed since the last frame.
*/
fun draw(mouse: Point, delta: Float) {
views.forEach(View::draw)
}

View File

@ -1,12 +1,17 @@
package net.shadowfacts.cacao.geometry
/**
* An axis in a 2D coordinate plane.
*
* @author shadowfacts
*/
enum class Axis {
HORIZONTAL, VERTICAL;
val other: Axis
/**
* Gets the axis that is perpendicular to this one.
*/
val perpendicular: Axis
get() = when (this) {
HORIZONTAL -> VERTICAL
VERTICAL -> HORIZONTAL

View File

@ -1,8 +1,21 @@
package net.shadowfacts.cacao.geometry
/**
* A relative position on a line along an axis.
*
* @author shadowfacts
*/
enum class AxisPosition {
LEADING, CENTER, TRAILING;
/**
* Top for vertical, left for horizontal.
*/
LEADING,
/**
* Center X/Y.
*/
CENTER,
/**
* Bottom for vertical, right for horizontal.
*/
TRAILING;
}

View File

@ -1,6 +1,8 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for defining 2D points.
*
* @author shadowfacts
*/
data class Point(val x: Double, val y: Double) {

View File

@ -1,6 +1,8 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for defining rectangles. Provides helper values for calculating perpendicular components of a rectangle based on X/Y/W/H.
*
* @author shadowfacts
*/
data class Rect(val left: Double, val top: Double, val width: Double, val height: Double) {

View File

@ -1,6 +1,8 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for specifying the size of objects.
*
* @author shadowfacts
*/
data class Size(val width: Double, val height: Double)

View File

@ -1,12 +1,24 @@
package net.shadowfacts.cacao.util
/**
* Helper class for Cacao colors.
*
* @author shadowfacts
* @param red The red component, from 0-255.
* @param green The green component, from 0-255.
* @param blue The blue component, from 0-255.
* @param alpha The alpha (i.e. transparency) component, from 0-255. (0 is transparent, 255 is opaque)
*/
data class Color(val red: Int, val green: Int, val blue: Int, val alpha: Int = 255) {
/**
* Constructs a color from the packed RGB color.
*/
constructor(rgb: Int, alpha: Int = 255): this(rgb shr 16, (rgb shr 8) and 255, rgb and 255, alpha)
/**
* The ARGB packed representation of this color.
*/
val argb: Int
get() = ((alpha and 255) shl 24) or ((red and 255) shl 16) or ((green and 255) shl 8) or (blue and 255)

View File

@ -4,10 +4,15 @@ import net.minecraft.client.gui.DrawableHelper
import net.shadowfacts.cacao.geometry.Rect
/**
* Helper methods for rendering using Minecraft's utilities from Cacao views.
*
* @author shadowfacts
*/
object RenderHelper {
/**
* Draws a solid [rect] filled with the given [color].
*/
fun fill(rect: Rect, color: Color) {
DrawableHelper.fill(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb)
}

View File

@ -6,15 +6,22 @@ import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
/**
* A simple View that displays text. Allows for controlling the color and shadow of the text. Label cannot be used
* for multi-line text, instead use [TextView].
*
* @author shadowfacts
* @param text The text of this label.
*/
class Label(text: String): View() {
companion object {
val textRenderer: TextRenderer
private val textRenderer: TextRenderer
get() = MinecraftClient.getInstance().textRenderer
}
/**
* The text of this label. Mutating this field will update the intrinsic content size and trigger a layout.
*/
var text: String = text
set(value) {
field = value

View File

@ -6,29 +6,54 @@ 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.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].
*/
class StackView(val axis: Axis, val distribution: Distribution = Distribution.FILL): View() {
val arrangedSubviews = mutableListOf<View>()
// the internal mutable, list of arranged subviews
private val _arrangedSubviews = LinkedList<View>()
/**
* 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<View> = _arrangedSubviews
private var leadingConnection: Constraint? = null
private var trailingConnection: Constraint? = null
private var arrangedSubviewConnections = mutableListOf<Constraint>()
/**
* 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 <T: View> addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T {
addSubview(view)
arrangedSubviews.add(index, view)
_arrangedSubviews.add(index, view)
addConstraintsForArrangedView(view, index)
return view
}
fun addConstraintsForArrangedView(view: View, index: Int) {
private fun addConstraintsForArrangedView(view: View, index: Int) {
if (index == 0) {
if (leadingConnection != null) {
solver.removeConstraint(leadingConnection)
@ -77,10 +102,107 @@ class StackView(val axis: Axis, val distribution: Distribution = Distribution.FI
return view.getAnchor(axis, position)
}
private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable {
return view.getAnchor(axis.other, position)
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 {
FILL, LEADING, CENTER, TRAILING
/**
* 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
}
}

View File

@ -11,29 +11,73 @@ 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
// in the coordinate system of the screen
/**
* 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 the parent view.
/**
* 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.
/**
* 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)
@ -42,11 +86,26 @@ open class View {
private var intrinsicContentSizeWidthConstraint: Constraint? = null
private var intrinsicContentSizeHeightConstraint: Constraint? = null
/**
* The background color of this view.
*/
var backgroundColor = Color.CLEAR
var parent: View? = null
val subviews = mutableListOf<View>()
/**
* 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 ->
@ -64,9 +123,15 @@ open class View {
}
}
fun addSubview(view: View): View {
subviews.add(view)
view.parent = this
/**
* 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()
@ -74,11 +139,19 @@ open class View {
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)
@ -103,15 +176,24 @@ open class View {
}
}
/**
* 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 parentLeft = parent?.leftAnchor?.value ?: 0.0
val parentTop = parent?.topAnchor?.value ?: 0.0
frame = Rect(leftAnchor.value - parentLeft, topAnchor.value - parentTop, widthAnchor.value, heightAnchor.value)
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)
@ -125,6 +207,11 @@ open class View {
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() {}
}