From ea572f3f382081d2e2bb82395368cf4451581f7a Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Sun, 2 Jun 2024 20:20:50 -0400 Subject: [PATCH 1/8] fix: Fix row and column placement not accumulating width/height respectively feat: Add layout modifiers that can alter placement and measurement feat: Add padding modifier feat: Add offset modifier --- .../mineinabyss/guiy/example/gui/Cursor.kt | 6 ++- .../guiy/example/gui/PaginatedMenu.kt | 2 + .../guiy/components/state/IntCoordinates.kt | 2 + .../com/mineinabyss/guiy/layout/Column.kt | 2 +- .../com/mineinabyss/guiy/layout/LayoutNode.kt | 31 ++++++------ .../mineinabyss/guiy/layout/MeasurePolicy.kt | 2 + .../kotlin/com/mineinabyss/guiy/layout/Row.kt | 2 +- .../guiy/layout/RowLikeMeasurePolicy.kt | 12 +++-- .../guiy/modifiers/LayoutChangingModifier.kt | 13 +++++ .../guiy/modifiers/PositionModifier.kt | 11 ----- .../guiy/modifiers/SizeModifier.kt | 47 ++++++++++++------- .../placement/absolute/PositionModifier.kt | 17 +++++++ .../placement/offset/OffsetModifier.kt | 17 +++++++ .../placement/offset/OffsetPlaceable.kt | 12 +++++ .../placement/padding/PaddingModifier.kt | 40 ++++++++++++++++ .../placement/padding/PaddingValues.kt | 12 +++++ 16 files changed, 180 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt delete mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/PositionModifier.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/absolute/PositionModifier.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetModifier.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetPlaceable.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingValues.kt diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt index f898e70..001bc48 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt @@ -9,8 +9,12 @@ import com.mineinabyss.guiy.inventory.LocalGuiyOwner import com.mineinabyss.guiy.layout.Box import com.mineinabyss.guiy.layout.Row import com.mineinabyss.guiy.layout.alignment.Alignment -import com.mineinabyss.guiy.modifiers.* +import com.mineinabyss.guiy.modifiers.Modifier import com.mineinabyss.guiy.modifiers.click.clickable +import com.mineinabyss.guiy.modifiers.fillMaxSize +import com.mineinabyss.guiy.modifiers.height +import com.mineinabyss.guiy.modifiers.placement.absolute.at +import com.mineinabyss.guiy.modifiers.width import org.bukkit.Material import org.bukkit.entity.Player diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt index b4297e8..466304e 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.* import com.mineinabyss.guiy.components.Item import com.mineinabyss.guiy.components.VerticalGrid import com.mineinabyss.guiy.components.canvases.Chest +import com.mineinabyss.guiy.components.lists.NavbarPosition import com.mineinabyss.guiy.components.lists.Paginated import com.mineinabyss.guiy.inventory.LocalGuiyOwner import com.mineinabyss.guiy.modifiers.Modifier @@ -32,6 +33,7 @@ fun PaginatedMenu(player: Player) { page = page, itemsPerPage = 9 * 5, previousButton = { Item(Material.RED_CONCRETE, "Previous", modifier = Modifier.clickable { page-- }) }, + navbarPosition = NavbarPosition.START, nextButton = { Item(Material.BLUE_CONCRETE, "Next", modifier = Modifier.clickable { page++ }) }, ) { pageItems -> VerticalGrid { diff --git a/src/main/kotlin/com/mineinabyss/guiy/components/state/IntCoordinates.kt b/src/main/kotlin/com/mineinabyss/guiy/components/state/IntCoordinates.kt index 49a9a41..b0f2b28 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/components/state/IntCoordinates.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/components/state/IntCoordinates.kt @@ -11,6 +11,8 @@ value class IntCoordinates(val pair: Long) { constructor(x: Int, y: Int) : this((x.toLong() shl 32) or y.toLong()) override fun toString(): String = "($x, $y)" + + operator fun plus(other: IntCoordinates) = IntCoordinates(x + other.x, y + other.y) } typealias IntOffset = IntCoordinates diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt index 21f3330..1f3b65f 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt @@ -24,7 +24,7 @@ fun Column( private data class ColumnMeasurePolicy( private val horizontalAlignment: Alignment.Horizontal, -) : RowLikeMeasurePolicy() { +) : RowLikeMeasurePolicy(sumHeight = true) { override fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult { return MeasureResult(width, height) { var childY = 0 diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt index 2406e41..f983041 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt @@ -1,6 +1,7 @@ package com.mineinabyss.guiy.layout import com.mineinabyss.guiy.components.state.IntCoordinates +import com.mineinabyss.guiy.components.state.IntOffset import com.mineinabyss.guiy.components.state.ItemPositions import com.mineinabyss.guiy.inventory.ClickResult import com.mineinabyss.guiy.inventory.GuiyCanvas @@ -37,8 +38,13 @@ internal class LayoutNode : Measurable, Placeable, GuiyNode { acc[element::class] = element acc } + layoutChangingModifiers = modifier.foldOut(mutableListOf()) { element, acc -> + if (element is LayoutChangingModifier) acc.add(element) + acc + } } var processedModifier = mapOf>, Modifier.Element<*>>() + var layoutChangingModifiers: List = emptyList() inline fun > get(): T? { return processedModifier[T::class] as T? @@ -59,12 +65,12 @@ internal class LayoutNode : Measurable, Placeable, GuiyNode { } override fun measure(constraints: Constraints): Placeable { - val modifierConstraints = - (get() - ?.let { SizeModifier(constraints).mergeWith(it).constraints } - ?: constraints) - .applyFill(get(), get()) - val result = measurePolicy.measure(children, modifierConstraints) + val (innerConstraints, layoutConstraints) = layoutChangingModifiers.fold( + constraints to constraints + ) { (inner, outer), modifier -> + modifier.modifyInnerConstraints(inner) to modifier.modifyLayoutConstraints(outer) + } + val result = measurePolicy.measure(children, innerConstraints) if (width != result.width || height != result.height) { get()?.onSizeChanged?.invoke(Size(result.width, result.height)) @@ -74,18 +80,15 @@ internal class LayoutNode : Measurable, Placeable, GuiyNode { result.placer.placeChildren() // Returned constraints will always appear as though they are in parent's bounds - return coercedConstraints(constraints) + return coercedConstraints(layoutConstraints) } override fun placeAt(x: Int, y: Int) { - val absolute = get() - if (absolute != null) { - this.x = absolute.x - this.y = absolute.y - } else { - this.x = x - this.y = y + val offset = layoutChangingModifiers.fold(IntOffset(x, y)) { acc, modifier -> + modifier.modifyPosition(acc) } + this.x = offset.x + this.y = offset.y } override fun renderTo(canvas: GuiyCanvas?) { diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/MeasurePolicy.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/MeasurePolicy.kt index 5417712..6a47fbf 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/MeasurePolicy.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/MeasurePolicy.kt @@ -43,3 +43,5 @@ interface Placeable { val size: IntSize get() = IntSize(width, height) } + + diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt index deaae56..47d5382 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt @@ -24,7 +24,7 @@ fun Row( private data class RowMeasurePolicy( private val verticalAlignment: Alignment.Vertical, -) : RowLikeMeasurePolicy() { +) : RowLikeMeasurePolicy(sumWidth = true) { override fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult { return MeasureResult(width, height) { var childX = 0 diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt index e97b6d8..4f3a984 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt @@ -1,14 +1,18 @@ package com.mineinabyss.guiy.layout import com.mineinabyss.guiy.modifiers.Constraints +import kotlin.math.max -abstract class RowLikeMeasurePolicy : MeasurePolicy { +abstract class RowLikeMeasurePolicy( + val sumWidth: Boolean = false, + val sumHeight: Boolean = false, +) : MeasurePolicy { override fun measure(measurables: List, constraints: Constraints): MeasureResult { val noMinConstraints = constraints.copy(minWidth = 0, minHeight = 0) val placeables = measurables.map { it.measure(noMinConstraints) } - val width = placeables.maxOfOrNull { it.width } ?: 0 - val height = placeables.maxOfOrNull { it.height } ?: 0 - return placeChildren(placeables, width, height) + val width = if (sumWidth) placeables.sumOf { it.width } else placeables.maxOfOrNull { it.width } ?: 0 + val height = if (sumHeight) placeables.sumOf { it.height } else placeables.maxOfOrNull { it.height } ?: 0 + return placeChildren(placeables, max(width, constraints.minWidth), max(height, constraints.minHeight)) } abstract fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt new file mode 100644 index 0000000..7455b6a --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt @@ -0,0 +1,13 @@ +package com.mineinabyss.guiy.modifiers + +import com.mineinabyss.guiy.components.state.IntOffset + +interface LayoutChangingModifier { + fun modifyPosition(offset: IntOffset): IntOffset = offset + + /** Modify constraints as they appear to parent nodes laying out this node. */ + fun modifyLayoutConstraints(constraints: Constraints): Constraints = constraints + + /** Modify constraints as they appear to this node when laying out its children. */ + fun modifyInnerConstraints(constraints: Constraints): Constraints = modifyLayoutConstraints(constraints) +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/PositionModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/PositionModifier.kt deleted file mode 100644 index cd6dc3d..0000000 --- a/src/main/kotlin/com/mineinabyss/guiy/modifiers/PositionModifier.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.mineinabyss.guiy.modifiers - -class PositionModifier( - val x: Int = 0, - val y: Int = 0, -) : Modifier.Element { - override fun mergeWith(other: PositionModifier) = other -} - -/** Places an element at an absolute offset in the inventory. */ -fun Modifier.at(x: Int = 0, y: Int = 0) = then(PositionModifier(x, y)) diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt index 00ba042..bc7e75d 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt @@ -1,12 +1,13 @@ package com.mineinabyss.guiy.modifiers +import androidx.compose.runtime.Stable import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt data class SizeModifier( val constraints: Constraints -) : Modifier.Element { +) : Modifier.Element, LayoutChangingModifier { override fun mergeWith(other: SizeModifier) = with(constraints) { SizeModifier( Constraints( @@ -17,46 +18,57 @@ data class SizeModifier( ) ) } + + override fun modifyLayoutConstraints(constraints: Constraints): Constraints { + return SizeModifier(constraints).mergeWith(this).constraints + } } data class HorizontalFillModifier( val percent: Double -) : Modifier.Element { +) : Modifier.Element, LayoutChangingModifier { override fun mergeWith(other: HorizontalFillModifier) = other + + override fun modifyLayoutConstraints(constraints: Constraints): Constraints { + val fillWidth = (constraints.minWidth + percent * (constraints.maxWidth - constraints.minWidth)).roundToInt() + return constraints.copy( + minWidth = fillWidth, + maxWidth = fillWidth + ) + } } data class VerticalFillModifier( val percent: Double -) : Modifier.Element { +) : Modifier.Element, LayoutChangingModifier { override fun mergeWith(other: VerticalFillModifier) = other -} - -fun Constraints.applyFill(horizontal: HorizontalFillModifier?, vertical: VerticalFillModifier?): Constraints { - val fillWidth = if (horizontal == null) null else - (minWidth + horizontal.percent * (maxWidth - minWidth)).roundToInt() - val fillHeight = if (vertical == null) null else - (minHeight + vertical.percent * (maxHeight - minHeight)).roundToInt() - return Constraints( - fillWidth ?: minWidth, - fillWidth ?: maxWidth, - fillHeight ?: minHeight, - fillHeight ?: maxHeight - ) + override fun modifyLayoutConstraints(constraints: Constraints): Constraints { + val fillHeight = + (constraints.minHeight + percent * (constraints.maxHeight - constraints.minHeight)).roundToInt() + return constraints.copy( + minHeight = fillHeight, + maxHeight = fillHeight + ) + } } /** Forces element width to a percentage between min and max width constraints */ +@Stable fun Modifier.fillMaxWidth(percent: Double = 1.0) = then(HorizontalFillModifier(percent)) /** Forces element height to a percentage between min and max height constraints */ +@Stable fun Modifier.fillMaxHeight(percent: Double = 1.0) = then(VerticalFillModifier(percent)) /** Forces element width and height to a percentage between min and max width and height constraints */ +@Stable fun Modifier.fillMaxSize(percent: Double = 1.0) = then(HorizontalFillModifier(percent)).then(VerticalFillModifier(percent)) /** * Sets min and max, width and height constraints for this element. */ +@Stable fun Modifier.sizeIn( minWidth: Int = 0, maxWidth: Int = Integer.MAX_VALUE, @@ -65,10 +77,13 @@ fun Modifier.sizeIn( ) = then(SizeModifier(Constraints(minWidth, maxWidth, minHeight, maxHeight))) /** Sets identical min/max width and height constraints for this element. */ +@Stable fun Modifier.size(width: Int, height: Int) = then(sizeIn(width, width, height, height)) /** Sets identical min/max width constraints for this element. */ +@Stable fun Modifier.width(width: Int) = then(sizeIn(width, width, 0, Integer.MAX_VALUE)) /** Sets identical min/max height constraints for this element. */ +@Stable fun Modifier.height(height: Int) = then(sizeIn(0, Integer.MAX_VALUE, height, height)) diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/absolute/PositionModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/absolute/PositionModifier.kt new file mode 100644 index 0000000..5b01b86 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/absolute/PositionModifier.kt @@ -0,0 +1,17 @@ +package com.mineinabyss.guiy.modifiers.placement.absolute + +import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.modifiers.LayoutChangingModifier +import com.mineinabyss.guiy.modifiers.Modifier + +class PositionModifier( + val x: Int = 0, + val y: Int = 0, +) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: PositionModifier) = other + + override fun modifyPosition(offset: IntOffset) = IntOffset(this.x, this.y) +} + +/** Places an element at an absolute offset in the inventory. */ +fun Modifier.at(x: Int = 0, y: Int = 0) = then(PositionModifier(x, y)) diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetModifier.kt new file mode 100644 index 0000000..b2d2dd0 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetModifier.kt @@ -0,0 +1,17 @@ +package com.mineinabyss.guiy.modifiers.placement.offset + +import androidx.compose.runtime.Stable +import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.modifiers.LayoutChangingModifier +import com.mineinabyss.guiy.modifiers.Modifier + +data class OffsetModifier( + val offset: IntOffset +) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: OffsetModifier) = other + + override fun modifyPosition(offset: IntOffset): IntOffset = offset + this.offset +} + +@Stable +fun Modifier.offset(x: Int, y: Int) = then(OffsetModifier(IntOffset(x, y))) diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetPlaceable.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetPlaceable.kt new file mode 100644 index 0000000..1f62410 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/offset/OffsetPlaceable.kt @@ -0,0 +1,12 @@ +package com.mineinabyss.guiy.modifiers.placement.offset + +import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.layout.Placeable + +class OffsetPlaceable( + val offset: IntOffset, + val inner: Placeable +) : Placeable by inner { + override fun placeAt(x: Int, y: Int) { + } +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt new file mode 100644 index 0000000..af4a817 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt @@ -0,0 +1,40 @@ +package com.mineinabyss.guiy.modifiers.placement.padding + +import androidx.compose.runtime.Stable +import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.modifiers.Constraints +import com.mineinabyss.guiy.modifiers.LayoutChangingModifier +import com.mineinabyss.guiy.modifiers.Modifier +import kotlin.math.max + +data class PaddingModifier( + val padding: PaddingValues +) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: PaddingModifier) = PaddingModifier( + PaddingValues( + max(padding.start, other.padding.start), + max(padding.end, other.padding.end), + max(padding.top, other.padding.top), + max(padding.bottom, other.padding.bottom), + ) + ) + + override fun modifyPosition(offset: IntOffset): IntOffset = offset + padding.getOffset() + + override fun modifyInnerConstraints(constraints: Constraints) = constraints.copy( + maxWidth = constraints.maxWidth - padding.end - padding.start, + maxHeight = constraints.maxHeight - padding.bottom - padding.top, + ) +} + +@Stable +fun Modifier.padding( + start: Int = 0, + end: Int = 0, + top: Int = 0, + bottom: Int = 0, +) = then(PaddingModifier(PaddingValues(start, end, bottom, top))) + +@Stable +fun Modifier.padding(horizontal: Int = 0, vertical: Int = 0) = + padding(horizontal, horizontal, vertical, vertical) diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingValues.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingValues.kt new file mode 100644 index 0000000..72d5cd7 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingValues.kt @@ -0,0 +1,12 @@ +package com.mineinabyss.guiy.modifiers.placement.padding + +import com.mineinabyss.guiy.components.state.IntOffset + +data class PaddingValues( + val start: Int = 0, + val end: Int = 0, + val top: Int = 0, + val bottom: Int = 0, +) { + fun getOffset() = IntOffset(start, top) +} From e046518099c54f75b5de05fceb0a4e5e255704b4 Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Mon, 3 Jun 2024 00:37:24 -0400 Subject: [PATCH 2/8] fix: fold Modifiers in instead of out (the expected left takes precedence over right behaviour) fix: First size always wins fix: Make padding behaviour consistent with Jetpack Compose's fix: Accidental double then() on some extensions calling other extensions refactor: modifyLayoutConstraints runs after inner constraints have been calculated and element measure --- .../mineinabyss/guiy/example/GuiyCommands.kt | 14 ++++-- .../guiy/example/gui/ArrangementMenu.kt | 31 ++++++++++++ .../guiy/components/canvases/Chest.kt | 3 +- .../com/mineinabyss/guiy/layout/LayoutNode.kt | 16 +++--- .../mineinabyss/guiy/modifiers/Constraints.kt | 49 ++++++++++++++++++- .../guiy/modifiers/LayoutChangingModifier.kt | 8 +-- .../guiy/modifiers/SizeModifier.kt | 26 +++++----- .../placement/padding/PaddingModifier.kt | 28 +++++++++-- 8 files changed, 143 insertions(+), 32 deletions(-) create mode 100644 guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/GuiyCommands.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/GuiyCommands.kt index b38aaca..99202c9 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/GuiyCommands.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/GuiyCommands.kt @@ -1,9 +1,6 @@ package com.mineinabyss.guiy.example -import com.mineinabyss.guiy.example.gui.AnimatedTitle -import com.mineinabyss.guiy.example.gui.CreativeMenu -import com.mineinabyss.guiy.example.gui.Cursor -import com.mineinabyss.guiy.example.gui.PaginatedMenu +import com.mineinabyss.guiy.example.gui.* import com.mineinabyss.guiy.inventory.guiy import com.mineinabyss.idofront.commands.execution.IdofrontCommandExecutor import com.mineinabyss.idofront.commands.extensions.actions.playerAction @@ -14,6 +11,13 @@ import org.bukkit.command.TabCompleter class GuiyCommands(val plugin: GuiyExamplePlugin) : IdofrontCommandExecutor(), TabCompleter { override val commands = commands(plugin) { "guiyexample" { + "arrangement" { + playerAction { + guiy { + ArrangementMenu(player) + } + } + } "animated" { playerAction { guiy { @@ -52,6 +56,6 @@ class GuiyCommands(val plugin: GuiyExamplePlugin) : IdofrontCommandExecutor(), T args: Array? ): List = if (command.name == "guiyexample") - listOf("animated", "creative", "cursor", "pagination") + listOf("arrangement", "animated", "creative", "cursor", "pagination") else listOf() } diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt new file mode 100644 index 0000000..c77526f --- /dev/null +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt @@ -0,0 +1,31 @@ +package com.mineinabyss.guiy.example.gui + +import androidx.compose.runtime.Composable +import com.mineinabyss.guiy.components.Item +import com.mineinabyss.guiy.components.canvases.Chest +import com.mineinabyss.guiy.inventory.LocalGuiyOwner +import com.mineinabyss.guiy.layout.Column +import com.mineinabyss.guiy.layout.Row +import com.mineinabyss.guiy.modifiers.* +import com.mineinabyss.guiy.modifiers.placement.padding.padding +import org.bukkit.Material +import org.bukkit.entity.Player + +@Composable +fun ArrangementMenu(player: Player) { + val owner = LocalGuiyOwner.current + Chest( + setOf(player), + "Arrangement example", + onClose = { owner.exit() }, + modifier = Modifier.fillMaxSize() + ) { + Row { + Item(Material.BLACK_STAINED_GLASS, modifier = Modifier.fillMaxHeight()) + Column { + Item(Material.RED_CONCRETE, modifier = Modifier.fillMaxWidth().height(3).padding(1)) + Item(Material.BLUE_CONCRETE, modifier = Modifier.fillMaxSize().padding(1)) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/components/canvases/Chest.kt b/src/main/kotlin/com/mineinabyss/guiy/components/canvases/Chest.kt index 3518c78..86f6d1b 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/components/canvases/Chest.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/components/canvases/Chest.kt @@ -54,7 +54,8 @@ fun Chest( content: @Composable () -> Unit, ) { var size by remember { mutableStateOf(Size()) } - val constrainedModifier = modifier.sizeIn(CHEST_WIDTH, CHEST_WIDTH, MIN_CHEST_HEIGHT, MAX_CHEST_HEIGHT) + val constrainedModifier = + Modifier.sizeIn(CHEST_WIDTH, CHEST_WIDTH, MIN_CHEST_HEIGHT, MAX_CHEST_HEIGHT).then(modifier) .onSizeChanged { if (size != it) size = it } val holder = rememberInventoryHolder(viewers, onClose) diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt index f983041..8a8817d 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/LayoutNode.kt @@ -2,6 +2,7 @@ package com.mineinabyss.guiy.layout import com.mineinabyss.guiy.components.state.IntCoordinates import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.components.state.IntSize import com.mineinabyss.guiy.components.state.ItemPositions import com.mineinabyss.guiy.inventory.ClickResult import com.mineinabyss.guiy.inventory.GuiyCanvas @@ -30,15 +31,15 @@ internal class LayoutNode : Measurable, Placeable, GuiyNode { override var modifier: Modifier = Modifier set(value) { field = value - processedModifier = modifier.foldOut(mutableMapOf()) { element, acc -> + processedModifier = modifier.foldIn(mutableMapOf()) { acc, element -> val existing = acc[element::class] if (existing != null) - acc[element::class] = element.unsafeMergeWith(existing) + acc[element::class] = existing.unsafeMergeWith(element) else acc[element::class] = element acc } - layoutChangingModifiers = modifier.foldOut(mutableListOf()) { element, acc -> + layoutChangingModifiers = modifier.foldIn(mutableListOf()) { acc, element -> if (element is LayoutChangingModifier) acc.add(element) acc } @@ -65,10 +66,8 @@ internal class LayoutNode : Measurable, Placeable, GuiyNode { } override fun measure(constraints: Constraints): Placeable { - val (innerConstraints, layoutConstraints) = layoutChangingModifiers.fold( - constraints to constraints - ) { (inner, outer), modifier -> - modifier.modifyInnerConstraints(inner) to modifier.modifyLayoutConstraints(outer) + val innerConstraints = layoutChangingModifiers.fold(constraints) { inner, modifier -> + modifier.modifyInnerConstraints(inner) } val result = measurePolicy.measure(children, innerConstraints) @@ -79,6 +78,9 @@ internal class LayoutNode : Measurable, Placeable, GuiyNode { height = result.height result.placer.placeChildren() + val layoutConstraints = layoutChangingModifiers.fold(constraints) { outer, modifier -> + modifier.modifyLayoutConstraints(IntSize(result.width, result.height), outer) + } // Returned constraints will always appear as though they are in parent's bounds return coercedConstraints(layoutConstraints) } diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/Constraints.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/Constraints.kt index 3ff36b2..f0ceb10 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/modifiers/Constraints.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/Constraints.kt @@ -1,11 +1,58 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.mineinabyss.guiy.modifiers import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable @Immutable -data class Constraints( +class Constraints( val minWidth: Int = 0, val maxWidth: Int = Int.MAX_VALUE, val minHeight: Int = 0, val maxHeight: Int = Int.MAX_VALUE +) { + fun copy( + minWidth: Int = this.minWidth, + maxWidth: Int = this.maxWidth, + minHeight: Int = this.minHeight, + maxHeight: Int = this.maxHeight + ) = Constraints( + minWidth.coerceAtMost(maxWidth), + maxWidth.coerceAtLeast(minWidth), + minHeight.coerceAtMost(maxHeight), + maxHeight.coerceAtLeast(minHeight) + ) + +} + +// Android +@Stable +fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) = Constraints( + (minWidth + horizontal).coerceAtLeast(0), + addMaxWithMinimum(maxWidth, horizontal), + (minHeight + vertical).coerceAtLeast(0), + addMaxWithMinimum(maxHeight, vertical) ) + +private fun addMaxWithMinimum(max: Int, value: Int): Int { + return if (max == Int.MAX_VALUE) { + max + } else { + (max + value).coerceAtLeast(0) + } +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt index 7455b6a..013d416 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/LayoutChangingModifier.kt @@ -1,13 +1,15 @@ package com.mineinabyss.guiy.modifiers import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.components.state.IntSize interface LayoutChangingModifier { fun modifyPosition(offset: IntOffset): IntOffset = offset /** Modify constraints as they appear to parent nodes laying out this node. */ - fun modifyLayoutConstraints(constraints: Constraints): Constraints = constraints + fun modifyLayoutConstraints(measuredSize: IntSize, constraints: Constraints): Constraints = + modifyInnerConstraints(constraints) - /** Modify constraints as they appear to this node when laying out its children. */ - fun modifyInnerConstraints(constraints: Constraints): Constraints = modifyLayoutConstraints(constraints) + /** Modify constraints as they appear to this node and its children for layout. */ + fun modifyInnerConstraints(constraints: Constraints): Constraints = constraints } diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt index bc7e75d..f26b141 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/SizeModifier.kt @@ -1,8 +1,6 @@ package com.mineinabyss.guiy.modifiers import androidx.compose.runtime.Stable -import kotlin.math.max -import kotlin.math.min import kotlin.math.roundToInt data class SizeModifier( @@ -11,15 +9,15 @@ data class SizeModifier( override fun mergeWith(other: SizeModifier) = with(constraints) { SizeModifier( Constraints( - max(minWidth, other.constraints.minWidth), - min(maxWidth, other.constraints.maxWidth), - max(minHeight, other.constraints.minHeight), - min(maxHeight, other.constraints.maxHeight), + other.constraints.minWidth.coerceIn(minWidth, maxWidth), + other.constraints.maxWidth.coerceIn(minWidth, maxWidth), + other.constraints.minHeight.coerceIn(minHeight, maxHeight), + other.constraints.maxHeight.coerceIn(minHeight, maxHeight), ) ) } - override fun modifyLayoutConstraints(constraints: Constraints): Constraints { + override fun modifyInnerConstraints(constraints: Constraints): Constraints { return SizeModifier(constraints).mergeWith(this).constraints } } @@ -29,7 +27,7 @@ data class HorizontalFillModifier( ) : Modifier.Element, LayoutChangingModifier { override fun mergeWith(other: HorizontalFillModifier) = other - override fun modifyLayoutConstraints(constraints: Constraints): Constraints { + override fun modifyInnerConstraints(constraints: Constraints): Constraints { val fillWidth = (constraints.minWidth + percent * (constraints.maxWidth - constraints.minWidth)).roundToInt() return constraints.copy( minWidth = fillWidth, @@ -43,7 +41,7 @@ data class VerticalFillModifier( ) : Modifier.Element, LayoutChangingModifier { override fun mergeWith(other: VerticalFillModifier) = other - override fun modifyLayoutConstraints(constraints: Constraints): Constraints { + override fun modifyInnerConstraints(constraints: Constraints): Constraints { val fillHeight = (constraints.minHeight + percent * (constraints.maxHeight - constraints.minHeight)).roundToInt() return constraints.copy( @@ -78,12 +76,16 @@ fun Modifier.sizeIn( /** Sets identical min/max width and height constraints for this element. */ @Stable -fun Modifier.size(width: Int, height: Int) = then(sizeIn(width, width, height, height)) +fun Modifier.size(width: Int, height: Int) = sizeIn(width, width, height, height) + +/** Sets identical min/max width and height constraints for this element. */ +@Stable +fun Modifier.size(size: Int) = size(size, size) /** Sets identical min/max width constraints for this element. */ @Stable -fun Modifier.width(width: Int) = then(sizeIn(width, width, 0, Integer.MAX_VALUE)) +fun Modifier.width(width: Int) = sizeIn(width, width, 0, Integer.MAX_VALUE) /** Sets identical min/max height constraints for this element. */ @Stable -fun Modifier.height(height: Int) = then(sizeIn(0, Integer.MAX_VALUE, height, height)) +fun Modifier.height(height: Int) = sizeIn(0, Integer.MAX_VALUE, height, height) diff --git a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt index af4a817..4fd7252 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/modifiers/placement/padding/PaddingModifier.kt @@ -2,9 +2,11 @@ package com.mineinabyss.guiy.modifiers.placement.padding import androidx.compose.runtime.Stable import com.mineinabyss.guiy.components.state.IntOffset +import com.mineinabyss.guiy.components.state.IntSize import com.mineinabyss.guiy.modifiers.Constraints import com.mineinabyss.guiy.modifiers.LayoutChangingModifier import com.mineinabyss.guiy.modifiers.Modifier +import com.mineinabyss.guiy.modifiers.offset import kotlin.math.max data class PaddingModifier( @@ -19,12 +21,28 @@ data class PaddingModifier( ) ) + val horizontal get() = padding.start + padding.end + val vertical get() = padding.top + padding.bottom + override fun modifyPosition(offset: IntOffset): IntOffset = offset + padding.getOffset() - override fun modifyInnerConstraints(constraints: Constraints) = constraints.copy( - maxWidth = constraints.maxWidth - padding.end - padding.start, - maxHeight = constraints.maxHeight - padding.bottom - padding.top, + // Shrink inside constraints by padding + override fun modifyInnerConstraints(constraints: Constraints) = constraints.offset( + horizontal = -horizontal, + vertical = -vertical, ) + + // Grow outside constraints by padding + override fun modifyLayoutConstraints(measuredSize: IntSize, constraints: Constraints): Constraints { + val width = (measuredSize.width + horizontal).coerceIn(constraints.minWidth, constraints.maxWidth) + val height = (measuredSize.height + vertical).coerceIn(constraints.minHeight, constraints.maxHeight) + return constraints.copy( + minWidth = width, + maxWidth = width, + minHeight = height, + maxHeight = height, + ) + } } @Stable @@ -38,3 +56,7 @@ fun Modifier.padding( @Stable fun Modifier.padding(horizontal: Int = 0, vertical: Int = 0) = padding(horizontal, horizontal, vertical, vertical) + +@Stable +fun Modifier.padding(all: Int = 0) = + padding(all, all, all, all) From 281b14f2444c3ae6682ac92bdd7e92ccb9b0fd2c Mon Sep 17 00:00:00 2001 From: Boy Date: Sun, 2 Jun 2024 21:38:36 +0200 Subject: [PATCH 3/8] refactor: cleanup CreativeItem --- .../mineinabyss/guiy/components/CreativeItem.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/mineinabyss/guiy/components/CreativeItem.kt b/src/main/kotlin/com/mineinabyss/guiy/components/CreativeItem.kt index c6baa08..2a06a1a 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/components/CreativeItem.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/components/CreativeItem.kt @@ -15,21 +15,20 @@ fun CreativeItem( ) { Item(itemStack, modifier.clickable { // Mimic all vanilla interactions - val shiftClick = clickType == ClickType.SHIFT_LEFT || clickType == ClickType.SHIFT_RIGHT val result: ItemStack? = when { - (shiftClick || clickType == ClickType.MIDDLE) && cursor == null -> itemStack?.clone() - ?.apply { amount = maxStackSize } + (clickType.isShiftClick || clickType == ClickType.MIDDLE) && cursor == null -> + itemStack?.asQuantity(itemStack.maxStackSize) clickType == ClickType.MIDDLE -> return@clickable - (clickType == ClickType.SHIFT_LEFT && cursor != null && cursor.isSimilar(itemStack)) -> - cursor.clone().apply { amount = maxStackSize } + clickType == ClickType.SHIFT_LEFT && cursor != null && cursor.isSimilar(itemStack) -> + cursor.asQuantity(cursor.maxStackSize) - cursor == null -> itemStack?.clone()?.apply { amount = 1 } + cursor == null -> itemStack?.clone()?.asOne() - clickType == ClickType.RIGHT || clickType == ClickType.SHIFT_RIGHT -> cursor.clone().subtract() + clickType.isRightClick -> cursor.clone().subtract() - (clickType == ClickType.LEFT || clickType == ClickType.SHIFT_LEFT) && !cursor.isSimilar(itemStack) -> null + clickType.isLeftClick && !cursor.isSimilar(itemStack) -> null else -> cursor.clone().add() } From 8f34574f1501a6034207d2c9032a72fe0c813c06 Mon Sep 17 00:00:00 2001 From: Boy Date: Sun, 2 Jun 2024 22:37:40 +0200 Subject: [PATCH 4/8] refactor: allow for customized navbarFiller item --- .../com/mineinabyss/guiy/components/lists/Paginated.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt b/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt index 5fab9e2..1070274 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt @@ -12,7 +12,9 @@ import com.mineinabyss.guiy.modifiers.Modifier import com.mineinabyss.guiy.modifiers.fillMaxHeight import com.mineinabyss.guiy.modifiers.fillMaxSize import com.mineinabyss.guiy.modifiers.fillMaxWidth +import com.mineinabyss.idofront.items.editItemMeta import org.bukkit.Material +import org.bukkit.inventory.ItemStack @Composable fun Paginated( @@ -22,6 +24,7 @@ fun Paginated( nextButton: @Composable () -> Unit, previousButton: @Composable () -> Unit, navbarPosition: NavbarPosition = NavbarPosition.BOTTOM, + navbarFiller: ItemStack? = ItemStack(Material.GRAY_STAINED_GLASS_PANE).editItemMeta { isHideTooltip = true }, content: @Composable (page: List) -> Unit, ) { Box(Modifier.fillMaxSize()) { @@ -40,7 +43,7 @@ fun Paginated( Box(contentAlignment = alignment, modifier = Modifier.fillMaxSize()) { when (navbarPosition) { NavbarPosition.START, NavbarPosition.END -> { - Item(Material.GRAY_STAINED_GLASS_PANE, hideTooltip = true, modifier = Modifier.fillMaxHeight()) + Item(navbarFiller, modifier = Modifier.fillMaxHeight()) Column { if (page > 0) previousButton() else Spacer(height = 1) @@ -52,7 +55,7 @@ fun Paginated( } NavbarPosition.TOP, NavbarPosition.BOTTOM -> { - Item(Material.GRAY_STAINED_GLASS_PANE, hideTooltip = true, modifier = Modifier.fillMaxWidth()) + Item(navbarFiller, modifier = Modifier.fillMaxWidth()) Row { if (page > 0) previousButton() else Spacer(width = 1) From 9368bc6fdc62195e6b2d1445647ee5ac5cb1e978 Mon Sep 17 00:00:00 2001 From: Boy Date: Sun, 2 Jun 2024 23:31:25 +0200 Subject: [PATCH 5/8] fix: fill paginated-content after navbar --- .../guiy/example/gui/PaginatedMenu.kt | 5 ++- .../guiy/components/lists/Paginated.kt | 44 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt index 466304e..1069aca 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt @@ -10,6 +10,7 @@ import com.mineinabyss.guiy.inventory.LocalGuiyOwner import com.mineinabyss.guiy.modifiers.Modifier import com.mineinabyss.guiy.modifiers.click.clickable import com.mineinabyss.guiy.modifiers.fillMaxSize +import com.mineinabyss.guiy.modifiers.placement.absolute.at import org.bukkit.Material import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack @@ -32,9 +33,9 @@ fun PaginatedMenu(player: Player) { items, page = page, itemsPerPage = 9 * 5, - previousButton = { Item(Material.RED_CONCRETE, "Previous", modifier = Modifier.clickable { page-- }) }, + previousButton = { Item(Material.RED_CONCRETE, "Previous", modifier = Modifier.at(0, 1).clickable { page-- }) }, navbarPosition = NavbarPosition.START, - nextButton = { Item(Material.BLUE_CONCRETE, "Next", modifier = Modifier.clickable { page++ }) }, + nextButton = { Item(Material.BLUE_CONCRETE, "Next", modifier = Modifier.at(0,3).clickable { page++ }) }, ) { pageItems -> VerticalGrid { pageItems.forEach { item -> diff --git a/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt b/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt index 1070274..d9ea33e 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt @@ -1,6 +1,7 @@ package com.mineinabyss.guiy.components.lists import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import com.mineinabyss.guiy.components.Item import com.mineinabyss.guiy.components.Spacer @@ -8,11 +9,9 @@ import com.mineinabyss.guiy.layout.Box import com.mineinabyss.guiy.layout.Column import com.mineinabyss.guiy.layout.Row import com.mineinabyss.guiy.layout.alignment.Alignment -import com.mineinabyss.guiy.modifiers.Modifier -import com.mineinabyss.guiy.modifiers.fillMaxHeight -import com.mineinabyss.guiy.modifiers.fillMaxSize -import com.mineinabyss.guiy.modifiers.fillMaxWidth +import com.mineinabyss.guiy.modifiers.* import com.mineinabyss.idofront.items.editItemMeta +import com.mineinabyss.idofront.messaging.broadcast import org.bukkit.Material import org.bukkit.inventory.ItemStack @@ -30,39 +29,38 @@ fun Paginated( Box(Modifier.fillMaxSize()) { val start = page * itemsPerPage val end = (page + 1) * itemsPerPage - if (start >= 0) { - val pageItems = remember(start, end) { items.subList(start, end.coerceAtMost(items.size)) } - content(pageItems) - } + val alignment = when (navbarPosition) { NavbarPosition.START -> Alignment.CenterStart NavbarPosition.END -> Alignment.CenterEnd NavbarPosition.TOP -> Alignment.TopCenter NavbarPosition.BOTTOM -> Alignment.BottomCenter } + + val pageItems = remember(start, end) { + items.subList(start, end.coerceAtMost(items.size)) + } + content(pageItems) + Box(contentAlignment = alignment, modifier = Modifier.fillMaxSize()) { + when (navbarPosition) { NavbarPosition.START, NavbarPosition.END -> { Item(navbarFiller, modifier = Modifier.fillMaxHeight()) - Column { - if (page > 0) previousButton() - else Spacer(height = 1) - Spacer(height = 1) - if (end < items.size) nextButton() - else Spacer(height = 1) - } - + if (page > 0) previousButton() + else Spacer(height = 1) + Spacer(height = 1) + if (end < items.size) nextButton() + else Spacer(height = 1) } NavbarPosition.TOP, NavbarPosition.BOTTOM -> { Item(navbarFiller, modifier = Modifier.fillMaxWidth()) - Row { - if (page > 0) previousButton() - else Spacer(width = 1) - Spacer(width = 1) - if (end < items.size) nextButton() - else Spacer(width = 1) - } + if (page > 0) previousButton() + else Spacer(width = 1) + Spacer(width = 1) + if (end < items.size) nextButton() + else Spacer(width = 1) } } } From 121bb67d00260bc10137082d9b00c90a23ece97c Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Mon, 3 Jun 2024 13:48:16 -0400 Subject: [PATCH 6/8] feat: Row and Column arrangement from jetpack refactor: Cleanup Paginated component to space things automatically --- .../guiy/example/gui/ArrangementMenu.kt | 36 +- .../mineinabyss/guiy/example/gui/Cursor.kt | 2 +- .../guiy/example/gui/PaginatedMenu.kt | 3 +- .../guiy/components/lists/Paginated.kt | 123 ++-- .../alignment => jetpack}/Alignment.kt | 119 ++- .../mineinabyss/guiy/jetpack/Arrangement.kt | 689 ++++++++++++++++++ .../com/mineinabyss/guiy/jetpack/Helpers.kt | 5 + .../guiy/jetpack/LayoutDirection.kt | 18 + .../kotlin/com/mineinabyss/guiy/layout/Box.kt | 9 +- .../com/mineinabyss/guiy/layout/Column.kt | 28 +- .../kotlin/com/mineinabyss/guiy/layout/Row.kt | 31 +- .../guiy/layout/RowColumnMeasurePolicy.kt | 33 + .../guiy/layout/RowLikeMeasurePolicy.kt | 19 - 13 files changed, 1008 insertions(+), 107 deletions(-) rename src/main/kotlin/com/mineinabyss/guiy/{layout/alignment => jetpack}/Alignment.kt (62%) create mode 100644 src/main/kotlin/com/mineinabyss/guiy/jetpack/Arrangement.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/jetpack/Helpers.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/jetpack/LayoutDirection.kt create mode 100644 src/main/kotlin/com/mineinabyss/guiy/layout/RowColumnMeasurePolicy.kt delete mode 100644 src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt index c77526f..cce55c5 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/ArrangementMenu.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Composable import com.mineinabyss.guiy.components.Item import com.mineinabyss.guiy.components.canvases.Chest import com.mineinabyss.guiy.inventory.LocalGuiyOwner +import com.mineinabyss.guiy.jetpack.Arrangement import com.mineinabyss.guiy.layout.Column import com.mineinabyss.guiy.layout.Row -import com.mineinabyss.guiy.modifiers.* -import com.mineinabyss.guiy.modifiers.placement.padding.padding +import com.mineinabyss.guiy.modifiers.Modifier +import com.mineinabyss.guiy.modifiers.fillMaxSize +import com.mineinabyss.guiy.modifiers.fillMaxWidth import org.bukkit.Material import org.bukkit.entity.Player @@ -20,12 +22,32 @@ fun ArrangementMenu(player: Player) { onClose = { owner.exit() }, modifier = Modifier.fillMaxSize() ) { - Row { - Item(Material.BLACK_STAINED_GLASS, modifier = Modifier.fillMaxHeight()) - Column { - Item(Material.RED_CONCRETE, modifier = Modifier.fillMaxWidth().height(3).padding(1)) - Item(Material.BLUE_CONCRETE, modifier = Modifier.fillMaxSize().padding(1)) + Column { + val modifier = Modifier.fillMaxWidth() + Row(modifier, horizontalArrangement = Arrangement.spacedBy(1)) { + Items(4) } + Row(modifier, horizontalArrangement = Arrangement.Center) { + Items(3) + } + Row(modifier, horizontalArrangement = Arrangement.SpaceAround) { + Items(3) + } + Row(modifier, horizontalArrangement = Arrangement.SpaceBetween) { + Items(3) + } +// Item(Material.BLACK_STAINED_GLASS, modifier = Modifier.fillMaxHeight()) +// Column { +// Item(Material.RED_CONCRETE, modifier = Modifier.fillMaxWidth().height(3).padding(1)) +// Item(Material.BLUE_CONCRETE, modifier = Modifier.fillMaxSize().padding(1)) +// } } } } + +@Composable +private fun Items(count: Int) { + repeat(count) { + Item(Material.RED_CONCRETE) + } +} diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt index 001bc48..f9d4188 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/Cursor.kt @@ -6,9 +6,9 @@ import com.mineinabyss.guiy.components.Spacer import com.mineinabyss.guiy.components.canvases.Chest import com.mineinabyss.guiy.components.state.IntOffset import com.mineinabyss.guiy.inventory.LocalGuiyOwner +import com.mineinabyss.guiy.jetpack.Alignment import com.mineinabyss.guiy.layout.Box import com.mineinabyss.guiy.layout.Row -import com.mineinabyss.guiy.layout.alignment.Alignment import com.mineinabyss.guiy.modifiers.Modifier import com.mineinabyss.guiy.modifiers.click.clickable import com.mineinabyss.guiy.modifiers.fillMaxSize diff --git a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt index 466304e..15c4c2a 100644 --- a/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt +++ b/guiy-example/src/main/kotlin/com/mineinabyss/guiy/example/gui/PaginatedMenu.kt @@ -31,9 +31,8 @@ fun PaginatedMenu(player: Player) { Paginated( items, page = page, - itemsPerPage = 9 * 5, - previousButton = { Item(Material.RED_CONCRETE, "Previous", modifier = Modifier.clickable { page-- }) }, navbarPosition = NavbarPosition.START, + previousButton = { Item(Material.RED_CONCRETE, "Previous", modifier = Modifier.clickable { page-- }) }, nextButton = { Item(Material.BLUE_CONCRETE, "Next", modifier = Modifier.clickable { page++ }) }, ) { pageItems -> VerticalGrid { diff --git a/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt b/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt index 5fab9e2..333e023 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/components/lists/Paginated.kt @@ -1,71 +1,112 @@ package com.mineinabyss.guiy.components.lists -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import com.mineinabyss.guiy.components.Item import com.mineinabyss.guiy.components.Spacer +import com.mineinabyss.guiy.jetpack.Arrangement import com.mineinabyss.guiy.layout.Box import com.mineinabyss.guiy.layout.Column import com.mineinabyss.guiy.layout.Row -import com.mineinabyss.guiy.layout.alignment.Alignment -import com.mineinabyss.guiy.modifiers.Modifier -import com.mineinabyss.guiy.modifiers.fillMaxHeight -import com.mineinabyss.guiy.modifiers.fillMaxSize -import com.mineinabyss.guiy.modifiers.fillMaxWidth +import com.mineinabyss.guiy.layout.Size +import com.mineinabyss.guiy.modifiers.* import org.bukkit.Material +import org.bukkit.inventory.ItemStack @Composable fun Paginated( items: List, page: Int, - itemsPerPage: Int, nextButton: @Composable () -> Unit, previousButton: @Composable () -> Unit, navbarPosition: NavbarPosition = NavbarPosition.BOTTOM, content: @Composable (page: List) -> Unit, ) { + var size by remember { mutableStateOf(Size(0, 0)) } + val itemsPerPage = size.width * size.height Box(Modifier.fillMaxSize()) { val start = page * itemsPerPage val end = (page + 1) * itemsPerPage - if (start >= 0) { - val pageItems = remember(start, end) { items.subList(start, end.coerceAtMost(items.size)) } - content(pageItems) + val pageItems = remember(start, end) { + if (start < 0) emptyList() + else items.subList(start, end.coerceAtMost(items.size)) } - val alignment = when (navbarPosition) { - NavbarPosition.START -> Alignment.CenterStart - NavbarPosition.END -> Alignment.CenterEnd - NavbarPosition.TOP -> Alignment.TopCenter - NavbarPosition.BOTTOM -> Alignment.BottomCenter - } - Box(contentAlignment = alignment, modifier = Modifier.fillMaxSize()) { - when (navbarPosition) { - NavbarPosition.START, NavbarPosition.END -> { - Item(Material.GRAY_STAINED_GLASS_PANE, hideTooltip = true, modifier = Modifier.fillMaxHeight()) - Column { - if (page > 0) previousButton() - else Spacer(height = 1) - Spacer(height = 1) - if (end < items.size) nextButton() - else Spacer(height = 1) - } - + NavbarLayout( + position = navbarPosition, + navbar = { + NavbarButtons(navbarPosition) { + if (page > 0) previousButton() + else Spacer(1, 1) + if (end < items.size) nextButton() + else Spacer(1, 1) } - - NavbarPosition.TOP, NavbarPosition.BOTTOM -> { - Item(Material.GRAY_STAINED_GLASS_PANE, hideTooltip = true, modifier = Modifier.fillMaxWidth()) - Row { - if (page > 0) previousButton() - else Spacer(width = 1) - Spacer(width = 1) - if (end < items.size) nextButton() - else Spacer(width = 1) - } + }, + content = { + Box(Modifier.fillMaxSize().onSizeChanged { + size = it + }) { + content(pageItems) } } - } + ) + } +} + +@Composable +private inline fun NavbarButtons( + navbarPosition: NavbarPosition, + background: ItemStack = ItemStack(Material.BLACK_STAINED_GLASS), + crossinline content: @Composable () -> Unit +) { + val navbarSize = + if (navbarPosition.isVertical()) Modifier.fillMaxHeight().width(1) + else Modifier.fillMaxWidth().height(1) + + Box(modifier = navbarSize) { + Item(background, modifier = Modifier.fillMaxSize()) + if (navbarPosition.isVertical()) + Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.SpaceAround) { content() } + else + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { content() } } } + enum class NavbarPosition { - START, END, TOP, BOTTOM + START, END, TOP, BOTTOM; + + fun isVertical() = this == START || this == END + fun isHorizontal() = this == TOP || this == BOTTOM +} + +@Composable +fun NavbarLayout( + position: NavbarPosition, + navbar: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + when (position) { + NavbarPosition.START -> + Row { + navbar() + content() + } + + NavbarPosition.END -> + Row { + content() + navbar() + } + + NavbarPosition.TOP -> + Column { + navbar() + content() + } + + NavbarPosition.BOTTOM -> + Column { + content() + navbar() + } + } } diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/alignment/Alignment.kt b/src/main/kotlin/com/mineinabyss/guiy/jetpack/Alignment.kt similarity index 62% rename from src/main/kotlin/com/mineinabyss/guiy/layout/alignment/Alignment.kt rename to src/main/kotlin/com/mineinabyss/guiy/jetpack/Alignment.kt index 2680a2f..f7f7a1b 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/alignment/Alignment.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/jetpack/Alignment.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mineinabyss.guiy.layout.alignment +package com.mineinabyss.guiy.jetpack import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -37,7 +37,7 @@ fun interface Alignment { * of size [space]. The returned offset can be negative or larger than `space - size`, * meaning that the box will be positioned partially or completely outside the area. */ - fun align(size: IntSize, space: IntSize): IntOffset + fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset /** * An interface to calculate the position of box of a certain width inside an available width. @@ -52,7 +52,7 @@ fun interface Alignment { * `space - size` meaning that the box will be positioned partially or completely outside * the area. */ - fun align(size: Int, space: Int): Int + fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int } /** @@ -78,53 +78,72 @@ fun interface Alignment { // 2D Alignments. @Stable val TopStart: Alignment = BiasAlignment(-1f, -1f) - @Stable val TopCenter: Alignment = BiasAlignment(0f, -1f) - @Stable val TopEnd: Alignment = BiasAlignment(1f, -1f) - @Stable val CenterStart: Alignment = BiasAlignment(-1f, 0f) - @Stable val Center: Alignment = BiasAlignment(0f, 0f) - @Stable val CenterEnd: Alignment = BiasAlignment(1f, 0f) - @Stable val BottomStart: Alignment = BiasAlignment(-1f, 1f) - @Stable val BottomCenter: Alignment = BiasAlignment(0f, 1f) - @Stable val BottomEnd: Alignment = BiasAlignment(1f, 1f) // 1D Alignment.Verticals. @Stable val Top: Vertical = BiasAlignment.Vertical(-1f) - @Stable val CenterVertically: Vertical = BiasAlignment.Vertical(0f) - @Stable val Bottom: Vertical = BiasAlignment.Vertical(1f) // 1D Alignment.Horizontals. @Stable val Start: Horizontal = BiasAlignment.Horizontal(-1f) - @Stable val CenterHorizontally: Horizontal = BiasAlignment.Horizontal(0f) - @Stable val End: Horizontal = BiasAlignment.Horizontal(1f) } } +/** + * A collection of common [Alignment]s unaware of the layout direction. + */ +object AbsoluteAlignment { + // 2D AbsoluteAlignments. + @Stable + val TopLeft: Alignment = BiasAbsoluteAlignment(-1f, -1f) + + @Stable + val TopRight: Alignment = BiasAbsoluteAlignment(1f, -1f) + + @Stable + val CenterLeft: Alignment = BiasAbsoluteAlignment(-1f, 0f) + + @Stable + val CenterRight: Alignment = BiasAbsoluteAlignment(1f, 0f) + + @Stable + val BottomLeft: Alignment = BiasAbsoluteAlignment(-1f, 1f) + + @Stable + val BottomRight: Alignment = BiasAbsoluteAlignment(1f, 1f) + + // 1D BiasAbsoluteAlignment.Horizontals. + @Stable + val Left: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(-1f) + + @Stable + val Right: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(1f) +} + /** * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the * start/top, a bias of 0 will represent centering, and a bias of 1 will represent end/bottom. @@ -143,11 +162,13 @@ data class BiasAlignment( override fun align( size: IntSize, space: IntSize, + layoutDirection: LayoutDirection ): IntOffset { - val centerX = (space.width - size.width) / 2.0f - val centerY = (space.height - size.height) / 2.0f - //TODO consider supporting layout directions - val resolvedHorizontalBias = if (true/*layoutDirection == LayoutDirection.Ltr*/) { + // Convert to Px first and only round at the end, to avoid rounding twice while calculating + // the new positions + val centerX = (space.width - size.width).toFloat() / 2f + val centerY = (space.height - size.height).toFloat() / 2f + val resolvedHorizontalBias = if (layoutDirection == LayoutDirection.Ltr) { horizontalBias } else { -1 * horizontalBias @@ -170,11 +191,11 @@ data class BiasAlignment( */ @Immutable data class Horizontal(private val bias: Float) : Alignment.Horizontal { - override fun align(size: Int, space: Int): Int { + override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int { // Convert to Px first and only round at the end, to avoid rounding twice while // calculating the new positions val center = (space - size).toFloat() / 2f - val resolvedBias = if (true/*layoutDirection == LayoutDirection.Ltr*/) bias else -1 * bias + val resolvedBias = if (layoutDirection == LayoutDirection.Ltr) bias else -1 * bias return (center * (1 + resolvedBias)).roundToInt() } } @@ -198,3 +219,59 @@ data class BiasAlignment( } } } + +/** + * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the + * left/top, a bias of 0 will represent centering, and a bias of 1 will represent right/bottom. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see AbsoluteAlignment + * @see Alignment + */ +@Immutable +data class BiasAbsoluteAlignment( + private val horizontalBias: Float, + private val verticalBias: Float +) : Alignment { + /** + * Returns the position of a 2D point in a container of a given size, according to this + * [BiasAbsoluteAlignment]. The position will not be mirrored in Rtl context. + */ + override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset { + // Convert to Px first and only round at the end, to avoid rounding twice while calculating + // the new positions + val remaining = IntSize(space.width - size.width, space.height - size.height) + val centerX = remaining.width.toFloat() / 2f + val centerY = remaining.height.toFloat() / 2f + + val x = centerX * (1 + horizontalBias) + val y = centerY * (1 + verticalBias) + return IntOffset(x.roundToInt(), y.roundToInt()) + } + + /** + * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment + * to the left, a bias of 0 will represent centering, and a bias of 1 will represent right. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see BiasAlignment.Horizontal + */ + @Immutable + data class Horizontal(private val bias: Float) : Alignment.Horizontal { + /** + * Returns the position of a 2D point in a container of a given size, + * according to this [BiasAbsoluteAlignment.Horizontal]. This position will not be + * mirrored in Rtl context. + */ + override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int { + // Convert to Px first and only round at the end, to avoid rounding twice while + // calculating the new positions + val center = (space - size).toFloat() / 2f + return (center * (1 + bias)).roundToInt() + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/jetpack/Arrangement.kt b/src/main/kotlin/com/mineinabyss/guiy/jetpack/Arrangement.kt new file mode 100644 index 0000000..1110bde --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/jetpack/Arrangement.kt @@ -0,0 +1,689 @@ +package com.mineinabyss.guiy.jetpack + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * Used to specify the arrangement of the layout's children in layouts like [Row] or [Column] in + * the main axis direction (horizontal and vertical, respectively). + * + * Below is an illustration of different horizontal arrangements in [Row]s: + * ![Row arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/row_arrangement_visualization.gif) + * + * Different vertical arrangements in [Column]s: + * ![Column arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/column_arrangement_visualization.gif) + */ +@Immutable +object Arrangement { + /** + * Used to specify the horizontal arrangement of the layout's children in layouts like [Row]. + */ + @Stable + interface Horizontal { + /** + * Spacing that should be added between any two adjacent layout children. + */ + val spacing get() = 0 + + /** + * Horizontally places the layout children. + * + * @param totalSize Available space that can be occupied by the children, in pixels. + * @param sizes An array of sizes of all children, in pixels. + * @param layoutDirection A layout direction, left-to-right or right-to-left, of the parent + * layout that should be taken into account when determining positions of the children. + * @param outPositions An array of the size of [sizes] that returns the calculated + * positions relative to the left, in pixels. + */ + fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) + } + + /** + * Used to specify the vertical arrangement of the layout's children in layouts like [Column]. + */ + @Stable + interface Vertical { + /** + * Spacing that should be added between any two adjacent layout children. + */ + val spacing get() = 0.dp + + /** + * Vertically places the layout children. + * + * @param totalSize Available space that can be occupied by the children, in pixels. + * @param sizes An array of sizes of all children, in pixels. + * @param outPositions An array of the size of [sizes] that returns the calculated + * positions relative to the top, in pixels. + */ + fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) + } + + /** + * Used to specify the horizontal arrangement of the layout's children in horizontal layouts + * like [Row], or the vertical arrangement of the layout's children in vertical layouts like + * [Column]. + */ + @Stable + interface HorizontalOrVertical : Horizontal, Vertical { + /** + * Spacing that should be added between any two adjacent layout children. + */ + override val spacing: Dp get() = 0.dp + } + + /** + * Place children horizontally such that they are as close as possible to the beginning of the + * horizontal axis (left if the layout direction is LTR, right otherwise). + * Visually: 123#### for LTR and ####321. + */ + @Stable + val Start = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeLeftOrTop(sizes, outPositions, reverseInput = false) + } else { + placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun toString() = "Arrangement#Start" + } + + /** + * Place children horizontally such that they are as close as possible to the end of the main + * axis. + * Visually: ####123 for LTR and 321#### for RTL. + */ + @Stable + val End = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeLeftOrTop(sizes, outPositions, reverseInput = true) + } + + override fun toString() = "Arrangement#End" + } + + /** + * Place children vertically such that they are as close as possible to the top of the main + * axis. + * Visually: (top) 123#### (bottom) + */ + @Stable + val Top = object : Vertical { + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeLeftOrTop(sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#Top" + } + + /** + * Place children vertically such that they are as close as possible to the bottom of the main + * axis. + * Visually: (top) ####123 (bottom) + */ + @Stable + val Bottom = object : Vertical { + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#Bottom" + } + + /** + * Place children such that they are as close as possible to the middle of the main axis. + * Visually: ##123## for LTR and ##321## for RTL. + */ + @Stable + val Center = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeCenter(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeCenter(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeCenter(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#Center" + } + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child. + * Visually: #1#2#3# for LTR and #3#2#1# for RTL. + */ + @Stable + val SpaceEvenly = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#SpaceEvenly" + } + + /** + * Place children such that they are spaced evenly across the main axis, without free + * space before the first child or after the last child. + * Visually: 1##2##3 for LTR or 3##2##1 for RTL. + */ + @Stable + val SpaceBetween = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#SpaceBetween" + } + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child, but half the amount of space + * existing otherwise between two consecutive children. + * Visually: #1##2##3# for LTR and #3##2##1# for RTL + */ + @Stable + val SpaceAround = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeSpaceAround(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#SpaceAround" + } + + /** + * Place children such that each two adjacent ones are spaced by a fixed [space] distance across + * the main axis. The spacing will be subtracted from the available space that the children + * can occupy. The [space] can be negative, in which case children will overlap. + * + * To change alignment of the spaced children horizontally or vertically, use [spacedBy] + * overloads with `alignment` parameter. + * + * @param space The space between adjacent children. + */ + @Stable + fun spacedBy(space: Dp): HorizontalOrVertical = + SpacedAligned(space, true) { size, layoutDirection -> + Alignment.Start.align(0, size, layoutDirection) + } + + /** + * Place children horizontally such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available width that the children + * can occupy. An [alignment] can be specified to align the spaced children horizontally + * inside the parent, in case there is empty width remaining. The [space] can be negative, + * in which case children will overlap. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(space, true) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + + /** + * Place children vertically such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available height that the children + * can occupy. An [alignment] can be specified to align the spaced children vertically + * inside the parent, in case there is empty height remaining. The [space] can be negative, + * in which case children will overlap. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = + SpacedAligned(space, false) { size, _ -> alignment.align(0, size) } + + /** + * Place children horizontally one next to the other and align the obtained group + * according to an [alignment]. + * + * @param alignment The alignment of the children inside the parent. + */ + @Stable + fun aligned(alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(0.dp, true) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + + /** + * Place children vertically one next to the other and align the obtained group + * according to an [alignment]. + * + * @param alignment The alignment of the children inside the parent. + */ + @Stable + fun aligned(alignment: Alignment.Vertical): Vertical = + SpacedAligned(0.dp, false) { size, _ -> alignment.align(0, size) } + + @Immutable + object Absolute { + /** + * Place children horizontally such that they are as close as possible to the left edge of + * the [Row]. + * + * Unlike [Arrangement.Start], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: 123#### + */ + @Stable + val Left = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeLeftOrTop(sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#Left" + } + + /** + * Place children such that they are as close as possible to the middle of the [Row]. + * + * Unlike [Arrangement.Center], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: ##123## + */ + @Stable + val Center = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeCenter(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#Center" + } + + /** + * Place children horizontally such that they are as close as possible to the right edge of + * the [Row]. + * + * Unlike [Arrangement.End], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: ####123 + */ + @Stable + val Right = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#Right" + } + + /** + * Place children such that they are spaced evenly across the main axis, without free + * space before the first child or after the last child. + * + * Unlike [Arrangement.SpaceBetween], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: 1##2##3 + */ + @Stable + val SpaceBetween = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#SpaceBetween" + } + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child. + * + * Unlike [Arrangement.SpaceEvenly], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: #1#2#3# + */ + @Stable + val SpaceEvenly = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#SpaceEvenly" + } + + /** + * Place children such that they are spaced evenly horizontally, including free + * space before the first child and after the last child, but half the amount of space + * existing otherwise between two consecutive children. + * + * Unlike [Arrangement.SpaceAround], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: #1##2##3##4# + */ + @Stable + val SpaceAround = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#SpaceAround" + } + + /** + * Place children such that each two adjacent ones are spaced by a fixed [space] distance across + * the main axis. The spacing will be subtracted from the available space that the children + * can occupy. + * + * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param space The space between adjacent children. + */ + @Stable + fun spacedBy(space: Dp): HorizontalOrVertical = + SpacedAligned(space, false, null) + + /** + * Place children horizontally such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available width that the children + * can occupy. An [alignment] can be specified to align the spaced children horizontally + * inside the parent, in case there is empty width remaining. + * + * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(space, false) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + + /** + * Place children vertically such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available height that the children + * can occupy. An [alignment] can be specified to align the spaced children vertically + * inside the parent, in case there is empty height remaining. + * + * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = + SpacedAligned(space, false) { size, _ -> alignment.align(0, size) } + + /** + * Place children horizontally one next to the other and align the obtained group + * according to an [alignment]. + * + * Unlike [Arrangement.aligned], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param alignment The alignment of the children inside the parent. + */ + @Stable + fun aligned(alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(0.dp, false) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + } + + /** + * Arrangement with spacing between adjacent children and alignment for the spaced group. + * Should not be instantiated directly, use [spacedBy] instead. + */ + @Immutable + internal data class SpacedAligned( + val space: Dp, + val rtlMirror: Boolean, + val alignment: ((Int, LayoutDirection) -> Int)? + ) : HorizontalOrVertical { + + override val spacing = space + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) { + if (sizes.isEmpty()) return + val spacePx = space + + var occupied = 0 + var lastSpace = 0 + val reversed = rtlMirror && layoutDirection == LayoutDirection.Rtl + sizes.forEachIndexed(reversed) { index, it -> + outPositions[index] = min(occupied, totalSize - it) + lastSpace = min(spacePx, totalSize - outPositions[index] - it) + occupied = outPositions[index] + it + lastSpace + } + occupied -= lastSpace + + if (alignment != null && occupied < totalSize) { + val groupPosition = alignment.invoke(totalSize - occupied, layoutDirection) + for (index in outPositions.indices) { + outPositions[index] += groupPosition + } + } + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = arrange(totalSize, sizes, LayoutDirection.Ltr, outPositions) + + override fun toString() = + "${if (rtlMirror) "" else "Absolute"}Arrangement#spacedAligned($space, $alignment)" + } + + internal fun placeRightOrBottom( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + var current = totalSize - consumedSize + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current + current += it + } + } + + internal fun placeLeftOrTop(size: IntArray, outPosition: IntArray, reverseInput: Boolean) { + var current = 0 + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current + current += it + } + } + + internal fun placeCenter( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + var current = (totalSize - consumedSize).toFloat() / 2 + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + } + } + + internal fun placeSpaceEvenly( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + val gapSize = (totalSize - consumedSize).toFloat() / (size.size + 1) + var current = gapSize + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + gapSize + } + } + + internal fun placeSpaceBetween( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + if (size.isEmpty()) return + + val consumedSize = size.fold(0) { a, b -> a + b } + val noOfGaps = maxOf(size.lastIndex, 1) + val gapSize = (totalSize - consumedSize).toFloat() / noOfGaps + + var current = 0f + if (reverseInput && size.size == 1) { + // If the layout direction is right-to-left and there is only one gap, + // we start current with the gap size. That forces the single item to be right-aligned. + current = gapSize + } + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + gapSize + } + } + + internal fun placeSpaceAround( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + val gapSize = if (size.isNotEmpty()) { + (totalSize - consumedSize).toFloat() / size.size + } else { + 0f + } + var current = gapSize / 2 + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + gapSize + } + } + + private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) { + if (!reversed) { + forEachIndexed(action) + } else { + for (i in (size - 1) downTo 0) { + action(i, get(i)) + } + } + } +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/jetpack/Helpers.kt b/src/main/kotlin/com/mineinabyss/guiy/jetpack/Helpers.kt new file mode 100644 index 0000000..d073069 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/jetpack/Helpers.kt @@ -0,0 +1,5 @@ +package com.mineinabyss.guiy.jetpack + +typealias Dp = Int + +inline val Int.dp: Int get() = this diff --git a/src/main/kotlin/com/mineinabyss/guiy/jetpack/LayoutDirection.kt b/src/main/kotlin/com/mineinabyss/guiy/jetpack/LayoutDirection.kt new file mode 100644 index 0000000..b9a558e --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/jetpack/LayoutDirection.kt @@ -0,0 +1,18 @@ +package com.mineinabyss.guiy.jetpack + +/** + * A class for defining layout directions. + * + * A layout direction can be left-to-right (LTR) or right-to-left (RTL). + */ +enum class LayoutDirection { + /** + * Horizontal layout direction is from Left to Right. + */ + Ltr, + + /** + * Horizontal layout direction is from Right to Left. + */ + Rtl +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/Box.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/Box.kt index 06a50c1..171ab61 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/Box.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/Box.kt @@ -3,7 +3,8 @@ package com.mineinabyss.guiy.layout import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.mineinabyss.guiy.components.state.IntSize -import com.mineinabyss.guiy.layout.alignment.Alignment +import com.mineinabyss.guiy.jetpack.Alignment +import com.mineinabyss.guiy.jetpack.LayoutDirection import com.mineinabyss.guiy.modifiers.Modifier @Composable @@ -20,13 +21,13 @@ fun Box( ) } -private data class BoxMeasurePolicy( +internal data class BoxMeasurePolicy( private val alignment: Alignment, -) : RowLikeMeasurePolicy() { +) : RowColumnMeasurePolicy() { override fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult { return MeasureResult(width, height) { for (child in placeables) { - child.placeAt(alignment.align(child.size, IntSize(width, height))) + child.placeAt(alignment.align(child.size, IntSize(width, height), LayoutDirection.Ltr)) } } } diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt index 1f3b65f..63cfedb 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/Column.kt @@ -2,7 +2,9 @@ package com.mineinabyss.guiy.layout import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.mineinabyss.guiy.layout.alignment.Alignment +import com.mineinabyss.guiy.jetpack.Alignment +import com.mineinabyss.guiy.jetpack.Arrangement +import com.mineinabyss.guiy.jetpack.LayoutDirection import com.mineinabyss.guiy.modifiers.Modifier /** @@ -11,10 +13,16 @@ import com.mineinabyss.guiy.modifiers.Modifier @Composable fun Column( modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, content: @Composable () -> Unit ) { - val measurePolicy = remember(horizontalAlignment) { ColumnMeasurePolicy(horizontalAlignment) } + val measurePolicy = remember(verticalArrangement, horizontalAlignment) { + ColumnMeasurePolicy( + verticalArrangement, + horizontalAlignment + ) + } Layout( measurePolicy, modifier = modifier, @@ -23,13 +31,23 @@ fun Column( } private data class ColumnMeasurePolicy( + private val verticalArrangement: Arrangement.Vertical, private val horizontalAlignment: Alignment.Horizontal, -) : RowLikeMeasurePolicy(sumHeight = true) { +) : RowColumnMeasurePolicy( + sumHeight = true, + arrangementSpacing = verticalArrangement.spacing +) { override fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult { + val positions = IntArray(placeables.size) + verticalArrangement.arrange( + totalSize = height, + sizes = placeables.map { it.height }.toIntArray(), + outPositions = positions + ) return MeasureResult(width, height) { var childY = 0 - for (child in placeables) { - child.placeAt(horizontalAlignment.align(child.height, height), childY) + placeables.forEachIndexed { index, child -> + child.placeAt(horizontalAlignment.align(child.height, height, LayoutDirection.Ltr), positions[index]) childY += child.height } } diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt index 47d5382..b6b3ff0 100644 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/Row.kt @@ -2,7 +2,9 @@ package com.mineinabyss.guiy.layout import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.mineinabyss.guiy.layout.alignment.Alignment +import com.mineinabyss.guiy.jetpack.Alignment +import com.mineinabyss.guiy.jetpack.Arrangement +import com.mineinabyss.guiy.jetpack.LayoutDirection import com.mineinabyss.guiy.modifiers.Modifier /** @@ -11,10 +13,16 @@ import com.mineinabyss.guiy.modifiers.Modifier @Composable fun Row( modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, content: @Composable () -> Unit ) { - val measurePolicy = remember(verticalAlignment) { RowMeasurePolicy(verticalAlignment) } + val measurePolicy = remember(horizontalArrangement, verticalAlignment) { + RowMeasurePolicy( + horizontalArrangement, + verticalAlignment + ) + } Layout( measurePolicy, modifier = modifier, @@ -23,14 +31,23 @@ fun Row( } private data class RowMeasurePolicy( + private val horizontalArrangement: Arrangement.Horizontal, private val verticalAlignment: Alignment.Vertical, -) : RowLikeMeasurePolicy(sumWidth = true) { +) : RowColumnMeasurePolicy( + sumWidth = true, + arrangementSpacing = horizontalArrangement.spacing +) { override fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult { + val positions = IntArray(placeables.size) + horizontalArrangement.arrange( + totalSize = width, + sizes = placeables.map { it.width }.toIntArray(), + layoutDirection = LayoutDirection.Ltr, + outPositions = positions + ) return MeasureResult(width, height) { - var childX = 0 - for (child in placeables) { - child.placeAt(childX, verticalAlignment.align(child.height, height)) - childX += child.width + placeables.forEachIndexed { index, child -> + child.placeAt(positions[index], verticalAlignment.align(child.height, height)) } } } diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/RowColumnMeasurePolicy.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/RowColumnMeasurePolicy.kt new file mode 100644 index 0000000..c615f03 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/guiy/layout/RowColumnMeasurePolicy.kt @@ -0,0 +1,33 @@ +package com.mineinabyss.guiy.layout + +import com.mineinabyss.guiy.modifiers.Constraints +import kotlin.math.max + +abstract class RowColumnMeasurePolicy( + val sumWidth: Boolean = false, + val sumHeight: Boolean = false, + val arrangementSpacing: Int = 0, +) : MeasurePolicy { + override fun measure(measurables: List, constraints: Constraints): MeasureResult { + var remainingConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val placeables = measurables.map { element -> + val measured = element.measure(remainingConstraints) + remainingConstraints = remainingConstraints.copy( + maxWidth = if (sumWidth) (remainingConstraints.maxWidth - measured.width).coerceAtLeast(0) else remainingConstraints.maxWidth, + maxHeight = if (sumHeight) (remainingConstraints.maxHeight - measured.height).coerceAtLeast(0) else remainingConstraints.maxHeight, + ) + measured + } + val extraSpacing = (arrangementSpacing * (placeables.size - 1)).coerceAtLeast(0) + val width = if (sumWidth) { + placeables.sumOf { it.width } + extraSpacing + } else placeables.maxOfOrNull { it.width } ?: 0 + + val height = if (sumHeight) { + placeables.sumOf { it.height } + extraSpacing + } else placeables.maxOfOrNull { it.height } ?: 0 + return placeChildren(placeables, max(width, constraints.minWidth), max(height, constraints.minHeight)) + } + + abstract fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult +} diff --git a/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt b/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt deleted file mode 100644 index 4f3a984..0000000 --- a/src/main/kotlin/com/mineinabyss/guiy/layout/RowLikeMeasurePolicy.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.mineinabyss.guiy.layout - -import com.mineinabyss.guiy.modifiers.Constraints -import kotlin.math.max - -abstract class RowLikeMeasurePolicy( - val sumWidth: Boolean = false, - val sumHeight: Boolean = false, -) : MeasurePolicy { - override fun measure(measurables: List, constraints: Constraints): MeasureResult { - val noMinConstraints = constraints.copy(minWidth = 0, minHeight = 0) - val placeables = measurables.map { it.measure(noMinConstraints) } - val width = if (sumWidth) placeables.sumOf { it.width } else placeables.maxOfOrNull { it.width } ?: 0 - val height = if (sumHeight) placeables.sumOf { it.height } else placeables.maxOfOrNull { it.height } ?: 0 - return placeChildren(placeables, max(width, constraints.minWidth), max(height, constraints.minHeight)) - } - - abstract fun placeChildren(placeables: List, width: Int, height: Int): MeasureResult -} From cb814801d4c7c15f38d7a4a3156ab55879238bdf Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Mon, 3 Jun 2024 15:14:39 -0400 Subject: [PATCH 7/8] chore: Update readme --- README.md | 49 ++++++++++++++++++++++++++--------------------- gradle.properties | 2 +- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8273ec8..2b4dfff 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,23 @@ # Guiy -[![Java CI with Gradle](https://github.com/MineInAbyss/guiy-compose/actions/workflows/gradle-ci.yml/badge.svg)](https://github.com/MineInAbyss/guiy-compose/actions/workflows/gradle-ci.yml) [![Maven](https://img.shields.io/maven-metadata/v?metadataUrl=https://repo.mineinabyss.com/releases/com/mineinabyss/guiy-compose/maven-metadata.xml)](https://repo.mineinabyss.com/#/releases/com/mineinabyss/guiy-compose) [![Contribute](https://shields.io/badge/Contribute-e57be5?logo=github%20sponsors&style=flat&logoColor=white)](https://wiki.mineinabyss.com/contribute/) -A Spigot/PaperMC UI library built on the [Jetpack Compose](https://developer.android.com/jetpack/compose) compiler. +A Minecraft UI library for PaperMC, built on the [Jetpack Compose](https://developer.android.com/jetpack/compose) +compiler. -If you are new to Compose, please read the link above. In short, it is a declarative UI library that makes working with -state nice, gives easy access to coroutines, and helps write complex UI faster. +Compose is commonly used in Android (and cross-platform) development, so many resources are available online to learn +the basics. It makes it much easier to work with changing UI state and has built in support for Kotlin Coroutines. -## Beta status - -We can't promise api stability yet, for the most part none of the existing elements should ever break entirely, but we -may change some behaviour like how `Grid` organizes itself. +> [!NOTE] +> Guiy is in active development as we continue to try new use-cases in our plugins. We can't promise api stability yet, +> but try to follow semver for breaking changes. ## Examples -See the `guiy-example` package for a full demonstration, below are snippets. +See the `guiy-example` package for a full demonstration of project setup and different features. ### Entry @@ -65,23 +64,31 @@ We use a similar modifier system to Jetpack Compose. ```kotlin // Entry to modifiers, though you are encouraged to pass a modifier parameter into your composables. Modifier - // Set the size of an element (can use min/max constraints too) - .size(width = 2, height = 2) - // Place at an absolute offset - .at(x = 1, y = 5) + // Set the width of an element (can use min/max constraints, or .size to set width and height) + .width(3) + // Fill based on parent constraints like Jetpack + .fillmaxHeight() + // Padding in # of blocks + .padding(vertical = 1) + // Place at an offset + .offset(x = 1, y = 5) // Do actions on click .clickable { doSomething() } ``` ### Alignment +Guiy provides Row, Column, and Box components based on Jetpack's, these come with Arrangement and Alignment too. We also +provide our components like Vertical/Horizontal Grids optimized for common Minecraft uses. + ```kotlin -// A horizontal group of 10 items -Row { - repeat(10) { +// A horizontal row of 10 items, with 1 space between each. +Row(horizontalArrangement = Arrangement.spacedBy(1)) { + repeat(4) { Item(...) } } + // Same but vertical Column { ... } @@ -91,8 +98,8 @@ Column { Row { ... } } -// Items aligned left to right, top to bottom, wrapped to be smaller than width. -Grid(Modifier.width(3)) { +// Items aligned left to right, top to bottom, wrapped to be smaller than width, useful for pages of items! +VerticalGrid(Modifier.width(3)) { repeat(7) { Item(...) } @@ -164,15 +171,13 @@ pluginManagement { ### Server setup -Guiy does not package the Kotlin runtime in itself, it uses our library idofront to load shared dependencies in an -isolated way. +Guiy does not package the Kotlin runtime, it depends on our helper plugin Idofront using Paper's isolated dependency +system. - [Download](https://github.com/MineInAbyss/guiy-compose/releases/latest) and install Guiy into your plugin folder. - [Download](https://github.com/MineInAbyss/Idofront/releases/latest) Idofront, a required dependency. - Depend on Guiy in a [paper-plugin](https://docs.papermc.io/paper/dev/getting-started/paper-plugins), this will give you access to Guiy and any libraries in Idofront in an isolated manner. -There is currently no support for shading guiy. - ## Thanks - Google for creating Jetpack Compose. diff --git a/gradle.properties b/gradle.properties index 718a0e2..4f3e6f8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official group=com.mineinabyss -version=0.11 +version=0.12 idofrontVersion=0.24.1 From 9e31ebd8e8e1be1e3615b7bd1727cbbaa2cd080a Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Mon, 3 Jun 2024 15:20:33 -0400 Subject: [PATCH 8/8] fix: Fix building PRs with java 17 --- .github/workflows/gradle-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle-ci.yml b/.github/workflows/gradle-ci.yml index 5cf235f..4ac50fe 100755 --- a/.github/workflows/gradle-ci.yml +++ b/.github/workflows/gradle-ci.yml @@ -17,7 +17,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: temurin - java-version: 17 + java-version: 21 cache: gradle - name: Build