Compare commits

...

7 Commits

11 changed files with 281 additions and 29 deletions

View File

@ -20,7 +20,7 @@ class TestCacaoScreen: CacaoScreen() {
init {
addWindow(Window().apply {
val stack = addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER).apply {
val stack = addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0).apply {
backgroundColor = Color.WHITE
})
val red = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16)).apply {
@ -41,9 +41,23 @@ class TestCacaoScreen: CacaoScreen() {
).apply {
handler = {
println("dropdown value: ${it.value}")
val dialog = DialogView(
"New redstone mode",
it.value.name,
arrayOf(DialogView.DefaultButtonType.OK),
Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16),
buttonCallback = { _, window ->
window.removeFromScreen()
}
)
val dialogWindow = Window()
dialogWindow.addView(dialog)
this@TestCacaoScreen.addWindow(dialogWindow)
}
})
val label = addView(Label("test ".repeat(100), maxLines = 5))
solver.dsl {
stack.topAnchor equalTo 0
stack.centerXAnchor equalTo this@apply.centerXAnchor
@ -51,9 +65,11 @@ class TestCacaoScreen: CacaoScreen() {
purple.centerXAnchor equalTo blue.centerXAnchor
purple.centerYAnchor equalTo blue.centerYAnchor
// purple.widthAnchor equalTo 50
}
layout()
label.topAnchor equalTo 0
label.leftAnchor equalTo 0
label.widthAnchor equalTo 100
}
// val red = addView(View().apply {

View File

@ -38,6 +38,7 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
fun <T: Window> addWindow(window: T, index: Int = _windows.size): T {
_windows.add(index, window)
window.screen = this
window.resize(width, height)
return window
}
@ -71,16 +72,12 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
when (result) {
true ->
return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
false ->
if (windows.size > 1) {
removeWindow(windows.last())
true
} else {
false
}
}
return result == true
}
}

View File

@ -86,7 +86,7 @@ class Window {
* Creates the internal constraints used by the window.
* If overridden, the super-class method must be called.
*/
protected open fun createInternalConstraints() {
protected fun createInternalConstraints() {
solver.dsl {
leftAnchor equalTo 0
topAnchor equalTo 0
@ -195,6 +195,11 @@ class Window {
if (view != null) {
val pointInView = Point(point.x - view.frame.left, point.y - view.frame.top)
return view.mouseClicked(pointInView, mouseButton)
} else {
// remove the window from the screen when the mouse clicks outside the window and this is not the primary window
if (screen.windows.size > 1) {
removeFromScreen()
}
}
return false
}

View File

@ -0,0 +1,115 @@
package net.shadowfacts.cacao.view
import net.minecraft.ChatFormat
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.kiwidsl.dsl
/**
* @author shadowfacts
*/
class DialogView(
val title: String,
val message: String,
val buttonTypes: Array<ButtonType>,
val iconTexture: Texture?,
val buttonCallback: (ButtonType, Window) -> Unit
): View() {
interface ButtonType {
val localizedName: String
}
enum class DefaultButtonType: ButtonType {
CANCEL, CONFIRM, OK, CLOSE;
override val localizedName: String
get() = name.toLowerCase().capitalize() // todo: actually localize me
}
private lateinit var background: NinePatchView
private lateinit var hStack: StackView
private var iconView: TextureView? = null
private lateinit var vStack: StackView
private lateinit var messageLabel: Label
private var buttonContainer: View? = null
private var buttonStack: StackView? = null
override fun wasAdded() {
background = addSubview(NinePatchView(NinePatchTexture.PANEL_BG).apply { zIndex = -1.0 })
hStack = addSubview(StackView(Axis.HORIZONTAL, StackView.Distribution.LEADING, spacing = 8.0))
if (iconTexture != null) {
iconView = hStack.addArrangedSubview(TextureView(iconTexture))
}
vStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL, spacing = 4.0))
vStack.addArrangedSubview(Label(ChatFormat.BOLD.toString() + title, shadow = false).apply {
textColor = Color(0x404040)
})
messageLabel = vStack.addArrangedSubview(Label(message, shadow = false).apply {
textColor = Color(0x404040)
})
if (buttonTypes.isNotEmpty()) {
buttonContainer = vStack.addArrangedSubview(View())
buttonStack = buttonContainer!!.addSubview(StackView(Axis.HORIZONTAL))
for (type in buttonTypes) {
buttonStack!!.addArrangedSubview(Button(Label(type.localizedName)).apply {
handler = {
this@DialogView.buttonCallback(type, this@DialogView.window)
}
})
}
}
super.wasAdded()
}
override fun createInternalConstraints() {
super.createInternalConstraints()
solver.dsl {
centerXAnchor equalTo window.centerXAnchor
centerYAnchor equalTo window.centerYAnchor
widthAnchor greaterThanOrEqualTo 175
background.leftAnchor equalTo leftAnchor - 8
background.rightAnchor equalTo rightAnchor + 8
background.topAnchor equalTo topAnchor - 8
background.bottomAnchor equalTo bottomAnchor + 8
hStack.leftAnchor equalTo leftAnchor
hStack.rightAnchor equalTo rightAnchor
hStack.topAnchor equalTo topAnchor
hStack.bottomAnchor equalTo bottomAnchor
if (iconView != null) {
hStack.bottomAnchor greaterThanOrEqualTo iconView!!.bottomAnchor
}
hStack.bottomAnchor greaterThanOrEqualTo vStack.bottomAnchor
if (iconView != null) {
iconView!!.widthAnchor equalTo 30
iconView!!.heightAnchor equalTo 30
}
messageLabel.heightAnchor greaterThanOrEqualTo 50
if (buttonContainer != null) {
buttonStack!!.heightAnchor equalTo buttonContainer!!.heightAnchor
buttonStack!!.centerYAnchor equalTo buttonContainer!!.centerYAnchor
buttonStack!!.rightAnchor equalTo buttonContainer!!.rightAnchor
}
}
}
}

View File

@ -13,14 +13,29 @@ import net.shadowfacts.cacao.util.RenderHelper
*
* @author shadowfacts
* @param text The text of this label.
* @param shadow Whether the text should be rendered with a shadow or not.
* @param maxLines The maximum number of lines of text to render.
* `0` means that there is no line limit and all lines will be rendered.
* When using a non-zero [maxLines] value, the [intrinsicContentSize] of the Label still assumes all
* content on one line. So, constraints must be provided to calculate the actual width to use for line
* wrapping.
*/
class Label(text: String): View() {
class Label(
text: String,
val shadow: Boolean = true,
val maxLines: Int = 0,
val wrappingMode: WrappingMode = WrappingMode.WRAP
): View() {
companion object {
private val textRenderer: TextRenderer
get() = MinecraftClient.getInstance().textRenderer
}
enum class WrappingMode {
WRAP, NO_WRAP
}
/**
* The text of this label. Mutating this field will update the intrinsic content size and trigger a layout.
*/
@ -28,9 +43,20 @@ class Label(text: String): View() {
set(value) {
field = value
updateIntrinsicContentSize()
window.layout()
}
private lateinit var lines: List<String>
var textColor = Color.WHITE
set(value) {
field = value
textColorARGB = value.argb
}
private var textColorARGB: Int = textColor.argb
private val drawFunc by lazy {
if (shadow) textRenderer::drawWithShadow else textRenderer::draw
}
override fun wasAdded() {
super.wasAdded()
@ -47,7 +73,38 @@ class Label(text: String): View() {
}
override fun drawContent(mouse: Point, delta: Float) {
textRenderer.draw(text, 0f, 0f, textColor.argb)
if (!this::lines.isInitialized) {
computeLines()
}
for (i in 0 until lines.size) {
val y = i * textRenderer.fontHeight
drawFunc(lines[i], 0f, y.toFloat(), textColorARGB)
}
}
override fun didLayout() {
super.didLayout()
computeLines()
}
private fun computeLines() {
var lines = text.split("\n")
if (wrappingMode == WrappingMode.WRAP) {
lines = lines.flatMap {
wrapStringToWidthAsList(it, bounds.width)
}
}
if (0 < maxLines && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
}
this.lines = lines
}
private fun wrapStringToWidthAsList(string: String, width: Double): List<String> {
if (RenderHelper.disabled) return listOf(string)
return textRenderer.wrapStringToWidthAsList(string, width.toInt())
}
}

View File

@ -19,7 +19,11 @@ import java.util.*
* @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() {
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<View>()
@ -75,10 +79,10 @@ class StackView(val axis: Axis, val distribution: Distribution = Distribution.FI
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))
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))
arrangedSubviewConnections.add(index - 1, anchor(TRAILING, previous) equalTo (anchor(LEADING, view) - spacing))
}
}
}

View File

@ -92,7 +92,7 @@ class DropdownButton<Value, ContentView: View>(
val dropdownBackground = dropdownWindow.addView(NinePatchView(NinePatchTexture.BUTTON_BG).apply {
zIndex = -1.0
})
val stack = dropdownWindow.addView(StackView(Axis.VERTICAL, StackView.Distribution.LEADING))
val stack = dropdownWindow.addView(StackView(Axis.VERTICAL, StackView.Distribution.FILL))
lateinit var selectedButton: View
val buttons = mutableListOf<Button>()
val last = allValues.count() - 1
@ -113,7 +113,9 @@ class DropdownButton<Value, ContentView: View>(
}
buttons.add(button)
dropdownWindow.solver.dsl {
stack.widthAnchor greaterThanOrEqualTo button.widthAnchor
if (button.content.intrinsicContentSize != null) {
button.widthAnchor greaterThanOrEqualTo button.content.intrinsicContentSize!!.width + 2 * button.padding
}
}
}
dropdownWindow.solver.dsl {
@ -128,12 +130,6 @@ class DropdownButton<Value, ContentView: View>(
dropdownBackground.bottomAnchor equalTo stack.bottomAnchor
}
dropdownWindow.layout()
dropdownWindow.solver.dsl {
buttons.forEach {
it.widthAnchor equalTo stack.frame.width
}
}
dropdownWindow.layout()
}
private fun valueSelected(value: Value) {

View File

@ -191,4 +191,57 @@ class StackViewLayoutTests {
assertEquals(100.0, three.rightAnchor.value)
}
@Test
fun testVerticalLayoutWithSpacing() {
val stack = window.addView(StackView(Axis.VERTICAL, spacing = 10.0))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
}
window.layout()
assertEquals(0.0, abs(one.topAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
assertEquals(50.0, one.bottomAnchor.value)
assertEquals(60.0, two.topAnchor.value)
assertEquals(135.0, two.bottomAnchor.value)
assertEquals(145.0, three.topAnchor.value)
assertEquals(195.0, three.bottomAnchor.value)
assertEquals(195.0, stack.heightAnchor.value)
}
@Test
fun testHorizontalLayoutWithSpacing() {
val stack = window.addView(StackView(Axis.HORIZONTAL, spacing = 10.0))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
window.solver.dsl {
stack.leftAnchor equalTo 0
}
window.layout()
assertEquals(0.0, abs(one.leftAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
assertEquals(50.0, one.rightAnchor.value)
assertEquals(60.0, two.leftAnchor.value)
assertEquals(135.0, two.rightAnchor.value)
assertEquals(145.0, three.leftAnchor.value)
assertEquals(195.0, three.rightAnchor.value)
assertEquals(195.0, stack.widthAnchor.value)
}
}

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
@ -14,11 +15,13 @@ import java.util.concurrent.CompletableFuture
*/
class ViewClickTests {
lateinit var screen: CacaoScreen
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
screen = CacaoScreen()
window = screen.addWindow(Window())
}
@Test

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
@ -17,11 +18,13 @@ import java.util.concurrent.CompletableFuture
*/
class ButtonClickTests {
lateinit var screen: CacaoScreen
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
screen = CacaoScreen()
window = screen.addWindow(Window())
}
@Test

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
@ -28,11 +29,13 @@ class EnumButtonTests {
ONE, TWO, THREE
}
lateinit var screen: CacaoScreen
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
screen = CacaoScreen()
window = screen.addWindow(Window())
}
@Test