diff --git a/docs/GUI_FRAMEWORK.md b/docs/GUI_FRAMEWORK.md new file mode 100644 index 000000000..5bee25edd --- /dev/null +++ b/docs/GUI_FRAMEWORK.md @@ -0,0 +1,366 @@ +# SURF-API GUI Framework + +A modern, React-inspired GUI framework for Minecraft servers, providing component-based architecture with reactive props and lifecycle management. + +## Overview + +The SURF-API GUI framework offers: + +- **Component-Based Architecture**: Build GUIs using reusable, composable components +- **Reactive Props**: Manage state with mutable, immutable, computed, and lazy props +- **React-Like Lifecycle**: Components support `onMount`, `onUnmount`, `onUpdate` hooks +- **Refs System**: Reference and update components programmatically +- **View Navigation**: Navigate between parent/child views with state preservation +- **Update Intervals**: Automatic component updates at configurable intervals + +## Core Concepts + +### Props + +Props are the state management system for GUIs. They come in several flavors: + +#### Prop Types + +- **ImmutableProp**: Static values that never change +- **MutableProp**: Dynamic values that can be updated +- **ComputedProp**: Values computed from a callback function +- **LazyProp**: Values loaded on first access +- **PaginationProp**: Built-in pagination support for lists + +#### Prop Scopes + +- **GLOBAL**: Shared across all viewers +- **VIEWER**: Isolated per viewer + +```kotlin +val props = props { + immutable("title", "My GUI", PropScope.GLOBAL) + mutable("clickCount", 0, PropScope.VIEWER) + computed("displayText", PropScope.VIEWER) { context -> + "Clicks: ${mutableProp.get(context)}" + } + lazy("expensiveData", scope = PropScope.VIEWER) { context -> + loadExpensiveData() + } + pagination("items", pageSize = 9) { + listOf(Material.DIAMOND, Material.GOLD_INGOT, ...) + } +} +``` + +### Components + +Components are the building blocks of GUIs. They render ItemStacks and handle user interactions. + +```kotlin +val component = dynamicComponent( + renderer = { context -> + buildItem(ItemType.DIAMOND) { + displayName { text("Click me!") } + } + } +) { + onClick = { + player.sendMessage("You clicked!") + update() // Re-render this component + } + + onMount = { + // Called when component is added to view + } + + onUpdate = { + // Called when component updates + } + + updateInterval = 5.seconds // Auto-update every 5 seconds +} +``` + +### Views + +Views are the containers that manage components and handle GUI lifecycle. + +```kotlin +object MyGuiView : BukkitGuiView() { + override fun onInit(config: ViewConfig) { + config.title = text("My GUI") + config.size = 54 // 6 rows + config.cancelOnClick = true + } + + override fun onFirstRender(context: RenderContext) { + // Place components in slots + context.slot(0, myComponent) + context.slot(1, anotherComponent) + } + + override fun onOpen(context: ViewContext) { + // Called when GUI opens for a player + } + + override fun onClose(context: ViewContext) { + // Called when GUI closes + } + + override fun onUpdate(context: ViewContext) { + // Called when view updates + } + + override fun onResume(context: ResumeContext) { + // Called when returning from child view + val origin = context.origin // The view we came from + } +} +``` + +### Refs + +Refs allow components to reference and update other components, enabling component communication. + +```kotlin +val counterRef = createRef() +val clickCountProp = MutableProp("count", 0, PropScope.VIEWER) + +// Counter display component +val counterDisplay = dynamicComponent( + renderer = { context -> + val count = clickCountProp.get(context.propContext) + buildItem(ItemType.GOLD_BLOCK) { + displayName { text("Count: $count") } + } + } +) { + ref = counterRef // Attach ref +} + +// Button that updates the counter +val incrementButton = component( + item = buildItem(ItemType.EMERALD) { + displayName { text("Increment") } + } +) { + onClick = { + val current = clickCountProp.get(propContext) + clickCountProp.set(propContext, current + 1) + counterRef.update() // Update the counter display + } +} +``` + +### Context + +All user actions and lifecycle events receive a context object with: + +- **player**: The player interacting with the GUI +- **view**: The current view +- **propContext**: For accessing props +- **Navigation methods**: `navigateTo()`, `navigateBack()`, `close()` +- **update()**: Re-render the view + +```kotlin +onClick = { context -> + val count = context.getProp(myProp) + context.player.sendMessage("Count: $count") + context.navigateTo(childView) + context.update() +} +``` + +## Navigation + +Views can have parent-child relationships for navigation: + +```kotlin +object ParentView : BukkitGuiView() { + override fun onFirstRender(context: RenderContext) { + context.slot(0, component( + item = buildItem(ItemType.COMPASS) { + displayName { text("Go to Child") } + } + ) { + onClick = { + navigateTo(ChildView, passProps = true) + } + }) + } +} + +object ChildView : BukkitGuiView() { + override fun onFirstRender(context: RenderContext) { + context.slot(0, component( + item = buildItem(ItemType.ARROW) { + displayName { text("Back") } + } + ) { + onClick = { + navigateBack() // Returns to ParentView + } + }) + } +} +``` + +## Lifecycle + +Components and views follow a React-like lifecycle: + +### Component Lifecycle +1. **onMount**: Component added to view +2. **render**: Component rendered to ItemStack +3. **onUpdate**: Component updated (manual or interval) +4. **onClick**: User clicks component +5. **onUnmount**: Component removed from view + +### View Lifecycle +1. **onInit**: View configuration initialized (once) +2. **onOpen**: View opened for player +3. **onFirstRender**: First render for player +4. **onUpdate**: View updated +5. **onResume**: Returning from child view +6. **onClose**: View closed + +## Complete Example + +```kotlin +object ShopView : BukkitGuiView() { + private val propsMap = props { + mutable("coins", 100, PropScope.VIEWER) + pagination("items", pageSize = 9) { + listOf(Material.DIAMOND, Material.GOLD_INGOT, Material.IRON_INGOT) + } + } + + private val coinsProp = propsMap["coins"] as MutableProp + private val itemsProp = propsMap["items"] as PaginationProp + private val coinsRef = createRef() + + override fun onInit(config: ViewConfig) { + config.title = text("Shop", NamedTextColor.GOLD) + config.size = 54 + } + + override fun onFirstRender(context: RenderContext) { + // Coins display + val coinsDisplay = dynamicComponent( + renderer = { ctx -> + val coins = coinsProp.get(ctx.propContext) + buildItem(ItemType.GOLD_INGOT) { + displayName { text("Coins: $coins", NamedTextColor.YELLOW) } + } + } + ) { + ref = coinsRef + } + context.slot(4, coinsDisplay) + + // Items + val pagination = itemsProp.get(context.propContext) + pagination.items.forEachIndexed { index, material -> + context.slot(9 + index, component( + item = ItemStack.of(material) + ) { + onClick = { + val coins = coinsProp.get(propContext) + if (coins >= 10) { + coinsProp.set(propContext, coins - 10) + player.inventory.addItem(ItemStack.of(material)) + coinsRef.update() + } + } + }) + } + + // Navigation buttons + if (pagination.hasPreviousPage) { + context.slot(45, component( + item = buildItem(ItemType.ARROW) { + displayName { text("Previous") } + } + ) { + onClick = { + itemsProp.previousPage(propContext) + update() + } + }) + } + + if (pagination.hasNextPage) { + context.slot(53, component( + item = buildItem(ItemType.ARROW) { + displayName { text("Next") } + } + ) { + onClick = { + itemsProp.nextPage(propContext) + update() + } + }) + } + } +} + +// Open the view +fun openShop(player: Player) { + ShopView.open(player) +} +``` + +## Opening Views + +```kotlin +// Open a view for a player +MyGuiView.open(player) + +// In a command +class OpenGuiCommand : CommandAPICommand("opengui") { + init { + playerExecutor { player, _ -> + MyGuiView.open(player) + } + } +} +``` + +## Best Practices + +1. **Use Props for State**: Don't store state in variables; use props for proper viewer isolation +2. **Refs for Communication**: Use refs when components need to update each other +3. **Computed Props for Derived State**: Use computed props instead of storing derived values +4. **Lifecycle Hooks**: Use lifecycle hooks for initialization and cleanup +5. **Update Intervals**: Use sparingly; prefer manual updates via refs +6. **Navigation**: Use parent-child relationships for related views + +## Migration from Old Framework + +The new framework replaces the old inventory-framework-based system. Key differences: + +### Old Way (inventory-framework) +```kotlin +object OldView : View() { + override fun onInit(config: ViewConfigBuilder) { + config.title("My GUI") + } + + override fun onFirstRender(render: RenderContext) { + render.slot(0, item).onClick { /* ... */ } + } +} +``` + +### New Way (SURF-API GUI) +```kotlin +object NewView : BukkitGuiView() { + override fun onInit(config: ViewConfig) { + config.title = text("My GUI") + } + + override fun onFirstRender(context: RenderContext) { + context.slot(0, component(item) { + onClick = { /* ... */ } + }) + } +} +``` + +The new framework provides more control, better type safety, and React-like patterns familiar to modern developers. diff --git a/gradle.properties b/gradle.properties index 690b873b6..1d392cd53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.56.0 +version=1.21.11-2.57.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false diff --git a/settings.gradle.kts b/settings.gradle.kts index c5d1fb12f..5845629f3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,8 +17,8 @@ include(":surf-api-core:surf-api-core-server") include(":surf-api-bukkit:surf-api-bukkit-api") include(":surf-api-bukkit:surf-api-bukkit-server") -include(":surf-api-hytale:surf-api-hytale-api") -include(":surf-api-hytale:surf-api-hytale-server") +//include(":surf-api-hytale:surf-api-hytale-api") +//include(":surf-api-hytale:surf-api-hytale-server") include(":surf-api-velocity:surf-api-velocity-api") include(":surf-api-velocity:surf-api-velocity-server") diff --git a/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts index ab4ec967c..7ad89b181 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts +++ b/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { compileOnlyApi(libs.reflection.remapper) compileOnlyApi(libs.more.persistent.data.types) compileOnlyApi(libs.stefvanschie.`if`) - api(libs.bundles.inventory.framework) api(libs.commandapi.bukkit.kotlin) compileOnlyApi(libs.mccoroutine.folia.api) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/GuiItem.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/GuiItem.kt new file mode 100644 index 000000000..3049348e4 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/GuiItem.kt @@ -0,0 +1,75 @@ +package dev.slne.surf.surfapi.bukkit.api.gui + +import org.bukkit.Material +import org.bukkit.inventory.ItemStack + +/** + * Represents an item in a GUI with utility functions. + * Wraps an ItemStack and provides additional functionality. + */ +class GuiItem( + val itemStack: ItemStack +) { + + /** + * Get the material of this item. + */ + val material: Material + get() = itemStack.type + + /** + * Get the amount of this item. + */ + val amount: Int + get() = itemStack.amount + + /** + * Check if this item is empty (AIR). + */ + val isEmpty: Boolean + get() = itemStack.type == Material.AIR || itemStack.amount == 0 + + /** + * Create a copy of this GuiItem. + */ + fun copy(): GuiItem = GuiItem(itemStack.clone()) + + /** + * Create a copy with modified properties. + */ + fun copyWith( + material: Material? = null, + amount: Int? = null + ): GuiItem { + val newStack = itemStack.clone() + material?.let { newStack.type = it } + amount?.let { newStack.amount = it } + return GuiItem(newStack) + } + + override fun toString(): String { + return "GuiItem(itemStack=$itemStack, material=$material, amount=$amount, isEmpty=$isEmpty)" + } + + companion object { + /** + * Create a GuiItem from an ItemStack. + */ + fun of(itemStack: ItemStack): GuiItem = GuiItem(itemStack) + + /** + * Create an empty GuiItem (AIR). + */ + fun empty(): GuiItem = GuiItem(ItemStack(Material.AIR)) + } +} + +/** + * Extension function to convert ItemStack to GuiItem. + */ +fun ItemStack.toGuiItem(): GuiItem = GuiItem.of(this) + +/** + * Extension function to convert GuiItem to ItemStack. + */ +fun GuiItem.toItemStack(): ItemStack = this.itemStack diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/Slot.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/Slot.kt new file mode 100644 index 000000000..f41531cc1 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/Slot.kt @@ -0,0 +1,46 @@ +package dev.slne.surf.surfapi.bukkit.api.gui + +/** + * Represents a slot in a GUI inventory. + * Can be created from either a linear index or x,y coordinates. + */ +data class Slot(val index: Int) { + /** + * Row (y coordinate, 0-based). + */ + val row: Int + get() = index / 9 + + /** + * Column (x coordinate, 0-based). + */ + val column: Int + get() = index % 9 + + /** + * Create a slot from x,y coordinates. + */ + constructor(column: Int, row: Int) : this(row * 9 + column) + + /** + * Convert to coordinates (column, row). + */ + fun toCoordinates(): Pair = column to row + + companion object { + /** + * Create a slot from coordinates. + */ + fun at(column: Int, row: Int): Slot = Slot(column, row) + + /** + * Create a slot from an index. + */ + fun of(index: Int): Slot = Slot(index) + } + + override fun toString(): String = "Slot(index=$index, column=$column, row=$row)" +} + +fun slot(index: Int): Slot = Slot.of(index) +fun slot(column: Int, row: Int): Slot = Slot.at(column, row) \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CircularArea.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CircularArea.kt new file mode 100644 index 000000000..624ef979f --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CircularArea.kt @@ -0,0 +1,51 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf +import it.unimi.dsi.fastutil.objects.ObjectSet +import kotlin.math.ceil +import kotlin.math.sqrt + +/** + * A circular area defined by a center point and radius. + * Useful for radial menus, circular patterns, or highlighting areas. + */ +data class CircularArea( + val center: Slot, + val radius: Double +) : ComponentArea { + override fun slots(): ObjectSet { + val slots = mutableObjectSetOf() + val radiusCeil = ceil(radius).toInt() + + // Check all slots within the bounding box + for (row in (center.row - radiusCeil)..(center.row + radiusCeil)) { + for (col in (center.column - radiusCeil)..(center.column + radiusCeil)) { + val slot = Slot.at(col, row) + if (contains(slot)) { + slots.add(slot) + } + } + } + + return slots + } + + override fun contains(slot: Slot): Boolean { + val dx = (slot.column - center.column).toDouble() + val dy = (slot.row - center.row).toDouble() + val distance = sqrt(dx * dx + dy * dy) + + return distance <= radius + } + + override fun toString(): String { + return "CircularArea(center=$center, radius=$radius, width=$width, height=$height)" + } + + override val width: Int + get() = (ceil(radius) * 2 + 1).toInt() + + override val height: Int + get() = (ceil(radius) * 2 + 1).toInt() +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/ComponentArea.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/ComponentArea.kt new file mode 100644 index 000000000..087fbd9e1 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/ComponentArea.kt @@ -0,0 +1,44 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import it.unimi.dsi.fastutil.objects.ObjectSet + +/** + * Defines the area that a component occupies in a GUI. + * Components can have different shaped areas (cuboid, circular, custom). + */ +interface ComponentArea { + /** + * Get all slots that are part of this area. + */ + fun slots(): ObjectSet + + /** + * Check if a slot is within this area. + */ + fun contains(slot: Slot): Boolean + + /** + * Width of the bounding box containing this area. + */ + val width: Int + + /** + * Height of the bounding box containing this area. + */ + val height: Int + + /** + * Get the first slot in this area based on index. + * + * @return The slot with the lowest index, or null if area is empty. + */ + fun first() = slots().minBy { it.index } + + /** + * Get the last slot in this area based on index. + * + * @return The slot with the highest index, or null if area is empty. + */ + fun last(): Slot = slots().maxBy { it.index } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CuboidArea.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CuboidArea.kt new file mode 100644 index 000000000..e77eea455 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CuboidArea.kt @@ -0,0 +1,43 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf +import it.unimi.dsi.fastutil.objects.ObjectSet + +/** + * A rectangular (cuboid) area defined by start and end slots. + * This is the most common area type for panels, grids, and paginated content. + */ +data class CuboidArea( + val startSlot: Slot, + val endSlot: Slot +) : ComponentArea { + override fun slots(): ObjectSet { + val slots = mutableObjectSetOf() + + for (row in startSlot.row..endSlot.row) { + for (col in startSlot.column..endSlot.column) { + slots.add(Slot.at(col, row)) + } + } + + return slots + } + + override fun contains(slot: Slot): Boolean { + return slot.column >= startSlot.column && + slot.column <= endSlot.column && + slot.row >= startSlot.row && + slot.row <= endSlot.row + } + + override fun toString(): String { + return "CuboidArea(startSlot=$startSlot, endSlot=$endSlot, width=$width, height=$height)" + } + + override val width: Int + get() = endSlot.column - startSlot.column + 1 + + override val height: Int + get() = endSlot.row - startSlot.row + 1 +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/SingleSlotArea.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/SingleSlotArea.kt new file mode 100644 index 000000000..f5e33c542 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/SingleSlotArea.kt @@ -0,0 +1,24 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.core.api.util.objectSetOf +import it.unimi.dsi.fastutil.objects.ObjectSet + +/** + * An area consisting of a single slot. + * Used for simple components like buttons or single items. + */ +data class SingleSlotArea( + val slot: Slot +) : ComponentArea { + override fun slots(): ObjectSet = objectSetOf(slot) + + override fun contains(slot: Slot): Boolean = this.slot == slot + + override fun toString(): String { + return "SingleSlotArea(slot=$slot, width=$width, height=$height)" + } + + override val width: Int = 1 + override val height: Int = 1 +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/Component.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/Component.kt new file mode 100644 index 000000000..b53a9efc2 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/Component.kt @@ -0,0 +1,195 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.area.ComponentArea +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleEventType +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.core.api.util.freeze +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import it.unimi.dsi.fastutil.objects.ObjectList +import org.bukkit.entity.Player +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Base interface for all GUI components. + * Components follow React-like lifecycle principles. + */ +abstract class Component { + /** + * The area this component occupies in the GUI. + * Can be any shape (cuboid, circular, custom). + */ + abstract val area: ComponentArea + + /** + * Priority for handling clicks and rendering when components overlap. + * Higher priority components are rendered on top and handle clicks first. + */ + abstract val priority: ComponentPriority + + /** + * Width of the component's bounding box. + * Delegates to the area's width. + */ + val width: Int + get() = area.width + + /** + * Height of the component's bounding box. + * Delegates to the area's height. + */ + val height: Int + get() = area.height + + /** + * Check if a slot is within this component's area. + * Delegates to the area's contains method. + */ + fun contains(slot: Slot): Boolean = area.contains(slot) + + /** + * The parent component, if any. + */ + var parent: Component? = null + internal set + + /** + * Whether to cancel click events on this component. + */ + open var cancelOnClick: Boolean = true + + private val firstRenderPerPlayer = ConcurrentHashMap.newKeySet() + + fun renderFirstRenderPerPlayer(player: Player): Boolean { + if (!firstRenderPerPlayer.add(player.uniqueId)) return false + + view?.let { + onFirstRender( + it.createLifecycleContext( + player, + LifecycleEventType.FIRST_RENDER + ) + ) + } + + return true + } + + fun hasFirstRender(player: Player): Boolean = firstRenderPerPlayer.contains(player.uniqueId) + + /** + * The children of this component. + */ + private val _children = mutableObjectListOf() + val children: ObjectList get() = _children.freeze() + + /** + * The view this component belongs to. + */ + var view: GuiView? = null + internal set(value) { + field = value + // Propagate view to existing children + _children.forEach { child -> + child.view = value + } + } + + /** + * Whether this component is hidden (not rendered at all). + */ + open var hidden: Boolean = false + + /** + * Whether this component is disabled (rendered but onClick is blocked). + */ + open var disabled: Boolean = false + + /** + * Props accessible by this component. + * Children can access parent props. + */ + protected open val props: ObjectList> = mutableObjectListOf() + + /** + * Ref attached to this component, if any. + */ + internal var attachedRef: Ref? = null + + open fun initComponent(context: LifecycleContext) {} + + /** + * Called when the component is rendered for the first time. + */ + open fun onFirstRender(context: LifecycleContext) {} + + /** + * Called when the component is updated. + */ + open fun onUpdate(context: LifecycleContext) {} + + /** + * Called when the component is clicked. + */ + open fun onClick(context: ClickContext) {} + + /** + * Renders the component to a GuiItem. + * For container components, return null. + */ + open fun render(context: ViewContext): GuiItem? = null + + /** + * For container components, render multiple items at specific slots. + * Returns a map of Slot to GuiItem. + */ + open fun renderSlots(context: ViewContext): Map = emptyMap() + + /** + * Add a child component. + */ + fun addChild(child: Component) { + child.parent = this + child.view = view + + _children.add(child) + } + + /** + * Remove a child component. + */ + fun removeChild(child: Component) { + _children.remove(child) + child.parent = null + } + + /** + * Get all props including parent props. + */ + fun getAllProps(): ObjectList> { + val allProps = mutableObjectListOf>() + + parent?.getAllProps()?.let { allProps.addAll(it) } + allProps.addAll(props) + + return allProps + } + + /** + * Trigger an update of this component. + */ + fun update(viewer: Player? = null) { + view?.updateComponent(this, viewer) + } + + override fun toString(): String { + return "Component(area=$area, priority=$priority, width=$width, height=$height, children=$children, hidden=$hidden, disabled=$disabled)" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ComponentPriority.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ComponentPriority.kt new file mode 100644 index 000000000..f06f2464f --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ComponentPriority.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component + +/** + * Priority levels for components. + * When multiple components overlap at the same slot, the one with highest priority + * is rendered and handles click events. + */ +enum class ComponentPriority(val value: Int) { + /** + * Lowest priority - background elements. + */ + LOWEST(0), + + /** + * Low priority. + */ + LOW(25), + + /** + * Normal priority - default for most components. + */ + NORMAL(50), + + /** + * High priority. + */ + HIGH(75), + + /** + * Highest priority - overlay elements. + */ + HIGHEST(100); + + companion object { + /** + * Get priority by value, defaults to NORMAL. + */ + fun fromValue(value: Int): ComponentPriority { + return entries.firstOrNull { it.value == value } ?: NORMAL + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ContainerComponent.kt new file mode 100644 index 000000000..2ee37645a --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ContainerComponent.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component.components + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.area.ComponentArea +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import it.unimi.dsi.fastutil.objects.Object2ObjectMap + +/** + * Container component that renders multiple items at specific slots. + * This is useful for paginated components, panels, or complex layouts. + * The area can be any shape (cuboid, circular, custom). + */ +abstract class ContainerComponent( + override val area: ComponentArea, + override val priority: ComponentPriority = ComponentPriority.NORMAL +) : Component() { + /** + * Render multiple items at their respective slots. + * Override this to provide the slot-to-item mapping. + */ + abstract override fun renderSlots(context: ViewContext): Object2ObjectMap + + /** + * Container doesn't render a single item. + */ + final override fun render(context: ViewContext): GuiItem? = null + + override fun toString(): String { + return "ContainerComponent(area=$area, priority=$priority) ${super.toString()}" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/DynamicComponent.kt new file mode 100644 index 000000000..26c120f44 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/DynamicComponent.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component.components + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.area.ComponentArea +import dev.slne.surf.surfapi.bukkit.api.gui.area.SingleSlotArea +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext + +/** + * Dynamic component that renders based on a callback at a single slot. + */ +open class DynamicComponent( + slot: Slot, + private val renderer: (ViewContext) -> GuiItem?, + override val priority: ComponentPriority = ComponentPriority.NORMAL, + private val clickHandler: (ClickContext.() -> Unit)? = null +) : Component() { + override val area: ComponentArea = SingleSlotArea(slot) + + override fun render(context: ViewContext): GuiItem? = renderer(context) + + override fun onClick(context: ClickContext) { + clickHandler?.invoke(context) + } + + override fun toString(): String { + return "DynamicComponent(renderer=$renderer, priority=$priority, clickHandler=$clickHandler, area=$area) ${super.toString()}" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ItemComponent.kt new file mode 100644 index 000000000..e5bd208a1 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ItemComponent.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component.components + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.area.ComponentArea +import dev.slne.surf.surfapi.bukkit.api.gui.area.SingleSlotArea +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext + +/** + * Simple item component that renders a static item at a single slot. + */ +open class ItemComponent( + slot: Slot, + private val item: GuiItem, + override val priority: ComponentPriority = ComponentPriority.NORMAL, + private val clickHandler: (ClickContext.() -> Unit)? = null +) : Component() { + override val area: ComponentArea = SingleSlotArea(slot) + + override fun render(context: ViewContext): GuiItem = item + + override fun onClick(context: ClickContext) { + clickHandler?.invoke(context) + } + + override fun toString(): String { + return "ItemComponent(item=$item, priority=$priority, clickHandler=$clickHandler, area=$area) ${super.toString()}" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/PaginationComponent.kt new file mode 100644 index 000000000..9c0d4d90e --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/PaginationComponent.kt @@ -0,0 +1,322 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component.components + +import dev.slne.surf.surfapi.bukkit.api.builder.ItemStack +import dev.slne.surf.surfapi.bukkit.api.builder.buildLore +import dev.slne.surf.surfapi.bukkit.api.builder.displayName +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.area.ComponentArea +import dev.slne.surf.surfapi.bukkit.api.gui.area.CuboidArea +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.component +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.dynamicComponent +import dev.slne.surf.surfapi.bukkit.api.gui.props.ViewerProp +import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf +import dev.slne.surf.surfapi.core.api.util.objectListOf +import dev.slne.surf.surfapi.core.api.util.toObjectList +import it.unimi.dsi.fastutil.objects.Object2ObjectMap +import it.unimi.dsi.fastutil.objects.ObjectList +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * Component for paginated content with built-in navigation buttons. + * Renders multiple items across specified slots based on current page. + * The last row is reserved for navigation buttons (previous, page indicator, next). + * + * Minimum dimensions: width ≥ 3, height ≥ 2 + */ +class PaginationComponent( + startSlot: Slot, + endSlot: Slot, + private val items: () -> ObjectList, + private val itemRenderer: (T, ViewContext) -> GuiItem?, + override val priority: ComponentPriority = ComponentPriority.NORMAL, + private val onItemClick: ((T, ClickContext) -> Unit)? = null, + private val previousButtonSlot: Slot? = null, + private val nextButtonSlot: Slot? = null, + private val pageIndicatorSlot: Slot? = null, + override val area: ComponentArea = CuboidArea(startSlot, endSlot) +) : ContainerComponent(area, priority) { + private val currentPages = ViewerProp.Mutable("pagination_current_page", 0) + + init { + require(area.width >= 3) { "PaginationComponent width must be at least 3 (current: ${area.width})" } + require(area.height >= 2) { "PaginationComponent height must be at least 2 (current: ${area.height})" } + + props.add(currentPages) + } + + /** + * Start slot of the area (convenience accessor). + */ + private val startSlot: Slot + get() = area.first() + + /** + * Calculate page size from the area. + * Items use height - 1 rows (last row is for buttons). + */ + private val itemsHeight: Int = height - 1 + private val pageSize: Int = width * itemsHeight + + /** + * Calculated button slots (centered in last row by default). + */ + private val calculatedPreviousButtonSlot: Slot + get() = previousButtonSlot ?: run { + val lastRowY = startSlot.row + height - 1 + val centerX = startSlot.column + (width - 3) / 2 + + Slot.at(centerX, lastRowY) + } + + private val calculatedPageIndicatorSlot: Slot + get() = pageIndicatorSlot ?: run { + val lastRowY = startSlot.row + height - 1 + val centerX = startSlot.column + (width - 3) / 2 + 1 + + Slot.at(centerX, lastRowY) + } + + private val calculatedNextButtonSlot: Slot + get() = nextButtonSlot ?: run { + val lastRowY = startSlot.row + height - 1 + val centerX = startSlot.column + (width - 3) / 2 + 2 + + Slot.at(centerX, lastRowY) + } + + private val previousButtonComponent = createPreviousButtonComponent() + private val nextButtonComponent = createNextButtonComponent() + private val pageIndicatorComponent = createPageIndicatorComponent() + + init { + addChild(previousButtonComponent) + addChild(pageIndicatorComponent) + addChild(nextButtonComponent) + } + + private fun createPreviousButtonComponent() = component( + slot = calculatedPreviousButtonSlot, + item = GuiItem(ItemStack(Material.ARROW) { + displayName { info("Previous Page") } + buildLore { + line { gray("Click to go to the previous page") } + } + }), + ) { + this.cancelOnClick = true + + initComponent = { + if (!hasPreviousPage(player)) { + disabled = true + hidden = true + } + } + + onClick = { + previousPage(player) + } + } + + private fun createPageIndicatorComponent() = dynamicComponent( + slot = calculatedPageIndicatorSlot, + renderer = { ctx -> + val currentPage = getCurrentPage(ctx.player) + 1 + val totalPages = getTotalPages() + + GuiItem(ItemStack(Material.PAPER) { + displayName { info("Page $currentPage") } + buildLore { + line { gray("Page $currentPage of $totalPages") } + } + }) + }, + ) { + this.cancelOnClick = true + } + + private fun createNextButtonComponent() = component( + slot = calculatedNextButtonSlot, + item = GuiItem(ItemStack(Material.ARROW) { + displayName { info("Next Page") } + buildLore { + line { gray("Click to go to the next page") } + } + }), + ) { + this.cancelOnClick = true + + initComponent = { + if (!hasNextPage(player)) { + disabled = true + hidden = true + } + } + onClick = { + nextPage(player) + } + } + + /** + * Get the current page for a viewer. + */ + fun getCurrentPage(viewer: Player): Int { + return currentPages.getOrDefault(viewer, 0) + } + + /** + * Get the total number of pages. + */ + fun getTotalPages(): Int { + val allItems = items() + + return (allItems.size + pageSize - 1) / pageSize + } + + /** + * Get the items for the current page of a viewer. + */ + fun getPageItems(viewer: Player): ObjectList { + val allItems = items() + val currentPage = getCurrentPage(viewer) + val startIndex = currentPage * pageSize + val endIndex = minOf(startIndex + pageSize, allItems.size) + + return if (startIndex < allItems.size) { + allItems.subList(startIndex, endIndex).toObjectList() + } else { + objectListOf() + } + } + + /** + * Check if there is a next page for a viewer. + */ + fun hasNextPage(viewer: Player): Boolean { + val currentPage = getCurrentPage(viewer) + + return currentPage < getTotalPages() - 1 + } + + /** + * Check if there is a previous page for a viewer. + */ + fun hasPreviousPage(viewer: Player): Boolean { + return getCurrentPage(viewer) > 0 + } + + /** + * Update the state of navigation buttons based on available pages. + */ + private fun updateNavigationButtonsState(viewer: Player) { + val hasPrev = hasPreviousPage(viewer) + val hasNext = hasNextPage(viewer) + + previousButtonComponent.hidden = !hasPrev + previousButtonComponent.disabled = !hasPrev + + nextButtonComponent.hidden = !hasNext + nextButtonComponent.disabled = !hasNext + } + + /** + * Go to the next page for a viewer. + */ + fun nextPage(viewer: Player) { + val currentPage = getCurrentPage(viewer) + + if (hasNextPage(viewer)) { + setPage(viewer, currentPage + 1) + this@PaginationComponent.update(viewer) + } + } + + /** + * Go to the previous page for a viewer. + */ + fun previousPage(viewer: Player) { + val currentPage = getCurrentPage(viewer) + + if (hasPreviousPage(viewer)) { + setPage(viewer, currentPage - 1) + this@PaginationComponent.update(viewer) + } + } + + /** + * Set a specific page for a viewer. + */ + fun setPage(viewer: Player, page: Int) { + if (page in 0 until getTotalPages()) { + currentPages.set(viewer, page) + } + } + + /** + * Clear the page state for a viewer. + */ + fun clearPage(viewer: Player) { + currentPages.clear(viewer) + } + + override fun onUpdate(context: LifecycleContext) { + updateNavigationButtonsState(context.player) + } + + override fun renderSlots(context: ViewContext): Object2ObjectMap { + val pageItems = getPageItems(context.player) + val renderedSlots = mutableObject2ObjectMapOf() + + pageItems.forEachIndexed { index, item -> + val guiItem = itemRenderer(item, context) + + if (guiItem != null) { + // Calculate slot position within the items area (height - 1) + val row = index / width + val col = index % width + + // Only render if within items area (not in button row) + if (row < itemsHeight) { + val slot = Slot.at(startSlot.column + col, startSlot.row + row) + + renderedSlots[slot] = guiItem + } + } + } + + return renderedSlots + } + + override fun onClick(context: ClickContext) { + // Check if click is in the items area (not button row) + val relativeRow = context.slot.row - startSlot.row + + if (relativeRow >= itemsHeight) { + // Click is in button row, let children handle it + return + } + + if (onItemClick != null) { + val pageItems = getPageItems(context.player) + + // Calculate the index within the pagination area + val relativeCol = context.slot.column - startSlot.column + val index = relativeRow * width + relativeCol + + if (index in pageItems.indices) { + val item = pageItems[index] + + onItemClick.invoke(item, context) + } + } + } + + override fun toString(): String { + return "PaginationComponent(items=$items, itemRenderer=$itemRenderer, priority=$priority, onItemClick=$onItemClick, previousButtonSlot=$previousButtonSlot, nextButtonSlot=$nextButtonSlot, pageIndicatorSlot=$pageIndicatorSlot, area=$area, currentPages=$currentPages, startSlot=$startSlot, itemsHeight=$itemsHeight, pageSize=$pageSize, calculatedPreviousButtonSlot=$calculatedPreviousButtonSlot, calculatedPageIndicatorSlot=$calculatedPageIndicatorSlot, calculatedNextButtonSlot=$calculatedNextButtonSlot, previousButtonComponent=$previousButtonComponent, nextButtonComponent=$nextButtonComponent, pageIndicatorComponent=$pageIndicatorComponent) ${super.toString()}" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ClickContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ClickContext.kt new file mode 100644 index 000000000..fe9b00b70 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ClickContext.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.toGuiItem +import org.bukkit.event.inventory.InventoryClickEvent + +/** + * Context for click events. + */ +interface ClickContext : ViewContext { + /** + * The click event. + */ + val event: InventoryClickEvent + + /** + * The clicked item. + */ + val item: GuiItem? + get() = event.currentItem?.toGuiItem() + + /** + * The slot that was clicked. + */ + val slot: Slot + get() = Slot.of(event.slot) + + /** + * The component that was clicked, if any. + */ + val component: Component? +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/InitializeContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/InitializeContext.kt new file mode 100644 index 000000000..60b0738ea --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/InitializeContext.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig + +/** + * Context for render operations. + */ +interface InitializeContext { + /** + * Get the view configuration. + */ + fun config(): ViewConfig + + /** + * Render a component. + * The component contains its own slot information (startSlot/endSlot). + */ + fun renderComponent(component: Component) + + /** + * Clear a slot. + */ + fun clearSlot(slot: Slot) + + /** + * Set an item at a slot without a component. + */ + fun setItem(slot: Slot, item: GuiItem) +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleContext.kt new file mode 100644 index 000000000..0b229f5f4 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleContext.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +/** + * Context for lifecycle events. + */ +interface LifecycleContext : ViewContext { + /** + * The type of lifecycle event. + */ + val eventType: LifecycleEventType +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleEventType.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleEventType.kt new file mode 100644 index 000000000..499918496 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleEventType.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +/** + * Types of lifecycle events. + */ +enum class LifecycleEventType { + UPDATE, + INIT_COMPONENT, + FIRST_RENDER, + OPEN, + CLOSE, + RESUME +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ResumeContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ResumeContext.kt new file mode 100644 index 000000000..e1f54af93 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ResumeContext.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView + +/** + * Context for resume events (navigation back). + */ +interface ResumeContext : LifecycleContext { + /** + * The view we're navigating from (origin). + */ + val origin: GuiView? + + /** + * The view we're navigating to (target, this view). + */ + val target: GuiView + get() = view +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ViewContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ViewContext.kt new file mode 100644 index 000000000..d7dbd0ecd --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ViewContext.kt @@ -0,0 +1,57 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.props.ViewerProp +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import org.bukkit.entity.Player + +/** + * Context representing a snapshot of the current GUI state and props. + * This is passed to all user action handlers and provides access to view, props, and player. + */ +interface ViewContext { + /** + * The view this context belongs to. + */ + val view: GuiView + + /** + * The player interacting with the GUI. + */ + val player: Player + + /** + * Get a prop value from the view. + */ + suspend fun getProp(prop: Prop): T? = prop.get() + + /** + * Get a prop value for a specific player. + */ + fun getPropForPlayer(prop: Prop, player: Player): T { + return when (prop) { + is ViewerProp -> prop.get(player) + else -> throw UnsupportedOperationException() + } + } + + /** + * Navigate to another view. + */ + fun navigateTo(view: GuiView, passProps: Boolean = false) + + /** + * Navigate back to parent view. + */ + fun navigateBack() + + /** + * Close the GUI. + */ + fun close() + + /** + * Update the current view. + */ + fun update() +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractClickContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractClickContext.kt new file mode 100644 index 000000000..29f8b6dcd --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractClickContext.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent + +@InternalSurfApi +class AbstractClickContext( + override val view: GuiView, + override val player: Player, + override val event: InventoryClickEvent, + override val component: Component? +) : ClickContext { + override fun navigateTo(view: GuiView, passProps: Boolean) { + NavigationHelper.navigateTo(this.view, view, player, passProps) + } + + override fun navigateBack() { + NavigationHelper.navigateBack(view, player) + } + + override fun close() { + NavigationHelper.close(player) + } + + override fun update() { + NavigationHelper.update(view, player) + } + + override fun toString(): String { + return "AbstractClickContext(view=$view, player=$player, event=$event, component=$component)" + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractInitializeContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractInitializeContext.kt new file mode 100644 index 000000000..f380b2864 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractInitializeContext.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.context.InitializeContext +import dev.slne.surf.surfapi.bukkit.api.gui.toItemStack +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi + +@InternalSurfApi +class AbstractInitializeContext( + val config: ViewConfig, + val view: GuiView +) : InitializeContext { + override fun config(): ViewConfig { + return config + } + + override fun renderComponent(component: Component) { + view.addComponent(component) + } + + override fun clearSlot(slot: Slot) { + // Find and remove all components that occupy this slot + val componentsToRemove = view.findComponentsBySlot(slot) + componentsToRemove.forEach { component -> + view.removeComponent(component) + } + } + + override fun setItem(slot: Slot, item: GuiItem) { + val inventory = view.inventory + + if (slot.index in 0 until inventory.size) { + inventory.setItem(slot.index, item.toItemStack()) + } + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractLifecycleContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractLifecycleContext.kt new file mode 100644 index 000000000..80dc3cf3a --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractLifecycleContext.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleEventType +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi +import org.bukkit.entity.Player + +@InternalSurfApi +class AbstractLifecycleContext( + override val view: GuiView, + override val player: Player, + override val eventType: LifecycleEventType +) : LifecycleContext { + override fun navigateTo(view: GuiView, passProps: Boolean) { + NavigationHelper.navigateTo(this.view, view, player, passProps) + } + + override fun navigateBack() { + NavigationHelper.navigateBack(view, player) + } + + override fun close() { + NavigationHelper.close(player) + } + + override fun update() { + NavigationHelper.update(view, player) + } + + override fun toString(): String { + return "AbstractLifecycleContext(view=$view, player=$player, eventType=$eventType)" + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractResumeContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractResumeContext.kt new file mode 100644 index 000000000..08f95413a --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractResumeContext.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleEventType +import dev.slne.surf.surfapi.bukkit.api.gui.context.ResumeContext +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi +import org.bukkit.entity.Player + +@InternalSurfApi +class AbstractResumeContext( + override val view: GuiView, + override val player: Player, + override val origin: GuiView? +) : ResumeContext { + override val eventType: LifecycleEventType = LifecycleEventType.RESUME + + override fun navigateTo(view: GuiView, passProps: Boolean) { + NavigationHelper.navigateTo(this.view, view, player, passProps) + } + + override fun navigateBack() { + NavigationHelper.navigateBack(view, player) + } + + override fun close() { + NavigationHelper.close(player) + } + + override fun update() { + NavigationHelper.update(view, player) + } + + override fun toString(): String { + return "AbstractResumeContext(view=$view, player=$player, origin=$origin, eventType=$eventType)" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractViewContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractViewContext.kt new file mode 100644 index 000000000..a0a14b949 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractViewContext.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi +import org.bukkit.entity.Player + +@InternalSurfApi +class AbstractViewContext( + override val view: GuiView, + override val player: Player +) : ViewContext { + override fun navigateTo(view: GuiView, passProps: Boolean) { + NavigationHelper.navigateTo(this.view, view, player, passProps) + } + + override fun navigateBack() { + NavigationHelper.navigateBack(view, player) + } + + override fun close() { + NavigationHelper.close(player) + } + + override fun update() { + NavigationHelper.update(view, player) + } + + override fun toString(): String { + return "AbstractViewContext(view=$view, player=$player)" + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/NavigationHelper.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/NavigationHelper.kt new file mode 100644 index 000000000..37d0ba62f --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/NavigationHelper.kt @@ -0,0 +1,37 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi +import org.bukkit.entity.Player + +@InternalSurfApi +object NavigationHelper { + fun navigateTo(currentView: GuiView, targetView: GuiView, player: Player, passProps: Boolean) { + targetView.withParent(currentView) + player.closeInventory() + targetView.open(player) + } + + fun navigateBack(currentView: GuiView, player: Player) { + val parent = currentView.parent + + if (parent is GuiView) { + player.closeInventory() + + val resumeContext = parent.createResumeContext(player, currentView) + + parent.onResume(resumeContext) + parent.open(player) + } else { + player.closeInventory() + } + } + + fun close(player: Player) { + player.closeInventory() + } + + fun update(view: GuiView, player: Player) { + view.refreshInventory(player) + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt new file mode 100644 index 000000000..2aebf8672 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt @@ -0,0 +1,203 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.dsl + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority +import dev.slne.surf.surfapi.bukkit.api.gui.component.components.DynamicComponent +import dev.slne.surf.surfapi.bukkit.api.gui.component.components.ItemComponent +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.InitializeContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.LifecycleContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import dev.slne.surf.surfapi.bukkit.api.gui.props.ComputedProp +import dev.slne.surf.surfapi.bukkit.api.gui.props.LazyProp +import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.props.ViewerProp +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import it.unimi.dsi.fastutil.objects.ObjectList + +/** + * DSL marker for component building. + */ +@DslMarker +annotation class ComponentDsl + +/** + * Builder for creating components. + */ +@ComponentDsl +class ComponentBuilder { + var ref: Ref? = null + var priority: ComponentPriority = ComponentPriority.NORMAL + var cancelOnClick: Boolean = true + var initComponent: (LifecycleContext.() -> Unit)? = null + var onFirstRender: (LifecycleContext.() -> Unit)? = null + var onUpdate: (LifecycleContext.() -> Unit)? = null + var onClick: (ClickContext.() -> Unit)? = null + var hidden: Boolean = false + var disabled: Boolean = false + + private val _props = mutableObjectListOf>() + + /** + * Add a prop to this component. + */ + fun prop(prop: Prop) { + _props.add(prop) + } + + /** + * Build the component. + */ + internal fun build(slot: Slot, renderer: (ViewContext) -> GuiItem?): Component { + return object : DynamicComponent(slot, renderer, priority, onClick) { + override val props: ObjectList> = _props + override var hidden by this@ComponentBuilder::hidden + override var disabled by this@ComponentBuilder::disabled + override var cancelOnClick by this@ComponentBuilder::cancelOnClick + + override fun initComponent(context: LifecycleContext) { + super.initComponent(context) + this@ComponentBuilder.initComponent?.invoke(context) + } + + override fun onFirstRender(context: LifecycleContext) { + super.onFirstRender(context) + this@ComponentBuilder.onFirstRender?.invoke(context) + } + + override fun onUpdate(context: LifecycleContext) { + super.onUpdate(context) + this@ComponentBuilder.onUpdate?.invoke(context) + } + + override fun toString(): String { + return "DSLGeneratedDynamicComponent(props=$props) ${super.toString()}" + } + }.also { component -> + ref?.set(component) + component.attachedRef = ref + } + } +} + +/** + * Create a component with a static item at a specific slot. + */ +fun component( + slot: Slot, + item: GuiItem, + builder: ComponentBuilder.() -> Unit = {} +): Component { + val componentBuilder = ComponentBuilder() + componentBuilder.builder() + return componentBuilder.build(slot) { item } +} + +/** + * Create a component with a dynamic renderer at a specific slot. + */ +fun dynamicComponent( + slot: Slot, + renderer: (ViewContext) -> GuiItem?, + builder: ComponentBuilder.() -> Unit = {} +): Component { + val componentBuilder = ComponentBuilder() + componentBuilder.builder() + return componentBuilder.build(slot, renderer) +} + +/** + * DSL for creating props. + */ +@ComponentDsl +class PropsBuilder { + private val _props = mutableObjectListOf>() + + /** + * Create an immutable prop. + */ + fun immutable(name: String, value: T): Prop.Immutable { + return Prop.Immutable(name, value).also { _props.add(it) } + } + + /** + * Create a mutable prop (global to view). + */ + fun mutable(name: String, initialValue: T?): Prop.Mutable { + return Prop.Mutable(name, initialValue).also { _props.add(it) } + } + + /** + * Create a viewer-specific immutable prop. + */ + fun viewerImmutable(name: String, initialValue: T): ViewerProp { + return ViewerProp(name, initialValue).also { _props.add(it) } + } + + /** + * Create a viewer-specific mutable prop. + */ + fun viewerMutable(name: String, initialValue: T?): ViewerProp.Mutable { + return ViewerProp.Mutable(name, initialValue).also { _props.add(it) } + } + + /** + * Create a computed prop. + */ + fun computed(name: String, compute: suspend () -> T): ComputedProp { + return ComputedProp(name, compute).also { _props.add(it) } + } + + /** + * Create an immutable lazy prop. + */ + fun immutableLazy(name: String, initializer: () -> T): LazyProp { + return LazyProp(name, initializer).also { _props.add(it) } + } + + /** + * Create a mutable lazy prop. + */ + fun mutableLazy(name: String, initializer: () -> T?): LazyProp.Mutable { + return LazyProp.Mutable(name, initializer).also { _props.add(it) } + } + + /** + * Get all props. + */ + internal fun build(): ObjectList> = mutableObjectListOf(_props) +} + +/** + * Create props using DSL. + */ +fun props(builder: PropsBuilder.() -> Unit): ObjectList> { + val propsBuilder = PropsBuilder() + propsBuilder.builder() + return propsBuilder.build() +} + +/** + * DSL for rendering components in a view. + */ +@ComponentDsl +fun InitializeContext.slot(component: Component) { + renderComponent(component) +} + +/** + * DSL for rendering components in a view with item at a specific slot. + */ +@ComponentDsl +fun InitializeContext.slot( + slot: Slot, + item: GuiItem, + priority: ComponentPriority = ComponentPriority.NORMAL, + onClick: (ClickContext.() -> Unit)? = null +) { + val component = ItemComponent(slot, item, priority, onClick) + renderComponent(component) +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ViewDsl.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ViewDsl.kt new file mode 100644 index 000000000..bb8615a83 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ViewDsl.kt @@ -0,0 +1,60 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.dsl + +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig +import org.bukkit.event.inventory.InventoryType +import net.kyori.adventure.text.Component as AdventureComponent + +/** + * DSL marker for view building. + */ +@DslMarker +annotation class ViewDsl + +/** + * Builder for creating view configurations. + */ +@ViewDsl +class ViewConfigBuilder { + var title: AdventureComponent = AdventureComponent.text("GUI") + var size: Int = 54 + var type: InventoryType = InventoryType.CHEST + var rows: Int + get() = if (type == InventoryType.CHEST) size / 9 else 1 + set(value) { + require(type == InventoryType.CHEST) { + "Rows can only be set for CHEST type inventories. For other types, the size is determined by the inventory type." + } + require(value in 1..6) { "Rows must be between 1 and 6" } + size = value * 9 + } + var cancelOnClick: Boolean = true + var closeOnClickOutside: Boolean = false + + internal fun build(): ViewConfig { + return ViewConfig().apply { + this.title = this@ViewConfigBuilder.title + this.type = this@ViewConfigBuilder.type + this.size = if (this.type == InventoryType.CHEST) { + this@ViewConfigBuilder.size + } else { + this.type.defaultSize + } + this.cancelOnClick = this@ViewConfigBuilder.cancelOnClick + } + } +} + +/** + * Configure a view using DSL. + */ +@ViewDsl +fun GuiView.configure(builder: ViewConfigBuilder.() -> Unit) { + val configBuilder = ViewConfigBuilder() + configBuilder.builder() + val builtConfig = configBuilder.build() + + config.title = builtConfig.title + config.size = builtConfig.size + config.cancelOnClick = builtConfig.cancelOnClick +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ComputedProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ComputedProp.kt new file mode 100644 index 000000000..c8395aa4b --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ComputedProp.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +/** + * Computed prop - accepts a callback that computes the value. + * The compute function is suspend to allow async operations. + */ +open class ComputedProp( + override val name: String, + private val compute: suspend () -> T +) : Prop { + override suspend fun get(): T = compute() + + override fun toString(): String { + return "ComputedProp(name='$name', compute=$compute)" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/LazyProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/LazyProp.kt new file mode 100644 index 000000000..0e23fe5fa --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/LazyProp.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +/** + * Immutable lazy prop - gets available when accessed, using a callback. + * Cannot be modified after initialization. + */ +open class LazyProp( + override val name: String, + private val initializer: () -> T +) : Prop { + private val value = lazy { initializer() } + + override suspend fun get(): T = value.value + + override fun toString(): String { + return "LazyProp(name='$name', initializer=$initializer, value=$value)" + } + + /** + * Mutable lazy prop - gets available when accessed, using a callback. + * Can be modified after initialization. + */ + open class Mutable( + override val name: String, + private val initializer: () -> T? + ) : Prop { + private val value = lazy { initializer() } + private var mutableValue: T? = null + + override suspend fun get(): T? { + return mutableValue ?: value.value + } + + fun set(value: T) { + mutableValue = value + } + + override fun toString(): String { + return "Mutable(name='$name', initializer=$initializer, value=$value, mutableValue=$mutableValue)" + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/Prop.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/Prop.kt new file mode 100644 index 000000000..7e8829cc0 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/Prop.kt @@ -0,0 +1,63 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty + +/** + * Base interface for all props in the GUI framework. + * All props are global to the view and shared across all viewers. + */ +sealed interface Prop { + /** + * Gets the value of this prop. + * Suspend function to support ComputedProp with async operations. + */ + suspend fun get(): T? + + /** + * The name of this prop. + */ + val name: String + + /** + * Immutable prop - always available and immutable after initialization. + */ + class Immutable( + override val name: String, + private val value: T + ) : Prop { + override suspend fun get(): T = value + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value + + override fun toString(): String { + return "Immutable(name='$name', value=$value)" + } + } + + /** + * Mutable prop - always available and mutable. + * Global to the view, shared across all viewers. + */ + open class Mutable( + override val name: String, + initialValue: T? + ) : Prop { + private val value = AtomicReference(initialValue) + + override suspend fun get(): T? = value.get() + + fun set(value: T) { + this.value.set(value) + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T? = value.get() + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value.set(value) + } + + override fun toString(): String { + return "Mutable(name='$name', value=$value)" + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerProp.kt new file mode 100644 index 000000000..d5fc8b145 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerProp.kt @@ -0,0 +1,55 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import org.bukkit.entity.Player + +/** + * Viewer-specific mutable prop - isolated per viewer. + */ +open class ViewerProp( + override val name: String, + initialValue: T +) : Prop { + private val storage = ViewerPropStorage { initialValue } + + override suspend fun get(): T = + throw UnsupportedOperationException("Use get(viewer: Player) for ViewerProp") + + fun get(viewer: Player): T = storage.get(viewer) + ?: throw IllegalStateException("Value for viewer ${viewer.uniqueId} is not set") + + fun clear(viewer: Player) { + storage.clear(viewer) + } + + override fun toString(): String { + return "ViewerProp(name='$name', storage=$storage)" + } + + class Mutable( + override val name: String, + initialValue: T? + ) : Prop { + private val storage = ViewerPropStorage { initialValue } + + override suspend fun get(): T = + throw UnsupportedOperationException("Use get(viewer: Player) for ViewerProp.MutableViewerProp") + + fun get(viewer: Player): T? = storage.get(viewer) + + fun getOrDefault(viewer: Player, defaultValue: T): T { + return storage.get(viewer) ?: defaultValue + } + + fun set(viewer: Player, value: T?) { + storage.set(viewer, value) + } + + fun clear(viewer: Player) { + storage.clear(viewer) + } + + override fun toString(): String { + return "Mutable(name='$name', storage=$storage)" + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerPropStorage.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerPropStorage.kt new file mode 100644 index 000000000..f1566bcba --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerPropStorage.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf +import org.bukkit.entity.Player +import java.util.* + +/** + * Viewer-specific prop storage. + * Maps viewer UUIDs to their prop values. + */ +class ViewerPropStorage(private val initialValue: () -> T) { + private val storage = mutableObject2ObjectMapOf() + + fun get(viewer: Player): T? = storage.getOrPut(viewer.uniqueId) { initialValue() } + + fun set(viewer: Player, value: T?) { + storage[viewer.uniqueId] = value + } + + fun clear(viewer: Player) { + storage.remove(viewer.uniqueId) + } + + override fun toString(): String { + return "ViewerPropStorage(initialValue=$initialValue, storage=$storage)" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt new file mode 100644 index 000000000..4c62fe2e7 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt @@ -0,0 +1,54 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.ref + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import org.bukkit.entity.Player +import java.util.concurrent.atomic.AtomicReference + +/** + * React-like ref system for component interaction. + * Allows components to reference and update other components. + */ +class Ref { + private val reference = AtomicReference() + + /** + * Gets the current component reference. + */ + val current: T? + get() = reference.get() + + /** + * Sets the component reference. + */ + fun set(component: T?) { + reference.set(component) + } + + /** + * Updates the referenced component. + * @param viewer The specific viewer to update for, or null to update for all viewers + */ + fun update(viewer: Player? = null) { + current?.update(viewer) + } + + /** + * Checks if the ref has a current value. + */ + fun isSet(): Boolean = current != null + + override fun toString(): String { + return "Ref(reference=$reference, current=$current)" + } +} + +/** + * Creates a new ref for a component. + */ +fun createRef(): Ref = Ref() + +/** + * DSL marker for ref operations. + */ +@DslMarker +annotation class RefMarker diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt new file mode 100644 index 000000000..f982cba72 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt @@ -0,0 +1,443 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.view + +import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin +import com.github.shynixn.mccoroutine.folia.entityDispatcher +import com.github.shynixn.mccoroutine.folia.launch +import dev.slne.surf.surfapi.bukkit.api.event.cancel +import dev.slne.surf.surfapi.bukkit.api.extensions.server +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.context.* +import dev.slne.surf.surfapi.bukkit.api.gui.context.abstract.* +import dev.slne.surf.surfapi.bukkit.api.gui.toItemStack +import dev.slne.surf.surfapi.core.api.util.freeze +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import dev.slne.surf.surfapi.core.api.util.toObjectList +import it.unimi.dsi.fastutil.objects.ObjectList +import kotlinx.coroutines.withContext +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryType +import org.bukkit.inventory.Inventory +import org.bukkit.plugin.java.JavaPlugin +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.properties.Delegates + +/** + * Base class for all GUI views. + * Views manage the overall GUI lifecycle and component rendering. + */ +abstract class GuiView { + /** + * Parent view for navigation. + */ + var parent: GuiView? = null + internal set + + var inventory: Inventory by Delegates.notNull() + private set + + /** + * Components in this view. + * Components can overlap, and priority determines rendering and click handling order. + */ + private val _components = mutableObjectListOf() + val components: ObjectList get() = _components.freeze() + + /** + * Find all components that contain the given slot, sorted by priority (highest first). + * Includes children recursively. + */ + fun findComponentsBySlot(slot: Slot): ObjectList { + val allComponents = mutableObjectListOf() + + // Helper function to recursively collect components and their children + fun collectComponents(component: Component) { + allComponents.add(component) + + component.children.forEach { child -> + collectComponents(child) + } + } + + // Collect all components including children + _components.forEach { collectComponents(it) } + + // Filter by slot, exclude hidden components, and sort by priority + return allComponents + .filter { it.contains(slot) && !it.hidden } + .sortedByDescending { it.priority.value } + .toObjectList() + } + + /** + * Whether this view has been initialized. + */ + @Volatile + private var initialized = false + + /** + * Current viewers of this view. + */ + private val viewers = ConcurrentHashMap.newKeySet() + + /** + * Configuration for this view. + */ + internal val config = ViewConfig() + + val title by config::title + val size by config::size + val type by config::type + val cancelOnClick by config::cancelOnClick + + protected fun init() { + onInit(createInitializeContext()) + recreateInventory() + } + + protected fun recreateInventory() { + this.inventory = when (config.type) { + InventoryType.CHEST -> { + Bukkit.createInventory(null, config.size, config.title) + } + + else -> { + Bukkit.createInventory(null, config.type, config.title) + } + } + } + + fun modifyConfig(modifier: (config: ViewConfig) -> Unit) { + modifier(config) + + initialized = false + + ensureInitialized() + + val plugin = JavaPlugin.getProvidingPlugin(GuiView::class.java) as SuspendingJavaPlugin + + plugin.launch { + viewerPlayers().forEach { viewer -> + withContext(plugin.entityDispatcher(viewer)) { + viewer.closeInventory() + open(viewer) + } + } + } + } + + /** + * Initialize the view configuration. + * Called once when the view is first created. + */ + open fun onInit(context: InitializeContext) {} + + /** + * Called when the view is opened for a player. + */ + open fun onOpen(context: ViewContext) {} + + /** + * Called when the view is updated. + */ + open fun onUpdate(context: ViewContext) {} + + /** + * Called when navigating back to this view from a child view. + */ + open fun onResume(context: ResumeContext) {} + + /** + * Called when the view is closed. + */ + open fun onClose(context: ViewContext) {} + + /** + * Initialize the view if not already initialized. + */ + internal fun ensureInitialized() { + if (!initialized) { + synchronized(this) { + if (!initialized) { + init() + initialized = true + } + } + } + } + + fun viewerPlayers(): List { + val players = mutableObjectListOf() + val it = viewers.iterator() + + while (it.hasNext()) { + val id = it.next() + val player = server.getPlayer(id) + if (player == null) { + it.remove() + } else { + players.add(player) + } + } + + return players + } + + /** + * Open this view for a player. + */ + open fun open(player: Player) { + ensureInitialized() + + ViewManager.setActiveView(player, this) + viewers.add(player.uniqueId) + + onOpen(createViewContext(player)) + + renderAllSlots(player) + + // Open inventory + player.openInventory(inventory) + } + + /** + * Close this view for a player. + */ + open fun close(player: Player) { + val context = createViewContext(player) + onClose(context) + viewers.remove(player.uniqueId) + ViewManager.removeActiveView(player) + } + + /** + * Update the view for all viewers. + */ + fun update() { + viewerPlayers().forEach { player -> + onUpdate(createViewContext(player)) + } + } + + private fun renderAllSlots(player: Player) { + val allSlots = (0 until inventory.size).map { Slot.of(it) } + + allSlots.forEach { slot -> + renderSlot(player, slot) + } + } + + private fun renderSlot( + player: Player, + slot: Slot + ) { + if (slot.index >= inventory.size) return + + val resolved = resolveSlot(player, slot) + resolved?.component?.renderFirstRenderPerPlayer(player) + + inventory.setItem( + slot.index, + resolved?.guiItem?.toItemStack() + ) + } + + + /** + * Refresh the inventory for a player. + */ + internal fun refreshInventory(player: Player) { + // Clear inventory first + inventory.clear() + + // Re-render all slots + renderAllSlots(player) + } + + /** + * Refresh all slots occupied by a specific component and its children. + */ + internal fun refreshComponentSlots(player: Player, component: Component) { + // Collect all slots from this component and all its children recursively + // Only refresh this component's own slots, not children's + // Children will refresh their own slots when updateChildrenRecursively calls them + fun collectAllSlots(comp: Component): Set { + val slots = mutableSetOf(*comp.area.slots().toTypedArray()) + + comp.children.forEach { child -> + slots.addAll(collectAllSlots(child)) + } + + return slots + } + + collectAllSlots(component).forEach { slot -> + renderSlot(player, slot) + } + } + + /** + * Update a specific component. + * Calls the onUpdate lifecycle hook and refreshes the component's slots in the inventory. + * @param component The component to update + * @param viewer The specific viewer to update for, or null to update for all viewers + */ + internal fun updateComponent(component: Component, viewer: Player? = null) { + val viewersToUpdate = if (viewer != null) { + listOf(viewer) + } else { + viewerPlayers() + } + + viewersToUpdate.forEach { player -> + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + + // Call onUpdate on this component + component.onUpdate(lifecycleContext) + + // Recursively call onUpdate on all children with the same viewer context + // This allows children to update their state (like hidden/disabled properties) + fun updateChildrenRecursively(comp: Component) { + comp.children.forEach { child -> + child.onUpdate(lifecycleContext) + updateChildrenRecursively(child) + } + } + + updateChildrenRecursively(component) + + // Refresh the component's slots to update the visual display + // This now includes all children's slots, so everything updates together + refreshComponentSlots(player, component) + } + } + + private fun resolveSlot( + player: Player, + slot: Slot + ): ResolvedSlot? { + val componentsAtSlot = findComponentsBySlot(slot) + if (componentsAtSlot.isEmpty()) return null + + val highestPriority = componentsAtSlot.first().priority.value + val candidates = componentsAtSlot + .takeWhile { it.priority.value == highestPriority } + + val viewContext = createViewContext(player) + + for (component in candidates) { + component.initComponent( + createLifecycleContext( + player, + LifecycleEventType.INIT_COMPONENT + ) + ) + if (component.hidden) continue + + val slots = component.renderSlots(viewContext) + + if (slots.isNotEmpty()) { + slots[slot]?.let { guiItem -> + return ResolvedSlot(component, guiItem) + } + } else if (slot == component.area.first()) { + component.render(viewContext)?.let { guiItem -> + return ResolvedSlot(component, guiItem) + } + } + } + + return null + } + + /** + * Handle click event. + */ + fun handleClick(player: Player, event: InventoryClickEvent) { + if (config.cancelOnClick) { + event.isCancelled = true + } + + val slot = Slot.of(event.rawSlot) + val resolved = resolveSlot(player, slot) ?: return + + if (resolved.component.disabled) return event.cancel() + if (resolved.component.cancelOnClick) event.cancel() + + resolved.component.onClick(createClickContext(player, event, resolved.component)) + } + + /** + * Add a component to the view. + */ + fun addComponent(component: Component) { + component.view = this + _components.add(component) + } + + /** + * Remove a component from the view. + */ + fun removeComponent(component: Component) { + _components.remove(component) + } + + /** + * Set the parent view for navigation. + */ + fun withParent(parentView: GuiView): GuiView { + this.parent = parentView + + return this + } + + /** + * Create a click context for a player and click event. + */ + fun createClickContext( + player: Player, + event: InventoryClickEvent, + component: Component, + ): ClickContext = AbstractClickContext(this, player, event, component) + + /** + * Create a view context for a player. + */ + fun createViewContext(player: Player): ViewContext = AbstractViewContext(this, player) + + /** + * Create a render context for a player. + */ + fun createInitializeContext(): InitializeContext = AbstractInitializeContext(config, this) + + /** + * Create a lifecycle context for a player. + */ + fun createLifecycleContext( + player: Player, + eventType: LifecycleEventType + ): LifecycleContext = AbstractLifecycleContext(this, player, eventType) + + /** + * Create a resume context for a player. + */ + fun createResumeContext( + player: Player, + origin: GuiView? + ): ResumeContext = AbstractResumeContext(this, player, origin) + + override fun toString(): String { + return "GuiView(parent=$parent, components=$components, initialized=$initialized, viewers=$viewers, config=$config)" + } +} + +/** + * Creates a child view with this view as parent. + */ +fun GuiView.childView(view: T): T { + view.parent = this + return view +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ResolvedSlot.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ResolvedSlot.kt new file mode 100644 index 000000000..b111497d2 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ResolvedSlot.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.view + +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component + +data class ResolvedSlot( + val component: Component, + val guiItem: GuiItem?, +) { + override fun toString(): String { + return "ResolvedSlot(component=$component, guiItem=$guiItem)" + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewConfig.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewConfig.kt new file mode 100644 index 000000000..72dc3acc4 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewConfig.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.view + +import net.kyori.adventure.text.Component +import org.bukkit.event.inventory.InventoryType + +/** + * Configuration for a GUI view. + */ +data class ViewConfig( + var title: Component = Component.text("GUI"), + var size: Int = 54, // 6 rows by default for CHEST + var type: InventoryType = InventoryType.CHEST, + var cancelOnClick: Boolean = true, +) { + /** + * Set rows (only for CHEST type). + * Automatically adjusts size. + */ + var rows: Int + get() = size / 9 + set(value) { + require(type == InventoryType.CHEST) { + "Rows can only be set for CHEST type inventories. For other types, the size is determined by the inventory type." + } + require(value in 1..6) { "Rows must be between 1 and 6" } + size = value * 9 + } + + override fun toString(): String { + return "ViewConfig(title=$title, size=$size, type=$type, cancelOnClick=$cancelOnClick, rows=$rows)" + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewManager.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewManager.kt new file mode 100644 index 000000000..ce827672b --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewManager.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.view + +import dev.slne.surf.surfapi.core.api.util.requiredService +import org.bukkit.entity.Player + +private val viewManager = requiredService() + +interface ViewManager { + fun getActiveView(player: Player): GuiView? + fun setActiveView(player: Player, view: GuiView) + fun removeActiveView(player: Player) + + companion object : ViewManager by viewManager { + val INSTANCE get() = viewManager + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt deleted file mode 100644 index 7f3cc95c0..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory - -import org.bukkit.entity.Player - -interface SinglePlayerGui : SurfGui { - val player: Player - - fun open() = gui.show(player) -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt deleted file mode 100644 index 0983099bb..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt +++ /dev/null @@ -1,51 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory - -import com.github.stefvanschie.inventoryframework.gui.type.util.NamedGui -import com.github.stefvanschie.inventoryframework.pane.StaticPane -import com.github.stefvanschie.inventoryframework.pane.util.Slot -import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.PaneMarker -import dev.slne.surf.surfapi.bukkit.api.inventory.item.SurfGuiItem -import org.bukkit.entity.HumanEntity -import org.bukkit.event.inventory.InventoryCloseEvent -import org.bukkit.inventory.ItemStack -import org.bukkit.plugin.java.JavaPlugin - -interface SurfGui { - val parent: SurfGui? - val gui: NamedGui - - fun HumanEntity.backToParent() { - server.scheduler.runTaskLater(JavaPlugin.getProvidingPlugin(SurfGui::class.java), Runnable { - if (parent != null) { - val gui = parent!!.gui - gui.show(this) - gui.update() - } else { - closeInventory(InventoryCloseEvent.Reason.PLUGIN) - } - }, 1L) - } - - fun walkParents(): List = generateSequence(this) { it.parent }.toList() - - fun StaticPane.item( - slot: Slot, - item: ItemStack? = null, - init: (@PaneMarker SurfGuiItem).() -> Unit, - ) { - val guiItem = SurfGuiItem(item) - guiItem.init() - - if (!guiItem.condition()) { - return - } - - if (this@SurfGui is SinglePlayerGui) { - if (guiItem.itemPermission?.let { player.hasPermission(it) } == false) { - return - } - } - - addItem(guiItem, slot) - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt deleted file mode 100644 index bb8cb9476..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt +++ /dev/null @@ -1,92 +0,0 @@ -@file:OptIn(ExperimentalContracts::class) - -package dev.slne.surf.surfapi.bukkit.api.inventory.dsl - -import com.github.stefvanschie.inventoryframework.gui.type.util.MergedGui -import com.github.stefvanschie.inventoryframework.pane.StaticPane -import com.github.stefvanschie.inventoryframework.pane.util.Slot -import dev.slne.surf.surfapi.bukkit.api.inventory.types.SurfChestGui -import dev.slne.surf.surfapi.bukkit.api.inventory.types.SurfChestSinglePlayerGui -import net.kyori.adventure.text.Component -import org.bukkit.entity.Player -import org.jetbrains.annotations.Range -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS) -@DslMarker -annotation class MenuMarker - -fun MergedGui.staticPane( - slot: Slot, - height: @Range(from = 1, to = 6) Int, - length: @Range(from = 1, to = 9) Int = 9, - init: (@PaneMarker StaticPane).() -> Unit, -) { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val pane = StaticPane(slot, length, height) - pane.init() - addPane(pane) -} - -fun menu( - title: Component, - rows: @Range(from = 2, to = 6) Int = 6, - init: @MenuMarker SurfChestGui.() -> Unit, -): SurfChestGui { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val menu = SurfChestGui(title, rows) - menu.init() - return menu -} - -fun playerMenu( - title: Component, - player: Player, - rows: @Range(from = 2, to = 6) Int = 6, - init: @MenuMarker SurfChestSinglePlayerGui.() -> Unit, -): SurfChestSinglePlayerGui { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val menu = SurfChestSinglePlayerGui(title, player, rows) - menu.init() - return menu -} - - -fun SurfChestGui.childMenu( - title: Component, - rows: @Range(from = 2, to = 6) Int, - init: @MenuMarker SurfChestGui.() -> Unit, -): SurfChestGui { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val menu = SurfChestGui(title, rows, this) - menu.init() - return menu -} - -fun SurfChestSinglePlayerGui.childPlayerMenu( - title: Component, - rows: @Range(from = 2, to = 6) Int, - init: @MenuMarker SurfChestSinglePlayerGui.() -> Unit, -): SurfChestSinglePlayerGui { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val menu = SurfChestSinglePlayerGui(title, player, rows, this) - menu.init() - return menu -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt deleted file mode 100644 index eee9a8250..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.dsl - -@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) -@DslMarker -annotation class PaneMarker diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt deleted file mode 100644 index 34635aaf1..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt +++ /dev/null @@ -1,131 +0,0 @@ -@file:OptIn(ExperimentalContracts::class) - -package dev.slne.surf.surfapi.bukkit.api.inventory.dsl - -import com.github.stefvanschie.inventoryframework.gui.GuiItem -import com.github.stefvanschie.inventoryframework.pane.OutlinePane -import com.github.stefvanschie.inventoryframework.pane.Pane -import com.github.stefvanschie.inventoryframework.pane.StaticPane -import com.github.stefvanschie.inventoryframework.pane.util.Slot -import dev.slne.surf.surfapi.bukkit.api.inventory.item.guiItem -import dev.slne.surf.surfapi.bukkit.api.inventory.pane.SubmitItemPane -import dev.slne.surf.surfapi.bukkit.api.inventory.types.SurfChestGui -import org.bukkit.Material -import org.bukkit.inventory.ItemStack -import org.jetbrains.annotations.Range -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@PaneMarker -class StaticPaneScope(slot: Slot, length: Int, height: Int) : StaticPane(slot, length, height) - -fun SurfChestGui.drawOutline( - slot: Slot, - height: @Range(from = 1, to = 6) Int, - length: @Range(from = 1, to = 9) Int = 9, - init: @PaneMarker OutlinePane.() -> Unit -): OutlinePane { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val pane = OutlinePane(slot, length, height, Pane.Priority.LOWEST) - pane.init() - addPane(pane) - - return pane -} - -fun SurfChestGui.drawOutline( - slot: Slot, - height: @Range(from = 1, to = 6) Int, - length: @Range(from = 1, to = 9) Int = 9, - item: GuiItem = guiItem(Material.GRAY_STAINED_GLASS_PANE) { isCancelled = true } -) = drawOutline(slot, height, length) { - addItem(item) - setRepeat(true) -} - -@OptIn(ExperimentalContracts::class) -fun SurfChestGui.drawOutlineRow( - row: @Range(from = 0, to = 5) Int, - length: @Range(from = 1, to = 9) Int = 9, - init: @PaneMarker OutlinePane.() -> Unit -): OutlinePane { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - return drawOutline(slot(0, row), 1, length, init) -} - -fun SurfChestGui.drawOutlineRow( - row: @Range(from = 0, to = 5) Int, - length: @Range(from = 1, to = 9) Int = 9, - item: GuiItem = guiItem(Material.GRAY_STAINED_GLASS_PANE) { isCancelled = true } -) = drawOutlineRow(row, length) { - addItem(item) - setRepeat(true) -} - -fun SurfChestGui.makeStaticPane( - slot: Slot, - height: @Range(from = 1, to = 6) Int, - length: @Range(from = 1, to = 9) Int, - init: StaticPane.() -> Unit -): StaticPane { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val pane = StaticPane(slot, length, height) - pane.init() - addPane(pane) - - return pane -} - -fun SurfChestGui.makeSubmitItemPane( - slot: Slot, - length: @Range(from = 1, to = 6) Int, - height: @Range(from = 1, to = 6) Int, - filter: List, - init: SubmitItemPane.() -> Unit = {} -): SubmitItemPane { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val pane = SubmitItemPane(slot, length, height, filter) - pane.init() - addPane(pane) - - return pane -} - -fun SurfChestGui.makeSubmitItemPane( - slot: Slot, - length: @Range(from = 1, to = 6) Int, - height: @Range(from = 1, to = 6) Int, - filter: (ItemStack) -> Boolean, - init: SubmitItemPane.() -> Unit = {}, -): SubmitItemPane { - contract { - callsInPlace(init, InvocationKind.EXACTLY_ONCE) - } - - val pane = SubmitItemPane(slot, length, height, filter) - pane.init() - addPane(pane) - - return pane -} - -fun SurfChestGui.addItem( - slot: Slot, - item: GuiItem -) = addPane(StaticPane(slot, 1, 1).apply { addItem(item, 0, 0) }) - -fun SurfChestGui.addItems( - vararg items: Pair -) = items.forEach { (slot, item) -> addItem(slot, item) } \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt deleted file mode 100644 index 60feb449d..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt +++ /dev/null @@ -1,2 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.dsl - diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt deleted file mode 100644 index 1624fc169..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.dsl - -import com.github.stefvanschie.inventoryframework.pane.util.Slot -import org.jetbrains.annotations.Range - -fun slot(x: @Range(from = 0, to = 8) Int, y: @Range(from = 0, to = 5) Int) = Slot.fromXY(x, y) -fun slot(index: Int) = Slot.fromIndex(index) \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/ViewFrameAccessor.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/ViewFrameAccessor.kt deleted file mode 100644 index 8e410f964..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/ViewFrameAccessor.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.framework - -import dev.slne.surf.surfapi.core.api.util.requiredService -import me.devnatan.inventoryframework.ViewFrame -import org.jetbrains.annotations.ApiStatus - -@ApiStatus.NonExtendable -interface ViewFrameAccessor { - fun viewFrame(): ViewFrame - - companion object { - val instance = requiredService() - } -} - -val viewFrame: ViewFrame - get() = ViewFrameAccessor.instance.viewFrame() \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/extensions.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/extensions.kt deleted file mode 100644 index 8a07228a6..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/extensions.kt +++ /dev/null @@ -1,39 +0,0 @@ -@file:JvmName("InventoryFrameworkExtensions") - -package dev.slne.surf.surfapi.bukkit.api.inventory.framework - -import dev.slne.surf.surfapi.core.api.messages.builder.SurfComponentBuilder -import me.devnatan.inventoryframework.View -import me.devnatan.inventoryframework.ViewConfigBuilder -import me.devnatan.inventoryframework.context.OpenContext -import org.bukkit.entity.Player - -fun View.register() { - viewFrame.with(this) -} - -fun View.unregister() { - viewFrame.remove(this) -} - -fun View.open(player: Player) { - viewFrame.open(javaClass, player) -} - -fun View.open(player: Player, data: Any) { - viewFrame.open(javaClass, player, data) -} - -fun View.open(players: Collection) { - viewFrame.open(javaClass, players) -} - -fun View.open(players: Collection, data: Any) { - viewFrame.open(javaClass, players, data) -} - -inline fun ViewConfigBuilder.titleBuilder(title: SurfComponentBuilder.() -> Unit): ViewConfigBuilder = - this.title(SurfComponentBuilder(title)) - -inline fun OpenContext.modifyConfig(modifier: ViewConfigBuilder.() -> Unit) = - this.modifyConfig().apply(modifier) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt deleted file mode 100644 index 3b02b8368..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt +++ /dev/null @@ -1,37 +0,0 @@ -@file:OptIn(ExperimentalContracts::class) - -package dev.slne.surf.surfapi.bukkit.api.inventory.item - -import com.github.stefvanschie.inventoryframework.gui.GuiItem -import org.bukkit.Material -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.ItemStack -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -fun guiItem(item: ItemStack, action: InventoryClickEvent.() -> Unit = {}): GuiItem { - contract { - callsInPlace(action, InvocationKind.UNKNOWN) - } - - return GuiItem(item, action) -} - -fun guiItem( - material: Material, - item: ItemStack.() -> Unit, - action: InventoryClickEvent.() -> Unit = {} -): GuiItem { - contract { - callsInPlace(item, InvocationKind.EXACTLY_ONCE) - callsInPlace(action, InvocationKind.UNKNOWN) - } - - return GuiItem(ItemStack(material).apply(item), action) -} - -fun guiItem( - material: Material, - action: InventoryClickEvent.() -> Unit = {} -) = guiItem(material, {}, action) \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt deleted file mode 100644 index 6a4cf99a0..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.item - -import com.github.stefvanschie.inventoryframework.gui.GuiItem -import dev.slne.surf.surfapi.bukkit.api.inventory.SinglePlayerGui -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.ItemStack - -class SurfGuiItem : GuiItem { - - constructor(item: ItemStack?) : super(item ?: ItemStack.empty()) - constructor() : super(ItemStack.empty()) - - var click: InventoryClickEvent.() -> Unit = {} - set(value) = setAction(value) - - var itemPermission: String? = null - private set - - var condition: () -> Boolean = { true } - - fun SinglePlayerGui.permission(permission: String) { - itemPermission = permission - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt deleted file mode 100644 index 80d60482e..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt +++ /dev/null @@ -1,2 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.item - diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt deleted file mode 100644 index 5d9315094..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt +++ /dev/null @@ -1,308 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.pane - -import com.github.stefvanschie.inventoryframework.gui.GuiItem -import com.github.stefvanschie.inventoryframework.gui.InventoryComponent -import com.github.stefvanschie.inventoryframework.gui.type.util.Gui -import com.github.stefvanschie.inventoryframework.pane.Pane -import com.github.stefvanschie.inventoryframework.pane.util.Slot -import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.slot -import org.bukkit.Material -import org.bukkit.event.inventory.InventoryAction -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.ItemStack -import org.jetbrains.annotations.Range -import kotlin.math.min - - -class SubmitItemPane @JvmOverloads constructor( - slot: Slot, - length: @Range(from = 1, to = 6) Int, - height: @Range(from = 1, to = 6) Int, - private val filter: ItemStack.() -> Boolean, - priority: Priority = Priority.NORMAL -) : Pane(slot, length, height, priority) { - - constructor( - slot: Slot, - length: @Range(from = 1, to = 6) Int, - height: @Range(from = 1, to = 6) Int, - filter: List, - priority: Priority = Priority.NORMAL - ) : this(slot, length, height, { filter.contains(type) }, priority) - - private val _items = mutableMapOf() - val submittedItems get() = _items.toMap() - - override fun display( - inventoryComponent: InventoryComponent, - paneOffsetX: Int, - paneOffsetY: Int, - maxLength: Int, - maxHeight: Int - ) { -// val length = min(length, maxLength) -// val height = min(height, maxHeight) -// -// for ((location, item) in _items.filter { (_, item) -> item.isVisible }) { -// val x = location.getX(getLength()) -// val y = location.getY(getHeight()) -// -// if (x < 0 || x >= length || y < 0 || y >= height) { -// continue -// } -// -// val slot = getSlot() -// val finalRow = slot.getY(maxLength) + y + paneOffsetY -// val finalColumn = slot.getX(maxLength) + x + paneOffsetX -// -// inventoryComponent.setItem(item, finalColumn, finalRow) -// } - } - - override fun click( - gui: Gui, - inventoryComponent: InventoryComponent, - event: InventoryClickEvent, - slot: Int, - paneOffsetX: Int, - paneOffsetY: Int, - maxLength: Int, - maxHeight: Int - ): Boolean { - event.apply { - when (action) { - InventoryAction.PICKUP_ALL, InventoryAction.PICKUP_SOME, InventoryAction.PICKUP_HALF, InventoryAction.PICKUP_ONE -> { - val item = currentItem - - if (item == null || filter(item)) { - if (item == null) { - _items.remove(slot(slot)) - } else { - if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) { - _items[slot(slot)] = item - } - } - - isCancelled = false - } else { - isCancelled = true - } - } - - InventoryAction.PLACE_ALL, InventoryAction.PLACE_SOME, InventoryAction.PLACE_ONE -> { - val item = cursor - - if (filter(item)) { - val previous = _items[slot(slot)] - if (previous == null || !previous.isSimilar(item)) { - if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) { - _items[slot(slot)] = item - } - } else { - if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) { - _items[slot(slot)] = previous.clone().apply { amount += item.amount } - } - } - - isCancelled = false - } else { - isCancelled = true - } - } - - InventoryAction.SWAP_WITH_CURSOR -> { - val cursorItem = cursor - val currentItem = currentItem - - if (currentItem == null || filter(currentItem)) { - if (currentItem == null) { - _items.remove(slot(slot)) - } else { - if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) { - _items[slot(slot)] = currentItem - } - } - - isCancelled = false - } - - if (filter(cursorItem)) { - val previous = _items[slot(slot)] - if (previous == null || !previous.isSimilar(cursorItem)) { - if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) { - _items[slot(slot)] = cursorItem - } - } else { - if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) { - _items[slot(slot)] = - previous.clone().apply { amount += cursorItem.amount } - } - } - - isCancelled = false - } else { - isCancelled = true - } - } - - else -> { - isCancelled = true - return false - } - } - } - - println("current items: $_items") - - -// val itemToCheck = currentItem ?: cursor -// -// if (!filter(itemToCheck)) { -// event.isCancelled = true // Item wird blockiert -// return false -// } - - - // Wenn das geklickte Inventar unser Pane ist, fügen wir das Item hinzu oder entfernen es -// if (event.clickedInventory == event.inventory) { -// // Wenn das Slot-Item Luft ist, löschen wir es aus der Map -// if (itemToCheck.type == Material.AIR) { -// println("Removing item from slot $slot") -//// _items.remove(slot(slot)) // Stack wird entfernt -// } else { -// // Andernfalls fügen wir den Stack hinzu oder aktualisieren ihn -// println("Adding item to slot $slot with amount ${itemToCheck.amount}") -//// _items[slot(slot)] = itemToCheck // Gesamter Stack wird gespeichert -// } -// } else { -// // Wenn der Klick im Player-Inventar stattfindet, erlauben wir die Bewegung -// println("Clicked inventory is player inventory") -// } -// event.isCancelled = false // Erlaubt das Bewegen des Items - -// event.isCancelled = false // allow the user to move the item - - // if item was moved to our pane add it to the submitted items if it was removed from our pane remove it from the submitted items -// if (event.clickedInventory == event.inventory) { -// val length = min(length, maxLength) -// val height = min(height, maxHeight) -// val paneSlot = getSlot() -// -// val xPosition = paneSlot.getX(maxLength) -// val yPosition = paneSlot.getY(maxLength) -// val totalLength = inventoryComponent.length -// -// val adjustedSlot = -// slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY) -// val x = adjustedSlot % totalLength -// val y = adjustedSlot / totalLength - - - //this isn't our item -// if (x < 0 || x >= length || y < 0 || y >= height) { -// return false -// } - -// for (y in 0 until height) { -// for (x in 0 until length) { -// val adjustedX = x + paneOffsetX -// val adjustedY = y + paneOffsetY -// -// val adjustedSlot1 = slot - adjustedX - totalLength * adjustedY -// val adjustedX1 = adjustedSlot1 % totalLength -// val adjustedY1 = adjustedSlot1 / totalLength -// -// if ((adjustedX1 < 0) || (adjustedX1 >= length) || (adjustedY1 < 0) || (adjustedY1 >= height)) { -// continue -// } -// -// val item = inventoryComponent.getItem(adjustedX1, adjustedY1) -// -// // update the item in the map -// if (item != null) { -// _items[slot(adjustedX1, adjustedY1)] = item -// } else { -// _items.remove(slot(adjustedX1, adjustedY1)) -// } -// } -// -// } - - // walk through all slots in the pane -// for (i in 0 until length * height) { -// -// val x1 = i % length -// val y1 = i / length -// val adjustedSlot1 = slot - (x1 + paneOffsetX) - totalLength * (y1 + paneOffsetY) -// val adjustedX = adjustedSlot1 % totalLength -// val adjustedY = adjustedSlot1 / totalLength -// -// if ((x1 < 0) || (x1 >= length) || (y1 < 0) || (y1 >= height)) { -// continue -// } -// -// val item = inventoryComponent.getItem(adjustedX, adjustedY) -// -// // update the item in the map -// if (item != null) { -// _items[slot(x1, y1)] = item -// } else { -// _items.remove(slot(x1, y1)) -// } -// } - -// println("clicked inventory is inventory") -// _items.remove(slot(slot)) -// } else { -// println("clicked inventory is not inventory") -// _items[slot(slot)] = currentItem ?: cursor -// } - - return true - } - - - private fun isInPane( - slot: Int, - paneOffsetX: Int, - paneOffsetY: Int, - maxLength: Int, - maxHeight: Int, - inventoryComponent: InventoryComponent - ): Boolean { - val length = min(length, maxLength) - val height = min(height, maxHeight) - - val paneSlot = getSlot() - - val xPosition = paneSlot.getX(maxLength) - val yPosition = paneSlot.getY(maxLength) - - val totalLength: Int = inventoryComponent.length - - val adjustedSlot = - slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY) - - val x = adjustedSlot % totalLength - val y = adjustedSlot / totalLength - - return !(x < 0 || x >= length || y < 0 || y >= height) - } - - - override fun copy(): Pane { - val pane = SubmitItemPane(slot, length, height, filter, priority) - - pane._items.putAll(_items.map { (slot, item) -> slot to item.clone() }.toMap(pane._items)) - pane.isVisible = isVisible - pane.onClick = onClick - pane.uuid = uuid - - return pane - } - - override fun getItems(): MutableCollection = mutableListOf() - override fun getPanes(): MutableCollection = mutableListOf() - override fun clear() { - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt deleted file mode 100644 index f8e990ac1..000000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.inventory.types - -import com.github.stefvanschie.inventoryframework.adventuresupport.ComponentHolder -import com.github.stefvanschie.inventoryframework.gui.type.ChestGui -import com.github.stefvanschie.inventoryframework.gui.type.util.NamedGui -import dev.slne.surf.surfapi.bukkit.api.inventory.SinglePlayerGui -import dev.slne.surf.surfapi.bukkit.api.inventory.SurfGui -import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.MenuMarker -import net.kyori.adventure.text.Component -import org.bukkit.entity.Player -import org.jetbrains.annotations.Range - -@MenuMarker -open class SurfChestGui internal constructor( - title: Component, - rows: @Range(from = 2, to = 6) Int = 6, - override val parent: SurfGui? = null -) : - ChestGui(rows, ComponentHolder.of(title)), SurfGui { - override val gui: NamedGui - get() = this - - init { - check(rows in 2..6) { "Rows must be between 2 and 6" } - - this.setOnBottomClick { event -> event.isCancelled = true } - this.setOnBottomDrag { event -> event.isCancelled = true } - this.setOnTopClick { event -> event.isCancelled = true } - this.setOnTopDrag { event -> event.isCancelled = true } - } -} - -@MenuMarker -class SurfChestSinglePlayerGui internal constructor( - title: Component, - override val player: Player, - rows: @Range(from = 2, to = 6) Int = 6, - override val parent: SurfGui? = null, -) : - SurfChestGui(title, rows, parent), SinglePlayerGui \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-plugin-test/build.gradle.kts index cf8e0f80a..7776161e5 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/build.gradle.kts +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/build.gradle.kts @@ -50,7 +50,7 @@ tasks { minecraftVersion(findProperty("mcVersion") as String) downloadPlugins { -// hangar("CommandAPI", libs.versions.commandapi.get()) TODO: update to 1.21.11 when released + hangar("CommandAPI", libs.versions.commandapi.get()) modrinth("packetevents", libs.versions.packetevents.plugin.get() + "+spigot") } } @@ -59,6 +59,5 @@ tasks { tasks { shadowJar { val relocationPrefix: String by project - relocate("me.devnatan.inventoryframework", "$relocationPrefix.devnatan.inventoryframework") } } \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java index 2111d881d..ff7b5e3af 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java @@ -3,7 +3,7 @@ import dev.jorel.commandapi.CommandAPICommand; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.CommandExceptionTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.GlowingTest; -import dev.slne.surf.surfapi.bukkit.test.command.subcommands.InventoryTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.GuiTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.MaxStacksizeTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketEntityTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketLoreTest; @@ -15,31 +15,29 @@ import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SuspendCommandExecutionTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.ToastTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.VisualizerTest; -import dev.slne.surf.surfapi.bukkit.test.command.subcommands.gui.InventoryFrameworkTest; public class SurfApiTestCommand extends CommandAPICommand { - public SurfApiTestCommand() { - super("surfapitest"); + public SurfApiTestCommand() { + super("surfapitest"); - withPermission("surfapitest.use"); + withPermission("surfapitest.use"); - withSubcommands( - new PacketLoreTest("packetlore"), - new ScoreboardTest("scoreboard"), - new SmoothTimeSkip("smoothtimeskip"), - new PacketEntityTest("packetentity"), - new ReflectionTest("reflection"), - new PrefixConfigTest("prefixconfig"), - new CommandExceptionTest("commandexception"), - new InventoryFrameworkTest("inventoryframework"), - new MaxStacksizeTest("maxstacksize"), - new VisualizerTest("visualizer"), - new GlowingTest("glowing"), - new PaginationTest("pagination"), - new InventoryTest("inventory"), - new ToastTest(("toast")), - new SuspendCommandExecutionTest("suspendCommandExecution") - ); - } + withSubcommands( + new PacketLoreTest("packetlore"), + new ScoreboardTest("scoreboard"), + new SmoothTimeSkip("smoothtimeskip"), + new PacketEntityTest("packetentity"), + new ReflectionTest("reflection"), + new PrefixConfigTest("prefixconfig"), + new CommandExceptionTest("commandexception"), + new MaxStacksizeTest("maxstacksize"), + new VisualizerTest("visualizer"), + new GlowingTest("glowing"), + new PaginationTest("pagination"), + new ToastTest("toast"), + new SuspendCommandExecutionTest("suspendCommandExecution"), + new GuiTest("gui") + ); + } } diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java deleted file mode 100644 index 8ea0bdf58..000000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.command.subcommands.gui; - -import com.github.stefvanschie.inventoryframework.gui.type.ChestGui; -import com.github.stefvanschie.inventoryframework.pane.StaticPane; -import dev.jorel.commandapi.CommandAPICommand; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; - -public class InventoryFrameworkTest extends CommandAPICommand { - - public InventoryFrameworkTest(String commandName) { - super(commandName); - - executesPlayer((player, commandArguments) -> { - final ChestGui testGui = new ChestGui(1, "Test"); - testGui.setOnGlobalClick(event -> event.setCancelled(true)); - - final StaticPane pane = new StaticPane(0, 0, 9, 1); - pane.fillWith(ItemStack.of(Material.DIAMOND), event -> { - event.setCancelled(true); - event.getWhoClicked().sendMessage("You clicked on a diamond!"); - }); - - testGui.addPane(pane); - - testGui.show(player); - }); - } -} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt index e54569ad9..c628786fd 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt @@ -2,11 +2,9 @@ package dev.slne.surf.surfapi.bukkit.test import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import dev.jorel.commandapi.CommandAPI -import dev.slne.surf.surfapi.bukkit.api.inventory.framework.register import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution import dev.slne.surf.surfapi.bukkit.api.packet.listener.packetListenerApi import dev.slne.surf.surfapi.bukkit.test.command.SurfApiTestCommand -import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInventoryView import dev.slne.surf.surfapi.bukkit.test.command.subcommands.reflection.Reflection import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener @@ -20,7 +18,6 @@ class BukkitPluginMain : SuspendingJavaPlugin() { surfComponentApi.load(this) packetListenerApi.registerListeners(ChatListener()) - TestInventoryView.register() } override suspend fun onEnableAsync() { diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt new file mode 100644 index 000000000..2c6cdd982 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.bukkit.test.command.subcommands + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.executors.PlayerCommandExecutor +import dev.slne.surf.surfapi.bukkit.test.gui.CounterGuiView +import dev.slne.surf.surfapi.bukkit.test.gui.PaginatedShopGuiView + +/** + * Command to test the new GUI framework. + */ +class GuiTest(name: String) : CommandAPICommand(name) { + init { + withPermission("surfapitest.use") + + // Counter subcommand + withSubcommands( + CommandAPICommand("counter") + .executesPlayer(PlayerCommandExecutor { player, _ -> + CounterGuiView.open(player) + }), + + // Shop subcommand + CommandAPICommand("shop") + .executesPlayer(PlayerCommandExecutor { player, _ -> + PaginatedShopGuiView().open(player) + }) + ) + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/InventoryTest.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/InventoryTest.kt deleted file mode 100644 index 681cea440..000000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/InventoryTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.command.subcommands - -import dev.jorel.commandapi.CommandAPICommand -import dev.jorel.commandapi.kotlindsl.playerExecutor -import dev.slne.surf.surfapi.bukkit.api.inventory.framework.open -import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInventoryView - -class InventoryTest(name: String) : CommandAPICommand(name) { - init { - playerExecutor { player, _ -> - TestInventoryView.open(player) - } - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/inventory/TestInventoryView.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/inventory/TestInventoryView.kt deleted file mode 100644 index a15424c71..000000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/inventory/TestInventoryView.kt +++ /dev/null @@ -1,42 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory - -import dev.slne.surf.surfapi.bukkit.api.builder.buildItem -import dev.slne.surf.surfapi.bukkit.api.builder.displayName -import dev.slne.surf.surfapi.bukkit.api.dialog.noticeDialog -import dev.slne.surf.surfapi.bukkit.api.inventory.framework.titleBuilder -import dev.slne.surf.surfapi.core.api.messages.adventure.text -import me.devnatan.inventoryframework.View -import me.devnatan.inventoryframework.ViewConfigBuilder -import me.devnatan.inventoryframework.ViewType -import me.devnatan.inventoryframework.context.CloseContext -import me.devnatan.inventoryframework.context.RenderContext -import org.bukkit.inventory.ItemType - - -object TestInventoryView : View() { - private val counterState = mutableState(0) - - override fun onInit(config: ViewConfigBuilder) { - config.titleBuilder { primary("Test Inventory View") } - config.type(ViewType.CHEST) - config.size(5) - config.cancelInteractions() - } - - override fun onFirstRender(render: RenderContext) { - render.slot(1, buildItem(ItemType.DIAMOND) { - displayName { - text("Diamond") - } - }).onClick(counterState::increment) - } - - override fun onClose(close: CloseContext) { - close.player.showDialog( - noticeDialog( - text("Du hast das Inventar geschlossen!"), - text("Du hast den Diamanten ${counterState.get(close)} mal geklickt!") - ) - ) - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/CounterGuiView.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/CounterGuiView.kt new file mode 100644 index 000000000..2cbe5450b --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/CounterGuiView.kt @@ -0,0 +1,220 @@ +package dev.slne.surf.surfapi.bukkit.test.gui + +import com.github.shynixn.mccoroutine.folia.launch +import dev.slne.surf.surfapi.bukkit.api.builder.ItemStack +import dev.slne.surf.surfapi.bukkit.api.builder.buildLore +import dev.slne.surf.surfapi.bukkit.api.builder.displayName +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.context.InitializeContext +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.component +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.dynamicComponent +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.slot +import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.bukkit.test.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.plain +import dev.slne.surf.surfapi.core.api.messages.adventure.text +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Material +import org.bukkit.event.inventory.InventoryType + +/** + * Example GUI demonstrating MutableProp usage with a counter. + * The counter is shared across all viewers (global state). + */ +object CounterGuiView : GuiView() { + private val counterProp = Prop.Mutable("counter", 0) + private val counterDisplayRef = Ref() + + enum class TitleColor(val color: NamedTextColor) { + RED(NamedTextColor.RED), + GREEN(NamedTextColor.GREEN), + BLUE(NamedTextColor.BLUE), + YELLOW(NamedTextColor.YELLOW), + PURPLE(NamedTextColor.LIGHT_PURPLE); + + fun next(): TitleColor { + val values = entries + + return values[(this.ordinal + 1) % values.size] + } + } + + private var titleColor = TitleColor.entries.random() + + override fun onInit(context: InitializeContext) { + context.config().type = InventoryType.CHEST + context.config().rows = 3 + context.config().title = text("Counter Gui", titleColor.color) + + context.slot( + component( + Slot.at(0, 0), + GuiItem.of(ItemStack(Material.NAME_TAG) { + displayName { + success("Randomize Title") + } + }) + ) { + onClick = { + val oldTitle = view.title.plain() + val nextColor = titleColor.next() + titleColor = nextColor + + view.modifyConfig { config -> + config.title = text(oldTitle, titleColor.color) + } + } + } + ) + + // Counter display at top center + context.slot( + dynamicComponent( + Slot.at(4, 0), + renderer = { ctx -> + val count = runBlocking { counterProp.get() } ?: 0 + + GuiItem.of(ItemStack(Material.PAPER) { + displayName { + info("Count: $count") + } + }) + } + ) { + ref = counterDisplayRef + }) + + // Increment +1 button + context.slot( + component( + Slot.at(2, 1), + GuiItem.of(ItemStack(Material.LIME_CONCRETE) { + displayName { + success("+1") + } + + buildLore { + line { + gray("Click to increment by 1") + } + } + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 + + counterProp.set(current + 1) + counterDisplayRef.update() + } + } + }) + + // Increment +10 button + context.slot( + component( + Slot.at(3, 1), + GuiItem.of(ItemStack(Material.GREEN_CONCRETE) { + displayName { + success("+10") + } + + buildLore { + line { + gray("Click to increment by 10") + } + } + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 + + counterProp.set(current + 10) + counterDisplayRef.update() + } + } + }) + + // Decrement -1 button + context.slot( + component( + Slot.at(5, 1), + GuiItem.of(ItemStack(Material.PINK_CONCRETE) { + displayName { + error("-1") + } + + buildLore { + line { + gray("Click to decrement by 1") + } + } + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 + + counterProp.set(current - 1) + counterDisplayRef.update() + } + } + }) + + // Decrement -10 button + context.slot( + component( + Slot.at(6, 1), + GuiItem.of(ItemStack(Material.RED_CONCRETE) { + displayName { + error("-10") + } + + buildLore { + line { + gray("Click to decrement by 10") + } + } + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 + + counterProp.set(current - 10) + counterDisplayRef.update() + } + } + }) + + // Reset button + context.slot( + component( + Slot.at(4, 2), + GuiItem.of(ItemStack(Material.BARRIER) { + displayName { + error("Reset Counter") + } + + buildLore { + line { + gray("Click to reset the counter to 0") + } + } + }) + ) { + onClick = { + plugin.launch { + counterProp.set(0) + counterDisplayRef.update() + } + } + }) + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/PaginatedShopGuiView.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/PaginatedShopGuiView.kt new file mode 100644 index 000000000..f71b59169 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/PaginatedShopGuiView.kt @@ -0,0 +1,116 @@ +package dev.slne.surf.surfapi.bukkit.test.gui + +import dev.slne.surf.surfapi.bukkit.api.builder.ItemStack +import dev.slne.surf.surfapi.bukkit.api.builder.buildItem +import dev.slne.surf.surfapi.bukkit.api.builder.buildLore +import dev.slne.surf.surfapi.bukkit.api.builder.displayName +import dev.slne.surf.surfapi.bukkit.api.gui.GuiItem +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.component.components.PaginationComponent +import dev.slne.surf.surfapi.bukkit.api.gui.context.InitializeContext +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.dynamicComponent +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.slot +import dev.slne.surf.surfapi.bukkit.api.gui.props.ViewerProp +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import dev.slne.surf.surfapi.core.api.messages.adventure.text +import dev.slne.surf.surfapi.core.api.util.toObjectList +import org.bukkit.Material +import org.bukkit.Registry +import org.bukkit.event.inventory.InventoryType +import org.bukkit.inventory.ItemType + +/** + * Example GUI demonstrating ViewerMutableProp and PaginationComponent. + * Each player has their own coins (viewer-specific state). + */ +class PaginatedShopGuiView : GuiView() { + private val coinsProp = ViewerProp.Mutable("coins", 1000) + private val coinsDisplayRef = Ref() + + // Shop items - lazy to avoid initialization issues + private val shopItems by lazy { + Registry.ITEM.map { + ShopItem(it, it.key.asString(), (10..500).random()) + }.shuffled().toObjectList() + } + + // Pagination component - includes built-in navigation buttons + // The component will use 4 rows: 3 for items + 1 for buttons + // Items area: rows 1-3, Button row: row 4 + private val paginationComponent = PaginationComponent( + startSlot = Slot.at(1, 1), // Column 0, Row 1 + endSlot = Slot.at(7, 4), // Column 8, Row 4 (9 cols x 4 rows = 36 slots total) + items = { shopItems }, + itemRenderer = { item, ctx -> + GuiItem.of(buildItem(item.type) { + displayName { + gold(item.name) + } + buildLore { + line { + gray("Price: ") + yellow("${item.price} coins") + } + line { + gray("Click to purchase") + } + } + }) + }, + onItemClick = { item, ctx -> + val coins = coinsProp.get(ctx.player) + if (coins != null && coins >= item.price) { + coinsProp.set(ctx.player, coins - item.price) + ctx.player.sendText { + success("Purchased ") + variableValue(item.name) + success(" for ") + gold("${item.price} coins") + } + coinsDisplayRef.update(ctx.player) + } else { + ctx.player.sendText { + error("You do not have enough coins to purchase ") + variableValue(item.name) + error(".") + } + } + } + // Navigation buttons will be automatically created and centered in the last row + ) + + override fun onInit(context: InitializeContext) { + context.config().type = InventoryType.CHEST + context.config().rows = 6 + context.config().title = text("Shop") + + context.slot( + dynamicComponent( + Slot.at(4, 0), + renderer = { ctx -> + val coins = coinsProp.get(ctx.player) ?: 0 + + GuiItem.of(ItemStack(Material.GOLD_INGOT) { + displayName { + gold("Coins: ") + yellow("$coins") + } + }) + } + ) { + ref = coinsDisplayRef + }) + + // Render pagination component (navigation buttons are now built-in as children) + context.slot(paginationComponent) + } + + private data class ShopItem( + val type: ItemType, + val name: String, + val price: Int + ) +} diff --git a/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts index 22b1a6758..31018685f 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts +++ b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts @@ -99,7 +99,6 @@ tasks.generatePaperPluginDescription { tasks { shadowJar { val relocationPrefix: String by project - relocate("me.devnatan.inventoryframework", "$relocationPrefix.devnatan.inventoryframework") relocate("net.kyori.adventure.nbt", "$relocationPrefix.kyori.nbt") } } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/BukkitInstance.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/BukkitInstance.kt index cf708f644..1154281c1 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/BukkitInstance.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/BukkitInstance.kt @@ -2,7 +2,6 @@ package dev.slne.surf.surfapi.bukkit.server import dev.slne.surf.surfapi.bukkit.api.surfBukkitApi import dev.slne.surf.surfapi.bukkit.server.impl.SurfBukkitApiImpl -import dev.slne.surf.surfapi.bukkit.server.inventory.framework.InventoryLoader import dev.slne.surf.surfapi.bukkit.server.listener.ListenerManager import dev.slne.surf.surfapi.bukkit.server.packet.PacketApiLoader import dev.slne.surf.surfapi.bukkit.server.reflection.Reflection @@ -15,15 +14,14 @@ object BukkitInstance : CoreInstance() { initObjects() PacketApiLoader.onLoad() - InventoryLoader.load() } override suspend fun onEnable() { super.onEnable() PacketApiLoader.onEnable() - InventoryLoader.enable() ListenerManager.registerListeners() + (surfBukkitApi as SurfBukkitApiImpl).onEnable() } @@ -32,7 +30,6 @@ object BukkitInstance : CoreInstance() { ListenerManager.unregisterListeners() PacketApiLoader.onDisable() - InventoryLoader.disable() } private fun initObjects() { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/GuiViewListener.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/GuiViewListener.kt new file mode 100644 index 000000000..3e2772709 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/GuiViewListener.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.surfapi.bukkit.server.gui.view + +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewManager +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent + +/** + * Global event listener for all GUI views. + */ +object GuiViewListener : Listener { + @EventHandler + fun onInventoryClick(event: InventoryClickEvent) { + val player = event.whoClicked as? Player ?: return + val view = ViewManager.getActiveView(player) ?: return + + view.handleClick(player, event) + } + + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + val player = event.player as? Player ?: return + val view = ViewManager.getActiveView(player) ?: return + + view.close(player) + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/ViewManagerImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/ViewManagerImpl.kt new file mode 100644 index 000000000..878d35c29 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/ViewManagerImpl.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.surfapi.bukkit.server.gui.view + +import com.github.benmanes.caffeine.cache.Caffeine +import com.google.auto.service.AutoService +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewManager +import org.bukkit.entity.Player +import java.util.* + +@AutoService(ViewManager::class) +class ViewManagerImpl : ViewManager { + private val activePerPlayer = Caffeine.newBuilder() + .maximumSize(10_000) + .build() + + override fun getActiveView(player: Player): GuiView? { + return activePerPlayer.getIfPresent(player.uniqueId) + } + + override fun setActiveView( + player: Player, + view: GuiView + ) { + activePerPlayer.put(player.uniqueId, view) + } + + override fun removeActiveView(player: Player) { + activePerPlayer.invalidate(player.uniqueId) + } + + override fun toString(): String { + return "ViewManagerImpl(activePerPlayer=$activePerPlayer)" + } +} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/framework/ViewFrameAccessorImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/framework/ViewFrameAccessorImpl.kt deleted file mode 100644 index da04eac5d..000000000 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/framework/ViewFrameAccessorImpl.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.server.impl.inventory.framework - -import com.google.auto.service.AutoService -import dev.slne.surf.surfapi.bukkit.api.inventory.framework.ViewFrameAccessor -import dev.slne.surf.surfapi.bukkit.server.inventory.framework.InventoryLoader -import me.devnatan.inventoryframework.ViewFrame - -@AutoService(ViewFrameAccessor::class) -class ViewFrameAccessorImpl: ViewFrameAccessor { - override fun viewFrame(): ViewFrame { - return InventoryLoader.viewFrame - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt deleted file mode 100644 index 7f9f12282..000000000 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.server.inventory.framework - -import dev.slne.surf.surfapi.bukkit.server.plugin -import me.devnatan.inventoryframework.ViewFrame - -object InventoryLoader { - lateinit var viewFrame: ViewFrame - - fun load() { - viewFrame = ViewFrame.create(plugin) - } - - fun enable() { - viewFrame.register() - } - - fun disable() { - viewFrame.unregister() - } -} \ No newline at end of file diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt index a6f45b378..206835fda 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt @@ -1,6 +1,7 @@ package dev.slne.surf.surfapi.bukkit.server.listener import dev.slne.surf.surfapi.bukkit.api.event.register +import dev.slne.surf.surfapi.bukkit.server.gui.view.GuiViewListener import dev.slne.surf.surfapi.bukkit.server.impl.glow.GlowingListener import dev.slne.surf.surfapi.bukkit.server.impl.visualizer.visualizer.VisualizerListener import dev.slne.surf.surfapi.bukkit.server.plugin @@ -14,6 +15,7 @@ object ListenerManager { Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord") VisualizerListener.register() GlowingListener.register() + GuiViewListener.register() } /**