From 70ed598e38621809a300f46354fc8387d66454ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:55:25 +0000 Subject: [PATCH 01/92] Initial plan From 6bd0222013bb6f267fcc1d8cea3632b60c182efe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:01:22 +0000 Subject: [PATCH 02/92] Add core GUI framework: props, components, views, context, refs, and DSL Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 171 +++++++++++++ .../bukkit/api/gui/context/ViewContext.kt | 149 ++++++++++++ .../bukkit/api/gui/dsl/ComponentDsl.kt | 168 +++++++++++++ .../bukkit/api/gui/props/PaginationProp.kt | 73 ++++++ .../surf/surfapi/bukkit/api/gui/props/Prop.kt | 133 +++++++++++ .../surf/surfapi/bukkit/api/gui/ref/Ref.kt | 48 ++++ .../surfapi/bukkit/api/gui/view/GuiView.kt | 221 +++++++++++++++++ .../server/gui/context/BukkitContexts.kt | 224 ++++++++++++++++++ .../bukkit/server/gui/view/BukkitGuiView.kt | 173 ++++++++++++++ 9 files changed, 1360 insertions(+) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/Component.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ViewContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/Prop.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt 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 00000000..b25a692b --- /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,171 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component + +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.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import org.bukkit.inventory.ItemStack +import kotlin.time.Duration + +/** + * Base interface for all GUI components. + * Components follow React-like lifecycle principles. + */ +abstract class Component { + /** + * The parent component, if any. + */ + var parent: Component? = null + internal set + + /** + * The children of this component. + */ + private val _children = mutableListOf() + val children: List get() = _children.toList() + + /** + * The view this component belongs to. + */ + lateinit var view: GuiView + internal set + + /** + * Update interval for this component, if any. + */ + open val updateInterval: Duration? = null + + /** + * Whether this component is currently mounted. + */ + var isMounted: Boolean = false + internal set + + /** + * Props accessible by this component. + * Children can access parent props. + */ + protected open val props: Map> = emptyMap() + + /** + * Ref attached to this component, if any. + */ + internal var attachedRef: Ref? = null + + /** + * Called when the component is mounted. + */ + open fun onMount(context: LifecycleContext) {} + + /** + * Called when the component is unmounted. + */ + open fun onUnmount(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 an ItemStack. + */ + abstract fun render(context: ViewContext): ItemStack + + /** + * 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(): Map> { + val allProps = mutableMapOf>() + parent?.getAllProps()?.let { allProps.putAll(it) } + allProps.putAll(props) + return allProps + } + + /** + * Trigger an update of this component. + */ + fun update() { + if (isMounted) { + view.updateComponent(this) + } + } + + /** + * Trigger an update of this component and all children. + */ + fun updateWithChildren() { + update() + children.forEach { it.updateWithChildren() } + } +} + +/** + * Simple item component that renders a static item. + */ +open class ItemComponent( + private val item: ItemStack, + private val clickHandler: (ClickContext.() -> Unit)? = null +) : Component() { + + override fun render(context: ViewContext): ItemStack = item + + override fun onClick(context: ClickContext) { + clickHandler?.invoke(context) + } +} + +/** + * Dynamic component that renders based on a callback. + */ +open class DynamicComponent( + private val renderer: (ViewContext) -> ItemStack, + private val clickHandler: (ClickContext.() -> Unit)? = null +) : Component() { + + override fun render(context: ViewContext): ItemStack = renderer(context) + + override fun onClick(context: ClickContext) { + clickHandler?.invoke(context) + } +} + +/** + * Container component that can hold multiple child components. + */ +abstract class ContainerComponent : Component() { + /** + * Layout children within the container. + * Returns a map of slot positions to components. + */ + abstract fun layout(context: ViewContext): Map + + override fun render(context: ViewContext): ItemStack { + // Container doesn't render itself, only lays out children + return ItemStack.empty() + } +} 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 00000000..456dcf7a --- /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,149 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.props.PropContext +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack +import java.util.UUID + +/** + * 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 + + /** + * The unique ID of the viewer. + */ + val viewerId: UUID + get() = player.uniqueId + + /** + * Props context for accessing prop values. + */ + val propContext: PropContext + get() = PropContext(viewerId, player) + + /** + * Get a prop value. + */ + fun getProp(prop: Prop): T = prop.get(propContext) + + /** + * 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() +} + +/** + * Context for click events. + */ +interface ClickContext : ViewContext { + /** + * The click event. + */ + val event: InventoryClickEvent + + /** + * The clicked item. + */ + val item: ItemStack? + get() = event.currentItem + + /** + * The slot that was clicked. + */ + val slot: Int + get() = event.slot + + /** + * The component that was clicked, if any. + */ + val component: Component? +} + +/** + * Context for render operations. + */ +interface RenderContext : ViewContext { + /** + * Render a component at the specified slot. + */ + fun renderComponent(slot: Int, component: Component) + + /** + * Clear a slot. + */ + fun clearSlot(slot: Int) + + /** + * Set an item at a slot without a component. + */ + fun setItem(slot: Int, item: ItemStack) +} + +/** + * Context for lifecycle events. + */ +interface LifecycleContext : ViewContext { + /** + * The type of lifecycle event. + */ + val eventType: LifecycleEventType +} + +/** + * Types of lifecycle events. + */ +enum class LifecycleEventType { + MOUNT, + UNMOUNT, + UPDATE, + FIRST_RENDER, + OPEN, + CLOSE, + RESUME +} + +/** + * 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/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 00000000..c408aca5 --- /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,168 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.dsl + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.component.DynamicComponent +import dev.slne.surf.surfapi.bukkit.api.gui.component.ItemComponent +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.RenderContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import dev.slne.surf.surfapi.bukkit.api.gui.props.* +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import org.bukkit.inventory.ItemStack +import kotlin.time.Duration + +/** + * DSL marker for component building. + */ +@DslMarker +annotation class ComponentDsl + +/** + * Builder for creating components. + */ +@ComponentDsl +class ComponentBuilder { + var updateInterval: Duration? = null + var ref: Ref? = null + var onMount: (LifecycleContext.() -> Unit)? = null + var onUnmount: (LifecycleContext.() -> Unit)? = null + var onUpdate: (LifecycleContext.() -> Unit)? = null + var onClick: (ClickContext.() -> Unit)? = null + + private val _props = mutableMapOf>() + + /** + * Add a prop to this component. + */ + fun prop(name: String, prop: Prop) { + _props[name] = prop + } + + /** + * Build the component. + */ + internal fun build(renderer: (ViewContext) -> ItemStack): Component { + return object : DynamicComponent(renderer, onClick) { + override val updateInterval: Duration? = this@ComponentBuilder.updateInterval + override val props: Map> = _props + + override fun onMount(context: LifecycleContext) { + super.onMount(context) + this@ComponentBuilder.onMount?.invoke(context) + } + + override fun onUnmount(context: LifecycleContext) { + super.onUnmount(context) + this@ComponentBuilder.onUnmount?.invoke(context) + } + + override fun onUpdate(context: LifecycleContext) { + super.onUpdate(context) + this@ComponentBuilder.onUpdate?.invoke(context) + } + }.also { component -> + ref?.set(component) + component.attachedRef = ref as? Ref + } + } +} + +/** + * Create a component with a static item. + */ +fun component( + item: ItemStack, + builder: ComponentBuilder.() -> Unit = {} +): Component { + val componentBuilder = ComponentBuilder() + componentBuilder.builder() + return componentBuilder.build { item } +} + +/** + * Create a component with a dynamic renderer. + */ +fun dynamicComponent( + renderer: (ViewContext) -> ItemStack, + builder: ComponentBuilder.() -> Unit = {} +): Component { + val componentBuilder = ComponentBuilder() + componentBuilder.builder() + return componentBuilder.build(renderer) +} + +/** + * DSL for creating props. + */ +@ComponentDsl +class PropsBuilder { + private val _props = mutableMapOf>() + + /** + * Create an immutable prop. + */ + fun immutable(name: String, value: T, scope: PropScope = PropScope.VIEWER): ImmutableProp { + return ImmutableProp(name, value, scope).also { _props[name] = it } + } + + /** + * Create a mutable prop. + */ + fun mutable(name: String, initialValue: T, scope: PropScope = PropScope.VIEWER): MutableProp { + return MutableProp(name, initialValue, scope).also { _props[name] = it } + } + + /** + * Create a computed prop. + */ + fun computed(name: String, scope: PropScope = PropScope.VIEWER, compute: (PropContext) -> T): ComputedProp { + return ComputedProp(name, compute, scope).also { _props[name] = it } + } + + /** + * Create a lazy prop. + */ + fun lazy(name: String, mutable: Boolean = false, scope: PropScope = PropScope.VIEWER, initializer: (PropContext) -> T): LazyProp { + return LazyProp(name, initializer, mutable, scope).also { _props[name] = it } + } + + /** + * Create a pagination prop. + */ + fun pagination(name: String = "pagination", pageSize: Int = 9, scope: PropScope = PropScope.VIEWER, items: () -> List): PaginationProp { + return PaginationProp(name, items, pageSize, scope).also { _props[name] = it } + } + + /** + * Get all props. + */ + internal fun build(): Map> = _props.toMap() +} + +/** + * Create props using DSL. + */ +fun props(builder: PropsBuilder.() -> Unit): Map> { + val propsBuilder = PropsBuilder() + propsBuilder.builder() + return propsBuilder.build() +} + +/** + * DSL for rendering components in a view. + */ +@ComponentDsl +fun RenderContext.slot(slot: Int, component: Component) { + renderComponent(slot, component) +} + +/** + * DSL for rendering components in a view with item. + */ +@ComponentDsl +fun RenderContext.slot(slot: Int, item: ItemStack, onClick: (ClickContext.() -> Unit)? = null) { + val component = ItemComponent(item, onClick) + renderComponent(slot, component) +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt new file mode 100644 index 00000000..54f16617 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt @@ -0,0 +1,73 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +/** + * Default pagination prop for managing paginated content in GUIs. + */ +class PaginationProp( + override val name: String = "pagination", + private val items: () -> List, + private val pageSize: Int = 9, + private val scope: PropScope = PropScope.VIEWER +) : Prop> { + + private val currentPages = mutableMapOf() + + override fun get(context: PropContext): PaginationState { + val currentPage = currentPages.getOrPut(context.viewerId) { 0 } + val allItems = items() + val totalPages = (allItems.size + pageSize - 1) / pageSize + + val startIndex = currentPage * pageSize + val endIndex = minOf(startIndex + pageSize, allItems.size) + val pageItems = if (startIndex < allItems.size) { + allItems.subList(startIndex, endIndex) + } else { + emptyList() + } + + return PaginationState( + items = pageItems, + currentPage = currentPage, + totalPages = totalPages, + pageSize = pageSize, + totalItems = allItems.size, + hasNextPage = currentPage < totalPages - 1, + hasPreviousPage = currentPage > 0 + ) + } + + fun nextPage(context: PropContext) { + val current = currentPages.getOrPut(context.viewerId) { 0 } + val state = get(context) + if (state.hasNextPage) { + currentPages[context.viewerId] = current + 1 + } + } + + fun previousPage(context: PropContext) { + val current = currentPages.getOrPut(context.viewerId) { 0 } + if (current > 0) { + currentPages[context.viewerId] = current - 1 + } + } + + fun setPage(context: PropContext, page: Int) { + val state = get(context) + if (page in 0 until state.totalPages) { + currentPages[context.viewerId] = page + } + } +} + +/** + * State object for pagination. + */ +data class PaginationState( + val items: List, + val currentPage: Int, + val totalPages: Int, + val pageSize: Int, + val totalItems: Int, + val hasNextPage: Boolean, + val hasPreviousPage: Boolean +) 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 00000000..da4b3fab --- /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,133 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import org.bukkit.entity.Player +import java.util.UUID +import kotlin.reflect.KProperty + +/** + * Base interface for all props in the GUI framework. + * Props can be scoped as global (shared across all viewers) or viewer-specific (isolated per viewer). + */ +sealed interface Prop { + /** + * Gets the value of this prop for the given context. + */ + fun get(context: PropContext): T + + /** + * The name of this prop. + */ + val name: String +} + +/** + * Context for prop evaluation, contains information about the current viewer. + */ +data class PropContext( + val viewerId: UUID, + val viewer: Player? +) + +/** + * Defines the scope of a prop. + */ +enum class PropScope { + /** Shared across all viewers */ + GLOBAL, + /** Isolated per viewer */ + VIEWER +} + +/** + * Immutable prop - always available and immutable after initialization. + */ +class ImmutableProp( + override val name: String, + private val value: T, + private val scope: PropScope = PropScope.VIEWER +) : Prop { + override fun get(context: PropContext): T = value + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value +} + +/** + * Mutable prop - always available and mutable. + */ +class MutableProp( + override val name: String, + initialValue: T, + private val scope: PropScope = PropScope.VIEWER +) : Prop { + private val globalValue = ThreadLocal.withInitial { initialValue } + private val viewerValues = mutableMapOf() + + override fun get(context: PropContext): T { + return when (scope) { + PropScope.GLOBAL -> globalValue.get() + PropScope.VIEWER -> viewerValues.getOrPut(context.viewerId) { globalValue.get() } + } + } + + fun set(context: PropContext, value: T) { + when (scope) { + PropScope.GLOBAL -> globalValue.set(value) + PropScope.VIEWER -> viewerValues[context.viewerId] = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = globalValue.get() + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + globalValue.set(value) + } +} + +/** + * Computed prop - accepts a callback that computes the value. + */ +class ComputedProp( + override val name: String, + private val compute: (PropContext) -> T, + private val scope: PropScope = PropScope.VIEWER +) : Prop { + override fun get(context: PropContext): T = compute(context) +} + +/** + * Lazy prop - gets available when accessed, using a callback. + */ +class LazyProp( + override val name: String, + private val initializer: (PropContext) -> T, + private val mutable: Boolean = false, + private val scope: PropScope = PropScope.VIEWER +) : Prop { + private val globalValue = lazy { initializer(PropContext(UUID.randomUUID(), null)) } + private val viewerValues = mutableMapOf() + private val initialized = mutableSetOf() + + override fun get(context: PropContext): T { + return when (scope) { + PropScope.GLOBAL -> { + if (mutable && context.viewerId in initialized) { + viewerValues.getOrPut(context.viewerId) { globalValue.value } + } else { + globalValue.value + } + } + PropScope.VIEWER -> { + viewerValues.getOrPut(context.viewerId) { + initialized.add(context.viewerId) + initializer(context) + } + } + } + } + + fun set(context: PropContext, value: T) { + if (!mutable) { + throw UnsupportedOperationException("Cannot set immutable lazy prop") + } + viewerValues[context.viewerId] = value + } +} 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 00000000..0368efb5 --- /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,48 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.ref + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +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. + */ + internal fun set(component: T?) { + reference.set(component) + } + + /** + * Updates the referenced component. + */ + fun update() { + current?.update() + } + + /** + * Checks if the ref has a current value. + */ + fun isSet(): Boolean = current != null +} + +/** + * 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 00000000..a502412a --- /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,221 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.view + +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.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.props.PropContext +import net.kyori.adventure.text.Component as AdventureComponent +import org.bukkit.entity.Player +import kotlin.time.Duration + +/** + * Configuration for a GUI view. + */ +data class ViewConfig( + var title: AdventureComponent = AdventureComponent.text("GUI"), + var size: Int = 54, // 6 rows by default + var cancelOnClick: Boolean = true, + var closeOnClickOutside: Boolean = false +) + +/** + * 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 + + /** + * Props for this view. + */ + protected open val props: Map> = emptyMap() + + /** + * Update interval for the entire view. + */ + open val updateInterval: Duration? = null + + /** + * Components in this view, mapped by slot. + */ + private val _components = mutableMapOf() + val components: Map get() = _components.toMap() + + /** + * Whether this view has been initialized. + */ + private var initialized = false + + /** + * Whether this view has been rendered for the first time. + */ + private val firstRenderPerViewer = mutableSetOf() + + /** + * Current viewers of this view. + */ + private val viewers = mutableMapOf() + + /** + * Configuration for this view. + */ + val config = ViewConfig() + + /** + * Initialize the view configuration. + * Called once when the view is first created. + */ + open fun onInit(config: ViewConfig) {} + + /** + * Called when the view is opened for a player. + */ + open fun onOpen(context: ViewContext) {} + + /** + * Called the first time the view is rendered for a player. + */ + open fun onFirstRender(context: RenderContext) {} + + /** + * 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) { + onInit(config) + initialized = true + } + } + + /** + * Open this view for a player. + */ + open fun open(player: Player) { + ensureInitialized() + viewers[player.uniqueId] = player + + val context = createViewContext(player) + onOpen(context) + + if (player.uniqueId !in firstRenderPerViewer) { + val renderContext = createRenderContext(player) + onFirstRender(renderContext) + firstRenderPerViewer.add(player.uniqueId) + } + } + + /** + * Close this view for a player. + */ + open fun close(player: Player) { + val context = createViewContext(player) + onClose(context) + viewers.remove(player.uniqueId) + } + + /** + * Update the view for all viewers. + */ + fun update() { + viewers.values.forEach { player -> + val context = createViewContext(player) + onUpdate(context) + } + } + + /** + * Update a specific component. + */ + internal fun updateComponent(component: Component) { + viewers.values.forEach { player -> + val context = createViewContext(player) + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + component.onUpdate(lifecycleContext) + } + } + + /** + * Add a component at a slot. + */ + fun addComponent(slot: Int, component: Component) { + component.view = this + _components[slot] = component + + if (!component.isMounted) { + viewers.values.forEach { player -> + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.MOUNT) + component.onMount(lifecycleContext) + } + component.isMounted = true + } + } + + /** + * Remove a component from a slot. + */ + fun removeComponent(slot: Int) { + _components[slot]?.let { component -> + viewers.values.forEach { player -> + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UNMOUNT) + component.onUnmount(lifecycleContext) + } + component.isMounted = false + } + _components.remove(slot) + } + + /** + * Get a prop value for a specific player. + */ + fun getProp(player: Player, prop: Prop): T { + val context = PropContext(player.uniqueId, player) + return prop.get(context) + } + + /** + * Create a view context for a player. + */ + protected abstract fun createViewContext(player: Player): ViewContext + + /** + * Create a render context for a player. + */ + protected abstract fun createRenderContext(player: Player): RenderContext + + /** + * Create a lifecycle context for a player. + */ + protected abstract fun createLifecycleContext(player: Player, eventType: LifecycleEventType): LifecycleContext + + /** + * Create a resume context for a player. + */ + protected abstract fun createResumeContext(player: Player, origin: GuiView?): ResumeContext +} + +/** + * 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-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt new file mode 100644 index 00000000..bc980808 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt @@ -0,0 +1,224 @@ +package dev.slne.surf.surfapi.bukkit.server.gui.context + +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.view.GuiView +import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack + +/** + * Bukkit implementation of ViewContext. + */ +internal class BukkitViewContext( + override val view: GuiView, + override val player: Player +) : ViewContext { + + override fun navigateTo(view: GuiView, passProps: Boolean) { + if (view is BukkitGuiView) { + view.parent = this.view + player.closeInventory() + view.open(player) + } + } + + override fun navigateBack() { + val parent = view.parent + if (parent is BukkitGuiView) { + player.closeInventory() + val resumeContext = parent.createResumeContext(player, view) + parent.onResume(resumeContext) + parent.open(player) + } else { + close() + } + } + + override fun close() { + player.closeInventory() + } + + override fun update() { + if (view is BukkitGuiView) { + view.refreshInventory(player) + } + } +} + +/** + * Bukkit implementation of ClickContext. + */ +internal class BukkitClickContext( + override val view: GuiView, + override val player: Player, + override val event: InventoryClickEvent, + override val component: Component? +) : ClickContext { + + override fun navigateTo(view: GuiView, passProps: Boolean) { + if (view is BukkitGuiView) { + view.parent = this.view + player.closeInventory() + view.open(player) + } + } + + override fun navigateBack() { + val parent = view.parent + if (parent is BukkitGuiView) { + player.closeInventory() + val resumeContext = parent.createResumeContext(player, view) + parent.onResume(resumeContext) + parent.open(player) + } else { + close() + } + } + + override fun close() { + player.closeInventory() + } + + override fun update() { + if (view is BukkitGuiView) { + view.refreshInventory(player) + } + } +} + +/** + * Bukkit implementation of RenderContext. + */ +internal class BukkitRenderContext( + override val view: GuiView, + override val player: Player, + private val bukkitView: BukkitGuiView +) : RenderContext { + + override fun renderComponent(slot: Int, component: Component) { + bukkitView.addComponent(slot, component) + } + + override fun clearSlot(slot: Int) { + bukkitView.removeComponent(slot) + } + + override fun setItem(slot: Int, item: ItemStack) { + val inventory = player.openInventory.topInventory + if (slot in 0 until inventory.size) { + inventory.setItem(slot, item) + } + } + + override fun navigateTo(view: GuiView, passProps: Boolean) { + if (view is BukkitGuiView) { + view.parent = this.view + player.closeInventory() + view.open(player) + } + } + + override fun navigateBack() { + val parent = view.parent + if (parent is BukkitGuiView) { + player.closeInventory() + val resumeContext = parent.createResumeContext(player, view) + parent.onResume(resumeContext) + parent.open(player) + } else { + close() + } + } + + override fun close() { + player.closeInventory() + } + + override fun update() { + bukkitView.refreshInventory(player) + } +} + +/** + * Bukkit implementation of LifecycleContext. + */ +internal class BukkitLifecycleContext( + override val view: GuiView, + override val player: Player, + override val eventType: LifecycleEventType +) : LifecycleContext { + + override fun navigateTo(view: GuiView, passProps: Boolean) { + if (view is BukkitGuiView) { + view.parent = this.view + player.closeInventory() + view.open(player) + } + } + + override fun navigateBack() { + val parent = view.parent + if (parent is BukkitGuiView) { + player.closeInventory() + val resumeContext = parent.createResumeContext(player, view) + parent.onResume(resumeContext) + parent.open(player) + } else { + close() + } + } + + override fun close() { + player.closeInventory() + } + + override fun update() { + if (view is BukkitGuiView) { + view.refreshInventory(player) + } + } +} + +/** + * Bukkit implementation of ResumeContext. + */ +internal class BukkitResumeContext( + 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) { + if (view is BukkitGuiView) { + view.parent = this.view + player.closeInventory() + view.open(player) + } + } + + override fun navigateBack() { + val parent = view.parent + if (parent is BukkitGuiView) { + player.closeInventory() + val resumeContext = parent.createResumeContext(player, view) + parent.onResume(resumeContext) + parent.open(player) + } else { + close() + } + } + + override fun close() { + player.closeInventory() + } + + override fun update() { + if (view is BukkitGuiView) { + view.refreshInventory(player) + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt new file mode 100644 index 00000000..69000d40 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -0,0 +1,173 @@ +package dev.slne.surf.surfapi.bukkit.server.gui.view + +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.view.GuiView +import dev.slne.surf.surfapi.bukkit.server.gui.context.* +import dev.slne.surf.surfapi.bukkit.server.plugin +import org.bukkit.Bukkit +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 +import org.bukkit.event.inventory.InventoryOpenEvent +import org.bukkit.inventory.Inventory +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +/** + * Bukkit-specific implementation of GuiView. + */ +abstract class BukkitGuiView : GuiView() { + + private val inventories = ConcurrentHashMap() + private val updateTasks = ConcurrentHashMap() + + override fun open(player: Player) { + super.open(player) + + // Create inventory + val inventory = Bukkit.createInventory(null, config.size, config.title) + inventories[player.uniqueId] = inventory + + // Render components + components.forEach { (slot, component) -> + val context = createViewContext(player) + val item = component.render(context) + inventory.setItem(slot, item) + } + + // Open inventory + player.openInventory(inventory) + + // Start update task if configured + updateInterval?.let { interval -> + val taskId = Bukkit.getScheduler().runTaskTimer( + plugin, + Runnable { update() }, + interval.inWholeSeconds * 20L, + interval.inWholeSeconds * 20L + ).taskId + updateTasks[player.uniqueId] = taskId + } + + // Start component update tasks + components.values.forEach { component -> + component.updateInterval?.let { interval -> + Bukkit.getScheduler().runTaskTimer( + plugin, + Runnable { + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + component.onUpdate(lifecycleContext) + refreshComponentSlot(player, component) + }, + interval.inWholeSeconds * 20L, + interval.inWholeSeconds * 20L + ) + } + } + } + + override fun close(player: Player) { + // Cancel update tasks + updateTasks.remove(player.uniqueId)?.let { taskId -> + Bukkit.getScheduler().cancelTask(taskId) + } + + // Clean up + inventories.remove(player.uniqueId) + + super.close(player) + } + + /** + * Refresh the inventory for a player. + */ + internal fun refreshInventory(player: Player) { + val inventory = inventories[player.uniqueId] ?: return + + components.forEach { (slot, component) -> + val context = createViewContext(player) + val item = component.render(context) + inventory.setItem(slot, item) + } + } + + /** + * Refresh a specific component slot. + */ + private fun refreshComponentSlot(player: Player, component: Component) { + val inventory = inventories[player.uniqueId] ?: return + val slot = components.entries.find { it.value == component }?.key ?: return + + val context = createViewContext(player) + val item = component.render(context) + inventory.setItem(slot, item) + } + + /** + * Handle click event. + */ + internal fun handleClick(player: Player, event: InventoryClickEvent) { + if (config.cancelOnClick) { + event.isCancelled = true + } + + val slot = event.slot + val component = components[slot] + + val clickContext = BukkitClickContext(this, player, event, component) + component?.onClick(clickContext) + } + + override fun createViewContext(player: Player): ViewContext { + return BukkitViewContext(this, player) + } + + override fun createRenderContext(player: Player): RenderContext { + return BukkitRenderContext(this, player, this) + } + + override fun createLifecycleContext(player: Player, eventType: LifecycleEventType): LifecycleContext { + return BukkitLifecycleContext(this, player, eventType) + } + + override fun createResumeContext(player: Player, origin: GuiView?): ResumeContext { + return BukkitResumeContext(this, player, origin) + } +} + +/** + * Global event listener for all GUI views. + */ +object GuiViewListener : Listener { + private val viewsByInventory = ConcurrentHashMap() + + fun registerView(inventory: Inventory, view: BukkitGuiView) { + viewsByInventory[inventory] = view + } + + fun unregisterView(inventory: Inventory) { + viewsByInventory.remove(inventory) + } + + @EventHandler + fun onInventoryClick(event: InventoryClickEvent) { + val inventory = event.clickedInventory ?: return + val view = viewsByInventory[inventory] ?: return + val player = event.whoClicked as? Player ?: return + + view.handleClick(player, event) + } + + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + val inventory = event.inventory + val view = viewsByInventory[inventory] ?: return + val player = event.player as? Player ?: return + + // Don't close if navigating or resuming + // This will be handled by the navigation logic + } +} From 81c60f5a4047c2acce599c928737ee323a04233c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:05:27 +0000 Subject: [PATCH 03/92] Add test GUI view and wire up new framework with test command Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../test/command/SurfApiTestCommand.java | 4 +- .../surfapi/bukkit/test/BukkitPluginMain.kt | 4 + .../test/command/subcommands/NewGuiTest.kt | 13 ++ .../surfapi/bukkit/test/gui/TestGuiView.kt | 120 ++++++++++++++++++ .../bukkit/server/gui/view/BukkitGuiView.kt | 9 +- 5 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt 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 2111d881..043be0c4 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 @@ -5,6 +5,7 @@ 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.MaxStacksizeTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.NewGuiTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketEntityTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketLoreTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PaginationTest; @@ -39,7 +40,8 @@ public SurfApiTestCommand() { new PaginationTest("pagination"), new InventoryTest("inventory"), new ToastTest(("toast")), - new SuspendCommandExecutionTest("suspendCommandExecution") + new SuspendCommandExecutionTest("suspendCommandExecution"), + new NewGuiTest("newgui") ); } } 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 6d368cc9..17c0d03f 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 @@ -5,6 +5,7 @@ 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.server.gui.view.GuiViewListener 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 @@ -19,6 +20,9 @@ class BukkitPluginMain : SuspendingJavaPlugin() { packetListenerApi.registerListeners(ChatListener()) TestInventoryView.register() + + // Register new GUI framework listener + server.pluginManager.registerEvents(GuiViewListener, this) } override fun onEnable() { diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt new file mode 100644 index 00000000..76fe955e --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt @@ -0,0 +1,13 @@ +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.test.gui.TestGuiView + +class NewGuiTest(name: String) : CommandAPICommand(name) { + init { + playerExecutor { player, _ -> + TestGuiView.open(player) + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt new file mode 100644 index 00000000..3ca08b33 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt @@ -0,0 +1,120 @@ +package dev.slne.surf.surfapi.bukkit.test.gui + +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.gui.component.Component +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.props +import dev.slne.surf.surfapi.bukkit.api.gui.dsl.slot +import dev.slne.surf.surfapi.bukkit.api.gui.props.MutableProp +import dev.slne.surf.surfapi.bukkit.api.gui.props.PropScope +import dev.slne.surf.surfapi.bukkit.api.gui.ref.createRef +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig +import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView +import dev.slne.surf.surfapi.core.api.messages.adventure.text +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Material +import org.bukkit.inventory.ItemType + +/** + * Test view demonstrating the new GUI framework. + */ +object TestGuiView : BukkitGuiView() { + + // Define props using the props DSL + private val propsMap = props { + mutable("clickCount", 0, PropScope.VIEWER) + } + + private val clickCountProp = propsMap["clickCount"] as MutableProp + + // Create a ref to the counter component for updates + private val counterRef = createRef() + + override fun onInit(config: ViewConfig) { + config.title = text("New GUI Framework Test", NamedTextColor.GOLD) + config.size = 54 // 6 rows + config.cancelOnClick = true + } + + override fun onFirstRender(context: dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext) { + // Counter display component (updates when clicked) + val counterComponent = dynamicComponent( + renderer = { ctx -> + val count = clickCountProp.get(ctx.propContext) + buildItem(ItemType.GOLD_BLOCK) { + displayName { + text("Click Counter: $count", NamedTextColor.YELLOW) + } + } + } + ) { + ref = counterRef + } + + context.slot(4, counterComponent) + + // Increment button + val incrementButton = component( + item = buildItem(ItemType.EMERALD) { + displayName { + text("Click to Increment!", NamedTextColor.GREEN) + } + } + ) { + onClick = { + val currentCount = clickCountProp.get(propContext) + clickCountProp.set(propContext, currentCount + 1) + + // Update the counter display using ref + counterRef.update() + } + } + + context.slot(13, incrementButton) + + // Close button + val closeButton = component( + item = buildItem(ItemType.BARRIER) { + displayName { + text("Close", NamedTextColor.RED) + } + } + ) { + onClick = { + player.sendMessage(text("Thanks for testing the new GUI framework!", NamedTextColor.AQUA)) + close() + } + } + + context.slot(49, closeButton) + + // Back button (for navigation demo) + val backButton = component( + item = buildItem(ItemType.ARROW) { + displayName { + text("Back to Parent", NamedTextColor.GRAY) + } + } + ) { + onClick = { + navigateBack() + } + } + + context.slot(45, backButton) + } + + override fun onOpen(context: dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext) { + context.player.sendMessage(text("Opening new GUI framework test!", NamedTextColor.GREEN)) + } + + override fun onClose(context: dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext) { + context.player.sendMessage(text("Closing GUI!", NamedTextColor.YELLOW)) + } + + override fun onUpdate(context: dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext) { + // Called when view is updated + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index 69000d40..07c6242b 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -31,6 +31,9 @@ abstract class BukkitGuiView : GuiView() { val inventory = Bukkit.createInventory(null, config.size, config.title) inventories[player.uniqueId] = inventory + // Register with listener + GuiViewListener.registerView(inventory, this) + // Render components components.forEach { (slot, component) -> val context = createViewContext(player) @@ -75,8 +78,10 @@ abstract class BukkitGuiView : GuiView() { Bukkit.getScheduler().cancelTask(taskId) } - // Clean up - inventories.remove(player.uniqueId) + // Unregister inventory + inventories.remove(player.uniqueId)?.let { inventory -> + GuiViewListener.unregisterView(inventory) + } super.close(player) } From 9adf0aed40b74c29c257436812dbc89a8fe3088e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:07:09 +0000 Subject: [PATCH 04/92] Remove old inventory framework and update dependencies Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surf-api-bukkit-api/build.gradle.kts | 1 - .../bukkit/api/inventory/SinglePlayerGui.kt | 9 - .../surfapi/bukkit/api/inventory/SurfGui.kt | 51 --- .../surfapi/bukkit/api/inventory/dsl/gui.kt | 92 ------ .../bukkit/api/inventory/dsl/markers.kt | 5 - .../surfapi/bukkit/api/inventory/dsl/panes.kt | 131 -------- .../bukkit/api/inventory/dsl/patterns.kt | 2 - .../surfapi/bukkit/api/inventory/dsl/slot.kt | 7 - .../inventory/framework/ViewFrameAccessor.kt | 17 - .../api/inventory/framework/extensions.kt | 39 --- .../bukkit/api/inventory/item/GuiItem.kt | 37 --- .../bukkit/api/inventory/item/SurfGuiItem.kt | 24 -- .../bukkit/api/inventory/item/button.kt | 2 - .../api/inventory/pane/SubmitItemPane.kt | 308 ------------------ .../api/inventory/types/SurfChestGui.kt | 40 --- .../build.gradle.kts | 1 - .../test/command/SurfApiTestCommand.java | 4 - .../gui/InventoryFrameworkTest.java | 29 -- .../surfapi/bukkit/test/BukkitPluginMain.kt | 3 - .../test/command/subcommands/InventoryTest.kt | 14 - .../inventory/TestInventoryView.kt | 42 --- .../surf-api-bukkit-server/build.gradle.kts | 1 - .../framework/ViewFrameAccessorImpl.kt | 13 - .../inventory/framework/InventoryLoader.kt | 20 -- 24 files changed, 892 deletions(-) delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/ViewFrameAccessor.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/framework/extensions.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java delete mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/InventoryTest.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/inventory/TestInventoryView.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/framework/ViewFrameAccessorImpl.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/inventory/framework/InventoryLoader.kt 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 ab4ec967..7ad89b18 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/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 7f3cc95c..00000000 --- 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 0983099b..00000000 --- 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 bb8cb947..00000000 --- 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 eee9a825..00000000 --- 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 34635aaf..00000000 --- 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 60feb449..00000000 --- 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 1624fc16..00000000 --- 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 8e410f96..00000000 --- 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 8a07228a..00000000 --- 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 3b02b836..00000000 --- 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 6a4cf99a..00000000 --- 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 80d60482..00000000 --- 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 5d931509..00000000 --- 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 f8e990ac..00000000 --- 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 cf8e0f80..d89ffb44 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 @@ -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 043be0c4..8b492e34 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,6 @@ 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.MaxStacksizeTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.NewGuiTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketEntityTest; @@ -16,7 +15,6 @@ 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 { @@ -33,12 +31,10 @@ public SurfApiTestCommand() { 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"), new NewGuiTest("newgui") 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 8ea0bdf5..00000000 --- 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 17c0d03f..e374125f 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,12 +2,10 @@ 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.server.gui.view.GuiViewListener 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 @@ -19,7 +17,6 @@ class BukkitPluginMain : SuspendingJavaPlugin() { ModernTestConfig.randomise() packetListenerApi.registerListeners(ChatListener()) - TestInventoryView.register() // Register new GUI framework listener server.pluginManager.registerEvents(GuiViewListener, this) 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 681cea44..00000000 --- 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 a15424c7..00000000 --- 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-server/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts index 22b1a675..31018685 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/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 da04eac5..00000000 --- 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 7f9f1228..00000000 --- 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 From 4acfd922d5555f3632956e5710e569781c2860c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:08:44 +0000 Subject: [PATCH 05/92] Add comprehensive documentation for new GUI framework Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- docs/GUI_FRAMEWORK.md | 366 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 docs/GUI_FRAMEWORK.md diff --git a/docs/GUI_FRAMEWORK.md b/docs/GUI_FRAMEWORK.md new file mode 100644 index 00000000..5bee25ed --- /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. From 076b73511e262d255041b6aa70da7d63bb032ff9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:10:53 +0000 Subject: [PATCH 06/92] Fix code review issues: navigation duplication, ThreadLocal, and prop access Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surf/surfapi/bukkit/api/gui/props/Prop.kt | 3 +- .../server/gui/context/BukkitContexts.kt | 139 +++++++----------- 2 files changed, 55 insertions(+), 87 deletions(-) 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 index da4b3fab..e5c0b7e3 100644 --- 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 @@ -2,6 +2,7 @@ package dev.slne.surf.surfapi.bukkit.api.gui.props import org.bukkit.entity.Player import java.util.UUID +import java.util.concurrent.atomic.AtomicReference import kotlin.reflect.KProperty /** @@ -59,7 +60,7 @@ class MutableProp( initialValue: T, private val scope: PropScope = PropScope.VIEWER ) : Prop { - private val globalValue = ThreadLocal.withInitial { initialValue } + private val globalValue = AtomicReference(initialValue) private val viewerValues = mutableMapOf() override fun get(context: PropContext): T { diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt index bc980808..bc0c2616 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt @@ -9,44 +9,65 @@ import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.inventory.ItemStack /** - * Bukkit implementation of ViewContext. + * Shared navigation logic for all context implementations. */ -internal class BukkitViewContext( - override val view: GuiView, - override val player: Player -) : ViewContext { - - override fun navigateTo(view: GuiView, passProps: Boolean) { - if (view is BukkitGuiView) { - view.parent = this.view +private object NavigationHelper { + fun navigateTo(currentView: GuiView, targetView: GuiView, player: Player, passProps: Boolean) { + if (targetView is BukkitGuiView) { + targetView.parent = currentView player.closeInventory() - view.open(player) + targetView.open(player) } } - override fun navigateBack() { - val parent = view.parent + fun navigateBack(currentView: GuiView, player: Player) { + val parent = currentView.parent if (parent is BukkitGuiView) { player.closeInventory() - val resumeContext = parent.createResumeContext(player, view) + val resumeContext = parent.createResumeContext(player, currentView) parent.onResume(resumeContext) parent.open(player) } else { - close() + player.closeInventory() } } - override fun close() { + fun close(player: Player) { player.closeInventory() } - override fun update() { + fun update(view: GuiView, player: Player) { if (view is BukkitGuiView) { view.refreshInventory(player) } } } +/** + * Bukkit implementation of ViewContext. + */ +internal class BukkitViewContext( + 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) + } +} + /** * Bukkit implementation of ClickContext. */ @@ -58,33 +79,19 @@ internal class BukkitClickContext( ) : ClickContext { override fun navigateTo(view: GuiView, passProps: Boolean) { - if (view is BukkitGuiView) { - view.parent = this.view - player.closeInventory() - view.open(player) - } + NavigationHelper.navigateTo(this.view, view, player, passProps) } override fun navigateBack() { - val parent = view.parent - if (parent is BukkitGuiView) { - player.closeInventory() - val resumeContext = parent.createResumeContext(player, view) - parent.onResume(resumeContext) - parent.open(player) - } else { - close() - } + NavigationHelper.navigateBack(view, player) } override fun close() { - player.closeInventory() + NavigationHelper.close(player) } override fun update() { - if (view is BukkitGuiView) { - view.refreshInventory(player) - } + NavigationHelper.update(view, player) } } @@ -113,31 +120,19 @@ internal class BukkitRenderContext( } override fun navigateTo(view: GuiView, passProps: Boolean) { - if (view is BukkitGuiView) { - view.parent = this.view - player.closeInventory() - view.open(player) - } + NavigationHelper.navigateTo(this.view, view, player, passProps) } override fun navigateBack() { - val parent = view.parent - if (parent is BukkitGuiView) { - player.closeInventory() - val resumeContext = parent.createResumeContext(player, view) - parent.onResume(resumeContext) - parent.open(player) - } else { - close() - } + NavigationHelper.navigateBack(view, player) } override fun close() { - player.closeInventory() + NavigationHelper.close(player) } override fun update() { - bukkitView.refreshInventory(player) + NavigationHelper.update(view, player) } } @@ -151,33 +146,19 @@ internal class BukkitLifecycleContext( ) : LifecycleContext { override fun navigateTo(view: GuiView, passProps: Boolean) { - if (view is BukkitGuiView) { - view.parent = this.view - player.closeInventory() - view.open(player) - } + NavigationHelper.navigateTo(this.view, view, player, passProps) } override fun navigateBack() { - val parent = view.parent - if (parent is BukkitGuiView) { - player.closeInventory() - val resumeContext = parent.createResumeContext(player, view) - parent.onResume(resumeContext) - parent.open(player) - } else { - close() - } + NavigationHelper.navigateBack(view, player) } override fun close() { - player.closeInventory() + NavigationHelper.close(player) } override fun update() { - if (view is BukkitGuiView) { - view.refreshInventory(player) - } + NavigationHelper.update(view, player) } } @@ -193,32 +174,18 @@ internal class BukkitResumeContext( override val eventType: LifecycleEventType = LifecycleEventType.RESUME override fun navigateTo(view: GuiView, passProps: Boolean) { - if (view is BukkitGuiView) { - view.parent = this.view - player.closeInventory() - view.open(player) - } + NavigationHelper.navigateTo(this.view, view, player, passProps) } override fun navigateBack() { - val parent = view.parent - if (parent is BukkitGuiView) { - player.closeInventory() - val resumeContext = parent.createResumeContext(player, view) - parent.onResume(resumeContext) - parent.open(player) - } else { - close() - } + NavigationHelper.navigateBack(view, player) } override fun close() { - player.closeInventory() + NavigationHelper.close(player) } override fun update() { - if (view is BukkitGuiView) { - view.refreshInventory(player) - } + NavigationHelper.update(view, player) } } From 1eb2afb3af5fd4a5cf2cdf00e30cc35fc612ad28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:18:14 +0000 Subject: [PATCH 07/92] Remove test files, move listener to server, add comprehensive DSL, and make Folia-compatible Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/dsl/ViewDsl.kt | 175 ++++++++++++++++++ .../test/command/SurfApiTestCommand.java | 4 +- .../surfapi/bukkit/test/BukkitPluginMain.kt | 4 - .../test/command/subcommands/NewGuiTest.kt | 13 -- .../surfapi/bukkit/test/gui/TestGuiView.kt | 120 ------------ .../surfapi/bukkit/server/BukkitInstance.kt | 9 +- .../bukkit/server/gui/view/BukkitGuiView.kt | 44 ++--- 7 files changed, 204 insertions(+), 165 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ViewDsl.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt 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 00000000..7d8c7455 --- /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,175 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.dsl + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig +import net.kyori.adventure.text.Component as AdventureComponent +import org.bukkit.Material +import org.bukkit.inventory.ItemStack + +/** + * 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 rows: Int + get() = size / 9 + set(value) { + size = value * 9 + } + var cancelOnClick: Boolean = true + var closeOnClickOutside: Boolean = false + + internal fun build(): ViewConfig { + return ViewConfig().apply { + this.title = this@ViewConfigBuilder.title + this.size = this@ViewConfigBuilder.size + this.cancelOnClick = this@ViewConfigBuilder.cancelOnClick + this.closeOnClickOutside = this@ViewConfigBuilder.closeOnClickOutside + } + } +} + +/** + * 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 + config.closeOnClickOutside = builtConfig.closeOnClickOutside +} + +/** + * Fill a range of slots with the same component. + */ +@ComponentDsl +fun RenderContext.fillSlots(slots: IntRange, component: Component) { + slots.forEach { slot -> + renderComponent(slot, component) + } +} + +/** + * Fill a range of slots with the same item. + */ +@ComponentDsl +fun RenderContext.fillSlots(slots: IntRange, item: ItemStack) { + slots.forEach { slot -> + setItem(slot, item) + } +} + +/** + * Fill a row with items. + */ +@ComponentDsl +fun RenderContext.fillRow(row: Int, component: Component) { + val startSlot = row * 9 + fillSlots(startSlot until startSlot + 9, component) +} + +/** + * Fill a row with items. + */ +@ComponentDsl +fun RenderContext.fillRow(row: Int, item: ItemStack) { + val startSlot = row * 9 + fillSlots(startSlot until startSlot + 9, item) +} + +/** + * Fill borders with a component. + */ +@ComponentDsl +fun RenderContext.fillBorders(component: Component) { + val rows = view.config.size / 9 + + // Top and bottom rows + fillRow(0, component) + fillRow(rows - 1, component) + + // Left and right columns + for (row in 1 until rows - 1) { + renderComponent(row * 9, component) + renderComponent(row * 9 + 8, component) + } +} + +/** + * Fill borders with an item. + */ +@ComponentDsl +fun RenderContext.fillBorders(item: ItemStack) { + val rows = view.config.size / 9 + + // Top and bottom rows + fillRow(0, item) + fillRow(rows - 1, item) + + // Left and right columns + for (row in 1 until rows - 1) { + setItem(row * 9, item) + setItem(row * 9 + 8, item) + } +} + +/** + * Create a glass pane item (commonly used for borders/fillers). + */ +fun glassPane(material: Material = Material.GRAY_STAINED_GLASS_PANE): ItemStack { + return ItemStack.of(material) +} + +/** + * Place component at row and column position. + */ +@ComponentDsl +fun RenderContext.slotAt(row: Int, column: Int, component: Component) { + val slot = row * 9 + column + renderComponent(slot, component) +} + +/** + * Place item at row and column position. + */ +@ComponentDsl +fun RenderContext.slotAt(row: Int, column: Int, item: ItemStack) { + val slot = row * 9 + column + setItem(slot, item) +} + +/** + * Center a component horizontally in a row. + */ +@ComponentDsl +fun RenderContext.centerInRow(row: Int, component: Component) { + val slot = row * 9 + 4 + renderComponent(slot, component) +} + +/** + * Place multiple components in a centered pattern. + */ +@ComponentDsl +fun RenderContext.centerComponents(row: Int, components: List) { + val centerOffset = 4 - (components.size / 2) + components.forEachIndexed { index, component -> + val slot = row * 9 + centerOffset + index + renderComponent(slot, component) + } +} 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 8b492e34..05004b95 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 @@ -4,7 +4,6 @@ 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.MaxStacksizeTest; -import dev.slne.surf.surfapi.bukkit.test.command.subcommands.NewGuiTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketEntityTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PacketLoreTest; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PaginationTest; @@ -36,8 +35,7 @@ public SurfApiTestCommand() { new GlowingTest("glowing"), new PaginationTest("pagination"), new ToastTest(("toast")), - new SuspendCommandExecutionTest("suspendCommandExecution"), - new NewGuiTest("newgui") + new SuspendCommandExecutionTest("suspendCommandExecution") ); } } 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 e374125f..90488d17 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 @@ -4,7 +4,6 @@ import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import dev.jorel.commandapi.CommandAPI 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.server.gui.view.GuiViewListener import dev.slne.surf.surfapi.bukkit.test.command.SurfApiTestCommand import dev.slne.surf.surfapi.bukkit.test.command.subcommands.reflection.Reflection import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig @@ -17,9 +16,6 @@ class BukkitPluginMain : SuspendingJavaPlugin() { ModernTestConfig.randomise() packetListenerApi.registerListeners(ChatListener()) - - // Register new GUI framework listener - server.pluginManager.registerEvents(GuiViewListener, this) } override fun onEnable() { diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt deleted file mode 100644 index 76fe955e..00000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/NewGuiTest.kt +++ /dev/null @@ -1,13 +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.test.gui.TestGuiView - -class NewGuiTest(name: String) : CommandAPICommand(name) { - init { - playerExecutor { player, _ -> - TestGuiView.open(player) - } - } -} diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt deleted file mode 100644 index 3ca08b33..00000000 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/TestGuiView.kt +++ /dev/null @@ -1,120 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.test.gui - -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.gui.component.Component -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.props -import dev.slne.surf.surfapi.bukkit.api.gui.dsl.slot -import dev.slne.surf.surfapi.bukkit.api.gui.props.MutableProp -import dev.slne.surf.surfapi.bukkit.api.gui.props.PropScope -import dev.slne.surf.surfapi.bukkit.api.gui.ref.createRef -import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig -import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView -import dev.slne.surf.surfapi.core.api.messages.adventure.text -import net.kyori.adventure.text.format.NamedTextColor -import org.bukkit.Material -import org.bukkit.inventory.ItemType - -/** - * Test view demonstrating the new GUI framework. - */ -object TestGuiView : BukkitGuiView() { - - // Define props using the props DSL - private val propsMap = props { - mutable("clickCount", 0, PropScope.VIEWER) - } - - private val clickCountProp = propsMap["clickCount"] as MutableProp - - // Create a ref to the counter component for updates - private val counterRef = createRef() - - override fun onInit(config: ViewConfig) { - config.title = text("New GUI Framework Test", NamedTextColor.GOLD) - config.size = 54 // 6 rows - config.cancelOnClick = true - } - - override fun onFirstRender(context: dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext) { - // Counter display component (updates when clicked) - val counterComponent = dynamicComponent( - renderer = { ctx -> - val count = clickCountProp.get(ctx.propContext) - buildItem(ItemType.GOLD_BLOCK) { - displayName { - text("Click Counter: $count", NamedTextColor.YELLOW) - } - } - } - ) { - ref = counterRef - } - - context.slot(4, counterComponent) - - // Increment button - val incrementButton = component( - item = buildItem(ItemType.EMERALD) { - displayName { - text("Click to Increment!", NamedTextColor.GREEN) - } - } - ) { - onClick = { - val currentCount = clickCountProp.get(propContext) - clickCountProp.set(propContext, currentCount + 1) - - // Update the counter display using ref - counterRef.update() - } - } - - context.slot(13, incrementButton) - - // Close button - val closeButton = component( - item = buildItem(ItemType.BARRIER) { - displayName { - text("Close", NamedTextColor.RED) - } - } - ) { - onClick = { - player.sendMessage(text("Thanks for testing the new GUI framework!", NamedTextColor.AQUA)) - close() - } - } - - context.slot(49, closeButton) - - // Back button (for navigation demo) - val backButton = component( - item = buildItem(ItemType.ARROW) { - displayName { - text("Back to Parent", NamedTextColor.GRAY) - } - } - ) { - onClick = { - navigateBack() - } - } - - context.slot(45, backButton) - } - - override fun onOpen(context: dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext) { - context.player.sendMessage(text("Opening new GUI framework test!", NamedTextColor.GREEN)) - } - - override fun onClose(context: dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext) { - context.player.sendMessage(text("Closing GUI!", NamedTextColor.YELLOW)) - } - - override fun onUpdate(context: dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext) { - // Called when view is updated - } -} 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 cf708f64..ce261b86 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 @@ -1,8 +1,8 @@ package dev.slne.surf.surfapi.bukkit.server import dev.slne.surf.surfapi.bukkit.api.surfBukkitApi +import dev.slne.surf.surfapi.bukkit.server.gui.view.GuiViewListener 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 +15,17 @@ object BukkitInstance : CoreInstance() { initObjects() PacketApiLoader.onLoad() - InventoryLoader.load() } override suspend fun onEnable() { super.onEnable() PacketApiLoader.onEnable() - InventoryLoader.enable() ListenerManager.registerListeners() + + // Register GUI framework listener + plugin.server.pluginManager.registerEvents(GuiViewListener, plugin) + (surfBukkitApi as SurfBukkitApiImpl).onEnable() } @@ -32,7 +34,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/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index 07c6242b..51d0be62 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -1,10 +1,14 @@ package dev.slne.surf.surfapi.bukkit.server.gui.view +import com.github.shynixn.mccoroutine.folia.entityDispatcher +import com.github.shynixn.mccoroutine.folia.launch 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.view.GuiView import dev.slne.surf.surfapi.bukkit.server.gui.context.* import dev.slne.surf.surfapi.bukkit.server.plugin +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import org.bukkit.Bukkit import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -15,14 +19,16 @@ import org.bukkit.event.inventory.InventoryOpenEvent import org.bukkit.inventory.Inventory import java.util.UUID import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.milliseconds /** * Bukkit-specific implementation of GuiView. + * Folia-compatible using entity dispatchers. */ abstract class BukkitGuiView : GuiView() { private val inventories = ConcurrentHashMap() - private val updateTasks = ConcurrentHashMap() + private val updateJobs = ConcurrentHashMap() override fun open(player: Player) { super.open(player) @@ -44,39 +50,35 @@ abstract class BukkitGuiView : GuiView() { // Open inventory player.openInventory(inventory) - // Start update task if configured + // Start update task if configured (Folia-compatible) updateInterval?.let { interval -> - val taskId = Bukkit.getScheduler().runTaskTimer( - plugin, - Runnable { update() }, - interval.inWholeSeconds * 20L, - interval.inWholeSeconds * 20L - ).taskId - updateTasks[player.uniqueId] = taskId + val job = plugin.launch(player.entityDispatcher) { + while (true) { + delay(interval) + update() + } + } + updateJobs[player.uniqueId] = job } - // Start component update tasks + // Start component update tasks (Folia-compatible) components.values.forEach { component -> component.updateInterval?.let { interval -> - Bukkit.getScheduler().runTaskTimer( - plugin, - Runnable { + plugin.launch(player.entityDispatcher) { + while (true) { + delay(interval) val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) component.onUpdate(lifecycleContext) refreshComponentSlot(player, component) - }, - interval.inWholeSeconds * 20L, - interval.inWholeSeconds * 20L - ) + } + } } } } override fun close(player: Player) { - // Cancel update tasks - updateTasks.remove(player.uniqueId)?.let { taskId -> - Bukkit.getScheduler().cancelTask(taskId) - } + // Cancel update jobs + updateJobs.remove(player.uniqueId)?.cancel() // Unregister inventory inventories.remove(player.uniqueId)?.let { inventory -> From 616b5b542eb724d307f4496fcfd746cd0ab078ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:19:33 +0000 Subject: [PATCH 08/92] Fix coroutine cancellation and component centering issues Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/dsl/ViewDsl.kt | 2 +- .../bukkit/server/gui/view/BukkitGuiView.kt | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) 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 index 7d8c7455..54ff5658 100644 --- 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 @@ -167,7 +167,7 @@ fun RenderContext.centerInRow(row: Int, component: Component) { */ @ComponentDsl fun RenderContext.centerComponents(row: Int, components: List) { - val centerOffset = 4 - (components.size / 2) + val centerOffset = 4 - ((components.size - 1) / 2) components.forEachIndexed { index, component -> val slot = row * 9 + centerOffset + index renderComponent(slot, component) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index 51d0be62..99e2a29d 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -29,6 +29,7 @@ abstract class BukkitGuiView : GuiView() { private val inventories = ConcurrentHashMap() private val updateJobs = ConcurrentHashMap() + private val componentJobs = ConcurrentHashMap>() override fun open(player: Player) { super.open(player) @@ -53,32 +54,42 @@ abstract class BukkitGuiView : GuiView() { // Start update task if configured (Folia-compatible) updateInterval?.let { interval -> val job = plugin.launch(player.entityDispatcher) { - while (true) { + while (isActive) { delay(interval) - update() + if (isActive) { + update() + } } } updateJobs[player.uniqueId] = job } // Start component update tasks (Folia-compatible) + val playerComponentJobs = mutableListOf() components.values.forEach { component -> component.updateInterval?.let { interval -> - plugin.launch(player.entityDispatcher) { - while (true) { + val job = plugin.launch(player.entityDispatcher) { + while (isActive) { delay(interval) - val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) - component.onUpdate(lifecycleContext) - refreshComponentSlot(player, component) + if (isActive) { + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + component.onUpdate(lifecycleContext) + refreshComponentSlot(player, component) + } } } + playerComponentJobs.add(job) } } + if (playerComponentJobs.isNotEmpty()) { + componentJobs[player.uniqueId] = playerComponentJobs + } } override fun close(player: Player) { - // Cancel update jobs + // Cancel all update jobs updateJobs.remove(player.uniqueId)?.cancel() + componentJobs.remove(player.uniqueId)?.forEach { it.cancel() } // Unregister inventory inventories.remove(player.uniqueId)?.let { inventory -> From c6487117794a3de324d76b5a6ad4cc70b8063b6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:28:03 +0000 Subject: [PATCH 09/92] Refactor props to be view-global, add container components, support different inventory types Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 30 +++-- .../bukkit/api/gui/context/ViewContext.kt | 11 +- .../bukkit/api/gui/dsl/ComponentDsl.kt | 33 +++-- .../surfapi/bukkit/api/gui/dsl/ViewDsl.kt | 13 +- .../bukkit/api/gui/props/PaginationProp.kt | 41 ++++-- .../surf/surfapi/bukkit/api/gui/props/Prop.kt | 126 +++++++++--------- .../surfapi/bukkit/api/gui/view/GuiView.kt | 19 ++- .../bukkit/server/gui/view/BukkitGuiView.kt | 65 +++++++-- 8 files changed, 215 insertions(+), 123 deletions(-) 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 index b25a692b..05a1915b 100644 --- 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 @@ -76,8 +76,15 @@ abstract class Component { /** * Renders the component to an ItemStack. + * For container components, return null. */ - abstract fun render(context: ViewContext): ItemStack + open fun render(context: ViewContext): ItemStack? = null + + /** + * For container components, render multiple items at specific slots. + * Returns a map of slot to ItemStack. + */ + open fun renderSlots(context: ViewContext): Map = emptyMap() /** * Add a child component. @@ -143,11 +150,11 @@ open class ItemComponent( * Dynamic component that renders based on a callback. */ open class DynamicComponent( - private val renderer: (ViewContext) -> ItemStack, + private val renderer: (ViewContext) -> ItemStack?, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override fun render(context: ViewContext): ItemStack = renderer(context) + override fun render(context: ViewContext): ItemStack? = renderer(context) override fun onClick(context: ClickContext) { clickHandler?.invoke(context) @@ -155,17 +162,18 @@ open class DynamicComponent( } /** - * Container component that can hold multiple child components. + * Container component that renders multiple items at specific slots. + * This is useful for paginated components or complex layouts. */ abstract class ContainerComponent : Component() { /** - * Layout children within the container. - * Returns a map of slot positions to components. + * Render multiple items at their respective slots. + * Override this to provide the slot-to-item mapping. */ - abstract fun layout(context: ViewContext): Map + abstract override fun renderSlots(context: ViewContext): Map - override fun render(context: ViewContext): ItemStack { - // Container doesn't render itself, only lays out children - return ItemStack.empty() - } + /** + * Container doesn't render a single item. + */ + final override fun render(context: ViewContext): ItemStack? = null } 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 index 456dcf7a..c658c417 100644 --- 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 @@ -2,7 +2,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context import dev.slne.surf.surfapi.bukkit.api.gui.component.Component import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop -import dev.slne.surf.surfapi.bukkit.api.gui.props.PropContext import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent @@ -31,15 +30,9 @@ interface ViewContext { get() = player.uniqueId /** - * Props context for accessing prop values. + * Get a prop value from the view. */ - val propContext: PropContext - get() = PropContext(viewerId, player) - - /** - * Get a prop value. - */ - fun getProp(prop: Prop): T = prop.get(propContext) + fun getProp(prop: Prop): T = prop.get() /** * Navigate to another view. 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 index c408aca5..4fb86af5 100644 --- 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 @@ -43,7 +43,7 @@ class ComponentBuilder { /** * Build the component. */ - internal fun build(renderer: (ViewContext) -> ItemStack): Component { + internal fun build(renderer: (ViewContext) -> ItemStack?): Component { return object : DynamicComponent(renderer, onClick) { override val updateInterval: Duration? = this@ComponentBuilder.updateInterval override val props: Map> = _props @@ -85,7 +85,7 @@ fun component( * Create a component with a dynamic renderer. */ fun dynamicComponent( - renderer: (ViewContext) -> ItemStack, + renderer: (ViewContext) -> ItemStack?, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() @@ -103,36 +103,43 @@ class PropsBuilder { /** * Create an immutable prop. */ - fun immutable(name: String, value: T, scope: PropScope = PropScope.VIEWER): ImmutableProp { - return ImmutableProp(name, value, scope).also { _props[name] = it } + fun immutable(name: String, value: T): ImmutableProp { + return ImmutableProp(name, value).also { _props[name] = it } } /** - * Create a mutable prop. + * Create a mutable prop (global to view). */ - fun mutable(name: String, initialValue: T, scope: PropScope = PropScope.VIEWER): MutableProp { - return MutableProp(name, initialValue, scope).also { _props[name] = it } + fun mutable(name: String, initialValue: T): MutableProp { + return MutableProp(name, initialValue).also { _props[name] = it } + } + + /** + * Create a viewer-specific mutable prop. + */ + fun viewerMutable(name: String, initialValue: T): ViewerMutableProp { + return ViewerMutableProp(name, initialValue).also { _props[name] = it } } /** * Create a computed prop. */ - fun computed(name: String, scope: PropScope = PropScope.VIEWER, compute: (PropContext) -> T): ComputedProp { - return ComputedProp(name, compute, scope).also { _props[name] = it } + fun computed(name: String, compute: () -> T): ComputedProp { + return ComputedProp(name, compute).also { _props[name] = it } } /** * Create a lazy prop. */ - fun lazy(name: String, mutable: Boolean = false, scope: PropScope = PropScope.VIEWER, initializer: (PropContext) -> T): LazyProp { - return LazyProp(name, initializer, mutable, scope).also { _props[name] = it } + fun lazy(name: String, mutable: Boolean = false, initializer: () -> T): LazyProp { + return LazyProp(name, initializer, mutable).also { _props[name] = it } } /** * Create a pagination prop. */ - fun pagination(name: String = "pagination", pageSize: Int = 9, scope: PropScope = PropScope.VIEWER, items: () -> List): PaginationProp { - return PaginationProp(name, items, pageSize, scope).also { _props[name] = it } + fun pagination(name: String = "pagination", pageSize: Int = 9, items: () -> List): PaginationProp { + return PaginationProp(name, items, pageSize).also { _props[name] = it } } /** 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 index 54ff5658..4fa3abaa 100644 --- 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 @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig import net.kyori.adventure.text.Component as AdventureComponent import org.bukkit.Material +import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.ItemStack /** @@ -21,9 +22,12 @@ annotation class ViewDsl class ViewConfigBuilder { var title: AdventureComponent = AdventureComponent.text("GUI") var size: Int = 54 + var type: InventoryType = InventoryType.CHEST var rows: Int - get() = size / 9 + get() = if (type == InventoryType.CHEST) size / 9 else 1 set(value) { + require(type == InventoryType.CHEST) { "Rows can only be set for CHEST type" } + require(value in 1..6) { "Rows must be between 1 and 6" } size = value * 9 } var cancelOnClick: Boolean = true @@ -32,7 +36,12 @@ class ViewConfigBuilder { internal fun build(): ViewConfig { return ViewConfig().apply { this.title = this@ViewConfigBuilder.title - this.size = this@ViewConfigBuilder.size + this.type = this@ViewConfigBuilder.type + this.size = if (this.type == InventoryType.CHEST) { + this@ViewConfigBuilder.size + } else { + this.type.defaultSize + } this.cancelOnClick = this@ViewConfigBuilder.cancelOnClick this.closeOnClickOutside = this@ViewConfigBuilder.closeOnClickOutside } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt index 54f16617..74651359 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt @@ -2,18 +2,27 @@ package dev.slne.surf.surfapi.bukkit.api.gui.props /** * Default pagination prop for managing paginated content in GUIs. + * Viewer-specific pagination state. */ class PaginationProp( override val name: String = "pagination", private val items: () -> List, - private val pageSize: Int = 9, - private val scope: PropScope = PropScope.VIEWER + private val pageSize: Int = 9 ) : Prop> { private val currentPages = mutableMapOf() - override fun get(context: PropContext): PaginationState { - val currentPage = currentPages.getOrPut(context.viewerId) { 0 } + override fun get(): PaginationState { + // Default state when no viewer context + return getState(0) + } + + fun get(viewerId: java.util.UUID): PaginationState { + val currentPage = currentPages.getOrPut(viewerId) { 0 } + return getState(currentPage) + } + + private fun getState(currentPage: Int): PaginationState { val allItems = items() val totalPages = (allItems.size + pageSize - 1) / pageSize @@ -36,27 +45,31 @@ class PaginationProp( ) } - fun nextPage(context: PropContext) { - val current = currentPages.getOrPut(context.viewerId) { 0 } - val state = get(context) + fun nextPage(viewerId: java.util.UUID) { + val current = currentPages.getOrPut(viewerId) { 0 } + val state = getState(current) if (state.hasNextPage) { - currentPages[context.viewerId] = current + 1 + currentPages[viewerId] = current + 1 } } - fun previousPage(context: PropContext) { - val current = currentPages.getOrPut(context.viewerId) { 0 } + fun previousPage(viewerId: java.util.UUID) { + val current = currentPages.getOrPut(viewerId) { 0 } if (current > 0) { - currentPages[context.viewerId] = current - 1 + currentPages[viewerId] = current - 1 } } - fun setPage(context: PropContext, page: Int) { - val state = get(context) + fun setPage(viewerId: java.util.UUID, page: Int) { + val state = getState(0) // Get state to check total pages if (page in 0 until state.totalPages) { - currentPages[context.viewerId] = page + currentPages[viewerId] = page } } + + fun clear(viewerId: java.util.UUID) { + currentPages.remove(viewerId) + } } /** 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 index e5c0b7e3..65c04acb 100644 --- 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 @@ -7,13 +7,13 @@ import kotlin.reflect.KProperty /** * Base interface for all props in the GUI framework. - * Props can be scoped as global (shared across all viewers) or viewer-specific (isolated per viewer). + * All props are global to the view and shared across all viewers. */ sealed interface Prop { /** - * Gets the value of this prop for the given context. + * Gets the value of this prop. */ - fun get(context: PropContext): T + fun get(): T /** * The name of this prop. @@ -22,21 +22,21 @@ sealed interface Prop { } /** - * Context for prop evaluation, contains information about the current viewer. + * Viewer-specific prop storage. + * Maps viewer UUIDs to their prop values. */ -data class PropContext( - val viewerId: UUID, - val viewer: Player? -) - -/** - * Defines the scope of a prop. - */ -enum class PropScope { - /** Shared across all viewers */ - GLOBAL, - /** Isolated per viewer */ - VIEWER +class ViewerPropStorage(private val initialValue: () -> T) { + private val storage = mutableMapOf() + + fun get(viewerId: UUID): T = storage.getOrPut(viewerId) { initialValue() } + + fun set(viewerId: UUID, value: T) { + storage[viewerId] = value + } + + fun clear(viewerId: UUID) { + storage.remove(viewerId) + } } /** @@ -44,42 +44,55 @@ enum class PropScope { */ class ImmutableProp( override val name: String, - private val value: T, - private val scope: PropScope = PropScope.VIEWER + private val value: T ) : Prop { - override fun get(context: PropContext): T = value + override fun get(): T = value operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value } /** * Mutable prop - always available and mutable. + * Global to the view, shared across all viewers. */ class MutableProp( override val name: String, - initialValue: T, - private val scope: PropScope = PropScope.VIEWER + initialValue: T ) : Prop { - private val globalValue = AtomicReference(initialValue) - private val viewerValues = mutableMapOf() + private val value = AtomicReference(initialValue) - override fun get(context: PropContext): T { - return when (scope) { - PropScope.GLOBAL -> globalValue.get() - PropScope.VIEWER -> viewerValues.getOrPut(context.viewerId) { globalValue.get() } - } - } + override fun get(): T = value.get() - fun set(context: PropContext, value: T) { - when (scope) { - PropScope.GLOBAL -> globalValue.set(value) - PropScope.VIEWER -> viewerValues[context.viewerId] = value - } + fun set(value: T) { + this.value.set(value) } - operator fun getValue(thisRef: Any?, property: KProperty<*>): T = globalValue.get() + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value.get() operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - globalValue.set(value) + this.value.set(value) + } +} + +/** + * Viewer-specific mutable prop - isolated per viewer. + */ +class ViewerMutableProp( + override val name: String, + initialValue: T +) : Prop { + private val globalDefault = initialValue + private val storage = ViewerPropStorage { globalDefault } + + override fun get(): T = globalDefault + + fun get(viewerId: UUID): T = storage.get(viewerId) + + fun set(viewerId: UUID, value: T) { + storage.set(viewerId, value) + } + + fun clear(viewerId: UUID) { + storage.clear(viewerId) } } @@ -88,10 +101,9 @@ class MutableProp( */ class ComputedProp( override val name: String, - private val compute: (PropContext) -> T, - private val scope: PropScope = PropScope.VIEWER + private val compute: () -> T ) : Prop { - override fun get(context: PropContext): T = compute(context) + override fun get(): T = compute() } /** @@ -99,36 +111,24 @@ class ComputedProp( */ class LazyProp( override val name: String, - private val initializer: (PropContext) -> T, - private val mutable: Boolean = false, - private val scope: PropScope = PropScope.VIEWER + private val initializer: () -> T, + private val mutable: Boolean = false ) : Prop { - private val globalValue = lazy { initializer(PropContext(UUID.randomUUID(), null)) } - private val viewerValues = mutableMapOf() - private val initialized = mutableSetOf() + private val value = lazy { initializer() } + private var mutableValue: T? = null - override fun get(context: PropContext): T { - return when (scope) { - PropScope.GLOBAL -> { - if (mutable && context.viewerId in initialized) { - viewerValues.getOrPut(context.viewerId) { globalValue.value } - } else { - globalValue.value - } - } - PropScope.VIEWER -> { - viewerValues.getOrPut(context.viewerId) { - initialized.add(context.viewerId) - initializer(context) - } - } + override fun get(): T { + return if (mutable && mutableValue != null) { + mutableValue!! + } else { + value.value } } - fun set(context: PropContext, value: T) { + fun set(value: T) { if (!mutable) { throw UnsupportedOperationException("Cannot set immutable lazy prop") } - viewerValues[context.viewerId] = value + mutableValue = value } } 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 index a502412a..dc8e7640 100644 --- 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 @@ -3,9 +3,9 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view 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.props.Prop -import dev.slne.surf.surfapi.bukkit.api.gui.props.PropContext import net.kyori.adventure.text.Component as AdventureComponent import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryType import kotlin.time.Duration /** @@ -13,10 +13,23 @@ import kotlin.time.Duration */ data class ViewConfig( var title: AdventureComponent = AdventureComponent.text("GUI"), - var size: Int = 54, // 6 rows by default + var size: Int = 54, // 6 rows by default for CHEST + var type: InventoryType = InventoryType.CHEST, var cancelOnClick: Boolean = true, var closeOnClickOutside: Boolean = false -) +) { + /** + * 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" } + require(value in 1..6) { "Rows must be between 1 and 6" } + size = value * 9 + } +} /** * Base class for all GUI views. diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index 99e2a29d..ec9de513 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -34,8 +34,15 @@ abstract class BukkitGuiView : GuiView() { override fun open(player: Player) { super.open(player) - // Create inventory - val inventory = Bukkit.createInventory(null, config.size, config.title) + // Create inventory based on type + val inventory = when (config.type) { + org.bukkit.event.inventory.InventoryType.CHEST -> { + Bukkit.createInventory(null, config.size, config.title) + } + else -> { + Bukkit.createInventory(null, config.type, config.title) + } + } inventories[player.uniqueId] = inventory // Register with listener @@ -44,8 +51,22 @@ abstract class BukkitGuiView : GuiView() { // Render components components.forEach { (slot, component) -> val context = createViewContext(player) - val item = component.render(context) - inventory.setItem(slot, item) + + // Handle container components (render multiple slots) + val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { + slotsToRender.forEach { (slotPos, item) -> + if (slotPos < inventory.size) { + inventory.setItem(slotPos, item) + } + } + } else { + // Regular component (single item) + val item = component.render(context) + if (item != null && slot < inventory.size) { + inventory.setItem(slot, item) + } + } } // Open inventory @@ -107,8 +128,22 @@ abstract class BukkitGuiView : GuiView() { components.forEach { (slot, component) -> val context = createViewContext(player) - val item = component.render(context) - inventory.setItem(slot, item) + + // Handle container components + val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { + slotsToRender.forEach { (slotPos, item) -> + if (slotPos < inventory.size) { + inventory.setItem(slotPos, item) + } + } + } else { + // Regular component + val item = component.render(context) + if (item != null && slot < inventory.size) { + inventory.setItem(slot, item) + } + } } } @@ -120,8 +155,22 @@ abstract class BukkitGuiView : GuiView() { val slot = components.entries.find { it.value == component }?.key ?: return val context = createViewContext(player) - val item = component.render(context) - inventory.setItem(slot, item) + + // Handle container components + val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { + slotsToRender.forEach { (slotPos, item) -> + if (slotPos < inventory.size) { + inventory.setItem(slotPos, item) + } + } + } else { + // Regular component + val item = component.render(context) + if (item != null && slot < inventory.size) { + inventory.setItem(slot, item) + } + } } /** From ccaecfec946e52095502e56ab759667e1480fcf6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:29:12 +0000 Subject: [PATCH 10/92] Improve error messages and simplify lazy prop logic Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../dev/slne/surf/surfapi/bukkit/api/gui/dsl/ViewDsl.kt | 4 +++- .../dev/slne/surf/surfapi/bukkit/api/gui/props/Prop.kt | 6 +----- .../dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt | 4 +++- 3 files changed, 7 insertions(+), 7 deletions(-) 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 index 4fa3abaa..29ce7ca7 100644 --- 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 @@ -26,7 +26,9 @@ class ViewConfigBuilder { 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" } + 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 } 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 index 65c04acb..d1124a7a 100644 --- 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 @@ -118,11 +118,7 @@ class LazyProp( private var mutableValue: T? = null override fun get(): T { - return if (mutable && mutableValue != null) { - mutableValue!! - } else { - value.value - } + return mutableValue ?: value.value } fun set(value: T) { 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 index dc8e7640..0ddc301d 100644 --- 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 @@ -25,7 +25,9 @@ data class ViewConfig( var rows: Int get() = size / 9 set(value) { - require(type == InventoryType.CHEST) { "Rows can only be set for CHEST type" } + 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 } From 5d5dfc14e11e3fac03c3525864e3d45e9d435899 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:41:49 +0000 Subject: [PATCH 11/92] Register GuiViewListener in ListenerManager like other listeners Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../dev/slne/surf/surfapi/bukkit/server/BukkitInstance.kt | 4 ---- .../surf/surfapi/bukkit/server/listener/ListenerManager.kt | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) 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 ce261b86..1154281c 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 @@ -1,7 +1,6 @@ package dev.slne.surf.surfapi.bukkit.server import dev.slne.surf.surfapi.bukkit.api.surfBukkitApi -import dev.slne.surf.surfapi.bukkit.server.gui.view.GuiViewListener import dev.slne.surf.surfapi.bukkit.server.impl.SurfBukkitApiImpl import dev.slne.surf.surfapi.bukkit.server.listener.ListenerManager import dev.slne.surf.surfapi.bukkit.server.packet.PacketApiLoader @@ -23,9 +22,6 @@ object BukkitInstance : CoreInstance() { PacketApiLoader.onEnable() ListenerManager.registerListeners() - // Register GUI framework listener - plugin.server.pluginManager.registerEvents(GuiViewListener, plugin) - (surfBukkitApi as SurfBukkitApiImpl).onEnable() } 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 a6f45b37..206835fd 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() } /** From 6bd3a2bb89e921a33b8bf2e476a27ad83144cbe3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:45:05 +0000 Subject: [PATCH 12/92] Fix entityDispatcher usage and implement onInventoryClose handler - Replace player.entityDispatcher with plugin.entityDispatcher(player) - Replace while(isActive) with while(true) for proper coroutine pattern - Implement onInventoryClose to properly close views when inventory closes Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/server/gui/view/BukkitGuiView.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index ec9de513..2c1e43ca 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -74,12 +74,10 @@ abstract class BukkitGuiView : GuiView() { // Start update task if configured (Folia-compatible) updateInterval?.let { interval -> - val job = plugin.launch(player.entityDispatcher) { - while (isActive) { + val job = plugin.launch(plugin.entityDispatcher(player)) { + while (true) { delay(interval) - if (isActive) { - update() - } + update() } } updateJobs[player.uniqueId] = job @@ -89,14 +87,12 @@ abstract class BukkitGuiView : GuiView() { val playerComponentJobs = mutableListOf() components.values.forEach { component -> component.updateInterval?.let { interval -> - val job = plugin.launch(player.entityDispatcher) { - while (isActive) { + val job = plugin.launch(plugin.entityDispatcher(player)) { + while (true) { delay(interval) - if (isActive) { - val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) - component.onUpdate(lifecycleContext) - refreshComponentSlot(player, component) - } + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + component.onUpdate(lifecycleContext) + refreshComponentSlot(player, component) } } playerComponentJobs.add(job) @@ -234,7 +230,7 @@ object GuiViewListener : Listener { val view = viewsByInventory[inventory] ?: return val player = event.player as? Player ?: return - // Don't close if navigating or resuming - // This will be handled by the navigation logic + // Close the view when inventory is closed + view.close(player) } } From 0764376ab6fa0078aa7290c282d96996c8750f7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:56:17 +0000 Subject: [PATCH 13/92] Refactor: split classes into separate files and update prop system - Split Component.kt into Component, ItemComponent, DynamicComponent, ContainerComponent - Split Prop.kt into individual prop type files - Replace LazyProp with ImmutableLazyProp and MutableLazyProp - Make ComputedProp.compute suspend for async operations - Remove utility functions from ViewDsl.kt (will be components later) - Update all Prop implementations to use suspend get() Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 47 ------- .../api/gui/component/ContainerComponent.kt | 21 +++ .../api/gui/component/DynamicComponent.kt | 20 +++ .../bukkit/api/gui/component/ItemComponent.kt | 20 +++ .../bukkit/api/gui/dsl/ComponentDsl.kt | 15 ++- .../surfapi/bukkit/api/gui/dsl/ViewDsl.kt | 124 ------------------ .../bukkit/api/gui/props/ComputedProp.kt | 12 ++ .../bukkit/api/gui/props/ImmutableLazyProp.kt | 14 ++ .../bukkit/api/gui/props/ImmutableProp.kt | 15 +++ .../bukkit/api/gui/props/MutableLazyProp.kt | 21 +++ .../bukkit/api/gui/props/MutableProp.kt | 26 ++++ .../bukkit/api/gui/props/PaginationProp.kt | 2 +- .../surf/surfapi/bukkit/api/gui/props/Prop.kt | 116 +--------------- .../bukkit/api/gui/props/ViewerMutableProp.kt | 26 ++++ .../bukkit/api/gui/props/ViewerPropStorage.kt | 21 +++ 15 files changed, 210 insertions(+), 290 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ComputedProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerPropStorage.kt 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 index 05a1915b..b36971a1 100644 --- 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 @@ -130,50 +130,3 @@ abstract class Component { children.forEach { it.updateWithChildren() } } } - -/** - * Simple item component that renders a static item. - */ -open class ItemComponent( - private val item: ItemStack, - private val clickHandler: (ClickContext.() -> Unit)? = null -) : Component() { - - override fun render(context: ViewContext): ItemStack = item - - override fun onClick(context: ClickContext) { - clickHandler?.invoke(context) - } -} - -/** - * Dynamic component that renders based on a callback. - */ -open class DynamicComponent( - private val renderer: (ViewContext) -> ItemStack?, - private val clickHandler: (ClickContext.() -> Unit)? = null -) : Component() { - - override fun render(context: ViewContext): ItemStack? = renderer(context) - - override fun onClick(context: ClickContext) { - clickHandler?.invoke(context) - } -} - -/** - * Container component that renders multiple items at specific slots. - * This is useful for paginated components or complex layouts. - */ -abstract class ContainerComponent : Component() { - /** - * Render multiple items at their respective slots. - * Override this to provide the slot-to-item mapping. - */ - abstract override fun renderSlots(context: ViewContext): Map - - /** - * Container doesn't render a single item. - */ - final override fun render(context: ViewContext): ItemStack? = null -} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt new file mode 100644 index 00000000..bddd82a4 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component + +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import org.bukkit.inventory.ItemStack + +/** + * Container component that renders multiple items at specific slots. + * This is useful for paginated components or complex layouts. + */ +abstract class ContainerComponent : Component() { + /** + * Render multiple items at their respective slots. + * Override this to provide the slot-to-item mapping. + */ + abstract override fun renderSlots(context: ViewContext): Map + + /** + * Container doesn't render a single item. + */ + final override fun render(context: ViewContext): ItemStack? = null +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt new file mode 100644 index 00000000..747f992c --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component + +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import org.bukkit.inventory.ItemStack + +/** + * Dynamic component that renders based on a callback. + */ +open class DynamicComponent( + private val renderer: (ViewContext) -> ItemStack?, + private val clickHandler: (ClickContext.() -> Unit)? = null +) : Component() { + + override fun render(context: ViewContext): ItemStack? = renderer(context) + + override fun onClick(context: ClickContext) { + clickHandler?.invoke(context) + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt new file mode 100644 index 00000000..9cf075a9 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.component + +import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import org.bukkit.inventory.ItemStack + +/** + * Simple item component that renders a static item. + */ +open class ItemComponent( + private val item: ItemStack, + private val clickHandler: (ClickContext.() -> Unit)? = null +) : Component() { + + override fun render(context: ViewContext): ItemStack = item + + override fun onClick(context: ClickContext) { + clickHandler?.invoke(context) + } +} 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 index 4fb86af5..daed17fa 100644 --- 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 @@ -124,15 +124,22 @@ class PropsBuilder { /** * Create a computed prop. */ - fun computed(name: String, compute: () -> T): ComputedProp { + fun computed(name: String, compute: suspend () -> T): ComputedProp { return ComputedProp(name, compute).also { _props[name] = it } } /** - * Create a lazy prop. + * Create an immutable lazy prop. */ - fun lazy(name: String, mutable: Boolean = false, initializer: () -> T): LazyProp { - return LazyProp(name, initializer, mutable).also { _props[name] = it } + fun immutableLazy(name: String, initializer: () -> T): ImmutableLazyProp { + return ImmutableLazyProp(name, initializer).also { _props[name] = it } + } + + /** + * Create a mutable lazy prop. + */ + fun mutableLazy(name: String, initializer: () -> T): MutableLazyProp { + return MutableLazyProp(name, initializer).also { _props[name] = it } } /** 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 index 29ce7ca7..ef460cd8 100644 --- 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 @@ -1,13 +1,9 @@ package dev.slne.surf.surfapi.bukkit.api.gui.dsl -import dev.slne.surf.surfapi.bukkit.api.gui.component.Component -import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig import net.kyori.adventure.text.Component as AdventureComponent -import org.bukkit.Material import org.bukkit.event.inventory.InventoryType -import org.bukkit.inventory.ItemStack /** * DSL marker for view building. @@ -64,123 +60,3 @@ fun GuiView.configure(builder: ViewConfigBuilder.() -> Unit) { config.cancelOnClick = builtConfig.cancelOnClick config.closeOnClickOutside = builtConfig.closeOnClickOutside } - -/** - * Fill a range of slots with the same component. - */ -@ComponentDsl -fun RenderContext.fillSlots(slots: IntRange, component: Component) { - slots.forEach { slot -> - renderComponent(slot, component) - } -} - -/** - * Fill a range of slots with the same item. - */ -@ComponentDsl -fun RenderContext.fillSlots(slots: IntRange, item: ItemStack) { - slots.forEach { slot -> - setItem(slot, item) - } -} - -/** - * Fill a row with items. - */ -@ComponentDsl -fun RenderContext.fillRow(row: Int, component: Component) { - val startSlot = row * 9 - fillSlots(startSlot until startSlot + 9, component) -} - -/** - * Fill a row with items. - */ -@ComponentDsl -fun RenderContext.fillRow(row: Int, item: ItemStack) { - val startSlot = row * 9 - fillSlots(startSlot until startSlot + 9, item) -} - -/** - * Fill borders with a component. - */ -@ComponentDsl -fun RenderContext.fillBorders(component: Component) { - val rows = view.config.size / 9 - - // Top and bottom rows - fillRow(0, component) - fillRow(rows - 1, component) - - // Left and right columns - for (row in 1 until rows - 1) { - renderComponent(row * 9, component) - renderComponent(row * 9 + 8, component) - } -} - -/** - * Fill borders with an item. - */ -@ComponentDsl -fun RenderContext.fillBorders(item: ItemStack) { - val rows = view.config.size / 9 - - // Top and bottom rows - fillRow(0, item) - fillRow(rows - 1, item) - - // Left and right columns - for (row in 1 until rows - 1) { - setItem(row * 9, item) - setItem(row * 9 + 8, item) - } -} - -/** - * Create a glass pane item (commonly used for borders/fillers). - */ -fun glassPane(material: Material = Material.GRAY_STAINED_GLASS_PANE): ItemStack { - return ItemStack.of(material) -} - -/** - * Place component at row and column position. - */ -@ComponentDsl -fun RenderContext.slotAt(row: Int, column: Int, component: Component) { - val slot = row * 9 + column - renderComponent(slot, component) -} - -/** - * Place item at row and column position. - */ -@ComponentDsl -fun RenderContext.slotAt(row: Int, column: Int, item: ItemStack) { - val slot = row * 9 + column - setItem(slot, item) -} - -/** - * Center a component horizontally in a row. - */ -@ComponentDsl -fun RenderContext.centerInRow(row: Int, component: Component) { - val slot = row * 9 + 4 - renderComponent(slot, component) -} - -/** - * Place multiple components in a centered pattern. - */ -@ComponentDsl -fun RenderContext.centerComponents(row: Int, components: List) { - val centerOffset = 4 - ((components.size - 1) / 2) - components.forEachIndexed { index, component -> - val slot = row * 9 + centerOffset + index - renderComponent(slot, component) - } -} 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 00000000..fda2d0f4 --- /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,12 @@ +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. + */ +class ComputedProp( + override val name: String, + private val compute: suspend () -> T +) : Prop { + override suspend fun get(): T = compute() +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt new file mode 100644 index 00000000..13090c29 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt @@ -0,0 +1,14 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +/** + * Immutable lazy prop - gets available when accessed, using a callback. + * Cannot be modified after initialization. + */ +class ImmutableLazyProp( + override val name: String, + private val initializer: () -> T +) : Prop { + private val value = lazy { initializer() } + + override suspend fun get(): T = value.value +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt new file mode 100644 index 00000000..826cf0c6 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt @@ -0,0 +1,15 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import kotlin.reflect.KProperty + +/** + * Immutable prop - always available and immutable after initialization. + */ +class ImmutableProp( + override val name: String, + private val value: T +) : Prop { + override suspend fun get(): T = value + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt new file mode 100644 index 00000000..727077c7 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +/** + * Mutable lazy prop - gets available when accessed, using a callback. + * Can be modified after initialization. + */ +class MutableLazyProp( + 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 + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt new file mode 100644 index 00000000..946853cf --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt @@ -0,0 +1,26 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty + +/** + * Mutable prop - always available and mutable. + * Global to the view, shared across all viewers. + */ +class MutableProp( + 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) + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt index 74651359..b0129c31 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt @@ -12,7 +12,7 @@ class PaginationProp( private val currentPages = mutableMapOf() - override fun get(): PaginationState { + override suspend fun get(): PaginationState { // Default state when no viewer context return getState(0) } 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 index d1124a7a..7b73da93 100644 --- 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 @@ -1,10 +1,5 @@ package dev.slne.surf.surfapi.bukkit.api.gui.props -import org.bukkit.entity.Player -import java.util.UUID -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. @@ -12,119 +7,12 @@ import kotlin.reflect.KProperty sealed interface Prop { /** * Gets the value of this prop. + * Suspend function to support ComputedProp with async operations. */ - fun get(): T + suspend fun get(): T /** * The name of this prop. */ val name: String } - -/** - * Viewer-specific prop storage. - * Maps viewer UUIDs to their prop values. - */ -class ViewerPropStorage(private val initialValue: () -> T) { - private val storage = mutableMapOf() - - fun get(viewerId: UUID): T = storage.getOrPut(viewerId) { initialValue() } - - fun set(viewerId: UUID, value: T) { - storage[viewerId] = value - } - - fun clear(viewerId: UUID) { - storage.remove(viewerId) - } -} - -/** - * Immutable prop - always available and immutable after initialization. - */ -class ImmutableProp( - override val name: String, - private val value: T -) : Prop { - override fun get(): T = value - - operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value -} - -/** - * Mutable prop - always available and mutable. - * Global to the view, shared across all viewers. - */ -class MutableProp( - override val name: String, - initialValue: T -) : Prop { - private val value = AtomicReference(initialValue) - - override 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) - } -} - -/** - * Viewer-specific mutable prop - isolated per viewer. - */ -class ViewerMutableProp( - override val name: String, - initialValue: T -) : Prop { - private val globalDefault = initialValue - private val storage = ViewerPropStorage { globalDefault } - - override fun get(): T = globalDefault - - fun get(viewerId: UUID): T = storage.get(viewerId) - - fun set(viewerId: UUID, value: T) { - storage.set(viewerId, value) - } - - fun clear(viewerId: UUID) { - storage.clear(viewerId) - } -} - -/** - * Computed prop - accepts a callback that computes the value. - */ -class ComputedProp( - override val name: String, - private val compute: () -> T -) : Prop { - override fun get(): T = compute() -} - -/** - * Lazy prop - gets available when accessed, using a callback. - */ -class LazyProp( - override val name: String, - private val initializer: () -> T, - private val mutable: Boolean = false -) : Prop { - private val value = lazy { initializer() } - private var mutableValue: T? = null - - override fun get(): T { - return mutableValue ?: value.value - } - - fun set(value: T) { - if (!mutable) { - throw UnsupportedOperationException("Cannot set immutable lazy prop") - } - mutableValue = value - } -} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt new file mode 100644 index 00000000..0539cd49 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt @@ -0,0 +1,26 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import java.util.UUID + +/** + * Viewer-specific mutable prop - isolated per viewer. + */ +class ViewerMutableProp( + override val name: String, + initialValue: T +) : Prop { + private val globalDefault = initialValue + private val storage = ViewerPropStorage { globalDefault } + + override suspend fun get(): T = globalDefault + + fun get(viewerId: UUID): T = storage.get(viewerId) + + fun set(viewerId: UUID, value: T) { + storage.set(viewerId, value) + } + + fun clear(viewerId: UUID) { + storage.clear(viewerId) + } +} 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 00000000..493c9709 --- /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,21 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.props + +import java.util.UUID + +/** + * Viewer-specific prop storage. + * Maps viewer UUIDs to their prop values. + */ +class ViewerPropStorage(private val initialValue: () -> T) { + private val storage = mutableMapOf() + + fun get(viewerId: UUID): T = storage.getOrPut(viewerId) { initialValue() } + + fun set(viewerId: UUID, value: T) { + storage[viewerId] = value + } + + fun clear(viewerId: UUID) { + storage.remove(viewerId) + } +} From 9c8c8e5264c2fb3c999583c102e4e5747907b408 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:56:50 +0000 Subject: [PATCH 14/92] Make ViewContext.getProp suspend to support ComputedProp Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../dev/slne/surf/surfapi/bukkit/api/gui/context/ViewContext.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c658c417..ac53c3d2 100644 --- 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 @@ -32,7 +32,7 @@ interface ViewContext { /** * Get a prop value from the view. */ - fun getProp(prop: Prop): T = prop.get() + suspend fun getProp(prop: Prop): T = prop.get() /** * Navigate to another view. From b6a183286241a5fe4838b6a60699a5cafb2886c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:08:40 +0000 Subject: [PATCH 15/92] Split ViewContext into separate files, remove mount/unmount lifecycle, add player-specific prop getter - Split ViewContext.kt into ViewContext, ClickContext, RenderContext, LifecycleContext, ResumeContext, LifecycleEventType - Remove onMount/onUnmount from Component and ComponentBuilder - Remove isMounted property from Component - Remove MOUNT/UNMOUNT from LifecycleEventType enum - Remove mount/unmount calls from GuiView addComponent/removeComponent - Add getPropForPlayer() to ViewContext for ViewerMutableProp and PaginationProp Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 20 +--- .../bukkit/api/gui/context/ClickContext.kt | 32 ++++++ .../api/gui/context/LifecycleContext.kt | 11 ++ .../api/gui/context/LifecycleEventType.kt | 12 ++ .../bukkit/api/gui/context/RenderContext.kt | 24 ++++ .../bukkit/api/gui/context/ResumeContext.kt | 19 ++++ .../bukkit/api/gui/context/ViewContext.kt | 103 +++--------------- .../bukkit/api/gui/dsl/ComponentDsl.kt | 12 -- .../surfapi/bukkit/api/gui/view/GuiView.kt | 15 --- 9 files changed, 113 insertions(+), 135 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ClickContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/LifecycleEventType.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/ResumeContext.kt 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 index b36971a1..48645413 100644 --- 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 @@ -37,12 +37,6 @@ abstract class Component { */ open val updateInterval: Duration? = null - /** - * Whether this component is currently mounted. - */ - var isMounted: Boolean = false - internal set - /** * Props accessible by this component. * Children can access parent props. @@ -54,16 +48,6 @@ abstract class Component { */ internal var attachedRef: Ref? = null - /** - * Called when the component is mounted. - */ - open fun onMount(context: LifecycleContext) {} - - /** - * Called when the component is unmounted. - */ - open fun onUnmount(context: LifecycleContext) {} - /** * Called when the component is updated. */ @@ -117,9 +101,7 @@ abstract class Component { * Trigger an update of this component. */ fun update() { - if (isMounted) { - view.updateComponent(this) - } + view.updateComponent(this) } /** 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 00000000..fc7cded2 --- /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,32 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack + +/** + * Context for click events. + */ +interface ClickContext : ViewContext { + /** + * The click event. + */ + val event: InventoryClickEvent + + /** + * The clicked item. + */ + val item: ItemStack? + get() = event.currentItem + + /** + * The slot that was clicked. + */ + val slot: Int + get() = 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/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 00000000..0b229f5f --- /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 00000000..0596a7ec --- /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,12 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +/** + * Types of lifecycle events. + */ +enum class LifecycleEventType { + UPDATE, + 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/RenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt new file mode 100644 index 00000000..de9e9640 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt @@ -0,0 +1,24 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context + +import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import org.bukkit.inventory.ItemStack + +/** + * Context for render operations. + */ +interface RenderContext : ViewContext { + /** + * Render a component at the specified slot. + */ + fun renderComponent(slot: Int, component: Component) + + /** + * Clear a slot. + */ + fun clearSlot(slot: Int) + + /** + * Set an item at a slot without a component. + */ + fun setItem(slot: Int, item: ItemStack) +} 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 00000000..e1f54af9 --- /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 index ac53c3d2..afce4974 100644 --- 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 @@ -1,11 +1,10 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context -import dev.slne.surf.surfapi.bukkit.api.gui.component.Component +import dev.slne.surf.surfapi.bukkit.api.gui.props.PaginationProp import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.props.ViewerMutableProp import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import org.bukkit.entity.Player -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.ItemStack import java.util.UUID /** @@ -34,6 +33,18 @@ interface ViewContext { */ suspend fun getProp(prop: Prop): T = prop.get() + /** + * Get a prop value for a specific player. + * This is useful for ViewerMutableProp and PaginationProp which have per-viewer state. + */ + fun getPropForPlayer(prop: Prop, playerId: UUID): T { + return when (prop) { + is ViewerMutableProp -> prop.get(playerId) + is PaginationProp -> prop.get(playerId) + else -> throw UnsupportedOperationException("getPropForPlayer only supports ViewerMutableProp and PaginationProp") + } + } + /** * Navigate to another view. */ @@ -54,89 +65,3 @@ interface ViewContext { */ fun update() } - -/** - * Context for click events. - */ -interface ClickContext : ViewContext { - /** - * The click event. - */ - val event: InventoryClickEvent - - /** - * The clicked item. - */ - val item: ItemStack? - get() = event.currentItem - - /** - * The slot that was clicked. - */ - val slot: Int - get() = event.slot - - /** - * The component that was clicked, if any. - */ - val component: Component? -} - -/** - * Context for render operations. - */ -interface RenderContext : ViewContext { - /** - * Render a component at the specified slot. - */ - fun renderComponent(slot: Int, component: Component) - - /** - * Clear a slot. - */ - fun clearSlot(slot: Int) - - /** - * Set an item at a slot without a component. - */ - fun setItem(slot: Int, item: ItemStack) -} - -/** - * Context for lifecycle events. - */ -interface LifecycleContext : ViewContext { - /** - * The type of lifecycle event. - */ - val eventType: LifecycleEventType -} - -/** - * Types of lifecycle events. - */ -enum class LifecycleEventType { - MOUNT, - UNMOUNT, - UPDATE, - FIRST_RENDER, - OPEN, - CLOSE, - RESUME -} - -/** - * 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/dsl/ComponentDsl.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt index daed17fa..2fa4038f 100644 --- 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 @@ -26,8 +26,6 @@ annotation class ComponentDsl class ComponentBuilder { var updateInterval: Duration? = null var ref: Ref? = null - var onMount: (LifecycleContext.() -> Unit)? = null - var onUnmount: (LifecycleContext.() -> Unit)? = null var onUpdate: (LifecycleContext.() -> Unit)? = null var onClick: (ClickContext.() -> Unit)? = null @@ -48,16 +46,6 @@ class ComponentBuilder { override val updateInterval: Duration? = this@ComponentBuilder.updateInterval override val props: Map> = _props - override fun onMount(context: LifecycleContext) { - super.onMount(context) - this@ComponentBuilder.onMount?.invoke(context) - } - - override fun onUnmount(context: LifecycleContext) { - super.onUnmount(context) - this@ComponentBuilder.onUnmount?.invoke(context) - } - override fun onUpdate(context: LifecycleContext) { super.onUpdate(context) this@ComponentBuilder.onUpdate?.invoke(context) 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 index 0ddc301d..d685dcc8 100644 --- 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 @@ -174,27 +174,12 @@ abstract class GuiView { fun addComponent(slot: Int, component: Component) { component.view = this _components[slot] = component - - if (!component.isMounted) { - viewers.values.forEach { player -> - val lifecycleContext = createLifecycleContext(player, LifecycleEventType.MOUNT) - component.onMount(lifecycleContext) - } - component.isMounted = true - } } /** * Remove a component from a slot. */ fun removeComponent(slot: Int) { - _components[slot]?.let { component -> - viewers.values.forEach { player -> - val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UNMOUNT) - component.onUnmount(lifecycleContext) - } - component.isMounted = false - } _components.remove(slot) } From e2d2bb6aef72e85fee5de878010bcaedc1fd6ce9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:23:03 +0000 Subject: [PATCH 16/92] Remove PaginationProp, create PaginationComponent, GuiItem, and Slot classes - Created Slot class with index and x,y coordinate constructors - Created GuiItem class to replace ItemStack with utility functions - Created PaginationComponent to replace PaginationProp - Updated all components to use GuiItem instead of ItemStack - Updated all slot parameters from Int to Slot throughout framework - Updated RenderContext and ClickContext to use Slot and GuiItem - Updated GuiView to use Slot in components map - Updated BukkitGuiView and BukkitRenderContext implementations - Removed PaginationProp.kt file - Removed pagination() function from PropsBuilder Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surf/surfapi/bukkit/api/gui/GuiItem.kt | 79 +++++++++++ .../slne/surf/surfapi/bukkit/api/gui/Slot.kt | 44 ++++++ .../bukkit/api/gui/component/Component.kt | 11 +- .../api/gui/component/ContainerComponent.kt | 7 +- .../api/gui/component/DynamicComponent.kt | 6 +- .../bukkit/api/gui/component/ItemComponent.kt | 6 +- .../api/gui/component/PaginationComponent.kt | 126 ++++++++++++++++++ .../bukkit/api/gui/context/ClickContext.kt | 12 +- .../bukkit/api/gui/context/RenderContext.kt | 9 +- .../bukkit/api/gui/dsl/ComponentDsl.kt | 20 +-- .../bukkit/api/gui/props/PaginationProp.kt | 86 ------------ .../surfapi/bukkit/api/gui/view/GuiView.kt | 17 +-- .../server/gui/context/BukkitContexts.kt | 14 +- .../bukkit/server/gui/view/BukkitGuiView.kt | 41 +++--- 14 files changed, 319 insertions(+), 159 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/GuiItem.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/Slot.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt 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 00000000..a84fbef6 --- /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,79 @@ +package dev.slne.surf.surfapi.bukkit.api.gui + +import net.kyori.adventure.text.Component +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) + } + + companion object { + /** + * Create a GuiItem from an ItemStack. + */ + fun of(itemStack: ItemStack): GuiItem = GuiItem(itemStack) + + /** + * Create a GuiItem from a material. + */ + fun of(material: Material, amount: Int = 1): GuiItem { + return GuiItem(ItemStack(material, amount)) + } + + /** + * 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 00000000..4bc14e49 --- /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,44 @@ +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)" +} 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 index 48645413..c9e25284 100644 --- 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 @@ -1,12 +1,13 @@ 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.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.props.Prop import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView -import org.bukkit.inventory.ItemStack import kotlin.time.Duration /** @@ -59,16 +60,16 @@ abstract class Component { open fun onClick(context: ClickContext) {} /** - * Renders the component to an ItemStack. + * Renders the component to a GuiItem. * For container components, return null. */ - open fun render(context: ViewContext): ItemStack? = null + open fun render(context: ViewContext): GuiItem? = null /** * For container components, render multiple items at specific slots. - * Returns a map of slot to ItemStack. + * Returns a map of Slot to GuiItem. */ - open fun renderSlots(context: ViewContext): Map = emptyMap() + open fun renderSlots(context: ViewContext): Map = emptyMap() /** * Add a child component. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt index bddd82a4..455f026c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -1,7 +1,8 @@ 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.context.ViewContext -import org.bukkit.inventory.ItemStack /** * Container component that renders multiple items at specific slots. @@ -12,10 +13,10 @@ abstract class ContainerComponent : Component() { * Render multiple items at their respective slots. * Override this to provide the slot-to-item mapping. */ - abstract override fun renderSlots(context: ViewContext): Map + abstract override fun renderSlots(context: ViewContext): Map /** * Container doesn't render a single item. */ - final override fun render(context: ViewContext): ItemStack? = null + final override fun render(context: ViewContext): GuiItem? = null } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index 747f992c..273041be 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -1,18 +1,18 @@ 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.context.ClickContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext -import org.bukkit.inventory.ItemStack /** * Dynamic component that renders based on a callback. */ open class DynamicComponent( - private val renderer: (ViewContext) -> ItemStack?, + private val renderer: (ViewContext) -> GuiItem?, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override fun render(context: ViewContext): ItemStack? = renderer(context) + override fun render(context: ViewContext): GuiItem? = renderer(context) override fun onClick(context: ClickContext) { clickHandler?.invoke(context) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index 9cf075a9..f4d7471d 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -1,18 +1,18 @@ 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.context.ClickContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext -import org.bukkit.inventory.ItemStack /** * Simple item component that renders a static item. */ open class ItemComponent( - private val item: ItemStack, + private val item: GuiItem, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override fun render(context: ViewContext): ItemStack = item + override fun render(context: ViewContext): GuiItem = item override fun onClick(context: ClickContext) { clickHandler?.invoke(context) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt new file mode 100644 index 00000000..b1cc2288 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -0,0 +1,126 @@ +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.context.ClickContext +import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext + +/** + * Component for paginated content. + * Renders multiple items across specified slots based on current page. + */ +class PaginationComponent( + private val items: () -> List, + private val pageSize: Int = 9, + private val itemRenderer: (T, ViewContext) -> GuiItem?, + private val onItemClick: ((T, ClickContext) -> Unit)? = null +) : ContainerComponent() { + + private val currentPages = mutableMapOf() + + /** + * Get the current page for a viewer. + */ + fun getCurrentPage(viewerId: java.util.UUID): Int { + return currentPages.getOrDefault(viewerId, 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(viewerId: java.util.UUID): List { + val allItems = items() + val currentPage = getCurrentPage(viewerId) + val startIndex = currentPage * pageSize + val endIndex = minOf(startIndex + pageSize, allItems.size) + + return if (startIndex < allItems.size) { + allItems.subList(startIndex, endIndex) + } else { + emptyList() + } + } + + /** + * Check if there is a next page for a viewer. + */ + fun hasNextPage(viewerId: java.util.UUID): Boolean { + val currentPage = getCurrentPage(viewerId) + return currentPage < getTotalPages() - 1 + } + + /** + * Check if there is a previous page for a viewer. + */ + fun hasPreviousPage(viewerId: java.util.UUID): Boolean { + return getCurrentPage(viewerId) > 0 + } + + /** + * Go to the next page for a viewer. + */ + fun nextPage(viewerId: java.util.UUID) { + if (hasNextPage(viewerId)) { + currentPages[viewerId] = getCurrentPage(viewerId) + 1 + } + } + + /** + * Go to the previous page for a viewer. + */ + fun previousPage(viewerId: java.util.UUID) { + val currentPage = getCurrentPage(viewerId) + if (currentPage > 0) { + currentPages[viewerId] = currentPage - 1 + } + } + + /** + * Set a specific page for a viewer. + */ + fun setPage(viewerId: java.util.UUID, page: Int) { + if (page in 0 until getTotalPages()) { + currentPages[viewerId] = page + } + } + + /** + * Clear the page state for a viewer. + */ + fun clearPage(viewerId: java.util.UUID) { + currentPages.remove(viewerId) + } + + override fun renderSlots(context: ViewContext): Map { + val pageItems = getPageItems(context.player.uniqueId) + val renderedSlots = mutableMapOf() + + pageItems.forEachIndexed { index, item -> + val guiItem = itemRenderer(item, context) + if (guiItem != null) { + renderedSlots[Slot.of(index)] = guiItem + } + } + + return renderedSlots + } + + override fun onClick(context: ClickContext) { + if (onItemClick != null) { + val pageItems = getPageItems(context.player.uniqueId) + val slotIndex = context.slot.index + if (slotIndex in pageItems.indices) { + val item = pageItems[slotIndex] + onItemClick.invoke(item, context) + } + } + } +} 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 index fc7cded2..fe9b00b7 100644 --- 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 @@ -1,8 +1,10 @@ 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 -import org.bukkit.inventory.ItemStack /** * Context for click events. @@ -16,14 +18,14 @@ interface ClickContext : ViewContext { /** * The clicked item. */ - val item: ItemStack? - get() = event.currentItem + val item: GuiItem? + get() = event.currentItem?.toGuiItem() /** * The slot that was clicked. */ - val slot: Int - get() = event.slot + val slot: Slot + get() = Slot.of(event.slot) /** * The component that was clicked, if any. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt index de9e9640..1eb95991 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt @@ -1,7 +1,8 @@ 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 org.bukkit.inventory.ItemStack /** * Context for render operations. @@ -10,15 +11,15 @@ interface RenderContext : ViewContext { /** * Render a component at the specified slot. */ - fun renderComponent(slot: Int, component: Component) + fun renderComponent(slot: Slot, component: Component) /** * Clear a slot. */ - fun clearSlot(slot: Int) + fun clearSlot(slot: Slot) /** * Set an item at a slot without a component. */ - fun setItem(slot: Int, item: ItemStack) + 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/dsl/ComponentDsl.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt index 2fa4038f..f597dfaa 100644 --- 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 @@ -1,5 +1,7 @@ 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.DynamicComponent import dev.slne.surf.surfapi.bukkit.api.gui.component.ItemComponent @@ -10,7 +12,6 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext import dev.slne.surf.surfapi.bukkit.api.gui.props.* import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView -import org.bukkit.inventory.ItemStack import kotlin.time.Duration /** @@ -41,7 +42,7 @@ class ComponentBuilder { /** * Build the component. */ - internal fun build(renderer: (ViewContext) -> ItemStack?): Component { + internal fun build(renderer: (ViewContext) -> GuiItem?): Component { return object : DynamicComponent(renderer, onClick) { override val updateInterval: Duration? = this@ComponentBuilder.updateInterval override val props: Map> = _props @@ -61,7 +62,7 @@ class ComponentBuilder { * Create a component with a static item. */ fun component( - item: ItemStack, + item: GuiItem, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() @@ -73,7 +74,7 @@ fun component( * Create a component with a dynamic renderer. */ fun dynamicComponent( - renderer: (ViewContext) -> ItemStack?, + renderer: (ViewContext) -> GuiItem?, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() @@ -130,13 +131,6 @@ class PropsBuilder { return MutableLazyProp(name, initializer).also { _props[name] = it } } - /** - * Create a pagination prop. - */ - fun pagination(name: String = "pagination", pageSize: Int = 9, items: () -> List): PaginationProp { - return PaginationProp(name, items, pageSize).also { _props[name] = it } - } - /** * Get all props. */ @@ -156,7 +150,7 @@ fun props(builder: PropsBuilder.() -> Unit): Map> { * DSL for rendering components in a view. */ @ComponentDsl -fun RenderContext.slot(slot: Int, component: Component) { +fun RenderContext.slot(slot: Slot, component: Component) { renderComponent(slot, component) } @@ -164,7 +158,7 @@ fun RenderContext.slot(slot: Int, component: Component) { * DSL for rendering components in a view with item. */ @ComponentDsl -fun RenderContext.slot(slot: Int, item: ItemStack, onClick: (ClickContext.() -> Unit)? = null) { +fun RenderContext.slot(slot: Slot, item: GuiItem, onClick: (ClickContext.() -> Unit)? = null) { val component = ItemComponent(item, onClick) renderComponent(slot, component) } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt deleted file mode 100644 index b0129c31..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/PaginationProp.kt +++ /dev/null @@ -1,86 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.props - -/** - * Default pagination prop for managing paginated content in GUIs. - * Viewer-specific pagination state. - */ -class PaginationProp( - override val name: String = "pagination", - private val items: () -> List, - private val pageSize: Int = 9 -) : Prop> { - - private val currentPages = mutableMapOf() - - override suspend fun get(): PaginationState { - // Default state when no viewer context - return getState(0) - } - - fun get(viewerId: java.util.UUID): PaginationState { - val currentPage = currentPages.getOrPut(viewerId) { 0 } - return getState(currentPage) - } - - private fun getState(currentPage: Int): PaginationState { - val allItems = items() - val totalPages = (allItems.size + pageSize - 1) / pageSize - - val startIndex = currentPage * pageSize - val endIndex = minOf(startIndex + pageSize, allItems.size) - val pageItems = if (startIndex < allItems.size) { - allItems.subList(startIndex, endIndex) - } else { - emptyList() - } - - return PaginationState( - items = pageItems, - currentPage = currentPage, - totalPages = totalPages, - pageSize = pageSize, - totalItems = allItems.size, - hasNextPage = currentPage < totalPages - 1, - hasPreviousPage = currentPage > 0 - ) - } - - fun nextPage(viewerId: java.util.UUID) { - val current = currentPages.getOrPut(viewerId) { 0 } - val state = getState(current) - if (state.hasNextPage) { - currentPages[viewerId] = current + 1 - } - } - - fun previousPage(viewerId: java.util.UUID) { - val current = currentPages.getOrPut(viewerId) { 0 } - if (current > 0) { - currentPages[viewerId] = current - 1 - } - } - - fun setPage(viewerId: java.util.UUID, page: Int) { - val state = getState(0) // Get state to check total pages - if (page in 0 until state.totalPages) { - currentPages[viewerId] = page - } - } - - fun clear(viewerId: java.util.UUID) { - currentPages.remove(viewerId) - } -} - -/** - * State object for pagination. - */ -data class PaginationState( - val items: List, - val currentPage: Int, - val totalPages: Int, - val pageSize: Int, - val totalItems: Int, - val hasNextPage: Boolean, - val hasPreviousPage: Boolean -) 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 index d685dcc8..7f8e5af2 100644 --- 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 @@ -1,5 +1,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view +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.props.Prop @@ -57,8 +58,8 @@ abstract class GuiView { /** * Components in this view, mapped by slot. */ - private val _components = mutableMapOf() - val components: Map get() = _components.toMap() + private val _components = mutableMapOf() + val components: Map get() = _components.toMap() /** * Whether this view has been initialized. @@ -171,7 +172,7 @@ abstract class GuiView { /** * Add a component at a slot. */ - fun addComponent(slot: Int, component: Component) { + fun addComponent(slot: Slot, component: Component) { component.view = this _components[slot] = component } @@ -179,18 +180,10 @@ abstract class GuiView { /** * Remove a component from a slot. */ - fun removeComponent(slot: Int) { + fun removeComponent(slot: Slot) { _components.remove(slot) } - /** - * Get a prop value for a specific player. - */ - fun getProp(player: Player, prop: Prop): T { - val context = PropContext(player.uniqueId, player) - return prop.get(context) - } - /** * Create a view context for a player. */ diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt index bc0c2616..b23df2c5 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt @@ -1,12 +1,14 @@ package dev.slne.surf.surfapi.bukkit.server.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.context.* +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.server.gui.view.BukkitGuiView import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.ItemStack /** * Shared navigation logic for all context implementations. @@ -104,18 +106,18 @@ internal class BukkitRenderContext( private val bukkitView: BukkitGuiView ) : RenderContext { - override fun renderComponent(slot: Int, component: Component) { + override fun renderComponent(slot: Slot, component: Component) { bukkitView.addComponent(slot, component) } - override fun clearSlot(slot: Int) { + override fun clearSlot(slot: Slot) { bukkitView.removeComponent(slot) } - override fun setItem(slot: Int, item: ItemStack) { + override fun setItem(slot: Slot, item: GuiItem) { val inventory = player.openInventory.topInventory - if (slot in 0 until inventory.size) { - inventory.setItem(slot, item) + if (slot.index in 0 until inventory.size) { + inventory.setItem(slot.index, item.toItemStack()) } } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index 2c1e43ca..40da43ba 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -2,8 +2,11 @@ package dev.slne.surf.surfapi.bukkit.server.gui.view import com.github.shynixn.mccoroutine.folia.entityDispatcher import com.github.shynixn.mccoroutine.folia.launch +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.* +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.server.gui.context.* import dev.slne.surf.surfapi.bukkit.server.plugin @@ -55,16 +58,16 @@ abstract class BukkitGuiView : GuiView() { // Handle container components (render multiple slots) val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { - slotsToRender.forEach { (slotPos, item) -> - if (slotPos < inventory.size) { - inventory.setItem(slotPos, item) + slotsToRender.forEach { (slotObj, guiItem) -> + if (slotObj.index < inventory.size) { + inventory.setItem(slotObj.index, guiItem.toItemStack()) } } } else { // Regular component (single item) - val item = component.render(context) - if (item != null && slot < inventory.size) { - inventory.setItem(slot, item) + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) } } } @@ -128,16 +131,16 @@ abstract class BukkitGuiView : GuiView() { // Handle container components val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { - slotsToRender.forEach { (slotPos, item) -> - if (slotPos < inventory.size) { - inventory.setItem(slotPos, item) + slotsToRender.forEach { (slotObj, guiItem) -> + if (slotObj.index < inventory.size) { + inventory.setItem(slotObj.index, guiItem.toItemStack()) } } } else { // Regular component - val item = component.render(context) - if (item != null && slot < inventory.size) { - inventory.setItem(slot, item) + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) } } } @@ -155,16 +158,16 @@ abstract class BukkitGuiView : GuiView() { // Handle container components val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { - slotsToRender.forEach { (slotPos, item) -> - if (slotPos < inventory.size) { - inventory.setItem(slotPos, item) + slotsToRender.forEach { (slotObj, guiItem) -> + if (slotObj.index < inventory.size) { + inventory.setItem(slotObj.index, guiItem.toItemStack()) } } } else { // Regular component - val item = component.render(context) - if (item != null && slot < inventory.size) { - inventory.setItem(slot, item) + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) } } } @@ -177,7 +180,7 @@ abstract class BukkitGuiView : GuiView() { event.isCancelled = true } - val slot = event.slot + val slot = Slot.of(event.slot) val component = components[slot] val clickContext = BukkitClickContext(this, player, event, component) From 67af27dac9d44e8d964f9f56413e86bbde3e5745 Mon Sep 17 00:00:00 2001 From: Ammo Date: Tue, 3 Feb 2026 21:58:34 +0100 Subject: [PATCH 17/92] feat: implement new GUI framework with refactored component and view configurations --- .../api/gui/component/DynamicComponent.kt | 3 +- .../bukkit/api/gui/component/ItemComponent.kt | 3 +- .../api/gui/component/PaginationComponent.kt | 74 ++++++++-------- .../bukkit/api/gui/context/ViewContext.kt | 24 ++---- .../bukkit/api/gui/dsl/ComponentDsl.kt | 23 +++-- .../surfapi/bukkit/api/gui/view/GuiView.kt | 84 +++++++------------ .../surfapi/bukkit/api/gui/view/ViewConfig.kt | 29 +++++++ 7 files changed, 120 insertions(+), 120 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewConfig.kt diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index 273041be..2ae094fd 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -11,9 +11,8 @@ open class DynamicComponent( private val renderer: (ViewContext) -> GuiItem?, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override fun render(context: ViewContext): GuiItem? = renderer(context) - + override fun onClick(context: ClickContext) { clickHandler?.invoke(context) } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index f4d7471d..f3c15c56 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -11,9 +11,8 @@ open class ItemComponent( private val item: GuiItem, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override fun render(context: ViewContext): GuiItem = item - + override fun onClick(context: ClickContext) { clickHandler?.invoke(context) } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index b1cc2288..f6859d2b 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -4,6 +4,8 @@ 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.context.ClickContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext +import org.bukkit.entity.Player +import java.util.* /** * Component for paginated content. @@ -15,16 +17,15 @@ class PaginationComponent( private val itemRenderer: (T, ViewContext) -> GuiItem?, private val onItemClick: ((T, ClickContext) -> Unit)? = null ) : ContainerComponent() { - - private val currentPages = mutableMapOf() - + private val currentPages = mutableMapOf() + /** * Get the current page for a viewer. */ - fun getCurrentPage(viewerId: java.util.UUID): Int { - return currentPages.getOrDefault(viewerId, 0) + fun getCurrentPage(viewer: Player): Int { + return currentPages.getOrDefault(viewer.uniqueId, 0) } - + /** * Get the total number of pages. */ @@ -32,93 +33,98 @@ class PaginationComponent( val allItems = items() return (allItems.size + pageSize - 1) / pageSize } - + /** * Get the items for the current page of a viewer. */ - fun getPageItems(viewerId: java.util.UUID): List { + fun getPageItems(viewer: Player): List { val allItems = items() - val currentPage = getCurrentPage(viewerId) + val currentPage = getCurrentPage(viewer) val startIndex = currentPage * pageSize val endIndex = minOf(startIndex + pageSize, allItems.size) - + return if (startIndex < allItems.size) { allItems.subList(startIndex, endIndex) } else { emptyList() } } - + /** * Check if there is a next page for a viewer. */ - fun hasNextPage(viewerId: java.util.UUID): Boolean { - val currentPage = getCurrentPage(viewerId) + 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(viewerId: java.util.UUID): Boolean { - return getCurrentPage(viewerId) > 0 + fun hasPreviousPage(viewer: Player): Boolean { + return getCurrentPage(viewer) > 0 } - + /** * Go to the next page for a viewer. */ - fun nextPage(viewerId: java.util.UUID) { - if (hasNextPage(viewerId)) { - currentPages[viewerId] = getCurrentPage(viewerId) + 1 + fun nextPage(viewer: Player) { + if (hasNextPage(viewer)) { + currentPages[viewer.uniqueId] = getCurrentPage(viewer) + 1 } } - + /** * Go to the previous page for a viewer. */ - fun previousPage(viewerId: java.util.UUID) { - val currentPage = getCurrentPage(viewerId) + fun previousPage(viewer: Player) { + val currentPage = getCurrentPage(viewer) + if (currentPage > 0) { - currentPages[viewerId] = currentPage - 1 + currentPages[viewer.uniqueId] = currentPage - 1 } } - + /** * Set a specific page for a viewer. */ - fun setPage(viewerId: java.util.UUID, page: Int) { + fun setPage(viewerId: UUID, page: Int) { if (page in 0 until getTotalPages()) { currentPages[viewerId] = page } } - + /** * Clear the page state for a viewer. */ - fun clearPage(viewerId: java.util.UUID) { + fun clearPage(viewerId: UUID) { currentPages.remove(viewerId) } - + override fun renderSlots(context: ViewContext): Map { - val pageItems = getPageItems(context.player.uniqueId) + val pageItems = getPageItems(context.player) val renderedSlots = mutableMapOf() - + pageItems.forEachIndexed { index, item -> val guiItem = itemRenderer(item, context) + if (guiItem != null) { renderedSlots[Slot.of(index)] = guiItem } } - + return renderedSlots } - + override fun onClick(context: ClickContext) { if (onItemClick != null) { - val pageItems = getPageItems(context.player.uniqueId) + val pageItems = getPageItems(context.player) val slotIndex = context.slot.index + if (slotIndex in pageItems.indices) { val item = pageItems[slotIndex] + onItemClick.invoke(item, context) } } 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 index afce4974..da6ef495 100644 --- 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 @@ -1,11 +1,10 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context -import dev.slne.surf.surfapi.bukkit.api.gui.props.PaginationProp import dev.slne.surf.surfapi.bukkit.api.gui.props.Prop import dev.slne.surf.surfapi.bukkit.api.gui.props.ViewerMutableProp import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import org.bukkit.entity.Player -import java.util.UUID +import java.util.* /** * Context representing a snapshot of the current GUI state and props. @@ -16,23 +15,17 @@ interface ViewContext { * The view this context belongs to. */ val view: GuiView - + /** * The player interacting with the GUI. */ val player: Player - - /** - * The unique ID of the viewer. - */ - val viewerId: UUID - get() = player.uniqueId - + /** * Get a prop value from the view. */ suspend fun getProp(prop: Prop): T = prop.get() - + /** * Get a prop value for a specific player. * This is useful for ViewerMutableProp and PaginationProp which have per-viewer state. @@ -40,26 +33,25 @@ interface ViewContext { fun getPropForPlayer(prop: Prop, playerId: UUID): T { return when (prop) { is ViewerMutableProp -> prop.get(playerId) - is PaginationProp -> prop.get(playerId) else -> throw UnsupportedOperationException("getPropForPlayer only supports ViewerMutableProp and PaginationProp") } } - + /** * 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. */ 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 index f597dfaa..93b5929a 100644 --- 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 @@ -11,7 +11,6 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext import dev.slne.surf.surfapi.bukkit.api.gui.props.* import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref -import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import kotlin.time.Duration /** @@ -29,16 +28,16 @@ class ComponentBuilder { var ref: Ref? = null var onUpdate: (LifecycleContext.() -> Unit)? = null var onClick: (ClickContext.() -> Unit)? = null - + private val _props = mutableMapOf>() - + /** * Add a prop to this component. */ fun prop(name: String, prop: Prop) { _props[name] = prop } - + /** * Build the component. */ @@ -46,7 +45,7 @@ class ComponentBuilder { return object : DynamicComponent(renderer, onClick) { override val updateInterval: Duration? = this@ComponentBuilder.updateInterval override val props: Map> = _props - + override fun onUpdate(context: LifecycleContext) { super.onUpdate(context) this@ComponentBuilder.onUpdate?.invoke(context) @@ -88,49 +87,49 @@ fun dynamicComponent( @ComponentDsl class PropsBuilder { private val _props = mutableMapOf>() - + /** * Create an immutable prop. */ fun immutable(name: String, value: T): ImmutableProp { return ImmutableProp(name, value).also { _props[name] = it } } - + /** * Create a mutable prop (global to view). */ fun mutable(name: String, initialValue: T): MutableProp { return MutableProp(name, initialValue).also { _props[name] = it } } - + /** * Create a viewer-specific mutable prop. */ fun viewerMutable(name: String, initialValue: T): ViewerMutableProp { return ViewerMutableProp(name, initialValue).also { _props[name] = it } } - + /** * Create a computed prop. */ fun computed(name: String, compute: suspend () -> T): ComputedProp { return ComputedProp(name, compute).also { _props[name] = it } } - + /** * Create an immutable lazy prop. */ fun immutableLazy(name: String, initializer: () -> T): ImmutableLazyProp { return ImmutableLazyProp(name, initializer).also { _props[name] = it } } - + /** * Create a mutable lazy prop. */ fun mutableLazy(name: String, initializer: () -> T): MutableLazyProp { return MutableLazyProp(name, initializer).also { _props[name] = it } } - + /** * Get all props. */ 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 index 7f8e5af2..28ed16a7 100644 --- 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 @@ -4,36 +4,9 @@ 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.props.Prop -import net.kyori.adventure.text.Component as AdventureComponent import org.bukkit.entity.Player -import org.bukkit.event.inventory.InventoryType import kotlin.time.Duration -/** - * Configuration for a GUI view. - */ -data class ViewConfig( - var title: AdventureComponent = AdventureComponent.text("GUI"), - var size: Int = 54, // 6 rows by default for CHEST - var type: InventoryType = InventoryType.CHEST, - var cancelOnClick: Boolean = true, - var closeOnClickOutside: Boolean = false -) { - /** - * 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 - } -} - /** * Base class for all GUI views. * Views manage the overall GUI lifecycle and component rendering. @@ -44,74 +17,74 @@ abstract class GuiView { */ var parent: GuiView? = null internal set - + /** * Props for this view. */ protected open val props: Map> = emptyMap() - + /** * Update interval for the entire view. */ open val updateInterval: Duration? = null - + /** * Components in this view, mapped by slot. */ private val _components = mutableMapOf() val components: Map get() = _components.toMap() - + /** * Whether this view has been initialized. */ private var initialized = false - + /** * Whether this view has been rendered for the first time. */ private val firstRenderPerViewer = mutableSetOf() - + /** * Current viewers of this view. */ private val viewers = mutableMapOf() - + /** * Configuration for this view. */ val config = ViewConfig() - + /** * Initialize the view configuration. * Called once when the view is first created. */ open fun onInit(config: ViewConfig) {} - + /** * Called when the view is opened for a player. */ open fun onOpen(context: ViewContext) {} - + /** * Called the first time the view is rendered for a player. */ open fun onFirstRender(context: RenderContext) {} - + /** * 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. */ @@ -121,24 +94,24 @@ abstract class GuiView { initialized = true } } - + /** * Open this view for a player. */ open fun open(player: Player) { ensureInitialized() viewers[player.uniqueId] = player - + val context = createViewContext(player) onOpen(context) - + if (player.uniqueId !in firstRenderPerViewer) { val renderContext = createRenderContext(player) onFirstRender(renderContext) firstRenderPerViewer.add(player.uniqueId) } } - + /** * Close this view for a player. */ @@ -147,7 +120,7 @@ abstract class GuiView { onClose(context) viewers.remove(player.uniqueId) } - + /** * Update the view for all viewers. */ @@ -157,7 +130,7 @@ abstract class GuiView { onUpdate(context) } } - + /** * Update a specific component. */ @@ -168,7 +141,7 @@ abstract class GuiView { component.onUpdate(lifecycleContext) } } - + /** * Add a component at a slot. */ @@ -176,29 +149,32 @@ abstract class GuiView { component.view = this _components[slot] = component } - + /** * Remove a component from a slot. */ fun removeComponent(slot: Slot) { _components.remove(slot) } - + /** * Create a view context for a player. */ protected abstract fun createViewContext(player: Player): ViewContext - + /** * Create a render context for a player. */ protected abstract fun createRenderContext(player: Player): RenderContext - + /** * Create a lifecycle context for a player. */ - protected abstract fun createLifecycleContext(player: Player, eventType: LifecycleEventType): LifecycleContext - + protected abstract fun createLifecycleContext( + player: Player, + eventType: LifecycleEventType + ): LifecycleContext + /** * Create a resume context for a player. */ 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 00000000..5fb25c59 --- /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,29 @@ +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, + var closeOnClickOutside: Boolean = false +) { + /** + * 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 + } +} \ No newline at end of file From eea6cbed2e58136b0647d40b980229e7fd24e426 Mon Sep 17 00:00:00 2001 From: Ammo Date: Tue, 3 Feb 2026 22:35:05 +0100 Subject: [PATCH 18/92] refactor: streamline navigation helper methods and enhance prop management in GUI framework --- .../bukkit/api/gui/context/ViewContext.kt | 12 ++- .../bukkit/api/gui/dsl/ComponentDsl.kt | 32 +++++--- .../bukkit/api/gui/props/ComputedProp.kt | 2 +- .../bukkit/api/gui/props/ImmutableLazyProp.kt | 14 ---- .../bukkit/api/gui/props/ImmutableProp.kt | 15 ---- .../surfapi/bukkit/api/gui/props/LazyProp.kt | 34 ++++++++ .../bukkit/api/gui/props/MutableLazyProp.kt | 21 ----- .../bukkit/api/gui/props/MutableProp.kt | 26 ------ .../surf/surfapi/bukkit/api/gui/props/Prop.kt | 41 +++++++++- .../bukkit/api/gui/props/ViewerMutableProp.kt | 26 ------ .../bukkit/api/gui/props/ViewerProp.kt | 39 +++++++++ .../bukkit/api/gui/props/ViewerPropStorage.kt | 14 ++-- .../surfapi/bukkit/api/gui/view/GuiView.kt | 29 ++++--- .../server/gui/context/BukkitContexts.kt | 54 ++++++------- .../bukkit/server/gui/view/BukkitGuiView.kt | 79 ++++++++++--------- 15 files changed, 231 insertions(+), 207 deletions(-) delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/LazyProp.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerProp.kt 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 index da6ef495..d7dbd0ec 100644 --- 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 @@ -1,10 +1,9 @@ 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.ViewerMutableProp +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 -import java.util.* /** * Context representing a snapshot of the current GUI state and props. @@ -24,16 +23,15 @@ interface ViewContext { /** * Get a prop value from the view. */ - suspend fun getProp(prop: Prop): T = prop.get() + suspend fun getProp(prop: Prop): T? = prop.get() /** * Get a prop value for a specific player. - * This is useful for ViewerMutableProp and PaginationProp which have per-viewer state. */ - fun getPropForPlayer(prop: Prop, playerId: UUID): T { + fun getPropForPlayer(prop: Prop, player: Player): T { return when (prop) { - is ViewerMutableProp -> prop.get(playerId) - else -> throw UnsupportedOperationException("getPropForPlayer only supports ViewerMutableProp and PaginationProp") + is ViewerProp -> prop.get(player) + else -> throw UnsupportedOperationException() } } 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 index 93b5929a..731aade7 100644 --- 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 @@ -9,7 +9,10 @@ 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.RenderContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext -import dev.slne.surf.surfapi.bukkit.api.gui.props.* +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 kotlin.time.Duration @@ -91,22 +94,29 @@ class PropsBuilder { /** * Create an immutable prop. */ - fun immutable(name: String, value: T): ImmutableProp { - return ImmutableProp(name, value).also { _props[name] = it } + fun immutable(name: String, value: T): Prop.Immutable { + return Prop.Immutable(name, value).also { _props[name] = it } } /** * Create a mutable prop (global to view). */ - fun mutable(name: String, initialValue: T): MutableProp { - return MutableProp(name, initialValue).also { _props[name] = it } + fun mutable(name: String, initialValue: T?): Prop.Mutable { + return Prop.Mutable(name, initialValue).also { _props[name] = it } + } + + /** + * Create a viewer-specific immutable prop. + */ + fun viewerImmutable(name: String, initialValue: T): ViewerProp { + return ViewerProp(name, initialValue).also { _props[name] = it } } /** * Create a viewer-specific mutable prop. */ - fun viewerMutable(name: String, initialValue: T): ViewerMutableProp { - return ViewerMutableProp(name, initialValue).also { _props[name] = it } + fun viewerMutable(name: String, initialValue: T?): ViewerProp.Mutable { + return ViewerProp.Mutable(name, initialValue).also { _props[name] = it } } /** @@ -119,15 +129,15 @@ class PropsBuilder { /** * Create an immutable lazy prop. */ - fun immutableLazy(name: String, initializer: () -> T): ImmutableLazyProp { - return ImmutableLazyProp(name, initializer).also { _props[name] = it } + fun immutableLazy(name: String, initializer: () -> T): LazyProp { + return LazyProp(name, initializer).also { _props[name] = it } } /** * Create a mutable lazy prop. */ - fun mutableLazy(name: String, initializer: () -> T): MutableLazyProp { - return MutableLazyProp(name, initializer).also { _props[name] = it } + fun mutableLazy(name: String, initializer: () -> T?): LazyProp.Mutable { + return LazyProp.Mutable(name, initializer).also { _props[name] = it } } /** 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 index fda2d0f4..afb36f65 100644 --- 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 @@ -4,7 +4,7 @@ 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. */ -class ComputedProp( +open class ComputedProp( override val name: String, private val compute: suspend () -> T ) : Prop { diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt deleted file mode 100644 index 13090c29..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableLazyProp.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.props - -/** - * Immutable lazy prop - gets available when accessed, using a callback. - * Cannot be modified after initialization. - */ -class ImmutableLazyProp( - override val name: String, - private val initializer: () -> T -) : Prop { - private val value = lazy { initializer() } - - override suspend fun get(): T = value.value -} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt deleted file mode 100644 index 826cf0c6..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ImmutableProp.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.props - -import kotlin.reflect.KProperty - -/** - * Immutable prop - always available and immutable after initialization. - */ -class ImmutableProp( - override val name: String, - private val value: T -) : Prop { - override suspend fun get(): T = value - - operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value -} 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 00000000..fc080ee8 --- /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,34 @@ +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 + + /** + * 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 + } + } +} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt deleted file mode 100644 index 727077c7..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableLazyProp.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.props - -/** - * Mutable lazy prop - gets available when accessed, using a callback. - * Can be modified after initialization. - */ -class MutableLazyProp( - 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 - } -} diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt deleted file mode 100644 index 946853cf..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/MutableProp.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.props - -import java.util.concurrent.atomic.AtomicReference -import kotlin.reflect.KProperty - -/** - * Mutable prop - always available and mutable. - * Global to the view, shared across all viewers. - */ -class MutableProp( - 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) - } -} 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 index 7b73da93..908e9cee 100644 --- 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 @@ -1,5 +1,8 @@ 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. @@ -9,10 +12,44 @@ sealed interface Prop { * Gets the value of this prop. * Suspend function to support ComputedProp with async operations. */ - suspend fun get(): T - + 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 + } + + /** + * 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) + } + } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt deleted file mode 100644 index 0539cd49..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ViewerMutableProp.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.props - -import java.util.UUID - -/** - * Viewer-specific mutable prop - isolated per viewer. - */ -class ViewerMutableProp( - override val name: String, - initialValue: T -) : Prop { - private val globalDefault = initialValue - private val storage = ViewerPropStorage { globalDefault } - - override suspend fun get(): T = globalDefault - - fun get(viewerId: UUID): T = storage.get(viewerId) - - fun set(viewerId: UUID, value: T) { - storage.set(viewerId, value) - } - - fun clear(viewerId: UUID) { - storage.clear(viewerId) - } -} 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 00000000..7528a4fb --- /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,39 @@ +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.uniqueId) + ?: throw IllegalStateException("Value for viewer ${viewer.uniqueId} is not set") + + 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.uniqueId) + + fun set(viewer: Player, value: T?) { + storage.set(viewer.uniqueId, value) + } + + fun clear(viewer: Player) { + storage.clear(viewer.uniqueId) + } + } +} 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 index 493c9709..1b8c1495 100644 --- 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 @@ -1,20 +1,20 @@ package dev.slne.surf.surfapi.bukkit.api.gui.props -import java.util.UUID +import java.util.* /** * Viewer-specific prop storage. * Maps viewer UUIDs to their prop values. */ class ViewerPropStorage(private val initialValue: () -> T) { - private val storage = mutableMapOf() - - fun get(viewerId: UUID): T = storage.getOrPut(viewerId) { initialValue() } - - fun set(viewerId: UUID, value: T) { + private val storage = mutableMapOf() + + fun get(viewerId: UUID): T? = storage.getOrPut(viewerId) { initialValue() } + + fun set(viewerId: UUID, value: T?) { storage[viewerId] = value } - + fun clear(viewerId: UUID) { storage.remove(viewerId) } 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 index 28ed16a7..dae2f0c4 100644 --- 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 @@ -3,7 +3,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view 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.props.Prop import org.bukkit.entity.Player import kotlin.time.Duration @@ -18,11 +17,6 @@ abstract class GuiView { var parent: GuiView? = null internal set - /** - * Props for this view. - */ - protected open val props: Map> = emptyMap() - /** * Update interval for the entire view. */ @@ -136,8 +130,8 @@ abstract class GuiView { */ internal fun updateComponent(component: Component) { viewers.values.forEach { player -> - val context = createViewContext(player) val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + component.onUpdate(lifecycleContext) } } @@ -147,6 +141,7 @@ abstract class GuiView { */ fun addComponent(slot: Slot, component: Component) { component.view = this + _components[slot] = component } @@ -157,20 +152,29 @@ abstract class GuiView { _components.remove(slot) } + /** + * Set the parent view for navigation. + */ + fun withParent(parentView: GuiView): GuiView { + this.parent = parentView + + return this + } + /** * Create a view context for a player. */ - protected abstract fun createViewContext(player: Player): ViewContext + abstract fun createViewContext(player: Player): ViewContext /** * Create a render context for a player. */ - protected abstract fun createRenderContext(player: Player): RenderContext + abstract fun createRenderContext(player: Player): RenderContext /** * Create a lifecycle context for a player. */ - protected abstract fun createLifecycleContext( + abstract fun createLifecycleContext( player: Player, eventType: LifecycleEventType ): LifecycleContext @@ -178,7 +182,10 @@ abstract class GuiView { /** * Create a resume context for a player. */ - protected abstract fun createResumeContext(player: Player, origin: GuiView?): ResumeContext + abstract fun createResumeContext( + player: Player, + origin: GuiView? + ): ResumeContext } /** diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt index b23df2c5..215e4b7e 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt @@ -16,28 +16,31 @@ import org.bukkit.event.inventory.InventoryClickEvent private object NavigationHelper { fun navigateTo(currentView: GuiView, targetView: GuiView, player: Player, passProps: Boolean) { if (targetView is BukkitGuiView) { - targetView.parent = currentView + targetView.withParent(currentView) player.closeInventory() targetView.open(player) } } - + fun navigateBack(currentView: GuiView, player: Player) { val parent = currentView.parent + if (parent is BukkitGuiView) { 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) { if (view is BukkitGuiView) { view.refreshInventory(player) @@ -52,19 +55,18 @@ internal class BukkitViewContext( 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) } @@ -79,19 +81,18 @@ internal class BukkitClickContext( 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) } @@ -105,34 +106,33 @@ internal class BukkitRenderContext( override val player: Player, private val bukkitView: BukkitGuiView ) : RenderContext { - override fun renderComponent(slot: Slot, component: Component) { bukkitView.addComponent(slot, component) } - + override fun clearSlot(slot: Slot) { bukkitView.removeComponent(slot) } - + override fun setItem(slot: Slot, item: GuiItem) { val inventory = player.openInventory.topInventory if (slot.index in 0 until inventory.size) { inventory.setItem(slot.index, item.toItemStack()) } } - + 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) } @@ -146,19 +146,18 @@ internal class BukkitLifecycleContext( 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) } @@ -172,21 +171,20 @@ internal class BukkitResumeContext( 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) } diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt index 40da43ba..35f3e556 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt @@ -2,7 +2,6 @@ package dev.slne.surf.surfapi.bukkit.server.gui.view import com.github.shynixn.mccoroutine.folia.entityDispatcher import com.github.shynixn.mccoroutine.folia.launch -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.* @@ -18,43 +17,43 @@ import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryCloseEvent -import org.bukkit.event.inventory.InventoryOpenEvent +import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.Inventory -import java.util.UUID +import java.util.* import java.util.concurrent.ConcurrentHashMap -import kotlin.time.Duration.Companion.milliseconds /** * Bukkit-specific implementation of GuiView. * Folia-compatible using entity dispatchers. */ abstract class BukkitGuiView : GuiView() { - private val inventories = ConcurrentHashMap() private val updateJobs = ConcurrentHashMap() private val componentJobs = ConcurrentHashMap>() - + override fun open(player: Player) { super.open(player) - + // Create inventory based on type val inventory = when (config.type) { - org.bukkit.event.inventory.InventoryType.CHEST -> { + InventoryType.CHEST -> { Bukkit.createInventory(null, config.size, config.title) } + else -> { Bukkit.createInventory(null, config.type, config.title) } } + inventories[player.uniqueId] = inventory - + // Register with listener GuiViewListener.registerView(inventory, this) - + // Render components components.forEach { (slot, component) -> val context = createViewContext(player) - + // Handle container components (render multiple slots) val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { @@ -71,10 +70,10 @@ abstract class BukkitGuiView : GuiView() { } } } - + // Open inventory player.openInventory(inventory) - + // Start update task if configured (Folia-compatible) updateInterval?.let { interval -> val job = plugin.launch(plugin.entityDispatcher(player)) { @@ -85,7 +84,7 @@ abstract class BukkitGuiView : GuiView() { } updateJobs[player.uniqueId] = job } - + // Start component update tasks (Folia-compatible) val playerComponentJobs = mutableListOf() components.values.forEach { component -> @@ -93,7 +92,8 @@ abstract class BukkitGuiView : GuiView() { val job = plugin.launch(plugin.entityDispatcher(player)) { while (true) { delay(interval) - val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + val lifecycleContext = + createLifecycleContext(player, LifecycleEventType.UPDATE) component.onUpdate(lifecycleContext) refreshComponentSlot(player, component) } @@ -105,29 +105,29 @@ abstract class BukkitGuiView : GuiView() { componentJobs[player.uniqueId] = playerComponentJobs } } - + override fun close(player: Player) { // Cancel all update jobs updateJobs.remove(player.uniqueId)?.cancel() componentJobs.remove(player.uniqueId)?.forEach { it.cancel() } - + // Unregister inventory inventories.remove(player.uniqueId)?.let { inventory -> GuiViewListener.unregisterView(inventory) } - + super.close(player) } - + /** * Refresh the inventory for a player. */ internal fun refreshInventory(player: Player) { val inventory = inventories[player.uniqueId] ?: return - + components.forEach { (slot, component) -> val context = createViewContext(player) - + // Handle container components val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { @@ -145,16 +145,16 @@ abstract class BukkitGuiView : GuiView() { } } } - + /** * Refresh a specific component slot. */ private fun refreshComponentSlot(player: Player, component: Component) { val inventory = inventories[player.uniqueId] ?: return val slot = components.entries.find { it.value == component }?.key ?: return - + val context = createViewContext(player) - + // Handle container components val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { @@ -171,7 +171,7 @@ abstract class BukkitGuiView : GuiView() { } } } - + /** * Handle click event. */ @@ -179,26 +179,29 @@ abstract class BukkitGuiView : GuiView() { if (config.cancelOnClick) { event.isCancelled = true } - + val slot = Slot.of(event.slot) val component = components[slot] - + val clickContext = BukkitClickContext(this, player, event, component) component?.onClick(clickContext) } - + override fun createViewContext(player: Player): ViewContext { return BukkitViewContext(this, player) } - + override fun createRenderContext(player: Player): RenderContext { return BukkitRenderContext(this, player, this) } - - override fun createLifecycleContext(player: Player, eventType: LifecycleEventType): LifecycleContext { + + override fun createLifecycleContext( + player: Player, + eventType: LifecycleEventType + ): LifecycleContext { return BukkitLifecycleContext(this, player, eventType) } - + override fun createResumeContext(player: Player, origin: GuiView?): ResumeContext { return BukkitResumeContext(this, player, origin) } @@ -209,30 +212,30 @@ abstract class BukkitGuiView : GuiView() { */ object GuiViewListener : Listener { private val viewsByInventory = ConcurrentHashMap() - + fun registerView(inventory: Inventory, view: BukkitGuiView) { viewsByInventory[inventory] = view } - + fun unregisterView(inventory: Inventory) { viewsByInventory.remove(inventory) } - + @EventHandler fun onInventoryClick(event: InventoryClickEvent) { val inventory = event.clickedInventory ?: return val view = viewsByInventory[inventory] ?: return val player = event.whoClicked as? Player ?: return - + view.handleClick(player, event) } - + @EventHandler fun onInventoryClose(event: InventoryCloseEvent) { val inventory = event.inventory val view = viewsByInventory[inventory] ?: return val player = event.player as? Player ?: return - + // Close the view when inventory is closed view.close(player) } From 4cfdf38f990ced6f6cbef4622af7017a7fe5bb77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Feb 2026 21:37:41 +0000 Subject: [PATCH 19/92] chore: update ABI and bump version [skip ci] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3a2f865a..690b873b 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.55.2 +version=1.21.11-2.56.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false From 62ea088ef107e2b3a97ad71540f08d6cd5e01840 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:41:40 +0000 Subject: [PATCH 20/92] Add example GUIs: CounterGui with MutableProp and PaginatedShopGui with pagination - Created CounterGuiView demonstrating global MutableProp usage - Created PaginatedShopGuiView demonstrating ViewerMutableProp and PaginationComponent - Added GuiTest command with counter and shop subcommands - Registered GuiTest in SurfApiTestCommand - Commands: /surfapitest gui counter and /surfapitest gui shop Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../test/command/SurfApiTestCommand.java | 4 +- .../test/command/subcommands/GuiTest.kt | 30 ++++ .../surfapi/bukkit/test/gui/CounterGuiView.kt | 120 +++++++++++++ .../bukkit/test/gui/PaginatedShopGuiView.kt | 165 ++++++++++++++++++ 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/CounterGuiView.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/PaginatedShopGuiView.kt 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 05004b95..42ceedf3 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,6 +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.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; @@ -35,7 +36,8 @@ public SurfApiTestCommand() { new GlowingTest("glowing"), new PaginationTest("pagination"), new ToastTest(("toast")), - new SuspendCommandExecutionTest("suspendCommandExecution") + new SuspendCommandExecutionTest("suspendCommandExecution"), + new GuiTest("gui") ); } } 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 00000000..5d5fded3 --- /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,30 @@ +package dev.slne.surf.surfapi.bukkit.test.command.subcommands + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.arguments.LiteralArgument +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/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 00000000..15789a08 --- /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,120 @@ +package dev.slne.surf.surfapi.bukkit.test.gui + +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.context.RenderContext +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.Prop +import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig +import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +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 : BukkitGuiView() { + // Global counter state (shared across all viewers) + private val counterProp = Prop.Mutable("counter", 0) + private val counterDisplayRef = Ref() + + override fun onInit(config: ViewConfig) { + config.type = InventoryType.CHEST + config.rows = 3 + config.title = Component.text("Counter GUI", NamedTextColor.GOLD, TextDecoration.BOLD) + } + + override fun onFirstRender(context: RenderContext) { + // Counter display at top center + context.renderComponent(Slot.at(4, 0), dynamicComponent( + renderer = { ctx -> + val count = runBlocking { counterProp.get() } ?: 0 + GuiItem.of(Material.DIAMOND).copyWith( + displayName = Component.text("Counter: $count", NamedTextColor.AQUA, TextDecoration.BOLD), + lore = listOf( + Component.text("This is a global counter", NamedTextColor.GRAY), + Component.text("shared across all viewers", NamedTextColor.GRAY) + ) + ) + } + ) { + ref = counterDisplayRef + }) + + // Increment +1 button + context.renderComponent(Slot.at(2, 1), component( + GuiItem.of(Material.LIME_CONCRETE).copyWith( + displayName = Component.text("+1", NamedTextColor.GREEN, TextDecoration.BOLD), + lore = listOf(Component.text("Click to increment by 1", NamedTextColor.GRAY)) + ) + ) { + onClick = { + val current = runBlocking { counterProp.get() } ?: 0 + counterProp.set(current + 1) + counterDisplayRef.update() + } + }) + + // Increment +10 button + context.renderComponent(Slot.at(3, 1), component( + GuiItem.of(Material.GREEN_CONCRETE).copyWith( + displayName = Component.text("+10", NamedTextColor.GREEN, TextDecoration.BOLD), + lore = listOf(Component.text("Click to increment by 10", NamedTextColor.GRAY)) + ) + ) { + onClick = { + val current = runBlocking { counterProp.get() } ?: 0 + counterProp.set(current + 10) + counterDisplayRef.update() + } + }) + + // Decrement -1 button + context.renderComponent(Slot.at(5, 1), component( + GuiItem.of(Material.ORANGE_CONCRETE).copyWith( + displayName = Component.text("-1", NamedTextColor.RED, TextDecoration.BOLD), + lore = listOf(Component.text("Click to decrement by 1", NamedTextColor.GRAY)) + ) + ) { + onClick = { + val current = runBlocking { counterProp.get() } ?: 0 + counterProp.set(current - 1) + counterDisplayRef.update() + } + }) + + // Decrement -10 button + context.renderComponent(Slot.at(6, 1), component( + GuiItem.of(Material.RED_CONCRETE).copyWith( + displayName = Component.text("-10", NamedTextColor.RED, TextDecoration.BOLD), + lore = listOf(Component.text("Click to decrement by 10", NamedTextColor.GRAY)) + ) + ) { + onClick = { + val current = runBlocking { counterProp.get() } ?: 0 + counterProp.set(current - 10) + counterDisplayRef.update() + } + }) + + // Reset button + context.renderComponent(Slot.at(4, 2), component( + GuiItem.of(Material.BARRIER).copyWith( + displayName = Component.text("Reset", NamedTextColor.YELLOW, TextDecoration.BOLD), + lore = listOf(Component.text("Click to reset counter to 0", NamedTextColor.GRAY)) + ) + ) { + onClick = { + 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 00000000..8e2106b4 --- /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,165 @@ +package dev.slne.surf.surfapi.bukkit.test.gui + +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.PaginationComponent +import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext +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.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig +import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import org.bukkit.Material +import org.bukkit.event.inventory.InventoryType + +/** + * Example GUI demonstrating ViewerMutableProp and PaginationComponent. + * Each player has their own coins (viewer-specific state). + */ +object PaginatedShopGuiView : BukkitGuiView() { + // Per-viewer coins (each player has their own balance) + private val coinsProp = ViewerProp.Mutable("coins", 1000) + private val coinsDisplayRef = Ref() + + // Shop items + private val shopItems = listOf( + ShopItem(Material.DIAMOND_SWORD, "Diamond Sword", 100), + ShopItem(Material.DIAMOND_PICKAXE, "Diamond Pickaxe", 80), + ShopItem(Material.DIAMOND_AXE, "Diamond Axe", 75), + ShopItem(Material.DIAMOND_SHOVEL, "Diamond Shovel", 70), + ShopItem(Material.DIAMOND_HOE, "Diamond Hoe", 65), + ShopItem(Material.IRON_SWORD, "Iron Sword", 50), + ShopItem(Material.IRON_PICKAXE, "Iron Pickaxe", 40), + ShopItem(Material.IRON_AXE, "Iron Axe", 35), + ShopItem(Material.IRON_SHOVEL, "Iron Shovel", 30), + ShopItem(Material.IRON_HOE, "Iron Hoe", 25), + ShopItem(Material.GOLDEN_SWORD, "Golden Sword", 80), + ShopItem(Material.GOLDEN_PICKAXE, "Golden Pickaxe", 70), + ShopItem(Material.GOLDEN_AXE, "Golden Axe", 65), + ShopItem(Material.GOLDEN_SHOVEL, "Golden Shovel", 60), + ShopItem(Material.GOLDEN_HOE, "Golden Hoe", 55), + ShopItem(Material.BOW, "Bow", 50), + ShopItem(Material.CROSSBOW, "Crossbow", 75), + ShopItem(Material.SHIELD, "Shield", 45), + ShopItem(Material.FISHING_ROD, "Fishing Rod", 30), + ShopItem(Material.SHEARS, "Shears", 20), + ShopItem(Material.FLINT_AND_STEEL, "Flint and Steel", 15), + ShopItem(Material.COMPASS, "Compass", 25), + ShopItem(Material.CLOCK, "Clock", 25), + ShopItem(Material.SPYGLASS, "Spyglass", 40), + ShopItem(Material.LEAD, "Lead", 20), + ShopItem(Material.NAME_TAG, "Name Tag", 50), + ShopItem(Material.SADDLE, "Saddle", 60), + ShopItem(Material.ELYTRA, "Elytra", 500) + ) + + // Pagination component + private val paginationComponent = PaginationComponent( + items = { shopItems }, + pageSize = 28, // 4 rows of 7 items + itemRenderer = { item, ctx -> + GuiItem.of(item.material).copyWith( + displayName = Component.text(item.name, NamedTextColor.GREEN, TextDecoration.BOLD), + lore = listOf( + Component.text("Price: ${item.price} coins", NamedTextColor.GOLD), + Component.text("", NamedTextColor.GRAY), + Component.text("Click to purchase!", NamedTextColor.YELLOW) + ) + ) + }, + onItemClick = { item, ctx -> + val coins = coinsProp.get(ctx.player) + if (coins != null && coins >= item.price) { + coinsProp.set(ctx.player, coins - item.price) + ctx.player.sendMessage( + Component.text("Purchased ${item.name} for ${item.price} coins!", NamedTextColor.GREEN) + ) + coinsDisplayRef.update() + } else { + ctx.player.sendMessage( + Component.text("Not enough coins! Need ${item.price}, have ${coins ?: 0}", NamedTextColor.RED) + ) + } + } + ) + + override fun onInit(config: ViewConfig) { + config.type = InventoryType.CHEST + config.rows = 6 + config.title = Component.text("Shop", NamedTextColor.GOLD, TextDecoration.BOLD) + } + + override fun onFirstRender(context: RenderContext) { + // Coins display at top center + context.renderComponent(Slot.at(4, 0), dynamicComponent( + renderer = { ctx -> + val coins = coinsProp.get(ctx.player) ?: 0 + GuiItem.of(Material.GOLD_INGOT).copyWith( + displayName = Component.text("Your Coins: $coins", NamedTextColor.GOLD, TextDecoration.BOLD), + lore = listOf( + Component.text("This is your personal balance", NamedTextColor.GRAY), + Component.text("Each player has their own coins", NamedTextColor.GRAY) + ) + ) + } + ) { + ref = coinsDisplayRef + }) + + // Render pagination component at rows 1-4 (slots 9-44) + context.renderComponent(Slot.of(9), paginationComponent) + + // Previous page button + context.renderComponent(Slot.at(2, 5), component( + GuiItem.of(Material.ARROW).copyWith( + displayName = Component.text("Previous Page", NamedTextColor.YELLOW, TextDecoration.BOLD) + ) + ) { + onClick = { + if (paginationComponent.hasPreviousPage(player)) { + paginationComponent.previousPage(player) + view.update(player) + } + } + }) + + // Page indicator + context.renderComponent(Slot.at(4, 5), dynamicComponent( + renderer = { ctx -> + val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 + val totalPages = paginationComponent.getTotalPages() + GuiItem.of(Material.PAPER).copyWith( + displayName = Component.text( + "Page $currentPage / $totalPages", + NamedTextColor.AQUA, + TextDecoration.BOLD + ) + ) + } + )) + + // Next page button + context.renderComponent(Slot.at(6, 5), component( + GuiItem.of(Material.ARROW).copyWith( + displayName = Component.text("Next Page", NamedTextColor.YELLOW, TextDecoration.BOLD) + ) + ) { + onClick = { + if (paginationComponent.hasNextPage(player)) { + paginationComponent.nextPage(player) + view.update(player) + } + } + }) + } + + private data class ShopItem( + val material: Material, + val name: String, + val price: Int + ) +} From c56706d7421a44531731213db227dd86420447b0 Mon Sep 17 00:00:00 2001 From: Ammo Date: Tue, 3 Feb 2026 23:30:02 +0100 Subject: [PATCH 21/92] feat: introduce abstract GUI context classes and navigation helper for improved GUI framework structure --- .../surf/surfapi/bukkit/api/SurfBukkitApi.kt | 11 + .../surf/surfapi/bukkit/api/gui/GuiItem.kt | 22 +- .../api/gui/component/PaginationComponent.kt | 71 +++++- .../context/abstract/AbstractClickContext.kt | 32 +++ .../abstract/AbstractLifecycleContext.kt | 30 +++ .../context/abstract/AbstractRenderContext.kt | 49 ++++ .../context/abstract/AbstractResumeContext.kt | 32 +++ .../context/abstract/AbstractViewContext.kt | 28 +++ .../gui/context/abstract/NavigationHelper.kt | 42 ++++ .../bukkit/api/gui/view/AbstractGuiView.kt} | 76 ++---- .../bukkit/api/gui/view/ViewManager.kt | 25 ++ .../surfapi/bukkit/test/gui/CounterGuiView.kt | 217 +++++++++++------- .../bukkit/test/gui/PaginatedShopGuiView.kt | 129 +++++------ .../server/gui/context/BukkitContexts.kt | 191 --------------- .../bukkit/server/gui/view/GuiViewListener.kt | 34 +++ .../bukkit/server/impl/SurfBukkitApiImpl.kt | 11 + 16 files changed, 576 insertions(+), 424 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractClickContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractLifecycleContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractResumeContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractViewContext.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/NavigationHelper.kt rename surf-api-bukkit/{surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt => surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt} (73%) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ViewManager.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/GuiViewListener.kt diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt index 69b929c1..2f9e246d 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt @@ -5,11 +5,15 @@ import dev.slne.surf.surfapi.bukkit.api.scoreboard.SurfScoreboardBuilder import dev.slne.surf.surfapi.bukkit.api.time.SkipOperations.SkipOperation import dev.slne.surf.surfapi.bukkit.api.time.TimeSkipResult import dev.slne.surf.surfapi.core.api.SurfCoreApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import net.kyori.adventure.text.Component import net.megavex.scoreboardlibrary.api.ScoreboardLibrary import org.bukkit.World +import org.bukkit.entity.Entity import org.bukkit.entity.Player import org.jetbrains.annotations.ApiStatus +import kotlin.coroutines.CoroutineContext /** * Represents the API for SurfBukkit. @@ -104,6 +108,13 @@ interface SurfBukkitApi : SurfCoreApi { */ suspend fun skipTimeSmoothly(skipOperation: SkipOperation): Map + fun launch( + context: CoroutineContext, + block: suspend CoroutineScope.() -> Unit + ): Job + + fun entityDispatcher(entity: Entity): CoroutineContext + companion object { @JvmStatic val instance get() = SurfCoreApi.instance as SurfBukkitApi 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 index a84fbef6..391b50aa 100644 --- 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 @@ -1,6 +1,5 @@ package dev.slne.surf.surfapi.bukkit.api.gui -import net.kyori.adventure.text.Component import org.bukkit.Material import org.bukkit.inventory.ItemStack @@ -11,30 +10,30 @@ import org.bukkit.inventory.ItemStack 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. */ @@ -47,20 +46,13 @@ class GuiItem( amount?.let { newStack.amount = it } return GuiItem(newStack) } - + companion object { /** * Create a GuiItem from an ItemStack. */ fun of(itemStack: ItemStack): GuiItem = GuiItem(itemStack) - - /** - * Create a GuiItem from a material. - */ - fun of(material: Material, amount: Int = 1): GuiItem { - return GuiItem(ItemStack(material, amount)) - } - + /** * Create an empty GuiItem (AIR). */ diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index f6859d2b..b9f59ef3 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -1,9 +1,15 @@ package dev.slne.surf.surfapi.bukkit.api.gui.component +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.context.ClickContext 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 org.bukkit.Material import org.bukkit.entity.Player import java.util.* @@ -124,9 +130,72 @@ class PaginationComponent( if (slotIndex in pageItems.indices) { val item = pageItems[slotIndex] - + onItemClick.invoke(item, context) } } } + + companion object { + val NEXT_PAGE_ITEM = GuiItem(ItemStack(Material.ARROW) { + displayName { + info("Next Page") + } + + buildLore { + line { + gray("Click to go to the next page") + } + } + }) + + fun buildNextPageComponent(paginationComponent: PaginationComponent<*>) = + component(NEXT_PAGE_ITEM) { + onClick = { + paginationComponent.nextPage(player) + view.update() + } + } + + val PREVIOUS_PAGE_ITEM = GuiItem(ItemStack(Material.ARROW) { + displayName { + info("Previous Page") + } + + buildLore { + line { + gray("Click to go to the previous page") + } + } + }) + + fun buildPreviousPageComponent(paginationComponent: PaginationComponent<*>) = + component(PREVIOUS_PAGE_ITEM) { + onClick = { + paginationComponent.previousPage(player) + view.update() + } + } + + fun buildPageIndicatorItem(pageNumber: Int, maxPages: Int) = + GuiItem(ItemStack(Material.PAPER) { + displayName { + info("Page $pageNumber") + } + + buildLore { + line { + gray("Page $pageNumber of $maxPages") + } + } + }) + + fun buildPageIndicatorComponent(paginationComponent: PaginationComponent<*>) = + dynamicComponent(renderer = { ctx -> + val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 + val totalPages = paginationComponent.getTotalPages() + + buildPageIndicatorItem(currentPage, totalPages) + }) + } } 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 00000000..b4793b18 --- /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,32 @@ +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.core.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) + } +} \ 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 00000000..e47e16cd --- /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,30 @@ +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.core.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) + } +} \ 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/AbstractRenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt new file mode 100644 index 00000000..841f9134 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt @@ -0,0 +1,49 @@ +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.RenderContext +import dev.slne.surf.surfapi.bukkit.api.gui.toItemStack +import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import org.bukkit.entity.Player + +@InternalSurfApi +class AbstractRenderContext( + override val view: GuiView, + override val player: Player, + private val bukkitView: AbstractGuiView +) : RenderContext { + override fun renderComponent(slot: Slot, component: Component) { + bukkitView.addComponent(slot, component) + } + + override fun clearSlot(slot: Slot) { + bukkitView.removeComponent(slot) + } + + override fun setItem(slot: Slot, item: GuiItem) { + val inventory = player.openInventory.topInventory + if (slot.index in 0 until inventory.size) { + inventory.setItem(slot.index, item.toItemStack()) + } + } + + 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) + } +} \ 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 00000000..9309f8fc --- /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,32 @@ +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.core.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) + } +} 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 00000000..18543876 --- /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,28 @@ +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.core.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) + } +} \ 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 00000000..ec643b9d --- /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,42 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract + +import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView +import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView +import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import org.bukkit.entity.Player + +@InternalSurfApi +object NavigationHelper { + fun navigateTo(currentView: GuiView, targetView: GuiView, player: Player, passProps: Boolean) { + if (targetView is AbstractGuiView) { + targetView.withParent(currentView) + player.closeInventory() + targetView.open(player) + } + } + + fun navigateBack(currentView: GuiView, player: Player) { + val parent = currentView.parent + + if (parent is AbstractGuiView) { + 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) { + if (view is AbstractGuiView) { + view.refreshInventory(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/BukkitGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt similarity index 73% rename from surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt rename to surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 35f3e556..ab9f2040 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/BukkitGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -1,32 +1,24 @@ -package dev.slne.surf.surfapi.bukkit.server.gui.view +package dev.slne.surf.surfapi.bukkit.api.gui.view -import com.github.shynixn.mccoroutine.folia.entityDispatcher -import com.github.shynixn.mccoroutine.folia.launch 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.bukkit.api.gui.view.GuiView -import dev.slne.surf.surfapi.bukkit.server.gui.context.* -import dev.slne.surf.surfapi.bukkit.server.plugin +import dev.slne.surf.surfapi.bukkit.api.surfBukkitApi +import dev.slne.surf.surfapi.core.api.util.InternalSurfApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay import org.bukkit.Bukkit 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 import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.Inventory import java.util.* import java.util.concurrent.ConcurrentHashMap -/** - * Bukkit-specific implementation of GuiView. - * Folia-compatible using entity dispatchers. - */ -abstract class BukkitGuiView : GuiView() { +@InternalSurfApi +open class AbstractGuiView : GuiView() { private val inventories = ConcurrentHashMap() private val updateJobs = ConcurrentHashMap() private val componentJobs = ConcurrentHashMap>() @@ -48,7 +40,7 @@ abstract class BukkitGuiView : GuiView() { inventories[player.uniqueId] = inventory // Register with listener - GuiViewListener.registerView(inventory, this) + ViewManager.registerView(inventory, this) // Render components components.forEach { (slot, component) -> @@ -76,7 +68,7 @@ abstract class BukkitGuiView : GuiView() { // Start update task if configured (Folia-compatible) updateInterval?.let { interval -> - val job = plugin.launch(plugin.entityDispatcher(player)) { + val job = surfBukkitApi.launch(surfBukkitApi.entityDispatcher(player)) { while (true) { delay(interval) update() @@ -89,7 +81,7 @@ abstract class BukkitGuiView : GuiView() { val playerComponentJobs = mutableListOf() components.values.forEach { component -> component.updateInterval?.let { interval -> - val job = plugin.launch(plugin.entityDispatcher(player)) { + val job = surfBukkitApi.launch(surfBukkitApi.entityDispatcher(player)) { while (true) { delay(interval) val lifecycleContext = @@ -113,7 +105,7 @@ abstract class BukkitGuiView : GuiView() { // Unregister inventory inventories.remove(player.uniqueId)?.let { inventory -> - GuiViewListener.unregisterView(inventory) + ViewManager.unregisterView(inventory) } super.close(player) @@ -175,7 +167,7 @@ abstract class BukkitGuiView : GuiView() { /** * Handle click event. */ - internal fun handleClick(player: Player, event: InventoryClickEvent) { + fun handleClick(player: Player, event: InventoryClickEvent) { if (config.cancelOnClick) { event.isCancelled = true } @@ -183,60 +175,26 @@ abstract class BukkitGuiView : GuiView() { val slot = Slot.of(event.slot) val component = components[slot] - val clickContext = BukkitClickContext(this, player, event, component) + val clickContext = AbstractClickContext(this, player, event, component) component?.onClick(clickContext) } override fun createViewContext(player: Player): ViewContext { - return BukkitViewContext(this, player) + return AbstractViewContext(this, player) } override fun createRenderContext(player: Player): RenderContext { - return BukkitRenderContext(this, player, this) + return AbstractRenderContext(this, player, this) } override fun createLifecycleContext( player: Player, eventType: LifecycleEventType ): LifecycleContext { - return BukkitLifecycleContext(this, player, eventType) + return AbstractLifecycleContext(this, player, eventType) } override fun createResumeContext(player: Player, origin: GuiView?): ResumeContext { - return BukkitResumeContext(this, player, origin) + return AbstractResumeContext(this, player, origin) } -} - -/** - * Global event listener for all GUI views. - */ -object GuiViewListener : Listener { - private val viewsByInventory = ConcurrentHashMap() - - fun registerView(inventory: Inventory, view: BukkitGuiView) { - viewsByInventory[inventory] = view - } - - fun unregisterView(inventory: Inventory) { - viewsByInventory.remove(inventory) - } - - @EventHandler - fun onInventoryClick(event: InventoryClickEvent) { - val inventory = event.clickedInventory ?: return - val view = viewsByInventory[inventory] ?: return - val player = event.whoClicked as? Player ?: return - - view.handleClick(player, event) - } - - @EventHandler - fun onInventoryClose(event: InventoryCloseEvent) { - val inventory = event.inventory - val view = viewsByInventory[inventory] ?: return - val player = event.player as? Player ?: return - - // Close the view when inventory is closed - view.close(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/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 00000000..295489fd --- /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,25 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.view + +import dev.slne.surf.surfapi.core.api.util.requiredService +import it.unimi.dsi.fastutil.objects.Object2ObjectMap +import org.bukkit.inventory.Inventory +import org.jetbrains.annotations.Unmodifiable + +private val viewManager = requiredService() + +interface ViewManager { + val views: @Unmodifiable Object2ObjectMap + + fun registerView(inventory: Inventory, view: GuiView) + + fun unregisterView(view: GuiView) + fun unregisterView(inventory: Inventory) + + fun findByInventory(inventory: Inventory): GuiView? { + return views[inventory] + } + + companion object : ViewManager by viewManager { + val INSTANCE get() = viewManager + } +} \ 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 index 15789a08..301c9e77 100644 --- 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 @@ -1,18 +1,22 @@ 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.RenderContext 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.Prop import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig -import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView +import dev.slne.surf.surfapi.bukkit.test.plugin +import dev.slne.surf.surfapi.core.api.messages.adventure.text import kotlinx.coroutines.runBlocking -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration import org.bukkit.Material import org.bukkit.event.inventory.InventoryType @@ -20,101 +24,152 @@ 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 : BukkitGuiView() { - // Global counter state (shared across all viewers) +object CounterGuiView : AbstractGuiView() { private val counterProp = Prop.Mutable("counter", 0) - private val counterDisplayRef = Ref() + private val counterDisplayRef = Ref() override fun onInit(config: ViewConfig) { config.type = InventoryType.CHEST config.rows = 3 - config.title = Component.text("Counter GUI", NamedTextColor.GOLD, TextDecoration.BOLD) + config.title = text("Counter Gui") } override fun onFirstRender(context: RenderContext) { // Counter display at top center - context.renderComponent(Slot.at(4, 0), dynamicComponent( - renderer = { ctx -> - val count = runBlocking { counterProp.get() } ?: 0 - GuiItem.of(Material.DIAMOND).copyWith( - displayName = Component.text("Counter: $count", NamedTextColor.AQUA, TextDecoration.BOLD), - lore = listOf( - Component.text("This is a global counter", NamedTextColor.GRAY), - Component.text("shared across all viewers", NamedTextColor.GRAY) - ) - ) - } - ) { - ref = counterDisplayRef - }) + context.renderComponent( + Slot.at(4, 0), dynamicComponent( + renderer = { ctx -> + val count = runBlocking { counterProp.get() } ?: 0 + + GuiItem.of(ItemStack(Material.PAPER) { + displayName { + info("$count GUI view") + } + }) + } + ) { + ref = counterDisplayRef + }) // Increment +1 button - context.renderComponent(Slot.at(2, 1), component( - GuiItem.of(Material.LIME_CONCRETE).copyWith( - displayName = Component.text("+1", NamedTextColor.GREEN, TextDecoration.BOLD), - lore = listOf(Component.text("Click to increment by 1", NamedTextColor.GRAY)) - ) - ) { - onClick = { - val current = runBlocking { counterProp.get() } ?: 0 - counterProp.set(current + 1) - counterDisplayRef.update() - } - }) + context.renderComponent( + Slot.at(2, 1), component( + 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.renderComponent(Slot.at(3, 1), component( - GuiItem.of(Material.GREEN_CONCRETE).copyWith( - displayName = Component.text("+10", NamedTextColor.GREEN, TextDecoration.BOLD), - lore = listOf(Component.text("Click to increment by 10", NamedTextColor.GRAY)) - ) - ) { - onClick = { - val current = runBlocking { counterProp.get() } ?: 0 - counterProp.set(current + 10) - counterDisplayRef.update() - } - }) + context.renderComponent( + Slot.at(3, 1), component( + 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.renderComponent(Slot.at(5, 1), component( - GuiItem.of(Material.ORANGE_CONCRETE).copyWith( - displayName = Component.text("-1", NamedTextColor.RED, TextDecoration.BOLD), - lore = listOf(Component.text("Click to decrement by 1", NamedTextColor.GRAY)) - ) - ) { - onClick = { - val current = runBlocking { counterProp.get() } ?: 0 - counterProp.set(current - 1) - counterDisplayRef.update() - } - }) + context.renderComponent( + Slot.at(5, 1), component( + 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.renderComponent(Slot.at(6, 1), component( - GuiItem.of(Material.RED_CONCRETE).copyWith( - displayName = Component.text("-10", NamedTextColor.RED, TextDecoration.BOLD), - lore = listOf(Component.text("Click to decrement by 10", NamedTextColor.GRAY)) - ) - ) { - onClick = { - val current = runBlocking { counterProp.get() } ?: 0 - counterProp.set(current - 10) - counterDisplayRef.update() - } - }) + context.renderComponent( + Slot.at(6, 1), component( + 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.renderComponent(Slot.at(4, 2), component( - GuiItem.of(Material.BARRIER).copyWith( - displayName = Component.text("Reset", NamedTextColor.YELLOW, TextDecoration.BOLD), - lore = listOf(Component.text("Click to reset counter to 0", NamedTextColor.GRAY)) - ) - ) { - onClick = { - counterProp.set(0) - counterDisplayRef.update() - } - }) + context.renderComponent( + Slot.at(4, 2), component( + GuiItem.of(ItemStack(Material.BARRIER) { + displayName { + error("Reset Counter") + } + + buildLore { + line { + gray("Click to reset the counter to 0") + } + } + }) + ) { + onClick = { + 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 index 8e2106b4..25aeef9a 100644 --- 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 @@ -1,30 +1,31 @@ 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.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.PaginationComponent import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext -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.bukkit.api.gui.ref.Ref +import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig -import dev.slne.surf.surfapi.bukkit.server.gui.view.BukkitGuiView -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import dev.slne.surf.surfapi.core.api.messages.adventure.text import org.bukkit.Material import org.bukkit.event.inventory.InventoryType +import org.bukkit.inventory.ItemStack /** * Example GUI demonstrating ViewerMutableProp and PaginationComponent. * Each player has their own coins (viewer-specific state). */ -object PaginatedShopGuiView : BukkitGuiView() { - // Per-viewer coins (each player has their own balance) +object PaginatedShopGuiView : AbstractGuiView() { private val coinsProp = ViewerProp.Mutable("coins", 1000) - private val coinsDisplayRef = Ref() - + private val coinsDisplayRef = Ref() + // Shop items private val shopItems = listOf( ShopItem(Material.DIAMOND_SWORD, "Diamond Sword", 100), @@ -56,33 +57,31 @@ object PaginatedShopGuiView : BukkitGuiView() { ShopItem(Material.SADDLE, "Saddle", 60), ShopItem(Material.ELYTRA, "Elytra", 500) ) - + // Pagination component private val paginationComponent = PaginationComponent( items = { shopItems }, pageSize = 28, // 4 rows of 7 items itemRenderer = { item, ctx -> - GuiItem.of(item.material).copyWith( - displayName = Component.text(item.name, NamedTextColor.GREEN, TextDecoration.BOLD), - lore = listOf( - Component.text("Price: ${item.price} coins", NamedTextColor.GOLD), - Component.text("", NamedTextColor.GRAY), - Component.text("Click to purchase!", NamedTextColor.YELLOW) - ) - ) + GuiItem.of(ItemStack(item.material)) }, onItemClick = { item, ctx -> val coins = coinsProp.get(ctx.player) if (coins != null && coins >= item.price) { coinsProp.set(ctx.player, coins - item.price) - ctx.player.sendMessage( - Component.text("Purchased ${item.name} for ${item.price} coins!", NamedTextColor.GREEN) - ) + ctx.player.sendText { + success("Purchased ") + variableValue(item.name) + success(" for ") + gold("${item.price} coins") + } coinsDisplayRef.update() } else { - ctx.player.sendMessage( - Component.text("Not enough coins! Need ${item.price}, have ${coins ?: 0}", NamedTextColor.RED) - ) + ctx.player.sendText { + error("You do not have enough coins to purchase ") + variableValue(item.name) + error(".") + } } } ) @@ -90,73 +89,49 @@ object PaginatedShopGuiView : BukkitGuiView() { override fun onInit(config: ViewConfig) { config.type = InventoryType.CHEST config.rows = 6 - config.title = Component.text("Shop", NamedTextColor.GOLD, TextDecoration.BOLD) + config.title = text("Shop") } override fun onFirstRender(context: RenderContext) { // Coins display at top center - context.renderComponent(Slot.at(4, 0), dynamicComponent( - renderer = { ctx -> - val coins = coinsProp.get(ctx.player) ?: 0 - GuiItem.of(Material.GOLD_INGOT).copyWith( - displayName = Component.text("Your Coins: $coins", NamedTextColor.GOLD, TextDecoration.BOLD), - lore = listOf( - Component.text("This is your personal balance", NamedTextColor.GRAY), - Component.text("Each player has their own coins", NamedTextColor.GRAY) - ) - ) - } - ) { - ref = coinsDisplayRef - }) + context.renderComponent( + Slot.at(4, 0), dynamicComponent( + 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 at rows 1-4 (slots 9-44) context.renderComponent(Slot.of(9), paginationComponent) // Previous page button - context.renderComponent(Slot.at(2, 5), component( - GuiItem.of(Material.ARROW).copyWith( - displayName = Component.text("Previous Page", NamedTextColor.YELLOW, TextDecoration.BOLD) - ) - ) { - onClick = { - if (paginationComponent.hasPreviousPage(player)) { - paginationComponent.previousPage(player) - view.update(player) - } - } - }) + context.renderComponent( + Slot.at(2, 5), + PaginationComponent.buildPreviousPageComponent(paginationComponent) + ) // Page indicator - context.renderComponent(Slot.at(4, 5), dynamicComponent( - renderer = { ctx -> - val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 - val totalPages = paginationComponent.getTotalPages() - GuiItem.of(Material.PAPER).copyWith( - displayName = Component.text( - "Page $currentPage / $totalPages", - NamedTextColor.AQUA, - TextDecoration.BOLD - ) - ) - } - )) + context.renderComponent( + Slot.at(4, 5), + PaginationComponent.buildPageIndicatorComponent(paginationComponent) + ) // Next page button - context.renderComponent(Slot.at(6, 5), component( - GuiItem.of(Material.ARROW).copyWith( - displayName = Component.text("Next Page", NamedTextColor.YELLOW, TextDecoration.BOLD) - ) - ) { - onClick = { - if (paginationComponent.hasNextPage(player)) { - paginationComponent.nextPage(player) - view.update(player) - } - } - }) + context.renderComponent( + Slot.at(6, 5), + PaginationComponent.buildNextPageComponent(paginationComponent) + ) } - + private data class ShopItem( val material: Material, val name: String, diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt deleted file mode 100644 index 215e4b7e..00000000 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/context/BukkitContexts.kt +++ /dev/null @@ -1,191 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.server.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.context.* -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.server.gui.view.BukkitGuiView -import org.bukkit.entity.Player -import org.bukkit.event.inventory.InventoryClickEvent - -/** - * Shared navigation logic for all context implementations. - */ -private object NavigationHelper { - fun navigateTo(currentView: GuiView, targetView: GuiView, player: Player, passProps: Boolean) { - if (targetView is BukkitGuiView) { - targetView.withParent(currentView) - player.closeInventory() - targetView.open(player) - } - } - - fun navigateBack(currentView: GuiView, player: Player) { - val parent = currentView.parent - - if (parent is BukkitGuiView) { - 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) { - if (view is BukkitGuiView) { - view.refreshInventory(player) - } - } -} - -/** - * Bukkit implementation of ViewContext. - */ -internal class BukkitViewContext( - 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) - } -} - -/** - * Bukkit implementation of ClickContext. - */ -internal class BukkitClickContext( - 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) - } -} - -/** - * Bukkit implementation of RenderContext. - */ -internal class BukkitRenderContext( - override val view: GuiView, - override val player: Player, - private val bukkitView: BukkitGuiView -) : RenderContext { - override fun renderComponent(slot: Slot, component: Component) { - bukkitView.addComponent(slot, component) - } - - override fun clearSlot(slot: Slot) { - bukkitView.removeComponent(slot) - } - - override fun setItem(slot: Slot, item: GuiItem) { - val inventory = player.openInventory.topInventory - if (slot.index in 0 until inventory.size) { - inventory.setItem(slot.index, item.toItemStack()) - } - } - - 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) - } -} - -/** - * Bukkit implementation of LifecycleContext. - */ -internal class BukkitLifecycleContext( - 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) - } -} - -/** - * Bukkit implementation of ResumeContext. - */ -internal class BukkitResumeContext( - 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) - } -} 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 00000000..554f7ca1 --- /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,34 @@ +package dev.slne.surf.surfapi.bukkit.server.gui.view + +import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView +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 inventory = event.clickedInventory ?: return + val view = ViewManager.findByInventory(inventory) ?: return + val player = event.whoClicked as? Player ?: return + + if (view is AbstractGuiView) { + view.handleClick(player, event) + } + } + + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + val inventory = event.inventory + val view = ViewManager.findByInventory(inventory) ?: return + val player = event.player as? 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/impl/SurfBukkitApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt index 2bd4b342..148fcec5 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt @@ -1,5 +1,7 @@ package dev.slne.surf.surfapi.bukkit.server.impl +import com.github.shynixn.mccoroutine.folia.entityDispatcher +import com.github.shynixn.mccoroutine.folia.launch import com.google.auto.service.AutoService import com.google.common.io.ByteStreams import dev.slne.surf.surfapi.bukkit.api.SurfBukkitApi @@ -14,12 +16,15 @@ import dev.slne.surf.surfapi.core.api.SurfCoreApi import dev.slne.surf.surfapi.core.api.util.checkInstantiationByServiceLoader import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf import dev.slne.surf.surfapi.core.server.impl.SurfCoreApiImpl +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.World +import org.bukkit.entity.Entity import java.util.* +import kotlin.coroutines.CoroutineContext @AutoService(SurfCoreApi::class) class SurfBukkitApiImpl : SurfCoreApiImpl(), SurfBukkitApi { @@ -103,4 +108,10 @@ class SurfBukkitApiImpl : SurfCoreApiImpl(), SurfBukkitApi { } }.mapValuesTo(mutableObject2ObjectMapOf(worlds.size)) { (_, def) -> def.await() } } + + override fun launch(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit) = + plugin.launch(context = context, block = block) + + override fun entityDispatcher(entity: Entity) = + plugin.entityDispatcher(entity) } From f5e0068633af3519bdbfea8499698c8e66a62125 Mon Sep 17 00:00:00 2001 From: Ammo Date: Tue, 3 Feb 2026 23:51:39 +0100 Subject: [PATCH 22/92] feat: implement ViewManager for GUI view registration and management --- .../build.gradle.kts | 2 +- .../test/command/SurfApiTestCommand.java | 40 +++++++++---------- .../test/command/subcommands/GuiTest.kt | 5 +-- .../bukkit/server/gui/view/ViewManagerImpl.kt | 30 ++++++++++++++ 4 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/gui/view/ViewManagerImpl.kt 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 d89ffb44..7776161e 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") } } 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 42ceedf3..ff7b5e3a 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 @@ -18,26 +18,26 @@ 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 MaxStacksizeTest("maxstacksize"), - new VisualizerTest("visualizer"), - new GlowingTest("glowing"), - new PaginationTest("pagination"), - new ToastTest(("toast")), - new SuspendCommandExecutionTest("suspendCommandExecution"), - new GuiTest("gui") - ); - } + 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/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 index 5d5fded3..a99cf8d0 100644 --- 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 @@ -1,7 +1,6 @@ package dev.slne.surf.surfapi.bukkit.test.command.subcommands import dev.jorel.commandapi.CommandAPICommand -import dev.jorel.commandapi.arguments.LiteralArgument import dev.jorel.commandapi.executors.PlayerCommandExecutor import dev.slne.surf.surfapi.bukkit.test.gui.CounterGuiView import dev.slne.surf.surfapi.bukkit.test.gui.PaginatedShopGuiView @@ -12,14 +11,14 @@ import dev.slne.surf.surfapi.bukkit.test.gui.PaginatedShopGuiView 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, _ -> 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 00000000..8b7ecf8b --- /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,30 @@ +package dev.slne.surf.surfapi.bukkit.server.gui.view + +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 dev.slne.surf.surfapi.core.api.util.freeze +import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf +import org.bukkit.inventory.Inventory + +@AutoService(ViewManager::class) +class ViewManagerImpl : ViewManager { + private val _views = mutableObject2ObjectMapOf() + override val views get() = _views.freeze() + + override fun registerView( + inventory: Inventory, + view: GuiView + ) { + _views[inventory] = view + } + + override fun unregisterView(view: GuiView) { + val entry = _views.entries.firstOrNull { it.value == view } ?: return + _views.remove(entry.key) + } + + override fun unregisterView(inventory: Inventory) { + _views.remove(inventory) + } +} \ No newline at end of file From 3031a59b237cedf00fdd3fc9485ae6c3c0f63631 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:01:42 +0000 Subject: [PATCH 23/92] Updated plan: Add component area with priority system for overlapping components Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 32 +++++++++++++++++++ .../api/gui/component/ContainerComponent.kt | 6 +++- .../api/gui/component/DynamicComponent.kt | 6 ++++ .../bukkit/api/gui/component/ItemComponent.kt | 6 ++++ 4 files changed, 49 insertions(+), 1 deletion(-) 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 index c9e25284..45bcd77d 100644 --- 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 @@ -15,6 +15,38 @@ import kotlin.time.Duration * Components follow React-like lifecycle principles. */ abstract class Component { + /** + * The start slot (top-left corner) of this component's area. + */ + abstract val startSlot: Slot + + /** + * The end slot (bottom-right corner) of this component's area. + */ + abstract val endSlot: Slot + + /** + * Width of the component in columns (derived from start and end slots). + */ + val width: Int + get() = endSlot.column - startSlot.column + 1 + + /** + * Height of the component in rows (derived from start and end slots). + */ + val height: Int + get() = endSlot.row - startSlot.row + 1 + + /** + * Check if a slot is within this component's area. + */ + fun contains(slot: Slot): Boolean { + return slot.column >= startSlot.column && + slot.column <= endSlot.column && + slot.row >= startSlot.row && + slot.row <= endSlot.row + } + /** * The parent component, if any. */ diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt index 455f026c..199be012 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -7,8 +7,12 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext /** * Container component that renders multiple items at specific slots. * This is useful for paginated components or complex layouts. + * The area is defined by startSlot (top-left) and endSlot (bottom-right). */ -abstract class ContainerComponent : Component() { +abstract class ContainerComponent( + override val startSlot: Slot, + override val endSlot: Slot +) : Component() { /** * Render multiple items at their respective slots. * Override this to provide the slot-to-item mapping. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index 2ae094fd..c6d09c57 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -1,16 +1,22 @@ 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.context.ClickContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext /** * Dynamic component that renders based on a callback. + * Takes a single slot for both start and end (1x1 area). */ open class DynamicComponent( + private val slot: Slot, private val renderer: (ViewContext) -> GuiItem?, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { + override val startSlot: Slot = slot + override val endSlot: Slot = slot + override fun render(context: ViewContext): GuiItem? = renderer(context) override fun onClick(context: ClickContext) { diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index f3c15c56..ba7466c5 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -1,16 +1,22 @@ 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.context.ClickContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext /** * Simple item component that renders a static item. + * Takes a single slot for both start and end (1x1 area). */ open class ItemComponent( + private val slot: Slot, private val item: GuiItem, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { + override val startSlot: Slot = slot + override val endSlot: Slot = slot + override fun render(context: ViewContext): GuiItem = item override fun onClick(context: ClickContext) { From 5bd44a72ab117d80d8117cd93232b5559bfc213b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:05:46 +0000 Subject: [PATCH 24/92] Add component area (startSlot/endSlot) with ComponentPriority enum for overlapping support - Created ComponentPriority enum (LOWEST, LOW, NORMAL, HIGH, HIGHEST) - Added startSlot, endSlot, priority, width, height, contains() to Component - Updated all component implementations (ItemComponent, DynamicComponent, ContainerComponent, PaginationComponent) - Changed GuiView to store components in list instead of map - Added findComponentsBySlot() to find all components at slot sorted by priority - Updated rendering: only highest priority component renders at each slot - Updated click handling: only highest priority component handles clicks - Updated DSL: components now include their slot, renderComponent() takes just component - Fixed PaginationComponent to calculate slot positions within its area - Components can now overlap, with priority determining render/click precedence Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 7 + .../api/gui/component/ComponentPriority.kt | 42 ++++++ .../api/gui/component/ContainerComponent.kt | 3 +- .../api/gui/component/DynamicComponent.kt | 1 + .../bukkit/api/gui/component/ItemComponent.kt | 1 + .../api/gui/component/PaginationComponent.kt | 30 +++-- .../bukkit/api/gui/context/RenderContext.kt | 5 +- .../context/abstract/AbstractRenderContext.kt | 10 +- .../bukkit/api/gui/dsl/ComponentDsl.kt | 28 ++-- .../bukkit/api/gui/view/AbstractGuiView.kt | 127 +++++++++++------- .../surfapi/bukkit/api/gui/view/GuiView.kt | 29 ++-- 11 files changed, 200 insertions(+), 83 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ComponentPriority.kt 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 index 45bcd77d..6a4b498e 100644 --- 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 @@ -25,6 +25,13 @@ abstract class Component { */ abstract val endSlot: Slot + /** + * Priority for handling clicks and rendering when components overlap. + * Higher priority components are rendered on top and handle clicks first. + * Default is NORMAL. + */ + open val priority: ComponentPriority = ComponentPriority.NORMAL + /** * Width of the component in columns (derived from start and end slots). */ 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 00000000..f06f2464 --- /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/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt index 199be012..97c2b3d8 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -11,7 +11,8 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext */ abstract class ContainerComponent( override val startSlot: Slot, - override val endSlot: Slot + override val endSlot: Slot, + override val priority: ComponentPriority = ComponentPriority.NORMAL ) : Component() { /** * Render multiple items at their respective slots. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index c6d09c57..b5f51ecb 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -12,6 +12,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext open class DynamicComponent( private val slot: Slot, private val renderer: (ViewContext) -> GuiItem?, + override val priority: ComponentPriority = ComponentPriority.NORMAL, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { override val startSlot: Slot = slot diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index ba7466c5..6a826763 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -12,6 +12,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext open class ItemComponent( private val slot: Slot, private val item: GuiItem, + override val priority: ComponentPriority = ComponentPriority.NORMAL, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { override val startSlot: Slot = slot diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index b9f59ef3..ae3a3bed 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -18,12 +18,19 @@ import java.util.* * Renders multiple items across specified slots based on current page. */ class PaginationComponent( + startSlot: Slot, + endSlot: Slot, private val items: () -> List, - private val pageSize: Int = 9, private val itemRenderer: (T, ViewContext) -> GuiItem?, + priority: ComponentPriority = ComponentPriority.NORMAL, private val onItemClick: ((T, ClickContext) -> Unit)? = null -) : ContainerComponent() { +) : ContainerComponent(startSlot, endSlot, priority) { private val currentPages = mutableMapOf() + + /** + * Calculate page size from the area. + */ + private val pageSize: Int = width * height /** * Get the current page for a viewer. @@ -116,7 +123,11 @@ class PaginationComponent( val guiItem = itemRenderer(item, context) if (guiItem != null) { - renderedSlots[Slot.of(index)] = guiItem + // Calculate slot position within the area + val row = index / width + val col = index % width + val slot = Slot.at(startSlot.column + col, startSlot.row + row) + renderedSlots[slot] = guiItem } } @@ -126,11 +137,14 @@ class PaginationComponent( override fun onClick(context: ClickContext) { if (onItemClick != null) { val pageItems = getPageItems(context.player) - val slotIndex = context.slot.index - - if (slotIndex in pageItems.indices) { - val item = pageItems[slotIndex] - + + // Calculate the index within the pagination area + val relativeCol = context.slot.column - startSlot.column + val relativeRow = context.slot.row - startSlot.row + val index = relativeRow * width + relativeCol + + if (index in pageItems.indices) { + val item = pageItems[index] onItemClick.invoke(item, context) } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt index 1eb95991..7b3c25fb 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt @@ -9,9 +9,10 @@ import dev.slne.surf.surfapi.bukkit.api.gui.component.Component */ interface RenderContext : ViewContext { /** - * Render a component at the specified slot. + * Render a component. + * The component contains its own slot information (startSlot/endSlot). */ - fun renderComponent(slot: Slot, component: Component) + fun renderComponent(component: Component) /** * Clear a slot. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt index 841f9134..77670262 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt @@ -16,12 +16,16 @@ class AbstractRenderContext( override val player: Player, private val bukkitView: AbstractGuiView ) : RenderContext { - override fun renderComponent(slot: Slot, component: Component) { - bukkitView.addComponent(slot, component) + override fun renderComponent(component: Component) { + bukkitView.addComponent(component) } override fun clearSlot(slot: Slot) { - bukkitView.removeComponent(slot) + // Find and remove all components that occupy this slot + val componentsToRemove = bukkitView.findComponentsBySlot(slot) + componentsToRemove.forEach { component -> + bukkitView.removeComponent(component) + } } override 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/dsl/ComponentDsl.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt index 731aade7..e08d65ed 100644 --- 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 @@ -3,6 +3,7 @@ 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.DynamicComponent import dev.slne.surf.surfapi.bukkit.api.gui.component.ItemComponent import dev.slne.surf.surfapi.bukkit.api.gui.context.ClickContext @@ -29,6 +30,7 @@ annotation class ComponentDsl class ComponentBuilder { var updateInterval: Duration? = null var ref: Ref? = null + var priority: ComponentPriority = ComponentPriority.NORMAL var onUpdate: (LifecycleContext.() -> Unit)? = null var onClick: (ClickContext.() -> Unit)? = null @@ -44,8 +46,8 @@ class ComponentBuilder { /** * Build the component. */ - internal fun build(renderer: (ViewContext) -> GuiItem?): Component { - return object : DynamicComponent(renderer, onClick) { + internal fun build(slot: Slot, renderer: (ViewContext) -> GuiItem?): Component { + return object : DynamicComponent(slot, renderer, priority, onClick) { override val updateInterval: Duration? = this@ComponentBuilder.updateInterval override val props: Map> = _props @@ -61,27 +63,29 @@ class ComponentBuilder { } /** - * Create a component with a static item. + * 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 { item } + return componentBuilder.build(slot) { item } } /** - * Create a component with a dynamic renderer. + * 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(renderer) + return componentBuilder.build(slot, renderer) } /** @@ -159,15 +163,15 @@ fun props(builder: PropsBuilder.() -> Unit): Map> { * DSL for rendering components in a view. */ @ComponentDsl -fun RenderContext.slot(slot: Slot, component: Component) { - renderComponent(slot, component) +fun RenderContext.slot(component: Component) { + renderComponent(component) } /** - * DSL for rendering components in a view with item. + * DSL for rendering components in a view with item at a specific slot. */ @ComponentDsl -fun RenderContext.slot(slot: Slot, item: GuiItem, onClick: (ClickContext.() -> Unit)? = null) { - val component = ItemComponent(item, onClick) - renderComponent(slot, component) +fun RenderContext.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/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index ab9f2040..cf8825c1 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -42,23 +42,33 @@ open class AbstractGuiView : GuiView() { // Register with listener ViewManager.registerView(inventory, this) - // Render components - components.forEach { (slot, component) -> - val context = createViewContext(player) - - // Handle container components (render multiple slots) - val slotsToRender = component.renderSlots(context) - if (slotsToRender.isNotEmpty()) { - slotsToRender.forEach { (slotObj, guiItem) -> - if (slotObj.index < inventory.size) { - inventory.setItem(slotObj.index, guiItem.toItemStack()) + // Render all components, respecting priority + // For each slot, render only the highest priority component + val allSlots = (0 until inventory.size).map { Slot.of(it) } + allSlots.forEach { slot -> + val componentsAtSlot = findComponentsBySlot(slot) + if (componentsAtSlot.isNotEmpty()) { + // Get highest priority component + val component = componentsAtSlot.first() + val context = createViewContext(player) + + // Check if it's a container component + val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { + // Only render this slot if it's in the container's output + slotsToRender[slot]?.let { guiItem -> + if (slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) + } + } + } else { + // Regular component - only render at its start slot + if (slot == component.startSlot) { + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) + } } - } - } else { - // Regular component (single item) - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) } } } @@ -79,7 +89,7 @@ open class AbstractGuiView : GuiView() { // Start component update tasks (Folia-compatible) val playerComponentJobs = mutableListOf() - components.values.forEach { component -> + components.forEach { component -> component.updateInterval?.let { interval -> val job = surfBukkitApi.launch(surfBukkitApi.entityDispatcher(player)) { while (true) { @@ -87,7 +97,7 @@ open class AbstractGuiView : GuiView() { val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) component.onUpdate(lifecycleContext) - refreshComponentSlot(player, component) + refreshComponentSlots(player, component) } } playerComponentJobs.add(job) @@ -117,49 +127,67 @@ open class AbstractGuiView : GuiView() { internal fun refreshInventory(player: Player) { val inventory = inventories[player.uniqueId] ?: return - components.forEach { (slot, component) -> - val context = createViewContext(player) - - // Handle container components - val slotsToRender = component.renderSlots(context) - if (slotsToRender.isNotEmpty()) { - slotsToRender.forEach { (slotObj, guiItem) -> - if (slotObj.index < inventory.size) { - inventory.setItem(slotObj.index, guiItem.toItemStack()) + // Clear inventory first + inventory.clear() + + // Render all slots, respecting priority + val allSlots = (0 until inventory.size).map { Slot.of(it) } + allSlots.forEach { slot -> + val componentsAtSlot = findComponentsBySlot(slot) + if (componentsAtSlot.isNotEmpty()) { + // Get highest priority component + val component = componentsAtSlot.first() + val context = createViewContext(player) + + // Check if it's a container component + val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { + // Only render this slot if it's in the container's output + slotsToRender[slot]?.let { guiItem -> + if (slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) + } + } + } else { + // Regular component - only render at its start slot + if (slot == component.startSlot) { + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) + } } - } - } else { - // Regular component - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) } } } } /** - * Refresh a specific component slot. + * Refresh all slots occupied by a specific component. */ - private fun refreshComponentSlot(player: Player, component: Component) { + private fun refreshComponentSlots(player: Player, component: Component) { val inventory = inventories[player.uniqueId] ?: return - val slot = components.entries.find { it.value == component }?.key ?: return - val context = createViewContext(player) - - // Handle container components + + // Get all slots this component occupies val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { - slotsToRender.forEach { (slotObj, guiItem) -> - if (slotObj.index < inventory.size) { - inventory.setItem(slotObj.index, guiItem.toItemStack()) + // Container component - refresh each slot it renders + slotsToRender.forEach { (slot, guiItem) -> + // Check if this component is still highest priority at this slot + val highestPriorityComponent = findComponentsBySlot(slot).firstOrNull() + if (highestPriorityComponent == component && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) } } } else { - // Regular component - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) + // Regular component - refresh its start slot + val slot = component.startSlot + val highestPriorityComponent = findComponentsBySlot(slot).firstOrNull() + if (highestPriorityComponent == component) { + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) + } } } } @@ -173,7 +201,12 @@ open class AbstractGuiView : GuiView() { } val slot = Slot.of(event.slot) - val component = components[slot] + + // Find all components at this slot, sorted by priority (highest first) + val componentsAtSlot = findComponentsBySlot(slot) + + // Only the highest priority component handles the click + val component = componentsAtSlot.firstOrNull() val clickContext = AbstractClickContext(this, player, event, component) component?.onClick(clickContext) 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 index dae2f0c4..5c32e373 100644 --- 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 @@ -23,10 +23,20 @@ abstract class GuiView { open val updateInterval: Duration? = null /** - * Components in this view, mapped by slot. + * Components in this view. + * Components can overlap, and priority determines rendering and click handling order. */ - private val _components = mutableMapOf() - val components: Map get() = _components.toMap() + private val _components = mutableListOf() + val components: List get() = _components.toList() + + /** + * Find all components that contain the given slot, sorted by priority (highest first). + */ + fun findComponentsBySlot(slot: Slot): List { + return _components + .filter { it.contains(slot) } + .sortedByDescending { it.priority.value } + } /** * Whether this view has been initialized. @@ -137,19 +147,18 @@ abstract class GuiView { } /** - * Add a component at a slot. + * Add a component to the view. */ - fun addComponent(slot: Slot, component: Component) { + fun addComponent(component: Component) { component.view = this - - _components[slot] = component + _components.add(component) } /** - * Remove a component from a slot. + * Remove a component from the view. */ - fun removeComponent(slot: Slot) { - _components.remove(slot) + fun removeComponent(component: Component) { + _components.remove(component) } /** From 304ef9df6cf9582b46286769c98f1c688b5bb78c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:08:50 +0000 Subject: [PATCH 25/92] Update example GUIs to use new component area API with slot embedded in components Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/test/gui/CounterGuiView.kt | 206 +++++++++--------- .../bukkit/test/gui/PaginatedShopGuiView.kt | 110 +++++++--- 2 files changed, 185 insertions(+), 131 deletions(-) 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 index 301c9e77..0f170385 100644 --- 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 @@ -36,140 +36,142 @@ object CounterGuiView : AbstractGuiView() { override fun onFirstRender(context: RenderContext) { // Counter display at top center - context.renderComponent( - Slot.at(4, 0), dynamicComponent( - renderer = { ctx -> - val count = runBlocking { counterProp.get() } ?: 0 - - GuiItem.of(ItemStack(Material.PAPER) { - displayName { - info("$count GUI view") - } - }) - } - ) { - ref = counterDisplayRef - }) + context.slot(dynamicComponent( + Slot.at(4, 0), + renderer = { ctx -> + val count = runBlocking { counterProp.get() } ?: 0 - // Increment +1 button - context.renderComponent( - Slot.at(2, 1), component( - GuiItem.of(ItemStack(Material.LIME_CONCRETE) { + GuiItem.of(ItemStack(Material.PAPER) { displayName { - success("+1") - } - - buildLore { - line { - gray("Click to increment by 1") - } + info("Count: $count") } }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + } + ) { + ref = counterDisplayRef + }) + + // Increment +1 button + context.slot(component( + Slot.at(2, 1), + GuiItem.of(ItemStack(Material.LIME_CONCRETE) { + displayName { + success("+1") + } - counterProp.set(current + 1) - counterDisplayRef.update() + buildLore { + line { + gray("Click to increment by 1") } } }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 - // Increment +10 button - context.renderComponent( - Slot.at(3, 1), component( - GuiItem.of(ItemStack(Material.GREEN_CONCRETE) { - displayName { - success("+10") - } + counterProp.set(current + 1) + counterDisplayRef.update() + } + } + }) - buildLore { - line { - gray("Click to increment by 10") - } - } - }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + // Increment +10 button + context.slot(component( + Slot.at(3, 1), + GuiItem.of(ItemStack(Material.GREEN_CONCRETE) { + displayName { + success("+10") + } - counterProp.set(current + 10) - counterDisplayRef.update() + buildLore { + line { + gray("Click to increment by 10") } } }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 - // Decrement -1 button - context.renderComponent( - Slot.at(5, 1), component( - GuiItem.of(ItemStack(Material.PINK_CONCRETE) { - displayName { - error("-1") - } + counterProp.set(current + 10) + counterDisplayRef.update() + } + } + }) - buildLore { - line { - gray("Click to decrement by 1") - } - } - }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + // Decrement -1 button + context.slot(component( + Slot.at(5, 1), + GuiItem.of(ItemStack(Material.PINK_CONCRETE) { + displayName { + error("-1") + } - counterProp.set(current - 1) - counterDisplayRef.update() + buildLore { + line { + gray("Click to decrement by 1") } } }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 - // Decrement -10 button - context.renderComponent( - Slot.at(6, 1), component( - GuiItem.of(ItemStack(Material.RED_CONCRETE) { - displayName { - error("-10") - } + counterProp.set(current - 1) + counterDisplayRef.update() + } + } + }) - buildLore { - line { - gray("Click to decrement by 10") - } - } - }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + // Decrement -10 button + context.slot(component( + Slot.at(6, 1), + GuiItem.of(ItemStack(Material.RED_CONCRETE) { + displayName { + error("-10") + } - counterProp.set(current - 10) - counterDisplayRef.update() + 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.renderComponent( - Slot.at(4, 2), component( - GuiItem.of(ItemStack(Material.BARRIER) { - displayName { - error("Reset Counter") - } + 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") - } + buildLore { + line { + gray("Click to reset the counter to 0") } - }) - ) { - onClick = { + } + }) + ) { + 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 index 25aeef9a..56557f15 100644 --- 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 @@ -1,12 +1,14 @@ 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.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.PaginationComponent import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext +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.bukkit.api.gui.ref.Ref @@ -16,7 +18,6 @@ import dev.slne.surf.surfapi.core.api.messages.adventure.sendText import dev.slne.surf.surfapi.core.api.messages.adventure.text import org.bukkit.Material import org.bukkit.event.inventory.InventoryType -import org.bukkit.inventory.ItemStack /** * Example GUI demonstrating ViewerMutableProp and PaginationComponent. @@ -58,12 +59,26 @@ object PaginatedShopGuiView : AbstractGuiView() { ShopItem(Material.ELYTRA, "Elytra", 500) ) - // Pagination component + // Pagination component - 4 rows of 7 items (columns 1-7, rows 1-4) private val paginationComponent = PaginationComponent( + startSlot = Slot.at(1, 1), // Column 1, Row 1 + endSlot = Slot.at(7, 4), // Column 7, Row 4 (28 slots total) items = { shopItems }, - pageSize = 28, // 4 rows of 7 items itemRenderer = { item, ctx -> - GuiItem.of(ItemStack(item.material)) + GuiItem.of(ItemStack(item.material) { + 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) @@ -94,42 +109,79 @@ object PaginatedShopGuiView : AbstractGuiView() { override fun onFirstRender(context: RenderContext) { // Coins display at top center - context.renderComponent( - Slot.at(4, 0), dynamicComponent( - renderer = { ctx -> - val coins = coinsProp.get(ctx.player) ?: 0 + 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 - }) + GuiItem.of(ItemStack(Material.GOLD_INGOT) { + displayName { + gold("Coins: ") + yellow("$coins") + } + }) + } + ) { + ref = coinsDisplayRef + }) - // Render pagination component at rows 1-4 (slots 9-44) - context.renderComponent(Slot.of(9), paginationComponent) + // Render pagination component + context.slot(paginationComponent) // Previous page button - context.renderComponent( + context.slot(component( Slot.at(2, 5), - PaginationComponent.buildPreviousPageComponent(paginationComponent) - ) + GuiItem.of(ItemStack(Material.ARROW) { + displayName { + info("Previous Page") + } + buildLore { + line { + gray("Click to go to the previous page") + } + } + }) + ) { + onClick = { + paginationComponent.previousPage(player) + view.update() + } + }) // Page indicator - context.renderComponent( + context.slot(dynamicComponent( Slot.at(4, 5), - PaginationComponent.buildPageIndicatorComponent(paginationComponent) - ) + renderer = { ctx -> + val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 + val totalPages = paginationComponent.getTotalPages() + + GuiItem.of(ItemStack(Material.PAPER) { + displayName { + info("Page $currentPage / $totalPages") + } + }) + } + )) // Next page button - context.renderComponent( + context.slot(component( Slot.at(6, 5), - PaginationComponent.buildNextPageComponent(paginationComponent) - ) + GuiItem.of(ItemStack(Material.ARROW) { + displayName { + info("Next Page") + } + buildLore { + line { + gray("Click to go to the next page") + } + } + }) + ) { + onClick = { + paginationComponent.nextPage(player) + view.update() + } + }) } private data class ShopItem( From 983876b6de6db614e3174bf7e31c7c7bc6fae2a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:15:02 +0000 Subject: [PATCH 26/92] Refactor components to use ComponentArea interface for flexible shapes - Created ComponentArea interface with slots(), contains(), width, height, priority - Implemented SingleSlotArea for single-slot components - Implemented CuboidArea for rectangular areas (replaces start/end slot) - Implemented CircularArea for circular/radial patterns around center point - Updated Component to use area instead of startSlot/endSlot - Updated all component implementations (ItemComponent, DynamicComponent, ContainerComponent, PaginationComponent) - Components can now have any shape defined by their area implementation - Existing rendering and click handling logic works via area.contains() and area.slots() Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/area/CircularArea.kt | 46 +++++++++++++++++++ .../bukkit/api/gui/area/ComponentArea.kt | 36 +++++++++++++++ .../surfapi/bukkit/api/gui/area/CuboidArea.kt | 37 +++++++++++++++ .../bukkit/api/gui/area/SingleSlotArea.kt | 20 ++++++++ .../bukkit/api/gui/component/Component.kt | 35 ++++++-------- .../api/gui/component/ContainerComponent.kt | 9 ++-- .../api/gui/component/DynamicComponent.kt | 12 ++--- .../bukkit/api/gui/component/ItemComponent.kt | 12 ++--- .../api/gui/component/PaginationComponent.kt | 22 ++++++--- 9 files changed, 185 insertions(+), 44 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CircularArea.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/ComponentArea.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CuboidArea.kt create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/SingleSlotArea.kt 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 00000000..4728bcf1 --- /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,46 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority +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, + override val priority: ComponentPriority = ComponentPriority.NORMAL +) : ComponentArea { + override fun slots(): Set { + val slots = mutableSetOf() + 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 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 00000000..b0abc5d1 --- /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,36 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority + +/** + * Defines the area that a component occupies in a GUI. + * Components can have different shaped areas (cuboid, circular, custom). + */ +interface ComponentArea { + /** + * Priority for rendering and click handling when components overlap. + * Higher priority components are rendered on top and handle clicks first. + */ + val priority: ComponentPriority + + /** + * Get all slots that are part of this area. + */ + fun slots(): Set + + /** + * 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 +} 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 00000000..ec58018b --- /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,37 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority + +/** + * 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, + override val priority: ComponentPriority = ComponentPriority.NORMAL +) : ComponentArea { + override fun slots(): Set { + val slots = mutableSetOf() + 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 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 00000000..22e03c37 --- /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,20 @@ +package dev.slne.surf.surfapi.bukkit.api.gui.area + +import dev.slne.surf.surfapi.bukkit.api.gui.Slot +import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority + +/** + * An area consisting of a single slot. + * Used for simple components like buttons or single items. + */ +data class SingleSlotArea( + val slot: Slot, + override val priority: ComponentPriority = ComponentPriority.NORMAL +) : ComponentArea { + override fun slots(): Set = setOf(slot) + + override fun contains(slot: Slot): Boolean = this.slot == slot + + 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 index 6a4b498e..a9d4cea7 100644 --- 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 @@ -2,6 +2,7 @@ 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.ViewContext @@ -16,43 +17,37 @@ import kotlin.time.Duration */ abstract class Component { /** - * The start slot (top-left corner) of this component's area. + * The area this component occupies in the GUI. + * Can be any shape (cuboid, circular, custom). */ - abstract val startSlot: Slot - - /** - * The end slot (bottom-right corner) of this component's area. - */ - abstract val endSlot: Slot + abstract val area: ComponentArea /** * Priority for handling clicks and rendering when components overlap. - * Higher priority components are rendered on top and handle clicks first. - * Default is NORMAL. + * Delegates to the area's priority. */ - open val priority: ComponentPriority = ComponentPriority.NORMAL + val priority: ComponentPriority + get() = area.priority /** - * Width of the component in columns (derived from start and end slots). + * Width of the component's bounding box. + * Delegates to the area's width. */ val width: Int - get() = endSlot.column - startSlot.column + 1 + get() = area.width /** - * Height of the component in rows (derived from start and end slots). + * Height of the component's bounding box. + * Delegates to the area's height. */ val height: Int - get() = endSlot.row - startSlot.row + 1 + 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 { - return slot.column >= startSlot.column && - slot.column <= endSlot.column && - slot.row >= startSlot.row && - slot.row <= endSlot.row - } + fun contains(slot: Slot): Boolean = area.contains(slot) /** * The parent component, if any. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt index 97c2b3d8..789e0703 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -2,17 +2,16 @@ 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.ViewContext /** * Container component that renders multiple items at specific slots. - * This is useful for paginated components or complex layouts. - * The area is defined by startSlot (top-left) and endSlot (bottom-right). + * This is useful for paginated components, panels, or complex layouts. + * The area can be any shape (cuboid, circular, custom). */ abstract class ContainerComponent( - override val startSlot: Slot, - override val endSlot: Slot, - override val priority: ComponentPriority = ComponentPriority.NORMAL + override val area: ComponentArea ) : Component() { /** * Render multiple items at their respective slots. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index b5f51ecb..1fc35a64 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -2,21 +2,21 @@ 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.area.SingleSlotArea 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. - * Takes a single slot for both start and end (1x1 area). + * Dynamic component that renders based on a callback at a single slot. */ open class DynamicComponent( - private val slot: Slot, + slot: Slot, private val renderer: (ViewContext) -> GuiItem?, - override val priority: ComponentPriority = ComponentPriority.NORMAL, + priority: ComponentPriority = ComponentPriority.NORMAL, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override val startSlot: Slot = slot - override val endSlot: Slot = slot + override val area: ComponentArea = SingleSlotArea(slot, priority) override fun render(context: ViewContext): GuiItem? = renderer(context) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index 6a826763..3b37ee9a 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -2,21 +2,21 @@ 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.area.SingleSlotArea 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. - * Takes a single slot for both start and end (1x1 area). + * Simple item component that renders a static item at a single slot. */ open class ItemComponent( - private val slot: Slot, + slot: Slot, private val item: GuiItem, - override val priority: ComponentPriority = ComponentPriority.NORMAL, + priority: ComponentPriority = ComponentPriority.NORMAL, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override val startSlot: Slot = slot - override val endSlot: Slot = slot + override val area: ComponentArea = SingleSlotArea(slot, priority) override fun render(context: ViewContext): GuiItem = item diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index ae3a3bed..0df6f893 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -5,6 +5,8 @@ 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.context.ClickContext import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext import dev.slne.surf.surfapi.bukkit.api.gui.dsl.component @@ -24,13 +26,19 @@ class PaginationComponent( private val itemRenderer: (T, ViewContext) -> GuiItem?, priority: ComponentPriority = ComponentPriority.NORMAL, private val onItemClick: ((T, ClickContext) -> Unit)? = null -) : ContainerComponent(startSlot, endSlot, priority) { +) : ContainerComponent(CuboidArea(startSlot, endSlot, priority)) { private val currentPages = mutableMapOf() /** * Calculate page size from the area. */ private val pageSize: Int = width * height + + /** + * Start slot of the area (convenience accessor). + */ + private val startSlot: Slot + get() = (area as CuboidArea).startSlot /** * Get the current page for a viewer. @@ -163,8 +171,8 @@ class PaginationComponent( } }) - fun buildNextPageComponent(paginationComponent: PaginationComponent<*>) = - component(NEXT_PAGE_ITEM) { + fun buildNextPageComponent(paginationComponent: PaginationComponent<*>, slot: Slot) = + component(slot, NEXT_PAGE_ITEM) { onClick = { paginationComponent.nextPage(player) view.update() @@ -183,8 +191,8 @@ class PaginationComponent( } }) - fun buildPreviousPageComponent(paginationComponent: PaginationComponent<*>) = - component(PREVIOUS_PAGE_ITEM) { + fun buildPreviousPageComponent(paginationComponent: PaginationComponent<*>, slot: Slot) = + component(slot, PREVIOUS_PAGE_ITEM) { onClick = { paginationComponent.previousPage(player) view.update() @@ -204,8 +212,8 @@ class PaginationComponent( } }) - fun buildPageIndicatorComponent(paginationComponent: PaginationComponent<*>) = - dynamicComponent(renderer = { ctx -> + fun buildPageIndicatorComponent(paginationComponent: PaginationComponent<*>, slot: Slot) = + dynamicComponent(slot, renderer = { ctx -> val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 val totalPages = paginationComponent.getTotalPages() From 6ab9173b27fae22f0d5c049b5436fde0beb2779e Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 00:18:23 +0100 Subject: [PATCH 27/92] refactor: simplify type declarations for UUID in GuiView class --- .../dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 5c32e373..9cfc7772 100644 --- 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 @@ -4,6 +4,7 @@ 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 org.bukkit.entity.Player +import java.util.* import kotlin.time.Duration /** @@ -28,7 +29,7 @@ abstract class GuiView { */ private val _components = mutableListOf() val components: List get() = _components.toList() - + /** * Find all components that contain the given slot, sorted by priority (highest first). */ @@ -46,12 +47,12 @@ abstract class GuiView { /** * Whether this view has been rendered for the first time. */ - private val firstRenderPerViewer = mutableSetOf() + private val firstRenderPerViewer = mutableSetOf() /** * Current viewers of this view. */ - private val viewers = mutableMapOf() + private val viewers = mutableMapOf() /** * Configuration for this view. From e11d8adc1a1464f78b255ec2a0326cdbf0344ac6 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 00:30:01 +0100 Subject: [PATCH 28/92] refactor: update component slot handling to use area boundaries for improved rendering logic --- .../bukkit/api/gui/area/ComponentArea.kt | 18 ++++++++++++++++++ .../bukkit/api/gui/view/AbstractGuiView.kt | 19 ++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) 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 index b0abc5d1..c7c79f5d 100644 --- 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 @@ -33,4 +33,22 @@ interface ComponentArea { * 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(): Slot? { + return slots().minByOrNull { 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? { + return slots().maxByOrNull { it.index } + } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index cf8825c1..6e8ce367 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -51,7 +51,7 @@ open class AbstractGuiView : GuiView() { // Get highest priority component val component = componentsAtSlot.first() val context = createViewContext(player) - + // Check if it's a container component val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { @@ -63,7 +63,7 @@ open class AbstractGuiView : GuiView() { } } else { // Regular component - only render at its start slot - if (slot == component.startSlot) { + if (slot == component.area.first()) { val guiItem = component.render(context) if (guiItem != null && slot.index < inventory.size) { inventory.setItem(slot.index, guiItem.toItemStack()) @@ -129,7 +129,7 @@ open class AbstractGuiView : GuiView() { // Clear inventory first inventory.clear() - + // Render all slots, respecting priority val allSlots = (0 until inventory.size).map { Slot.of(it) } allSlots.forEach { slot -> @@ -138,7 +138,7 @@ open class AbstractGuiView : GuiView() { // Get highest priority component val component = componentsAtSlot.first() val context = createViewContext(player) - + // Check if it's a container component val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { @@ -150,7 +150,7 @@ open class AbstractGuiView : GuiView() { } } else { // Regular component - only render at its start slot - if (slot == component.startSlot) { + if (slot == component.area.first()) { val guiItem = component.render(context) if (guiItem != null && slot.index < inventory.size) { inventory.setItem(slot.index, guiItem.toItemStack()) @@ -167,7 +167,7 @@ open class AbstractGuiView : GuiView() { private fun refreshComponentSlots(player: Player, component: Component) { val inventory = inventories[player.uniqueId] ?: return val context = createViewContext(player) - + // Get all slots this component occupies val slotsToRender = component.renderSlots(context) if (slotsToRender.isNotEmpty()) { @@ -181,8 +181,9 @@ open class AbstractGuiView : GuiView() { } } else { // Regular component - refresh its start slot - val slot = component.startSlot + val slot = component.area.first() ?: return val highestPriorityComponent = findComponentsBySlot(slot).firstOrNull() + if (highestPriorityComponent == component) { val guiItem = component.render(context) if (guiItem != null && slot.index < inventory.size) { @@ -201,10 +202,10 @@ open class AbstractGuiView : GuiView() { } val slot = Slot.of(event.slot) - + // Find all components at this slot, sorted by priority (highest first) val componentsAtSlot = findComponentsBySlot(slot) - + // Only the highest priority component handles the click val component = componentsAtSlot.firstOrNull() From b175ee381ea5b1d824c985850b38af9ec8968833 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:32:47 +0000 Subject: [PATCH 29/92] Move priority from ComponentArea to Component - Removed priority property from ComponentArea interface - Added priority as abstract property to Component base class - Updated all area implementations (SingleSlotArea, CuboidArea, CircularArea) to remove priority - Updated all component implementations to include priority property - Priority is now part of component, not area - areas define shape, components define priority Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../slne/surf/surfapi/bukkit/api/gui/area/CircularArea.kt | 4 +--- .../slne/surf/surfapi/bukkit/api/gui/area/ComponentArea.kt | 7 ------- .../slne/surf/surfapi/bukkit/api/gui/area/CuboidArea.kt | 4 +--- .../surf/surfapi/bukkit/api/gui/area/SingleSlotArea.kt | 4 +--- .../surf/surfapi/bukkit/api/gui/component/Component.kt | 5 ++--- .../surfapi/bukkit/api/gui/component/ContainerComponent.kt | 3 ++- .../surfapi/bukkit/api/gui/component/DynamicComponent.kt | 4 ++-- .../surf/surfapi/bukkit/api/gui/component/ItemComponent.kt | 4 ++-- .../bukkit/api/gui/component/PaginationComponent.kt | 4 ++-- 9 files changed, 13 insertions(+), 26 deletions(-) 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 index 4728bcf1..747024df 100644 --- 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 @@ -1,7 +1,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.area import dev.slne.surf.surfapi.bukkit.api.gui.Slot -import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority import kotlin.math.ceil import kotlin.math.sqrt @@ -11,8 +10,7 @@ import kotlin.math.sqrt */ data class CircularArea( val center: Slot, - val radius: Double, - override val priority: ComponentPriority = ComponentPriority.NORMAL + val radius: Double ) : ComponentArea { override fun slots(): Set { val slots = mutableSetOf() 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 index c7c79f5d..08dfc863 100644 --- 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 @@ -1,19 +1,12 @@ package dev.slne.surf.surfapi.bukkit.api.gui.area import dev.slne.surf.surfapi.bukkit.api.gui.Slot -import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority /** * Defines the area that a component occupies in a GUI. * Components can have different shaped areas (cuboid, circular, custom). */ interface ComponentArea { - /** - * Priority for rendering and click handling when components overlap. - * Higher priority components are rendered on top and handle clicks first. - */ - val priority: ComponentPriority - /** * Get all slots that are part of this area. */ 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 index ec58018b..c1ca123e 100644 --- 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 @@ -1,7 +1,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.area import dev.slne.surf.surfapi.bukkit.api.gui.Slot -import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority /** * A rectangular (cuboid) area defined by start and end slots. @@ -9,8 +8,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority */ data class CuboidArea( val startSlot: Slot, - val endSlot: Slot, - override val priority: ComponentPriority = ComponentPriority.NORMAL + val endSlot: Slot ) : ComponentArea { override fun slots(): Set { val slots = mutableSetOf() 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 index 22e03c37..6f78b94f 100644 --- 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 @@ -1,15 +1,13 @@ package dev.slne.surf.surfapi.bukkit.api.gui.area import dev.slne.surf.surfapi.bukkit.api.gui.Slot -import dev.slne.surf.surfapi.bukkit.api.gui.component.ComponentPriority /** * An area consisting of a single slot. * Used for simple components like buttons or single items. */ data class SingleSlotArea( - val slot: Slot, - override val priority: ComponentPriority = ComponentPriority.NORMAL + val slot: Slot ) : ComponentArea { override fun slots(): Set = setOf(slot) 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 index a9d4cea7..972f828a 100644 --- 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 @@ -24,10 +24,9 @@ abstract class Component { /** * Priority for handling clicks and rendering when components overlap. - * Delegates to the area's priority. + * Higher priority components are rendered on top and handle clicks first. */ - val priority: ComponentPriority - get() = area.priority + abstract val priority: ComponentPriority /** * Width of the component's bounding box. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt index 789e0703..5aae50b0 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -11,7 +11,8 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext * The area can be any shape (cuboid, circular, custom). */ abstract class ContainerComponent( - override val area: ComponentArea + override val area: ComponentArea, + override val priority: ComponentPriority = ComponentPriority.NORMAL ) : Component() { /** * Render multiple items at their respective slots. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index 1fc35a64..89b9b154 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -13,10 +13,10 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext open class DynamicComponent( slot: Slot, private val renderer: (ViewContext) -> GuiItem?, - priority: ComponentPriority = ComponentPriority.NORMAL, + override val priority: ComponentPriority = ComponentPriority.NORMAL, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override val area: ComponentArea = SingleSlotArea(slot, priority) + override val area: ComponentArea = SingleSlotArea(slot) override fun render(context: ViewContext): GuiItem? = renderer(context) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index 3b37ee9a..e90f14d5 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -13,10 +13,10 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.ViewContext open class ItemComponent( slot: Slot, private val item: GuiItem, - priority: ComponentPriority = ComponentPriority.NORMAL, + override val priority: ComponentPriority = ComponentPriority.NORMAL, private val clickHandler: (ClickContext.() -> Unit)? = null ) : Component() { - override val area: ComponentArea = SingleSlotArea(slot, priority) + override val area: ComponentArea = SingleSlotArea(slot) override fun render(context: ViewContext): GuiItem = item diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 0df6f893..78642f37 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -24,9 +24,9 @@ class PaginationComponent( endSlot: Slot, private val items: () -> List, private val itemRenderer: (T, ViewContext) -> GuiItem?, - priority: ComponentPriority = ComponentPriority.NORMAL, + override val priority: ComponentPriority = ComponentPriority.NORMAL, private val onItemClick: ((T, ClickContext) -> Unit)? = null -) : ContainerComponent(CuboidArea(startSlot, endSlot, priority)) { +) : ContainerComponent(CuboidArea(startSlot, endSlot), priority) { private val currentPages = mutableMapOf() /** From f825d51a60e5f6d2ed3a9e9bed5e0c79aec6bc21 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 00:41:05 +0100 Subject: [PATCH 30/92] refactor: enhance slot usage in CounterGuiView for improved readability and maintainability --- .../slne/surf/surfapi/bukkit/api/gui/Slot.kt | 16 +- .../surfapi/bukkit/test/gui/CounterGuiView.kt | 217 +++++++++--------- 2 files changed, 121 insertions(+), 112 deletions(-) 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 index 4bc14e49..f41531cc 100644 --- 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 @@ -5,40 +5,42 @@ package dev.slne.surf.surfapi.bukkit.api.gui * 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-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 index 0f170385..efe66bc9 100644 --- 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 @@ -10,6 +10,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.component.Component import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext 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.AbstractGuiView @@ -36,142 +37,148 @@ object CounterGuiView : AbstractGuiView() { override fun onFirstRender(context: RenderContext) { // Counter display at top center - context.slot(dynamicComponent( - Slot.at(4, 0), - renderer = { ctx -> - val count = runBlocking { counterProp.get() } ?: 0 + 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 + }) - GuiItem.of(ItemStack(Material.PAPER) { + // Increment +1 button + context.slot( + component( + Slot.at(2, 1), + GuiItem.of(ItemStack(Material.LIME_CONCRETE) { displayName { - info("Count: $count") + success("+1") } - }) - } - ) { - 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 - buildLore { - line { - gray("Click to increment by 1") + counterProp.set(current + 1) + counterDisplayRef.update() } } }) - ) { - 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") - } + context.slot( + component( + Slot.at(3, 1), + GuiItem.of(ItemStack(Material.GREEN_CONCRETE) { + displayName { + success("+10") + } - buildLore { - line { - gray("Click to increment by 10") + buildLore { + line { + gray("Click to increment by 10") + } } - } - }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 - counterProp.set(current + 10) - counterDisplayRef.update() + 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") - } + context.slot( + component( + Slot.at(5, 1), + GuiItem.of(ItemStack(Material.PINK_CONCRETE) { + displayName { + error("-1") + } - buildLore { - line { - gray("Click to decrement by 1") + buildLore { + line { + gray("Click to decrement by 1") + } } - } - }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 - counterProp.set(current - 1) - counterDisplayRef.update() + 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") - } + context.slot( + component( + Slot.at(6, 1), + GuiItem.of(ItemStack(Material.RED_CONCRETE) { + displayName { + error("-10") + } - buildLore { - line { - gray("Click to decrement by 10") + buildLore { + line { + gray("Click to decrement by 10") + } } - } - }) - ) { - onClick = { - plugin.launch { - val current = counterProp.get() ?: 0 + }) + ) { + onClick = { + plugin.launch { + val current = counterProp.get() ?: 0 - counterProp.set(current - 10) - counterDisplayRef.update() + counterProp.set(current - 10) + counterDisplayRef.update() + } } - } - }) + }) // Reset button - context.slot(component( - Slot.at(4, 2), - GuiItem.of(ItemStack(Material.BARRIER) { - displayName { - error("Reset Counter") - } + 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") + buildLore { + line { + gray("Click to reset the counter to 0") + } + } + }) + ) { + onClick = { + plugin.launch { + counterProp.set(0) + counterDisplayRef.update() } } }) - ) { - onClick = { - plugin.launch { - counterProp.set(0) - counterDisplayRef.update() - } - } - }) } } From 94ec968e40d72376d2931b332299bd03ec9c4269 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 00:45:37 +0100 Subject: [PATCH 31/92] refactor: optimize shop items initialization and enhance slot usage in PaginatedShopGuiView --- .../bukkit/test/gui/PaginatedShopGuiView.kt | 150 ++++++++---------- 1 file changed, 64 insertions(+), 86 deletions(-) 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 index 56557f15..f13cdd15 100644 --- 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 @@ -10,6 +10,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.component.PaginationComponent import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext 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.ViewerProp import dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView @@ -28,36 +29,9 @@ object PaginatedShopGuiView : AbstractGuiView() { private val coinsDisplayRef = Ref() // Shop items - private val shopItems = listOf( - ShopItem(Material.DIAMOND_SWORD, "Diamond Sword", 100), - ShopItem(Material.DIAMOND_PICKAXE, "Diamond Pickaxe", 80), - ShopItem(Material.DIAMOND_AXE, "Diamond Axe", 75), - ShopItem(Material.DIAMOND_SHOVEL, "Diamond Shovel", 70), - ShopItem(Material.DIAMOND_HOE, "Diamond Hoe", 65), - ShopItem(Material.IRON_SWORD, "Iron Sword", 50), - ShopItem(Material.IRON_PICKAXE, "Iron Pickaxe", 40), - ShopItem(Material.IRON_AXE, "Iron Axe", 35), - ShopItem(Material.IRON_SHOVEL, "Iron Shovel", 30), - ShopItem(Material.IRON_HOE, "Iron Hoe", 25), - ShopItem(Material.GOLDEN_SWORD, "Golden Sword", 80), - ShopItem(Material.GOLDEN_PICKAXE, "Golden Pickaxe", 70), - ShopItem(Material.GOLDEN_AXE, "Golden Axe", 65), - ShopItem(Material.GOLDEN_SHOVEL, "Golden Shovel", 60), - ShopItem(Material.GOLDEN_HOE, "Golden Hoe", 55), - ShopItem(Material.BOW, "Bow", 50), - ShopItem(Material.CROSSBOW, "Crossbow", 75), - ShopItem(Material.SHIELD, "Shield", 45), - ShopItem(Material.FISHING_ROD, "Fishing Rod", 30), - ShopItem(Material.SHEARS, "Shears", 20), - ShopItem(Material.FLINT_AND_STEEL, "Flint and Steel", 15), - ShopItem(Material.COMPASS, "Compass", 25), - ShopItem(Material.CLOCK, "Clock", 25), - ShopItem(Material.SPYGLASS, "Spyglass", 40), - ShopItem(Material.LEAD, "Lead", 20), - ShopItem(Material.NAME_TAG, "Name Tag", 50), - ShopItem(Material.SADDLE, "Saddle", 60), - ShopItem(Material.ELYTRA, "Elytra", 500) - ) + private val shopItems = Material.entries.filter { + it.isItem && !it.isAir + }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled() // Pagination component - 4 rows of 7 items (columns 1-7, rows 1-4) private val paginationComponent = PaginationComponent( @@ -109,79 +83,83 @@ object PaginatedShopGuiView : AbstractGuiView() { override fun onFirstRender(context: RenderContext) { // Coins display at top center - context.slot(dynamicComponent( - Slot.at(4, 0), - renderer = { ctx -> - val coins = coinsProp.get(ctx.player) ?: 0 + 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 - }) + GuiItem.of(ItemStack(Material.GOLD_INGOT) { + displayName { + gold("Coins: ") + yellow("$coins") + } + }) + } + ) { + ref = coinsDisplayRef + }) // Render pagination component context.slot(paginationComponent) // Previous page button - context.slot(component( - Slot.at(2, 5), - GuiItem.of(ItemStack(Material.ARROW) { - displayName { - info("Previous Page") - } - buildLore { - line { - gray("Click to go to the previous page") + context.slot( + component( + Slot.at(2, 5), + GuiItem.of(ItemStack(Material.ARROW) { + displayName { + info("Previous Page") + } + buildLore { + line { + gray("Click to go to the previous page") + } } + }) + ) { + onClick = { + paginationComponent.previousPage(player) + view.update() } }) - ) { - onClick = { - paginationComponent.previousPage(player) - view.update() - } - }) // Page indicator - context.slot(dynamicComponent( - Slot.at(4, 5), - renderer = { ctx -> - val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 - val totalPages = paginationComponent.getTotalPages() + context.slot( + dynamicComponent( + Slot.at(4, 5), + renderer = { ctx -> + val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 + val totalPages = paginationComponent.getTotalPages() - GuiItem.of(ItemStack(Material.PAPER) { - displayName { - info("Page $currentPage / $totalPages") - } - }) - } - )) + GuiItem.of(ItemStack(Material.PAPER) { + displayName { + info("Page $currentPage / $totalPages") + } + }) + } + )) // Next page button - context.slot(component( - Slot.at(6, 5), - GuiItem.of(ItemStack(Material.ARROW) { - displayName { - info("Next Page") - } - buildLore { - line { - gray("Click to go to the next page") + context.slot( + component( + Slot.at(6, 5), + GuiItem.of(ItemStack(Material.ARROW) { + displayName { + info("Next Page") } + buildLore { + line { + gray("Click to go to the next page") + } + } + }) + ) { + onClick = { + paginationComponent.nextPage(player) + view.update() } }) - ) { - onClick = { - paginationComponent.nextPage(player) - view.update() - } - }) } private data class ShopItem( From d557cc8fbe5d7db38f1ec5b8ce85231b84c76b33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:54:09 +0000 Subject: [PATCH 32/92] Fix refs and remove unused children system - Changed Ref.set() from internal to public for better accessibility - Removed unused addChild/removeChild methods from Component - Removed unused children tracking (_children, children property) - Removed unused parent property - Removed getAllProps() method (was for parent prop inheritance) - Removed updateWithChildren() method - Simplified Component class by removing unused features - Refs now work properly with ref.current accessible after creation Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 47 ------------------- .../surf/surfapi/bukkit/api/gui/ref/Ref.kt | 2 +- 2 files changed, 1 insertion(+), 48 deletions(-) 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 index 972f828a..5030c8b0 100644 --- 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 @@ -48,18 +48,6 @@ abstract class Component { */ fun contains(slot: Slot): Boolean = area.contains(slot) - /** - * The parent component, if any. - */ - var parent: Component? = null - internal set - - /** - * The children of this component. - */ - private val _children = mutableListOf() - val children: List get() = _children.toList() - /** * The view this component belongs to. */ @@ -104,45 +92,10 @@ abstract class Component { */ 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(): Map> { - val allProps = mutableMapOf>() - parent?.getAllProps()?.let { allProps.putAll(it) } - allProps.putAll(props) - return allProps - } - /** * Trigger an update of this component. */ fun update() { view.updateComponent(this) } - - /** - * Trigger an update of this component and all children. - */ - fun updateWithChildren() { - update() - children.forEach { it.updateWithChildren() } - } } 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 index 0368efb5..37879614 100644 --- 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 @@ -19,7 +19,7 @@ class Ref { /** * Sets the component reference. */ - internal fun set(component: T?) { + fun set(component: T?) { reference.set(component) } From b5c3aa396cb4e454308da803ed31120c585293bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:00:16 +0000 Subject: [PATCH 33/92] Restore child/parent system and investigate ref.current null issue Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) 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 index 5030c8b0..972f828a 100644 --- 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 @@ -48,6 +48,18 @@ abstract class Component { */ fun contains(slot: Slot): Boolean = area.contains(slot) + /** + * The parent component, if any. + */ + var parent: Component? = null + internal set + + /** + * The children of this component. + */ + private val _children = mutableListOf() + val children: List get() = _children.toList() + /** * The view this component belongs to. */ @@ -92,10 +104,45 @@ abstract class Component { */ 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(): Map> { + val allProps = mutableMapOf>() + parent?.getAllProps()?.let { allProps.putAll(it) } + allProps.putAll(props) + return allProps + } + /** * Trigger an update of this component. */ fun update() { view.updateComponent(this) } + + /** + * Trigger an update of this component and all children. + */ + fun updateWithChildren() { + update() + children.forEach { it.updateWithChildren() } + } } From ab476e63b18c7edb3b808cb9b5093af9de901d34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:00:48 +0000 Subject: [PATCH 34/92] Add debug logging to Ref to trace ref.current null issue - Added println statements in Ref.set() and Ref.current getter - Logs when component is set and when current is accessed - Shows component hashCode to verify instance identity - Use this to diagnose why ref.current is null during clicks - Check console output when running /surfapitest gui counter Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 index 37879614..348b9562 100644 --- 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 @@ -14,13 +14,19 @@ class Ref { * Gets the current component reference. */ val current: T? - get() = reference.get() + get() { + val comp = reference.get() + println("[Ref] Getting current: $comp (hashCode: ${comp?.hashCode()})") + return comp + } /** * Sets the component reference. */ fun set(component: T?) { + println("[Ref] Setting component: $component (hashCode: ${component.hashCode()})") reference.set(component) + println("[Ref] After set, current is: $current (hashCode: ${current?.hashCode()})") } /** From 827521321d38ff820e12a63f9cf10d7b4f5e5a56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:08:38 +0000 Subject: [PATCH 35/92] Remove debug logging - refs are working correctly per console logs Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 index 348b9562..37879614 100644 --- 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 @@ -14,19 +14,13 @@ class Ref { * Gets the current component reference. */ val current: T? - get() { - val comp = reference.get() - println("[Ref] Getting current: $comp (hashCode: ${comp?.hashCode()})") - return comp - } + get() = reference.get() /** * Sets the component reference. */ fun set(component: T?) { - println("[Ref] Setting component: $component (hashCode: ${component.hashCode()})") reference.set(component) - println("[Ref] After set, current is: $current (hashCode: ${current?.hashCode()})") } /** From 4e911c1d4077e5058972092a33b0f50bf61d9f1c Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 01:26:15 +0100 Subject: [PATCH 36/92] refactor: simplify attachedRef assignment in ComponentDsl for improved type handling --- .../dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e08d65ed..8df9bef3 100644 --- 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 @@ -57,7 +57,7 @@ class ComponentBuilder { } }.also { component -> ref?.set(component) - component.attachedRef = ref as? Ref + component.attachedRef = ref } } } From 9e0f75320194f177250e3ce75e2584c1c77a152b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:26:53 +0000 Subject: [PATCH 37/92] Fix onUpdate to actually refresh component items in inventory - Made updateComponent() call refreshComponentSlotsInternal() after onUpdate() - Added abstract refreshComponentSlotsInternal() method to GuiView - Implemented refreshComponentSlotsInternal() in AbstractGuiView - Now when ref.update() or component.update() is called, the items are actually re-rendered - Fixed area.first() to use area.slots().firstOrNull() for proper slot access - This resolves the issue where onUpdate lifecycle ran but items didn't refresh Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 11 +++++++++-- .../slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt | 10 ++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 6e8ce367..56b1b4dd 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -164,7 +164,7 @@ open class AbstractGuiView : GuiView() { /** * Refresh all slots occupied by a specific component. */ - private fun refreshComponentSlots(player: Player, component: Component) { + internal fun refreshComponentSlots(player: Player, component: Component) { val inventory = inventories[player.uniqueId] ?: return val context = createViewContext(player) @@ -181,7 +181,7 @@ open class AbstractGuiView : GuiView() { } } else { // Regular component - refresh its start slot - val slot = component.area.first() ?: return + val slot = component.area.slots().firstOrNull() ?: return val highestPriorityComponent = findComponentsBySlot(slot).firstOrNull() if (highestPriorityComponent == component) { @@ -192,6 +192,13 @@ open class AbstractGuiView : GuiView() { } } } + + /** + * Implementation of refreshComponentSlotsInternal for GuiView. + */ + override fun refreshComponentSlotsInternal(player: Player, component: Component) { + refreshComponentSlots(player, component) + } /** * Handle click event. 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 index 9cfc7772..30d372b4 100644 --- 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 @@ -138,14 +138,24 @@ abstract class GuiView { /** * Update a specific component. + * Calls the onUpdate lifecycle hook and refreshes the component's slots in the inventory. */ internal fun updateComponent(component: Component) { viewers.values.forEach { player -> val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) component.onUpdate(lifecycleContext) + + // Refresh the component's slots to update the visual display + refreshComponentSlotsInternal(player, component) } } + + /** + * Internal method to refresh component slots. + * Must be implemented by concrete view implementations. + */ + protected abstract fun refreshComponentSlotsInternal(player: Player, component: Component) /** * Add a component to the view. From 7d34e2f65ca552d6a9669c95069d97bf65ed7b52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:35:20 +0000 Subject: [PATCH 38/92] Refactor PaginationComponent with built-in navigation buttons as children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Navigation buttons (previous, page indicator, next) are now children of the component - Minimum dimensions enforced: width ≥ 3, height ≥ 2 - Items use height - 1 rows (last row reserved for buttons) - Buttons automatically centered in last row by default - User can optionally specify custom button slots - Removed companion object helper functions (no longer needed) - Updated PaginatedShopGuiView to use new simplified API - Navigation buttons have HIGH priority to ensure they render on top Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../api/gui/component/PaginationComponent.kt | 190 +++++++++++------- .../bukkit/test/gui/PaginatedShopGuiView.kt | 69 +------ 2 files changed, 124 insertions(+), 135 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 78642f37..1ceeb8e2 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -16,8 +16,11 @@ import org.bukkit.entity.Player import java.util.* /** - * Component for paginated content. + * 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, @@ -25,20 +28,113 @@ class PaginationComponent( private val items: () -> List, private val itemRenderer: (T, ViewContext) -> GuiItem?, override val priority: ComponentPriority = ComponentPriority.NORMAL, - private val onItemClick: ((T, ClickContext) -> Unit)? = null + private val onItemClick: ((T, ClickContext) -> Unit)? = null, + private val previousButtonSlot: Slot? = null, + private val nextButtonSlot: Slot? = null, + private val pageIndicatorSlot: Slot? = null ) : ContainerComponent(CuboidArea(startSlot, endSlot), priority) { + + init { + val cuboidArea = area as CuboidArea + require(cuboidArea.width >= 3) { "PaginationComponent width must be at least 3 (current: ${cuboidArea.width})" } + require(cuboidArea.height >= 2) { "PaginationComponent height must be at least 2 (current: ${cuboidArea.height})" } + } private val currentPages = mutableMapOf() + /** + * Start slot of the area (convenience accessor). + */ + private val startSlot: Slot + get() = (area as CuboidArea).startSlot + /** * Calculate page size from the area. + * Items use height - 1 rows (last row is for buttons). */ - private val pageSize: Int = width * height + private val itemsHeight: Int = height - 1 + private val pageSize: Int = width * itemsHeight /** - * Start slot of the area (convenience accessor). + * Calculated button slots (centered in last row by default). */ - private val startSlot: Slot - get() = (area as CuboidArea).startSlot + 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) + } + + init { + // Create navigation button children + addChild(createPreviousButtonComponent()) + addChild(createPageIndicatorComponent()) + addChild(createNextButtonComponent()) + } + + 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") } + } + }), + priority = ComponentPriority.HIGH + ) { + onClick = { + if (hasPreviousPage(player)) { + previousPage(player) + view.update() + } + } + } + + 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") } + } + }) + }, + priority = ComponentPriority.HIGH + ) + + 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") } + } + }), + priority = ComponentPriority.HIGH + ) { + onClick = { + if (hasNextPage(player)) { + nextPage(player) + view.update() + } + } + } /** * Get the current page for a viewer. @@ -131,11 +227,15 @@ class PaginationComponent( val guiItem = itemRenderer(item, context) if (guiItem != null) { - // Calculate slot position within the area + // Calculate slot position within the items area (height - 1) val row = index / width val col = index % width - val slot = Slot.at(startSlot.column + col, startSlot.row + row) - renderedSlots[slot] = guiItem + + // 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 + } } } @@ -143,12 +243,19 @@ class PaginationComponent( } 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 relativeRow = context.slot.row - startSlot.row val index = relativeRow * width + relativeCol if (index in pageItems.indices) { @@ -157,67 +264,4 @@ class PaginationComponent( } } } - - companion object { - val NEXT_PAGE_ITEM = GuiItem(ItemStack(Material.ARROW) { - displayName { - info("Next Page") - } - - buildLore { - line { - gray("Click to go to the next page") - } - } - }) - - fun buildNextPageComponent(paginationComponent: PaginationComponent<*>, slot: Slot) = - component(slot, NEXT_PAGE_ITEM) { - onClick = { - paginationComponent.nextPage(player) - view.update() - } - } - - val PREVIOUS_PAGE_ITEM = GuiItem(ItemStack(Material.ARROW) { - displayName { - info("Previous Page") - } - - buildLore { - line { - gray("Click to go to the previous page") - } - } - }) - - fun buildPreviousPageComponent(paginationComponent: PaginationComponent<*>, slot: Slot) = - component(slot, PREVIOUS_PAGE_ITEM) { - onClick = { - paginationComponent.previousPage(player) - view.update() - } - } - - fun buildPageIndicatorItem(pageNumber: Int, maxPages: Int) = - GuiItem(ItemStack(Material.PAPER) { - displayName { - info("Page $pageNumber") - } - - buildLore { - line { - gray("Page $pageNumber of $maxPages") - } - } - }) - - fun buildPageIndicatorComponent(paginationComponent: PaginationComponent<*>, slot: Slot) = - dynamicComponent(slot, renderer = { ctx -> - val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 - val totalPages = paginationComponent.getTotalPages() - - buildPageIndicatorItem(currentPage, totalPages) - }) - } } 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 index f13cdd15..3cc198a1 100644 --- 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 @@ -33,10 +33,12 @@ object PaginatedShopGuiView : AbstractGuiView() { it.isItem && !it.isAir }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled() - // Pagination component - 4 rows of 7 items (columns 1-7, rows 1-4) + // 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 1, Row 1 - endSlot = Slot.at(7, 4), // Column 7, Row 4 (28 slots total) + startSlot = Slot.at(0, 1), // Column 0, Row 1 + endSlot = Slot.at(8, 4), // Column 8, Row 4 (9 cols x 4 rows = 36 slots total) items = { shopItems }, itemRenderer = { item, ctx -> GuiItem.of(ItemStack(item.material) { @@ -73,6 +75,7 @@ object PaginatedShopGuiView : AbstractGuiView() { } } } + // Navigation buttons will be automatically created and centered in the last row ) override fun onInit(config: ViewConfig) { @@ -100,66 +103,8 @@ object PaginatedShopGuiView : AbstractGuiView() { ref = coinsDisplayRef }) - // Render pagination component + // Render pagination component (navigation buttons are now built-in as children) context.slot(paginationComponent) - - // Previous page button - context.slot( - component( - Slot.at(2, 5), - GuiItem.of(ItemStack(Material.ARROW) { - displayName { - info("Previous Page") - } - buildLore { - line { - gray("Click to go to the previous page") - } - } - }) - ) { - onClick = { - paginationComponent.previousPage(player) - view.update() - } - }) - - // Page indicator - context.slot( - dynamicComponent( - Slot.at(4, 5), - renderer = { ctx -> - val currentPage = paginationComponent.getCurrentPage(ctx.player) + 1 - val totalPages = paginationComponent.getTotalPages() - - GuiItem.of(ItemStack(Material.PAPER) { - displayName { - info("Page $currentPage / $totalPages") - } - }) - } - )) - - // Next page button - context.slot( - component( - Slot.at(6, 5), - GuiItem.of(ItemStack(Material.ARROW) { - displayName { - info("Next Page") - } - buildLore { - line { - gray("Click to go to the next page") - } - } - }) - ) { - onClick = { - paginationComponent.nextPage(player) - view.update() - } - }) } private data class ShopItem( From ee1d0526986915ee8ab510a00159f26c27cbefc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:52:27 +0000 Subject: [PATCH 39/92] Fix ExceptionInInitializerError in PaginatedShopGuiView by making shopItems lazy - Changed shopItems from eager initialization to lazy initialization - Prevents static initialization errors when Material.entries is accessed too early - Fixes /surfapitest gui shop command crash with ExceptionInInitializerError Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/test/gui/PaginatedShopGuiView.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 3cc198a1..5822eaba 100644 --- 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 @@ -28,10 +28,12 @@ object PaginatedShopGuiView : AbstractGuiView() { private val coinsProp = ViewerProp.Mutable("coins", 1000) private val coinsDisplayRef = Ref() - // Shop items - private val shopItems = Material.entries.filter { - it.isItem && !it.isAir - }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled() + // Shop items - lazy to avoid initialization issues + private val shopItems by lazy { + Material.entries.filter { + it.isItem && !it.isAir + }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled() + } // Pagination component - includes built-in navigation buttons // The component will use 4 rows: 3 for items + 1 for buttons From 74cd4d54a43a8403e81edbbeb2f45ce2a5a26ff1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:55:07 +0000 Subject: [PATCH 40/92] Fix view initialization order issue in addChild - check if view is initialized before setting - Modified addChild() to check if parent's view is initialized before setting child.view - Added custom setter to view property that propagates to existing children when set - Fixes ExceptionInInitializerError when PaginationComponent calls addChild in init block - Children now properly get their view set when parent's view is assigned by framework Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/component/Component.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 index 972f828a..1ed39408 100644 --- 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 @@ -64,7 +64,15 @@ abstract class Component { * The view this component belongs to. */ lateinit var view: GuiView - internal set + internal set(value) { + field = value + // Propagate view to existing children + _children.forEach { child -> + if (!child::view.isInitialized) { + child.view = value + } + } + } /** * Update interval for this component, if any. @@ -109,7 +117,10 @@ abstract class Component { */ fun addChild(child: Component) { child.parent = this - child.view = view + // Only set view if parent's view is already initialized + if (::view.isInitialized) { + child.view = view + } _children.add(child) } From 6e4f795007303be60648196dc2a3b9cbbeeacbca Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 01:58:05 +0100 Subject: [PATCH 41/92] refactor: streamline slot handling and improve component initialization in PaginationComponent --- .../bukkit/api/gui/area/ComponentArea.kt | 14 ++-- .../api/gui/component/PaginationComponent.kt | 67 ++++++++++--------- .../bukkit/api/gui/dsl/ComponentDsl.kt | 11 ++- 3 files changed, 49 insertions(+), 43 deletions(-) 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 index 08dfc863..bd45e447 100644 --- 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 @@ -11,17 +11,17 @@ interface ComponentArea { * Get all slots that are part of this area. */ fun slots(): Set - + /** * 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. */ @@ -32,16 +32,12 @@ interface ComponentArea { * * @return The slot with the lowest index, or null if area is empty. */ - fun first(): Slot? { - return slots().minByOrNull { it.index } - } + 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? { - return slots().maxByOrNull { it.index } - } + 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/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 1ceeb8e2..c7d61b7c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -31,29 +31,29 @@ class PaginationComponent( private val onItemClick: ((T, ClickContext) -> Unit)? = null, private val previousButtonSlot: Slot? = null, private val nextButtonSlot: Slot? = null, - private val pageIndicatorSlot: Slot? = null -) : ContainerComponent(CuboidArea(startSlot, endSlot), priority) { - + private val pageIndicatorSlot: Slot? = null, + override val area: ComponentArea = CuboidArea(startSlot, endSlot) +) : ContainerComponent(area, priority) { init { - val cuboidArea = area as CuboidArea - require(cuboidArea.width >= 3) { "PaginationComponent width must be at least 3 (current: ${cuboidArea.width})" } - require(cuboidArea.height >= 2) { "PaginationComponent height must be at least 2 (current: ${cuboidArea.height})" } + 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})" } } + private val currentPages = mutableMapOf() - + /** * Start slot of the area (convenience accessor). */ private val startSlot: Slot - get() = (area as CuboidArea).startSlot - + 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). */ @@ -61,30 +61,32 @@ class PaginationComponent( 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) } - + init { - // Create navigation button children addChild(createPreviousButtonComponent()) addChild(createPageIndicatorComponent()) addChild(createNextButtonComponent()) } - + private fun createPreviousButtonComponent() = component( slot = calculatedPreviousButtonSlot, item = GuiItem(ItemStack(Material.ARROW) { @@ -96,18 +98,17 @@ class PaginationComponent( priority = ComponentPriority.HIGH ) { onClick = { - if (hasPreviousPage(player)) { - previousPage(player) - view.update() - } + previousPage(player) + this@PaginationComponent.update() } } - + 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 { @@ -117,7 +118,7 @@ class PaginationComponent( }, priority = ComponentPriority.HIGH ) - + private fun createNextButtonComponent() = component( slot = calculatedNextButtonSlot, item = GuiItem(ItemStack(Material.ARROW) { @@ -129,10 +130,8 @@ class PaginationComponent( priority = ComponentPriority.HIGH ) { onClick = { - if (hasNextPage(player)) { - nextPage(player) - view.update() - } + nextPage(player) + this@PaginationComponent.update() } } @@ -148,6 +147,7 @@ class PaginationComponent( */ fun getTotalPages(): Int { val allItems = items() + return (allItems.size + pageSize - 1) / pageSize } @@ -206,17 +206,17 @@ class PaginationComponent( /** * Set a specific page for a viewer. */ - fun setPage(viewerId: UUID, page: Int) { + fun setPage(viewer: Player, page: Int) { if (page in 0 until getTotalPages()) { - currentPages[viewerId] = page + currentPages[viewer.uniqueId] = page } } /** * Clear the page state for a viewer. */ - fun clearPage(viewerId: UUID) { - currentPages.remove(viewerId) + fun clearPage(viewer: Player) { + currentPages.remove(viewer.uniqueId) } override fun renderSlots(context: ViewContext): Map { @@ -230,10 +230,11 @@ class PaginationComponent( // 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 } } @@ -245,15 +246,15 @@ class PaginationComponent( 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 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 index 8df9bef3..e108e379 100644 --- 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 @@ -68,9 +68,11 @@ class ComponentBuilder { fun component( slot: Slot, item: GuiItem, + priority: ComponentPriority = ComponentPriority.NORMAL, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() + componentBuilder.priority = priority componentBuilder.builder() return componentBuilder.build(slot) { item } } @@ -81,9 +83,11 @@ fun component( fun dynamicComponent( slot: Slot, renderer: (ViewContext) -> GuiItem?, + priority: ComponentPriority = ComponentPriority.NORMAL, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() + componentBuilder.priority = priority componentBuilder.builder() return componentBuilder.build(slot, renderer) } @@ -171,7 +175,12 @@ fun RenderContext.slot(component: Component) { * DSL for rendering components in a view with item at a specific slot. */ @ComponentDsl -fun RenderContext.slot(slot: Slot, item: GuiItem, priority: ComponentPriority = ComponentPriority.NORMAL, onClick: (ClickContext.() -> Unit)? = null) { +fun RenderContext.slot( + slot: Slot, + item: GuiItem, + priority: ComponentPriority = ComponentPriority.NORMAL, + onClick: (ClickContext.() -> Unit)? = null +) { val component = ItemComponent(slot, item, priority, onClick) renderComponent(component) } From 05c70c034f354e30d67fb70223cbe58d84b2e946 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 01:59:45 +0100 Subject: [PATCH 42/92] refactor: simplify view assignment and update handling in Component class --- .../bukkit/api/gui/component/Component.kt | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) 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 index 1ed39408..e1c16289 100644 --- 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 @@ -21,109 +21,105 @@ abstract class Component { * 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 - + /** * The children of this component. */ private val _children = mutableListOf() val children: List get() = _children.toList() - + /** * The view this component belongs to. */ - lateinit var view: GuiView + var view: GuiView? = null internal set(value) { field = value // Propagate view to existing children _children.forEach { child -> - if (!child::view.isInitialized) { - child.view = value - } + child.view = value } } - + /** * Update interval for this component, if any. */ open val updateInterval: Duration? = null - + /** * Props accessible by this component. * Children can access parent props. */ protected open val props: Map> = emptyMap() - + /** * Ref attached to this component, if any. */ internal var attachedRef: Ref? = null - + /** * 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 - // Only set view if parent's view is already initialized - if (::view.isInitialized) { - child.view = view - } + child.view = view + _children.add(child) } - + /** * Remove a child component. */ @@ -131,7 +127,7 @@ abstract class Component { _children.remove(child) child.parent = null } - + /** * Get all props including parent props. */ @@ -141,14 +137,14 @@ abstract class Component { allProps.putAll(props) return allProps } - + /** * Trigger an update of this component. */ fun update() { - view.updateComponent(this) + view?.updateComponent(this) } - + /** * Trigger an update of this component and all children. */ From 416e197d53847f7d6b288310ec9d00570c8dc082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:02:16 +0000 Subject: [PATCH 43/92] Fix children components not being rendered - include them in findComponentsBySlot - Modified findComponentsBySlot() to recursively collect children components - Helper function collectComponents() traverses component tree depth-first - All components including nested children are now considered for rendering - Fixes navigation buttons in PaginationComponent not appearing - Children are properly rendered with their priority respected Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surf/surfapi/bukkit/api/gui/view/GuiView.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 index 30d372b4..968b4add 100644 --- 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 @@ -32,9 +32,24 @@ abstract class GuiView { /** * Find all components that contain the given slot, sorted by priority (highest first). + * Includes children recursively. */ fun findComponentsBySlot(slot: Slot): List { - return _components + val allComponents = mutableListOf() + + // 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 and sort by priority + return allComponents .filter { it.contains(slot) } .sortedByDescending { it.priority.value } } From 681af70a2d8f4379aca0c64024a039c344b7b2c0 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:02:26 +0100 Subject: [PATCH 44/92] refactor: adjust pagination component slot assignments for improved layout --- .../surf/surfapi/bukkit/test/gui/PaginatedShopGuiView.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 5822eaba..0bb84b01 100644 --- 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 @@ -8,7 +8,6 @@ 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.PaginationComponent import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext -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.ViewerProp @@ -39,8 +38,8 @@ object PaginatedShopGuiView : AbstractGuiView() { // 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(0, 1), // Column 0, Row 1 - endSlot = Slot.at(8, 4), // Column 8, Row 4 (9 cols x 4 rows = 36 slots total) + 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(ItemStack(item.material) { From 61fc99aaf3f16727ba14558b70cf2e8006ef688b Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:08:30 +0100 Subject: [PATCH 45/92] refactor: use component's priority for button components in PaginationComponent --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index c7d61b7c..7d4e6b0c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -95,7 +95,7 @@ class PaginationComponent( line { gray("Click to go to the previous page") } } }), - priority = ComponentPriority.HIGH + priority = this@PaginationComponent.priority ) { onClick = { previousPage(player) @@ -116,7 +116,7 @@ class PaginationComponent( } }) }, - priority = ComponentPriority.HIGH + priority = this@PaginationComponent.priority ) private fun createNextButtonComponent() = component( @@ -127,7 +127,7 @@ class PaginationComponent( line { gray("Click to go to the next page") } } }), - priority = ComponentPriority.HIGH + priority = this@PaginationComponent.priority ) { onClick = { nextPage(player) From f1219a8443085a5d77f7fbdf94f3f905d0d2bfa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:11:01 +0000 Subject: [PATCH 46/92] Fix page indicator not updating and add hidden/disabled features to components - Added ref to page indicator in PaginationComponent and update it when page changes - Added hidden property to Component (hides component from rendering completely) - Added disabled property to Component (renders but blocks onClick) - Added setHidden/setDisabled methods to update state - Modified findComponentsBySlot to filter out hidden components - Modified click handling to skip disabled components - Page indicator now updates to show current page when navigating - Components can be dynamically hidden/disabled and refreshed with update() Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 24 +++++++++++++++++++ .../api/gui/component/PaginationComponent.kt | 8 ++++++- .../bukkit/api/gui/view/AbstractGuiView.kt | 8 ++++--- .../surfapi/bukkit/api/gui/view/GuiView.kt | 4 ++-- 4 files changed, 38 insertions(+), 6 deletions(-) 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 index e1c16289..eeb1b283 100644 --- 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 @@ -77,6 +77,30 @@ abstract class Component { */ open val updateInterval: Duration? = null + /** + * Whether this component is hidden (not rendered at all). + */ + var hidden: Boolean = false + + /** + * Whether this component is disabled (rendered but onClick is blocked). + */ + var disabled: Boolean = false + + /** + * Set the hidden state of this component. + */ + fun setHidden(hidden: Boolean) { + this.hidden = hidden + } + + /** + * Set the disabled state of this component. + */ + fun setDisabled(disabled: Boolean) { + this.disabled = disabled + } + /** * Props accessible by this component. * Children can access parent props. diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 7d4e6b0c..8151278e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -40,6 +40,7 @@ class PaginationComponent( } private val currentPages = mutableMapOf() + private val pageIndicatorRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() /** * Start slot of the area (convenience accessor). @@ -117,7 +118,9 @@ class PaginationComponent( }) }, priority = this@PaginationComponent.priority - ) + ) { + ref = pageIndicatorRef + } private fun createNextButtonComponent() = component( slot = calculatedNextButtonSlot, @@ -189,6 +192,7 @@ class PaginationComponent( fun nextPage(viewer: Player) { if (hasNextPage(viewer)) { currentPages[viewer.uniqueId] = getCurrentPage(viewer) + 1 + pageIndicatorRef.update() } } @@ -200,6 +204,7 @@ class PaginationComponent( if (currentPage > 0) { currentPages[viewer.uniqueId] = currentPage - 1 + pageIndicatorRef.update() } } @@ -209,6 +214,7 @@ class PaginationComponent( fun setPage(viewer: Player, page: Int) { if (page in 0 until getTotalPages()) { currentPages[viewer.uniqueId] = page + pageIndicatorRef.update() } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 56b1b4dd..9863aedb 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -213,11 +213,13 @@ open class AbstractGuiView : GuiView() { // Find all components at this slot, sorted by priority (highest first) val componentsAtSlot = findComponentsBySlot(slot) - // Only the highest priority component handles the click + // Only the highest priority component handles the click, and only if not disabled val component = componentsAtSlot.firstOrNull() - val clickContext = AbstractClickContext(this, player, event, component) - component?.onClick(clickContext) + if (component != null && !component.disabled) { + val clickContext = AbstractClickContext(this, player, event, component) + component.onClick(clickContext) + } } override fun createViewContext(player: Player): ViewContext { 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 index 968b4add..fc7d9082 100644 --- 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 @@ -48,9 +48,9 @@ abstract class GuiView { // Collect all components including children _components.forEach { collectComponents(it) } - // Filter by slot and sort by priority + // Filter by slot, exclude hidden components, and sort by priority return allComponents - .filter { it.contains(slot) } + .filter { it.contains(slot) && !it.hidden } .sortedByDescending { it.priority.value } } From b25b929c0831ddb9e5c3bd176034440efff37f64 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:15:54 +0100 Subject: [PATCH 47/92] refactor: remove unused setHidden and setDisabled methods from Component class --- .../surfapi/bukkit/api/gui/component/Component.kt | 14 -------------- 1 file changed, 14 deletions(-) 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 index eeb1b283..878e5c18 100644 --- 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 @@ -87,20 +87,6 @@ abstract class Component { */ var disabled: Boolean = false - /** - * Set the hidden state of this component. - */ - fun setHidden(hidden: Boolean) { - this.hidden = hidden - } - - /** - * Set the disabled state of this component. - */ - fun setDisabled(disabled: Boolean) { - this.disabled = disabled - } - /** * Props accessible by this component. * Children can access parent props. From cc5e8b98bbb8a0ade068354a95d27092bbcd57d1 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:16:08 +0100 Subject: [PATCH 48/92] refactor: simplify page navigation logic in PaginationComponent --- .../bukkit/api/gui/component/PaginationComponent.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 8151278e..6073448e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -190,9 +190,10 @@ class PaginationComponent( * Go to the next page for a viewer. */ fun nextPage(viewer: Player) { + val currentPage = getCurrentPage(viewer) + if (hasNextPage(viewer)) { - currentPages[viewer.uniqueId] = getCurrentPage(viewer) + 1 - pageIndicatorRef.update() + setPage(viewer, currentPage + 1) } } @@ -202,9 +203,8 @@ class PaginationComponent( fun previousPage(viewer: Player) { val currentPage = getCurrentPage(viewer) - if (currentPage > 0) { - currentPages[viewer.uniqueId] = currentPage - 1 - pageIndicatorRef.update() + if (hasPreviousPage(viewer)) { + setPage(viewer, currentPage - 1) } } From 42349ecde767ba90ab24abeeca894bb74709fb2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:18:26 +0000 Subject: [PATCH 49/92] Disable and hide navigation buttons when no further pages available - Added refs to previous and next button components - Created updateNavigationButtonsState() method to update button states - Previous button is disabled and hidden when on first page - Next button is disabled and hidden when on last page - Button states update on page changes (nextPage, previousPage, setPage) - Button states initialized on renderSlots for each viewer Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../api/gui/component/PaginationComponent.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 6073448e..932e3e1c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -41,6 +41,8 @@ class PaginationComponent( private val currentPages = mutableMapOf() private val pageIndicatorRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() + private val previousButtonRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() + private val nextButtonRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() /** * Start slot of the area (convenience accessor). @@ -98,6 +100,7 @@ class PaginationComponent( }), priority = this@PaginationComponent.priority ) { + ref = previousButtonRef onClick = { previousPage(player) this@PaginationComponent.update() @@ -132,6 +135,7 @@ class PaginationComponent( }), priority = this@PaginationComponent.priority ) { + ref = nextButtonRef onClick = { nextPage(player) this@PaginationComponent.update() @@ -186,6 +190,28 @@ class PaginationComponent( 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) + + // Update previous button + previousButtonRef.current?.let { button -> + button.setDisabled(!hasPrev) + button.setHidden(!hasPrev) + button.update() + } + + // Update next button + nextButtonRef.current?.let { button -> + button.setDisabled(!hasNext) + button.setHidden(!hasNext) + button.update() + } + } + /** * Go to the next page for a viewer. */ @@ -194,6 +220,7 @@ class PaginationComponent( if (hasNextPage(viewer)) { setPage(viewer, currentPage + 1) + updateNavigationButtonsState(viewer) } } @@ -205,6 +232,7 @@ class PaginationComponent( if (hasPreviousPage(viewer)) { setPage(viewer, currentPage - 1) + updateNavigationButtonsState(viewer) } } @@ -215,6 +243,7 @@ class PaginationComponent( if (page in 0 until getTotalPages()) { currentPages[viewer.uniqueId] = page pageIndicatorRef.update() + updateNavigationButtonsState(viewer) } } @@ -226,6 +255,9 @@ class PaginationComponent( } override fun renderSlots(context: ViewContext): Map { + // Update navigation button states for this viewer + updateNavigationButtonsState(context.player) + val pageItems = getPageItems(context.player) val renderedSlots = mutableMapOf() From 5a8382090c8e177bd6c11611ade0fe8f75d6b941 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:23:02 +0000 Subject: [PATCH 50/92] Make component updates cascade to children automatically - Modified GuiView.updateComponent() to recursively update all children - Removed manual pageIndicatorRef.update() call from PaginationComponent.setPage() - Removed manual button.update() calls from updateNavigationButtonsState() - Parent component update now automatically updates all child components - Makes API more consistent and intuitive - no need to manually update child refs Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 6 +++--- .../dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 932e3e1c..4a4db0f0 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -201,14 +201,14 @@ class PaginationComponent( previousButtonRef.current?.let { button -> button.setDisabled(!hasPrev) button.setHidden(!hasPrev) - button.update() + // No need to call update() - parent update will cascade } // Update next button nextButtonRef.current?.let { button -> button.setDisabled(!hasNext) button.setHidden(!hasNext) - button.update() + // No need to call update() - parent update will cascade } } @@ -242,7 +242,7 @@ class PaginationComponent( fun setPage(viewer: Player, page: Int) { if (page in 0 until getTotalPages()) { currentPages[viewer.uniqueId] = page - pageIndicatorRef.update() + // No need to call pageIndicatorRef.update() - parent update will cascade to children updateNavigationButtonsState(viewer) } } 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 index fc7d9082..5d573a4d 100644 --- 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 @@ -164,6 +164,11 @@ abstract class GuiView { // Refresh the component's slots to update the visual display refreshComponentSlotsInternal(player, component) } + + // Recursively update all children + component.children.forEach { child -> + updateComponent(child) + } } /** From 279c9cd05908037badd8f9c9b4cd50ab57c919a4 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:23:07 +0100 Subject: [PATCH 51/92] refactor: update button state management in PaginationComponent --- .../api/gui/component/PaginationComponent.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 932e3e1c..4881e148 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -8,9 +8,11 @@ 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.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.ref.Ref import org.bukkit.Material import org.bukkit.entity.Player import java.util.* @@ -40,9 +42,9 @@ class PaginationComponent( } private val currentPages = mutableMapOf() - private val pageIndicatorRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() - private val previousButtonRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() - private val nextButtonRef = dev.slne.surf.surfapi.bukkit.api.gui.ref.Ref() + private val pageIndicatorRef = Ref() + private val previousButtonRef = Ref() + private val nextButtonRef = Ref() /** * Start slot of the area (convenience accessor). @@ -199,15 +201,17 @@ class PaginationComponent( // Update previous button previousButtonRef.current?.let { button -> - button.setDisabled(!hasPrev) - button.setHidden(!hasPrev) + button.disabled = !hasPrev + button.hidden = !hasPrev + button.update() } // Update next button nextButtonRef.current?.let { button -> - button.setDisabled(!hasNext) - button.setHidden(!hasNext) + button.disabled = !hasNext + button.hidden = !hasNext + button.update() } } @@ -242,8 +246,7 @@ class PaginationComponent( fun setPage(viewer: Player, page: Int) { if (page in 0 until getTotalPages()) { currentPages[viewer.uniqueId] = page - pageIndicatorRef.update() - updateNavigationButtonsState(viewer) + this@PaginationComponent.update() } } @@ -254,10 +257,15 @@ class PaginationComponent( currentPages.remove(viewer.uniqueId) } + override fun onUpdate(context: LifecycleContext) { + pageIndicatorRef.update() + updateNavigationButtonsState(context.player) + } + override fun renderSlots(context: ViewContext): Map { // Update navigation button states for this viewer updateNavigationButtonsState(context.player) - + val pageItems = getPageItems(context.player) val renderedSlots = mutableMapOf() From bc7f1090bfa480077de3b538737ce888bae98428 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:31:14 +0100 Subject: [PATCH 52/92] refactor: streamline button component management in PaginationComponent --- .../api/gui/component/PaginationComponent.kt | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 9988b6eb..45b9fc1e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -12,7 +12,6 @@ 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.ref.Ref import org.bukkit.Material import org.bukkit.entity.Player import java.util.* @@ -42,9 +41,6 @@ class PaginationComponent( } private val currentPages = mutableMapOf() - private val pageIndicatorRef = Ref() - private val previousButtonRef = Ref() - private val nextButtonRef = Ref() /** * Start slot of the area (convenience accessor). @@ -86,10 +82,14 @@ class PaginationComponent( Slot.at(centerX, lastRowY) } + private val previousButtonComponent = createPreviousButtonComponent() + private val nextButtonComponent = createNextButtonComponent() + private val pageIndicatorComponent = createPageIndicatorComponent() + init { - addChild(createPreviousButtonComponent()) - addChild(createPageIndicatorComponent()) - addChild(createNextButtonComponent()) + addChild(previousButtonComponent) + addChild(pageIndicatorComponent) + addChild(nextButtonComponent) } private fun createPreviousButtonComponent() = component( @@ -102,10 +102,8 @@ class PaginationComponent( }), priority = this@PaginationComponent.priority ) { - ref = previousButtonRef onClick = { previousPage(player) - this@PaginationComponent.update() } } @@ -123,9 +121,7 @@ class PaginationComponent( }) }, priority = this@PaginationComponent.priority - ) { - ref = pageIndicatorRef - } + ) private fun createNextButtonComponent() = component( slot = calculatedNextButtonSlot, @@ -137,10 +133,8 @@ class PaginationComponent( }), priority = this@PaginationComponent.priority ) { - ref = nextButtonRef onClick = { nextPage(player) - this@PaginationComponent.update() } } @@ -199,16 +193,14 @@ class PaginationComponent( val hasPrev = hasPreviousPage(viewer) val hasNext = hasNextPage(viewer) - // Update previous button - previousButtonRef.current?.let { button -> - button.disabled = !hasPrev - button.hidden = !hasPrev + previousButtonComponent.apply { + disabled = !hasPrev + hidden = !hasPrev } - // Update next button - nextButtonRef.current?.let { button -> - button.disabled = !hasNext - button.hidden = !hasNext + nextButtonComponent.apply { + disabled = !hasNext + hidden = !hasNext } } @@ -220,7 +212,7 @@ class PaginationComponent( if (hasNextPage(viewer)) { setPage(viewer, currentPage + 1) - updateNavigationButtonsState(viewer) + this@PaginationComponent.update() } } @@ -232,7 +224,7 @@ class PaginationComponent( if (hasPreviousPage(viewer)) { setPage(viewer, currentPage - 1) - updateNavigationButtonsState(viewer) + this@PaginationComponent.update() } } @@ -253,7 +245,6 @@ class PaginationComponent( } override fun onUpdate(context: LifecycleContext) { - pageIndicatorRef.update() updateNavigationButtonsState(context.player) } @@ -302,6 +293,7 @@ class PaginationComponent( if (index in pageItems.indices) { val item = pageItems[index] + onItemClick.invoke(item, context) } } From fcb17f59e58a915ac7aeb9dc8b370a842dc2f629 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:34:14 +0000 Subject: [PATCH 53/92] Fix navigation buttons not rendering by setting HIGH priority - Changed button priority from parent's priority to ComponentPriority.HIGH - Navigation buttons now render above pagination items - Ensures buttons are visible in the last row of the pagination area Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 45b9fc1e..a28950a5 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -100,7 +100,7 @@ class PaginationComponent( line { gray("Click to go to the previous page") } } }), - priority = this@PaginationComponent.priority + priority = ComponentPriority.HIGH // High priority to render above items ) { onClick = { previousPage(player) @@ -120,7 +120,7 @@ class PaginationComponent( } }) }, - priority = this@PaginationComponent.priority + priority = ComponentPriority.HIGH // High priority to render above items ) private fun createNextButtonComponent() = component( @@ -131,7 +131,7 @@ class PaginationComponent( line { gray("Click to go to the next page") } } }), - priority = this@PaginationComponent.priority + priority = ComponentPriority.HIGH // High priority to render above items ) { onClick = { nextPage(player) From 4896c998aec884c368e3b3a55f4a87b1153a1490 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 02:42:08 +0100 Subject: [PATCH 54/92] refactor: change PaginatedShopGuiView from object to class and update instantiation --- .../surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt | 2 +- .../slne/surf/surfapi/bukkit/test/gui/PaginatedShopGuiView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index a99cf8d0..2c6cdd98 100644 --- 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 @@ -22,7 +22,7 @@ class GuiTest(name: String) : CommandAPICommand(name) { // Shop subcommand CommandAPICommand("shop") .executesPlayer(PlayerCommandExecutor { player, _ -> - PaginatedShopGuiView.open(player) + PaginatedShopGuiView().open(player) }) ) } 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 index 0bb84b01..48b65730 100644 --- 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 @@ -23,7 +23,7 @@ import org.bukkit.event.inventory.InventoryType * Example GUI demonstrating ViewerMutableProp and PaginationComponent. * Each player has their own coins (viewer-specific state). */ -object PaginatedShopGuiView : AbstractGuiView() { +class PaginatedShopGuiView : AbstractGuiView() { private val coinsProp = ViewerProp.Mutable("coins", 1000) private val coinsDisplayRef = Ref() From ee005bbb80c64b0acd8a04fb4e0a55375265338a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:43:53 +0000 Subject: [PATCH 55/92] Fix child component rendering in empty parent slots and button state updates - Modified refreshComponentSlots to try each component by priority until one renders - Children now render at slots where parent doesn't render anything - Changed updateNavigationButtonsState to use setHidden/setDisabled methods - Button states now properly update when reaching first or last page - Fixes buttons not showing/hiding correctly during navigation Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../api/gui/component/PaginationComponent.kt | 12 ++---- .../bukkit/api/gui/view/AbstractGuiView.kt | 42 +++++++++++-------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index a28950a5..f8d3ebd9 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -193,15 +193,11 @@ class PaginationComponent( val hasPrev = hasPreviousPage(viewer) val hasNext = hasNextPage(viewer) - previousButtonComponent.apply { - disabled = !hasPrev - hidden = !hasPrev - } + previousButtonComponent.setHidden(!hasPrev) + previousButtonComponent.setDisabled(!hasPrev) - nextButtonComponent.apply { - disabled = !hasNext - hidden = !hasNext - } + nextButtonComponent.setHidden(!hasNext) + nextButtonComponent.setDisabled(!hasNext) } /** diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 9863aedb..56bf667c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -168,26 +168,32 @@ open class AbstractGuiView : GuiView() { val inventory = inventories[player.uniqueId] ?: return val context = createViewContext(player) - // Get all slots this component occupies - val slotsToRender = component.renderSlots(context) - if (slotsToRender.isNotEmpty()) { - // Container component - refresh each slot it renders - slotsToRender.forEach { (slot, guiItem) -> - // Check if this component is still highest priority at this slot - val highestPriorityComponent = findComponentsBySlot(slot).firstOrNull() - if (highestPriorityComponent == component && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) + // Get all slots this component could occupy + component.area.slots().forEach { slot -> + if (slot.index >= inventory.size) return@forEach + + // Find all components at this slot, sorted by priority + val componentsAtSlot = findComponentsBySlot(slot) + + // Try each component from highest to lowest priority until one renders something + var rendered = false + for (comp in componentsAtSlot) { + val renderedItems = comp.renderSlots(context) + val item = renderedItems[slot] + + if (item != null) { + // This component renders something at this slot + inventory.setItem(slot.index, item.toItemStack()) + rendered = true + break } } - } else { - // Regular component - refresh its start slot - val slot = component.area.slots().firstOrNull() ?: return - val highestPriorityComponent = findComponentsBySlot(slot).firstOrNull() - - if (highestPriorityComponent == component) { - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) + + if (!rendered) { + // No component rendered anything at this slot - check if we should clear it + // Only clear if the component we're refreshing was responsible for this slot + if (componentsAtSlot.contains(component)) { + inventory.setItem(slot.index, null) } } } From 878d6408cc79b14f249fa77f57f89a7f51724c78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:46:58 +0000 Subject: [PATCH 56/92] Replace setHidden/setDisabled method calls with direct property assignment - Changed setHidden() calls to direct hidden property assignment - Changed setDisabled() calls to direct disabled property assignment - Fixes compilation errors from non-existent methods - Methods were removed, only var properties remain Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/PaginationComponent.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index f8d3ebd9..1f46723c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -193,11 +193,11 @@ class PaginationComponent( val hasPrev = hasPreviousPage(viewer) val hasNext = hasNextPage(viewer) - previousButtonComponent.setHidden(!hasPrev) - previousButtonComponent.setDisabled(!hasPrev) + previousButtonComponent.hidden = !hasPrev + previousButtonComponent.disabled = !hasPrev - nextButtonComponent.setHidden(!hasNext) - nextButtonComponent.setDisabled(!hasNext) + nextButtonComponent.hidden = !hasNext + nextButtonComponent.disabled = !hasNext } /** From 1bc4a1fbc9e6f24ba4965f3407f5e03b4ab94a5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:52:35 +0000 Subject: [PATCH 57/92] Fix components disappearing by removing slot clearing during refresh - Removed slot clearing logic from refreshComponentSlots - Components only update slots they render to, not clear others - Fixes pagination buttons disappearing after page change - Fixes coins display disappearing when clicking shop items Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 56bf667c..dd99187e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -176,7 +176,6 @@ open class AbstractGuiView : GuiView() { val componentsAtSlot = findComponentsBySlot(slot) // Try each component from highest to lowest priority until one renders something - var rendered = false for (comp in componentsAtSlot) { val renderedItems = comp.renderSlots(context) val item = renderedItems[slot] @@ -184,18 +183,10 @@ open class AbstractGuiView : GuiView() { if (item != null) { // This component renders something at this slot inventory.setItem(slot.index, item.toItemStack()) - rendered = true break } } - - if (!rendered) { - // No component rendered anything at this slot - check if we should clear it - // Only clear if the component we're refreshing was responsible for this slot - if (componentsAtSlot.contains(component)) { - inventory.setItem(slot.index, null) - } - } + // Don't clear slots - let components maintain their own rendering } } From e182ba17bad979158ac6592c73df45e7014f6e42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 01:59:10 +0000 Subject: [PATCH 58/92] Fix IllegalAccessError by accessing PaginatedShopGuiView as singleton object - Changed PaginatedShopGuiView().open(player) to PaginatedShopGuiView.open(player) - PaginatedShopGuiView is an object (singleton), not a class - Cannot instantiate with constructor call - Fixes IllegalAccessError about private constructor access Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 2c6cdd98..a99cf8d0 100644 --- 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 @@ -22,7 +22,7 @@ class GuiTest(name: String) : CommandAPICommand(name) { // Shop subcommand CommandAPICommand("shop") .executesPlayer(PlayerCommandExecutor { player, _ -> - PaginatedShopGuiView().open(player) + PaginatedShopGuiView.open(player) }) ) } From 9ce41e7bbfe51c0c6616afd63e157e02cda5ab17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:01:58 +0000 Subject: [PATCH 59/92] Fix GuiTest to instantiate PaginatedShopGuiView as a class - Changed PaginatedShopGuiView.open(player) to PaginatedShopGuiView().open(player) - PaginatedShopGuiView is now a class, not an object - Must instantiate with constructor before calling open() - Fixes IllegalAccessError Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surf/surfapi/bukkit/test/command/subcommands/GuiTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a99cf8d0..2c6cdd98 100644 --- 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 @@ -22,7 +22,7 @@ class GuiTest(name: String) : CommandAPICommand(name) { // Shop subcommand CommandAPICommand("shop") .executesPlayer(PlayerCommandExecutor { player, _ -> - PaginatedShopGuiView.open(player) + PaginatedShopGuiView().open(player) }) ) } From 6e0ddcea1b8fc2303ea9e44635338052b836ff5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:06:18 +0000 Subject: [PATCH 60/92] Fix navigation button visibility by calling update() after state changes - Added update() calls after changing hidden/disabled properties - Ensures buttons re-render when visibility state changes - Fixes previous button not showing after page change - Both previous and next buttons now properly show/hide based on page state Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 1f46723c..1d3aa3d2 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -195,9 +195,11 @@ class PaginationComponent( previousButtonComponent.hidden = !hasPrev previousButtonComponent.disabled = !hasPrev + previousButtonComponent.update() nextButtonComponent.hidden = !hasNext nextButtonComponent.disabled = !hasNext + nextButtonComponent.update() } /** From 1f6b4285d69759108f22aca96301c1cdadce3a52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:14:19 +0000 Subject: [PATCH 61/92] Remove duplicate updateNavigationButtonsState calls and manual updates - Removed updateNavigationButtonsState() call from renderSlots() (was duplicate) - Removed manual update() calls on buttons (cascade handles this automatically) - updateNavigationButtonsState() now only called in onUpdate() lifecycle - Fixes button visibility issues caused by duplicate and conflicting state updates - Cascade update properly propagates button state changes to display Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 1d3aa3d2..d14148fc 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -195,11 +195,9 @@ class PaginationComponent( previousButtonComponent.hidden = !hasPrev previousButtonComponent.disabled = !hasPrev - previousButtonComponent.update() nextButtonComponent.hidden = !hasNext nextButtonComponent.disabled = !hasNext - nextButtonComponent.update() } /** @@ -247,9 +245,6 @@ class PaginationComponent( } override fun renderSlots(context: ViewContext): Map { - // Update navigation button states for this viewer - updateNavigationButtonsState(context.player) - val pageItems = getPageItems(context.player) val renderedSlots = mutableMapOf() From f29aeb728c160a890916c083111aca827a1d36b6 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 03:17:42 +0100 Subject: [PATCH 62/92] refactor: remove unnecessary whitespace in PaginationComponent --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index d14148fc..f71c210a 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -286,7 +286,7 @@ class PaginationComponent( if (index in pageItems.indices) { val item = pageItems[index] - + onItemClick.invoke(item, context) } } From 140a8765443a9a8137c64e1c1daf39241775a824 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:22:49 +0000 Subject: [PATCH 63/92] Fix viewer-specific component updates for coins display and button visibility - Added viewer parameter to Component.update() and Ref.update() - Modified GuiView.updateComponent() to only update for specific viewer - Pass viewer context through cascade updates to children - Update PaginationComponent.nextPage/previousPage to pass viewer - Update PaginatedShopGuiView to pass viewer when updating coins display - Fixes coins display not updating for correct viewer - Fixes navigation buttons showing incorrect state across viewers - Each viewer now has independent component state updates Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 4 ++-- .../api/gui/component/PaginationComponent.kt | 4 ++-- .../surf/surfapi/bukkit/api/gui/ref/Ref.kt | 5 +++-- .../surfapi/bukkit/api/gui/view/GuiView.kt | 18 +++++++++++++----- .../bukkit/test/gui/PaginatedShopGuiView.kt | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) 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 index 878e5c18..999b283b 100644 --- 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 @@ -151,8 +151,8 @@ abstract class Component { /** * Trigger an update of this component. */ - fun update() { - view?.updateComponent(this) + fun update(viewer: Player? = null) { + view?.updateComponent(this, viewer) } /** diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index f71c210a..4df8edbf 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -208,7 +208,7 @@ class PaginationComponent( if (hasNextPage(viewer)) { setPage(viewer, currentPage + 1) - this@PaginationComponent.update() + this@PaginationComponent.update(viewer) } } @@ -220,7 +220,7 @@ class PaginationComponent( if (hasPreviousPage(viewer)) { setPage(viewer, currentPage - 1) - this@PaginationComponent.update() + this@PaginationComponent.update(viewer) } } 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 index 37879614..9f49824a 100644 --- 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 @@ -25,9 +25,10 @@ class Ref { /** * Updates the referenced component. + * @param viewer The specific viewer to update for, or null to update for all viewers */ - fun update() { - current?.update() + fun update(viewer: Player? = null) { + current?.update(viewer) } /** 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 index 5d573a4d..78e8f514 100644 --- 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 @@ -154,9 +154,17 @@ abstract class GuiView { /** * Update a specific component. * Calls the onUpdate lifecycle hook and refreshes the component's slots in the inventory. - */ - internal fun updateComponent(component: Component) { - viewers.values.forEach { player -> + * @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) { + listOfNotNull(viewers[viewer.uniqueId]) + } else { + viewers.values.toList() + } + + viewersToUpdate.forEach { player -> val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) component.onUpdate(lifecycleContext) @@ -165,9 +173,9 @@ abstract class GuiView { refreshComponentSlotsInternal(player, component) } - // Recursively update all children + // Recursively update all children with the same viewer context component.children.forEach { child -> - updateComponent(child) + updateComponent(child, viewer) } } 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 index 48b65730..323931ce 100644 --- 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 @@ -67,7 +67,7 @@ class PaginatedShopGuiView : AbstractGuiView() { success(" for ") gold("${item.price} coins") } - coinsDisplayRef.update() + coinsDisplayRef.update(ctx.player) } else { ctx.player.sendText { error("You do not have enough coins to purchase ") From 156f71ed7fedec1b3bf873e2376492327664a41e Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 03:29:52 +0100 Subject: [PATCH 64/92] refactor: add Player import to Component and Ref classes --- .../surf/surfapi/bukkit/api/gui/component/Component.kt | 1 + .../dev/slne/surf/surfapi/bukkit/api/gui/ref/Ref.kt | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) 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 index 999b283b..5181e9aa 100644 --- 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 @@ -9,6 +9,7 @@ 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 org.bukkit.entity.Player import kotlin.time.Duration /** 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 index 9f49824a..eb6f9603 100644 --- 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 @@ -1,6 +1,7 @@ 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 /** @@ -9,20 +10,20 @@ import java.util.concurrent.atomic.AtomicReference */ 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 @@ -30,7 +31,7 @@ class Ref { fun update(viewer: Player? = null) { current?.update(viewer) } - + /** * Checks if the ref has a current value. */ From 4e01595ca45240d276eea89cd797536217d11f25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:33:11 +0000 Subject: [PATCH 65/92] Fix update cascade timing - update children before refreshing parent slots - Moved cascade to children inside viewersToUpdate loop - Children now update BEFORE parent's slots are refreshed - Ensures findComponentsBySlot sees correct hidden states - Fixes navigation buttons not showing/hiding correctly - Fixes coins display not updating when purchasing items - Children update with correct viewer context before parent renders Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 index 78e8f514..f4f07cb1 100644 --- 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 @@ -169,14 +169,16 @@ abstract class GuiView { component.onUpdate(lifecycleContext) + // Recursively update all children first with the same viewer context + // This ensures children are updated before we refresh parent's slots + component.children.forEach { child -> + updateComponent(child, if (viewer != null) player else null) + } + // Refresh the component's slots to update the visual display + // Children are now up-to-date, so findComponentsBySlot will see correct hidden states refreshComponentSlotsInternal(player, component) } - - // Recursively update all children with the same viewer context - component.children.forEach { child -> - updateComponent(child, viewer) - } } /** From db5b48446846092215b4191d07209b2c4f99ab74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:15:52 +0000 Subject: [PATCH 66/92] Fix rendering system to properly handle child components and slot clearing - refreshComponentSlots now collects slots from component AND all children recursively - Ensures all child slots (like navigation buttons) are refreshed when parent updates - Clear slots when no component renders (fixes hidden components properly disappearing) - Simplified cascade logic - call onUpdate recursively, then refresh all slots once - Fixes navigation buttons not updating visibility correctly - Fixes coins display not updating when purchasing items - All component state changes now properly reflected in inventory Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/view/AbstractGuiView.kt | 25 ++++++++++++++++--- .../surfapi/bukkit/api/gui/view/GuiView.kt | 15 +++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index dd99187e..2337ed40 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -162,20 +162,31 @@ open class AbstractGuiView : GuiView() { } /** - * Refresh all slots occupied by a specific component. + * Refresh all slots occupied by a specific component and its children. */ internal fun refreshComponentSlots(player: Player, component: Component) { val inventory = inventories[player.uniqueId] ?: return val context = createViewContext(player) - // Get all slots this component could occupy - component.area.slots().forEach { slot -> + // Collect all slots from this component and all its children recursively + fun collectAllSlots(comp: Component): Set { + val slots = mutableSetOf() + slots.addAll(comp.area.slots()) + comp.children.forEach { child -> + slots.addAll(collectAllSlots(child)) + } + return slots + } + + // Get all slots this component and its children could occupy + collectAllSlots(component).forEach { slot -> if (slot.index >= inventory.size) return@forEach // Find all components at this slot, sorted by priority val componentsAtSlot = findComponentsBySlot(slot) // Try each component from highest to lowest priority until one renders something + var rendered = false for (comp in componentsAtSlot) { val renderedItems = comp.renderSlots(context) val item = renderedItems[slot] @@ -183,10 +194,16 @@ open class AbstractGuiView : GuiView() { if (item != null) { // This component renders something at this slot inventory.setItem(slot.index, item.toItemStack()) + rendered = true break } } - // Don't clear slots - let components maintain their own rendering + + // If no component rendered at this slot, clear it + // This ensures slots are properly cleared when components become hidden + if (!rendered) { + inventory.setItem(slot.index, null) + } } } 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 index f4f07cb1..af84fd16 100644 --- 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 @@ -167,16 +167,21 @@ abstract class GuiView { viewersToUpdate.forEach { player -> val lifecycleContext = createLifecycleContext(player, LifecycleEventType.UPDATE) + // Call onUpdate on this component component.onUpdate(lifecycleContext) - // Recursively update all children first with the same viewer context - // This ensures children are updated before we refresh parent's slots - component.children.forEach { child -> - updateComponent(child, if (viewer != null) player else null) + // 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 - // Children are now up-to-date, so findComponentsBySlot will see correct hidden states + // This now includes all children's slots, so everything updates together refreshComponentSlotsInternal(player, component) } } From 3c7269704b30dc29d73707268bb6f5dfd53bfa6b Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 15:44:57 +0100 Subject: [PATCH 67/92] add tostring to everything --- .../surf/surfapi/bukkit/api/gui/GuiItem.kt | 4 +++ .../bukkit/api/gui/area/CircularArea.kt | 14 +++++++---- .../surfapi/bukkit/api/gui/area/CuboidArea.kt | 10 +++++--- .../bukkit/api/gui/area/SingleSlotArea.kt | 6 ++++- .../bukkit/api/gui/component/Component.kt | 8 ++---- .../api/gui/component/ContainerComponent.kt | 6 ++++- .../api/gui/component/DynamicComponent.kt | 6 ++++- .../bukkit/api/gui/component/ItemComponent.kt | 6 ++++- .../api/gui/component/PaginationComponent.kt | 4 +++ .../context/abstract/AbstractClickContext.kt | 4 +++ .../abstract/AbstractLifecycleContext.kt | 4 +++ .../context/abstract/AbstractRenderContext.kt | 4 +++ .../context/abstract/AbstractResumeContext.kt | 4 +++ .../context/abstract/AbstractViewContext.kt | 4 +++ .../bukkit/api/gui/props/ComputedProp.kt | 4 +++ .../surfapi/bukkit/api/gui/props/LazyProp.kt | 8 ++++++ .../surf/surfapi/bukkit/api/gui/props/Prop.kt | 8 ++++++ .../bukkit/api/gui/props/ViewerProp.kt | 8 ++++++ .../bukkit/api/gui/props/ViewerPropStorage.kt | 4 +++ .../surf/surfapi/bukkit/api/gui/ref/Ref.kt | 4 +++ .../bukkit/api/gui/view/AbstractGuiView.kt | 20 +++++++++++---- .../surfapi/bukkit/api/gui/view/GuiView.kt | 25 +++++++++++++------ .../surfapi/bukkit/api/gui/view/ViewConfig.kt | 4 +++ .../bukkit/server/gui/view/ViewManagerImpl.kt | 4 +++ 24 files changed, 143 insertions(+), 30 deletions(-) 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 index 391b50aa..3049348e 100644 --- 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 @@ -47,6 +47,10 @@ class GuiItem( 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. 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 index 747024df..b7547c3e 100644 --- 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 @@ -15,7 +15,7 @@ data class CircularArea( override fun slots(): Set { val slots = mutableSetOf() 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)) { @@ -25,20 +25,24 @@ data class CircularArea( } } } - + 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/CuboidArea.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/area/CuboidArea.kt index c1ca123e..112be87b 100644 --- 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 @@ -19,17 +19,21 @@ data class CuboidArea( } 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 index 6f78b94f..2cc82e6f 100644 --- 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 @@ -10,9 +10,13 @@ data class SingleSlotArea( val slot: Slot ) : ComponentArea { override fun slots(): Set = setOf(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 index 5181e9aa..f4121635 100644 --- 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 @@ -156,11 +156,7 @@ abstract class Component { view?.updateComponent(this, viewer) } - /** - * Trigger an update of this component and all children. - */ - fun updateWithChildren() { - update() - children.forEach { it.updateWithChildren() } + override fun toString(): String { + return "Component(area=$area, priority=$priority, width=$width, height=$height, children=$children, view=$view, updateInterval=$updateInterval, hidden=$hidden, disabled=$disabled, props=$props, attachedRef=${attachedRef.hashCode()})" } } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt index 5aae50b0..5ea0f16e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt @@ -19,9 +19,13 @@ abstract class ContainerComponent( * Override this to provide the slot-to-item mapping. */ abstract override fun renderSlots(context: ViewContext): Map - + /** * 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/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt index 89b9b154..94252796 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt @@ -17,10 +17,14 @@ open class DynamicComponent( 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/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt index e90f14d5..34b5842b 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt @@ -17,10 +17,14 @@ open class ItemComponent( 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/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 4df8edbf..fd547f3a 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -291,4 +291,8 @@ class PaginationComponent( } } } + + 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/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 index b4793b18..56939924 100644 --- 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 @@ -29,4 +29,8 @@ class AbstractClickContext( 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/AbstractLifecycleContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractLifecycleContext.kt index e47e16cd..3874f349 100644 --- 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 @@ -27,4 +27,8 @@ class AbstractLifecycleContext( 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/AbstractRenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt index 77670262..d88e1b92 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt @@ -50,4 +50,8 @@ class AbstractRenderContext( override fun update() { NavigationHelper.update(view, player) } + + override fun toString(): String { + return "AbstractRenderContext(view=$view, player=$player, bukkitView=$bukkitView)" + } } \ 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 index 9309f8fc..cf357f65 100644 --- 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 @@ -29,4 +29,8 @@ class AbstractResumeContext( 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 index 18543876..2a026267 100644 --- 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 @@ -25,4 +25,8 @@ class AbstractViewContext( 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/props/ComputedProp.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/props/ComputedProp.kt index afb36f65..c8395aa4 100644 --- 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 @@ -9,4 +9,8 @@ open class ComputedProp( 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 index fc080ee8..0e23fe5f 100644 --- 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 @@ -12,6 +12,10 @@ open class LazyProp( 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. @@ -30,5 +34,9 @@ open class LazyProp( 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 index 908e9cee..7e8829cc 100644 --- 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 @@ -29,6 +29,10 @@ sealed interface 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)" + } } /** @@ -51,5 +55,9 @@ sealed interface Prop { 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 index 7528a4fb..3d110e12 100644 --- 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 @@ -17,6 +17,10 @@ open class ViewerProp( fun get(viewer: Player): T = storage.get(viewer.uniqueId) ?: throw IllegalStateException("Value for viewer ${viewer.uniqueId} is not set") + override fun toString(): String { + return "ViewerProp(name='$name', storage=$storage)" + } + class Mutable( override val name: String, initialValue: T? @@ -35,5 +39,9 @@ open class ViewerProp( fun clear(viewer: Player) { storage.clear(viewer.uniqueId) } + + 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 index 1b8c1495..de81326c 100644 --- 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 @@ -18,4 +18,8 @@ class ViewerPropStorage(private val initialValue: () -> T) { fun clear(viewerId: UUID) { storage.remove(viewerId) } + + 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 index eb6f9603..4c62fe2e 100644 --- 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 @@ -36,6 +36,10 @@ class Ref { * Checks if the ref has a current value. */ fun isSet(): Boolean = current != null + + override fun toString(): String { + return "Ref(reference=$reference, current=$current)" + } } /** diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 2337ed40..4b4c2bbb 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -178,19 +178,23 @@ open class AbstractGuiView : GuiView() { return slots } + println("=".repeat(20)) // Get all slots this component and its children could occupy collectAllSlots(component).forEach { slot -> + println("Trying to update slot $slot") if (slot.index >= inventory.size) return@forEach - + // Find all components at this slot, sorted by priority val componentsAtSlot = findComponentsBySlot(slot) - + println("Components at slot: $componentsAtSlot") + // Try each component from highest to lowest priority until one renders something var rendered = false for (comp in componentsAtSlot) { val renderedItems = comp.renderSlots(context) val item = renderedItems[slot] - + println("Found item at slot $item") + if (item != null) { // This component renders something at this slot inventory.setItem(slot.index, item.toItemStack()) @@ -198,15 +202,17 @@ open class AbstractGuiView : GuiView() { break } } - + // If no component rendered at this slot, clear it // This ensures slots are properly cleared when components become hidden if (!rendered) { + println("Item not rendered, clearing slot $slot") inventory.setItem(slot.index, null) } } + println("#".repeat(20)) } - + /** * Implementation of refreshComponentSlotsInternal for GuiView. */ @@ -254,4 +260,8 @@ open class AbstractGuiView : GuiView() { override fun createResumeContext(player: Player, origin: GuiView?): ResumeContext { return AbstractResumeContext(this, player, origin) } + + override fun toString(): String { + return "AbstractGuiView(inventories=$inventories, updateJobs=$updateJobs, componentJobs=$componentJobs)" + } } \ 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/GuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt index af84fd16..7ee41581 100644 --- 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 @@ -36,7 +36,7 @@ abstract class GuiView { */ fun findComponentsBySlot(slot: Slot): List { val allComponents = mutableListOf() - + // Helper function to recursively collect components and their children fun collectComponents(component: Component) { allComponents.add(component) @@ -44,10 +44,10 @@ abstract class GuiView { 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 } @@ -158,34 +158,41 @@ abstract class GuiView { * @param viewer The specific viewer to update for, or null to update for all viewers */ internal fun updateComponent(component: Component, viewer: Player? = null) { + println("Updating component ${component} for viewer ${viewer?.name ?: "all viewers"}") val viewersToUpdate = if (viewer != null) { listOfNotNull(viewers[viewer.uniqueId]) } else { viewers.values.toList() } - + 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 -> + println("#".repeat(20)) + println("Calling child ${child} recursive update") child.onUpdate(lifecycleContext) updateChildrenRecursively(child) + println("#".repeat(20)) } } + println("-".repeat(20)) + println("Calling main child recursive update") updateChildrenRecursively(component) - + println("-".repeat(20)) + // Refresh the component's slots to update the visual display // This now includes all children's slots, so everything updates together refreshComponentSlotsInternal(player, component) } } - + /** * Internal method to refresh component slots. * Must be implemented by concrete view implementations. @@ -241,6 +248,10 @@ abstract class GuiView { player: Player, origin: GuiView? ): ResumeContext + + override fun toString(): String { + return "GuiView(parent=$parent, updateInterval=$updateInterval, components=$components, initialized=$initialized, firstRenderPerViewer=$firstRenderPerViewer, viewers=$viewers, config=$config)" + } } /** 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 index 5fb25c59..386dff2d 100644 --- 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 @@ -26,4 +26,8 @@ data class ViewConfig( 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, closeOnClickOutside=$closeOnClickOutside, rows=$rows)" + } } \ 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 index 8b7ecf8b..0014ccc9 100644 --- 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 @@ -27,4 +27,8 @@ class ViewManagerImpl : ViewManager { override fun unregisterView(inventory: Inventory) { _views.remove(inventory) } + + override fun toString(): String { + return "ViewManagerImpl(views=$views)" + } } \ No newline at end of file From 9ec90a17cb3a24d83cebc994b394763acd27ce83 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 15:56:43 +0100 Subject: [PATCH 68/92] refactor: clean up whitespace and enhance component rendering logic --- .../surfapi/bukkit/api/gui/component/PaginationComponent.kt | 2 ++ .../slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 3 +++ .../dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt | 1 + 3 files changed, 6 insertions(+) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index fd547f3a..1155764c 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -193,6 +193,8 @@ class PaginationComponent( val hasPrev = hasPreviousPage(viewer) val hasNext = hasNextPage(viewer) + println("Click caused: hasPrev=$hasPrev, hasNext=$hasNext for viewer ${viewer.name}") + previousButtonComponent.hidden = !hasPrev previousButtonComponent.disabled = !hasPrev diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 4b4c2bbb..9202f784 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -134,6 +134,7 @@ open class AbstractGuiView : GuiView() { val allSlots = (0 until inventory.size).map { Slot.of(it) } allSlots.forEach { slot -> val componentsAtSlot = findComponentsBySlot(slot) + if (componentsAtSlot.isNotEmpty()) { // Get highest priority component val component = componentsAtSlot.first() @@ -141,6 +142,7 @@ open class AbstractGuiView : GuiView() { // Check if it's a container component val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { // Only render this slot if it's in the container's output slotsToRender[slot]?.let { guiItem -> @@ -152,6 +154,7 @@ open class AbstractGuiView : GuiView() { // Regular component - only render at its start slot if (slot == component.area.first()) { val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { inventory.setItem(slot.index, guiItem.toItemStack()) } 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 index 7ee41581..8a8d549c 100644 --- 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 @@ -40,6 +40,7 @@ abstract class GuiView { // Helper function to recursively collect components and their children fun collectComponents(component: Component) { allComponents.add(component) + component.children.forEach { child -> collectComponents(child) } From e231cfc521d184822ca46df1b79c5ed7f148de45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:01:51 +0000 Subject: [PATCH 69/92] Fix button disappearing - remove collectAllSlots, each component refreshes only own slots - Removed collectAllSlots() function and debug prints - refreshComponentSlots now only iterates component.area.slots() - Each component refreshes only its own slots - Children refresh independently via updateChildrenRecursively - Fixes navigation buttons disappearing after page change Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/view/AbstractGuiView.kt | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 9202f784..095b4e10 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -172,31 +172,19 @@ open class AbstractGuiView : GuiView() { val context = createViewContext(player) // Collect all slots from this component and all its children recursively - fun collectAllSlots(comp: Component): Set { - val slots = mutableSetOf() - slots.addAll(comp.area.slots()) - comp.children.forEach { child -> - slots.addAll(collectAllSlots(child)) - } - return slots - } - - println("=".repeat(20)) - // Get all slots this component and its children could occupy - collectAllSlots(component).forEach { slot -> - println("Trying to update slot $slot") + // Only refresh this component's own slots, not children's + // Children will refresh their own slots when updateChildrenRecursively calls them + component.area.slots().forEach { slot -> if (slot.index >= inventory.size) return@forEach // Find all components at this slot, sorted by priority val componentsAtSlot = findComponentsBySlot(slot) - println("Components at slot: $componentsAtSlot") // Try each component from highest to lowest priority until one renders something var rendered = false for (comp in componentsAtSlot) { val renderedItems = comp.renderSlots(context) val item = renderedItems[slot] - println("Found item at slot $item") if (item != null) { // This component renders something at this slot @@ -209,11 +197,9 @@ open class AbstractGuiView : GuiView() { // If no component rendered at this slot, clear it // This ensures slots are properly cleared when components become hidden if (!rendered) { - println("Item not rendered, clearing slot $slot") inventory.setItem(slot.index, null) } } - println("#".repeat(20)) } /** From 0d977d4f076f9e33b49cb7b23d57b9688e38ac31 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 16:29:52 +0100 Subject: [PATCH 70/92] refactor: simplify inventory rendering and enhance component update logic --- .../bukkit/api/gui/dsl/ComponentDsl.kt | 2 + .../bukkit/api/gui/view/AbstractGuiView.kt | 70 +++++++------------ .../surfapi/bukkit/api/gui/view/GuiView.kt | 10 +-- 3 files changed, 30 insertions(+), 52 deletions(-) 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 index e108e379..b3a9e993 100644 --- 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 @@ -84,10 +84,12 @@ fun dynamicComponent( slot: Slot, renderer: (ViewContext) -> GuiItem?, priority: ComponentPriority = ComponentPriority.NORMAL, + onUpdate: (LifecycleContext.() -> Unit)? = null, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() componentBuilder.priority = priority + componentBuilder.onUpdate = onUpdate componentBuilder.builder() return componentBuilder.build(slot, renderer) } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 095b4e10..4cf8a725 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -42,36 +42,7 @@ open class AbstractGuiView : GuiView() { // Register with listener ViewManager.registerView(inventory, this) - // Render all components, respecting priority - // For each slot, render only the highest priority component - val allSlots = (0 until inventory.size).map { Slot.of(it) } - allSlots.forEach { slot -> - val componentsAtSlot = findComponentsBySlot(slot) - if (componentsAtSlot.isNotEmpty()) { - // Get highest priority component - val component = componentsAtSlot.first() - val context = createViewContext(player) - - // Check if it's a container component - val slotsToRender = component.renderSlots(context) - if (slotsToRender.isNotEmpty()) { - // Only render this slot if it's in the container's output - slotsToRender[slot]?.let { guiItem -> - if (slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) - } - } - } else { - // Regular component - only render at its start slot - if (slot == component.area.first()) { - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) - } - } - } - } - } + renderAllSlots(player, inventory) // Open inventory player.openInventory(inventory) @@ -121,20 +92,11 @@ open class AbstractGuiView : GuiView() { super.close(player) } - /** - * Refresh the inventory for a player. - */ - internal fun refreshInventory(player: Player) { - val inventory = inventories[player.uniqueId] ?: return - - // Clear inventory first - inventory.clear() - - // Render all slots, respecting priority + private fun renderAllSlots(player: Player, inventory: Inventory) { val allSlots = (0 until inventory.size).map { Slot.of(it) } + allSlots.forEach { slot -> val componentsAtSlot = findComponentsBySlot(slot) - if (componentsAtSlot.isNotEmpty()) { // Get highest priority component val component = componentsAtSlot.first() @@ -142,7 +104,6 @@ open class AbstractGuiView : GuiView() { // Check if it's a container component val slotsToRender = component.renderSlots(context) - if (slotsToRender.isNotEmpty()) { // Only render this slot if it's in the container's output slotsToRender[slot]?.let { guiItem -> @@ -154,7 +115,6 @@ open class AbstractGuiView : GuiView() { // Regular component - only render at its start slot if (slot == component.area.first()) { val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { inventory.setItem(slot.index, guiItem.toItemStack()) } @@ -164,6 +124,19 @@ open class AbstractGuiView : GuiView() { } } + /** + * Refresh the inventory for a player. + */ + internal fun refreshInventory(player: Player) { + val inventory = inventories[player.uniqueId] ?: return + + // Clear inventory first + inventory.clear() + + // Re-render all slots + renderAllSlots(player, inventory) + } + /** * Refresh all slots occupied by a specific component and its children. */ @@ -174,7 +147,16 @@ open class AbstractGuiView : GuiView() { // 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 - component.area.slots().forEach { slot -> + fun collectAllSlots(comp: Component): Set { + val slots = mutableSetOf() + slots.addAll(comp.area.slots()) + comp.children.forEach { child -> + slots.addAll(collectAllSlots(child)) + } + return slots + } + + collectAllSlots(component).forEach { slot -> if (slot.index >= inventory.size) return@forEach // Find all components at this slot, sorted by priority 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 index 8a8d549c..f16535a3 100644 --- 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 @@ -40,7 +40,7 @@ abstract class GuiView { // Helper function to recursively collect components and their children fun collectComponents(component: Component) { allComponents.add(component) - + component.children.forEach { child -> collectComponents(child) } @@ -159,7 +159,6 @@ abstract class GuiView { * @param viewer The specific viewer to update for, or null to update for all viewers */ internal fun updateComponent(component: Component, viewer: Player? = null) { - println("Updating component ${component} for viewer ${viewer?.name ?: "all viewers"}") val viewersToUpdate = if (viewer != null) { listOfNotNull(viewers[viewer.uniqueId]) } else { @@ -176,17 +175,12 @@ abstract class GuiView { // This allows children to update their state (like hidden/disabled properties) fun updateChildrenRecursively(comp: Component) { comp.children.forEach { child -> - println("#".repeat(20)) - println("Calling child ${child} recursive update") child.onUpdate(lifecycleContext) updateChildrenRecursively(child) - println("#".repeat(20)) } } - println("-".repeat(20)) - println("Calling main child recursive update") + updateChildrenRecursively(component) - println("-".repeat(20)) // Refresh the component's slots to update the visual display // This now includes all children's slots, so everything updates together From ed3008a69006aa7fa721183923037d74f15ce78d Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 16:30:49 +0100 Subject: [PATCH 71/92] refactor: improve slot rendering logic in AbstractGuiView --- .../slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 4cf8a725..b01892f0 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -97,6 +97,7 @@ open class AbstractGuiView : GuiView() { allSlots.forEach { slot -> val componentsAtSlot = findComponentsBySlot(slot) + if (componentsAtSlot.isNotEmpty()) { // Get highest priority component val component = componentsAtSlot.first() @@ -104,6 +105,7 @@ open class AbstractGuiView : GuiView() { // Check if it's a container component val slotsToRender = component.renderSlots(context) + if (slotsToRender.isNotEmpty()) { // Only render this slot if it's in the container's output slotsToRender[slot]?.let { guiItem -> From 74c0b637ecc1e337be554d7571d996c11906a69c Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 16:59:16 +0100 Subject: [PATCH 72/92] refactor: streamline slot rendering logic and enhance component refresh in AbstractGuiView --- .../api/gui/component/PaginationComponent.kt | 2 - .../bukkit/api/gui/dsl/ComponentDsl.kt | 4 + .../bukkit/api/gui/view/AbstractGuiView.kt | 85 ++++++++----------- 3 files changed, 39 insertions(+), 52 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index 1155764c..fd547f3a 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -193,8 +193,6 @@ class PaginationComponent( val hasPrev = hasPreviousPage(viewer) val hasNext = hasNextPage(viewer) - println("Click caused: hasPrev=$hasPrev, hasNext=$hasNext for viewer ${viewer.name}") - previousButtonComponent.hidden = !hasPrev previousButtonComponent.disabled = !hasPrev 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 index b3a9e993..cae8acda 100644 --- 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 @@ -55,6 +55,10 @@ class ComponentBuilder { super.onUpdate(context) this@ComponentBuilder.onUpdate?.invoke(context) } + + override fun toString(): String { + return "DSLGeneratedDynamicComponent(updateInterval=$updateInterval, props=$props) ${super.toString()}" + } }.also { component -> ref?.set(component) component.attachedRef = ref diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index b01892f0..abe6a509 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -85,6 +85,7 @@ open class AbstractGuiView : GuiView() { componentJobs.remove(player.uniqueId)?.forEach { it.cancel() } // Unregister inventory + // TODO: Fix, this currently can cause issues if multiple inventories are open inventories.remove(player.uniqueId)?.let { inventory -> ViewManager.unregisterView(inventory) } @@ -96,30 +97,36 @@ open class AbstractGuiView : GuiView() { val allSlots = (0 until inventory.size).map { Slot.of(it) } allSlots.forEach { slot -> - val componentsAtSlot = findComponentsBySlot(slot) - - if (componentsAtSlot.isNotEmpty()) { - // Get highest priority component - val component = componentsAtSlot.first() - val context = createViewContext(player) - - // Check if it's a container component - val slotsToRender = component.renderSlots(context) - - if (slotsToRender.isNotEmpty()) { - // Only render this slot if it's in the container's output - slotsToRender[slot]?.let { guiItem -> - if (slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) - } + renderSlot(player, inventory, slot) + } + } + + private fun renderSlot(player: Player, inventory: Inventory, slot: Slot) { + if (slot.index >= inventory.size) return + + val componentsAtSlot = findComponentsBySlot(slot) + + if (componentsAtSlot.isNotEmpty()) { + // Get highest priority component + val component = componentsAtSlot.first() + val context = createViewContext(player) + + // Check if it's a container component + val slotsToRender = component.renderSlots(context) + + if (slotsToRender.isNotEmpty()) { + // Only render this slot if it's in the container's output + slotsToRender[slot]?.let { guiItem -> + if (slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) } - } else { - // Regular component - only render at its start slot - if (slot == component.area.first()) { - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) - } + } + } else { + // Regular component - only render at its start slot + if (slot == component.area.first()) { + val guiItem = component.render(context) + if (guiItem != null && slot.index < inventory.size) { + inventory.setItem(slot.index, guiItem.toItemStack()) } } } @@ -144,45 +151,22 @@ open class AbstractGuiView : GuiView() { */ internal fun refreshComponentSlots(player: Player, component: Component) { val inventory = inventories[player.uniqueId] ?: return - val context = createViewContext(player) // 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() - slots.addAll(comp.area.slots()) + val slots = mutableSetOf(*comp.area.slots().toTypedArray()) + comp.children.forEach { child -> slots.addAll(collectAllSlots(child)) } + return slots } collectAllSlots(component).forEach { slot -> - if (slot.index >= inventory.size) return@forEach - - // Find all components at this slot, sorted by priority - val componentsAtSlot = findComponentsBySlot(slot) - - // Try each component from highest to lowest priority until one renders something - var rendered = false - for (comp in componentsAtSlot) { - val renderedItems = comp.renderSlots(context) - val item = renderedItems[slot] - - if (item != null) { - // This component renders something at this slot - inventory.setItem(slot.index, item.toItemStack()) - rendered = true - break - } - } - - // If no component rendered at this slot, clear it - // This ensures slots are properly cleared when components become hidden - if (!rendered) { - inventory.setItem(slot.index, null) - } + renderSlot(player, inventory, slot) } } @@ -211,6 +195,7 @@ open class AbstractGuiView : GuiView() { if (component != null && !component.disabled) { val clickContext = AbstractClickContext(this, player, event, component) + component.onClick(clickContext) } } From cf29234b8410edd12367c3234c0d70be51254399 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 17:02:15 +0100 Subject: [PATCH 73/92] refactor: enhance slot rendering logic to ensure inventory updates correctly --- .../surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index abe6a509..aa3fa373 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -106,6 +106,8 @@ open class AbstractGuiView : GuiView() { val componentsAtSlot = findComponentsBySlot(slot) + var rendered = false + if (componentsAtSlot.isNotEmpty()) { // Get highest priority component val component = componentsAtSlot.first() @@ -119,6 +121,7 @@ open class AbstractGuiView : GuiView() { slotsToRender[slot]?.let { guiItem -> if (slot.index < inventory.size) { inventory.setItem(slot.index, guiItem.toItemStack()) + rendered = true } } } else { @@ -127,10 +130,15 @@ open class AbstractGuiView : GuiView() { val guiItem = component.render(context) if (guiItem != null && slot.index < inventory.size) { inventory.setItem(slot.index, guiItem.toItemStack()) + rendered = true } } } } + + if (!rendered) { + inventory.setItem(slot.index, null) + } } /** From eca37b0961b8aa8cd9af64d4f71905fe475508c1 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 17:08:35 +0100 Subject: [PATCH 74/92] refactor: add onUpdate callback to ComponentBuilder for enhanced lifecycle management --- .../dev/slne/surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index cae8acda..e66f5ebb 100644 --- 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 @@ -73,10 +73,12 @@ fun component( slot: Slot, item: GuiItem, priority: ComponentPriority = ComponentPriority.NORMAL, + onUpdate: (LifecycleContext.() -> Unit)? = null, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() componentBuilder.priority = priority + componentBuilder.onUpdate = onUpdate componentBuilder.builder() return componentBuilder.build(slot) { item } } From 6a153beed82ba82368ba9d8840ae0138e393b22c Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 17:24:57 +0100 Subject: [PATCH 75/92] refactor: update prop handling to use ViewerProp for improved state management --- .../bukkit/api/gui/component/Component.kt | 12 +++++++----- .../api/gui/component/PaginationComponent.kt | 14 ++++++++------ .../surfapi/bukkit/api/gui/props/ViewerProp.kt | 16 ++++++++++++---- .../bukkit/api/gui/props/ViewerPropStorage.kt | 14 ++++++++------ 4 files changed, 35 insertions(+), 21 deletions(-) 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 index f4121635..1cc8d687 100644 --- 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 @@ -92,7 +92,7 @@ abstract class Component { * Props accessible by this component. * Children can access parent props. */ - protected open val props: Map> = emptyMap() + protected open val props: MutableList> = mutableListOf() /** * Ref attached to this component, if any. @@ -142,10 +142,12 @@ abstract class Component { /** * Get all props including parent props. */ - fun getAllProps(): Map> { - val allProps = mutableMapOf>() - parent?.getAllProps()?.let { allProps.putAll(it) } - allProps.putAll(props) + fun getAllProps(): List> { + val allProps = mutableListOf>() + + parent?.getAllProps()?.let { allProps.addAll(it) } + allProps.addAll(props) + return allProps } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt index fd547f3a..8edf50be 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt @@ -12,9 +12,9 @@ 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 org.bukkit.Material import org.bukkit.entity.Player -import java.util.* /** * Component for paginated content with built-in navigation buttons. @@ -35,12 +35,14 @@ class PaginationComponent( 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})" } - } - private val currentPages = mutableMapOf() + props.add(currentPages) + } /** * Start slot of the area (convenience accessor). @@ -142,7 +144,7 @@ class PaginationComponent( * Get the current page for a viewer. */ fun getCurrentPage(viewer: Player): Int { - return currentPages.getOrDefault(viewer.uniqueId, 0) + return currentPages.getOrDefault(viewer, 0) } /** @@ -229,7 +231,7 @@ class PaginationComponent( */ fun setPage(viewer: Player, page: Int) { if (page in 0 until getTotalPages()) { - currentPages[viewer.uniqueId] = page + currentPages.set(viewer, page) } } @@ -237,7 +239,7 @@ class PaginationComponent( * Clear the page state for a viewer. */ fun clearPage(viewer: Player) { - currentPages.remove(viewer.uniqueId) + currentPages.clear(viewer) } override fun onUpdate(context: LifecycleContext) { 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 index 3d110e12..d5fc8b14 100644 --- 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 @@ -14,9 +14,13 @@ open class ViewerProp( override suspend fun get(): T = throw UnsupportedOperationException("Use get(viewer: Player) for ViewerProp") - fun get(viewer: Player): T = storage.get(viewer.uniqueId) + 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)" } @@ -30,14 +34,18 @@ open class ViewerProp( override suspend fun get(): T = throw UnsupportedOperationException("Use get(viewer: Player) for ViewerProp.MutableViewerProp") - fun get(viewer: Player): T? = storage.get(viewer.uniqueId) + 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.uniqueId, value) + storage.set(viewer, value) } fun clear(viewer: Player) { - storage.clear(viewer.uniqueId) + storage.clear(viewer) } override fun toString(): String { 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 index de81326c..f1566bcb 100644 --- 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 @@ -1,5 +1,7 @@ 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.* /** @@ -7,16 +9,16 @@ import java.util.* * Maps viewer UUIDs to their prop values. */ class ViewerPropStorage(private val initialValue: () -> T) { - private val storage = mutableMapOf() + private val storage = mutableObject2ObjectMapOf() - fun get(viewerId: UUID): T? = storage.getOrPut(viewerId) { initialValue() } + fun get(viewer: Player): T? = storage.getOrPut(viewer.uniqueId) { initialValue() } - fun set(viewerId: UUID, value: T?) { - storage[viewerId] = value + fun set(viewer: Player, value: T?) { + storage[viewer.uniqueId] = value } - fun clear(viewerId: UUID) { - storage.remove(viewerId) + fun clear(viewer: Player) { + storage.remove(viewer.uniqueId) } override fun toString(): String { From a4feb751f6612a5df4ceb9bd5f76c13767c4c20d Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 17:41:31 +0100 Subject: [PATCH 76/92] refactor: enhance rendering logic to support first render lifecycle events --- .../bukkit/api/gui/component/Component.kt | 5 +++ .../{ => components}/ContainerComponent.kt | 6 ++-- .../{ => components}/DynamicComponent.kt | 4 ++- .../{ => components}/ItemComponent.kt | 4 ++- .../{ => components}/PaginationComponent.kt | 6 ++-- .../bukkit/api/gui/dsl/ComponentDsl.kt | 33 +++++++++++-------- .../bukkit/api/gui/view/AbstractGuiView.kt | 30 +++++++++++++---- .../surfapi/bukkit/api/gui/view/GuiView.kt | 12 +++---- .../bukkit/test/gui/PaginatedShopGuiView.kt | 2 +- 9 files changed, 66 insertions(+), 36 deletions(-) rename surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/{ => components}/ContainerComponent.kt (84%) rename surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/{ => components}/DynamicComponent.kt (84%) rename surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/{ => components}/ItemComponent.kt (83%) rename surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/{ => components}/PaginationComponent.kt (97%) 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 index 1cc8d687..ebbdd1cd 100644 --- 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 @@ -99,6 +99,11 @@ abstract class Component { */ internal var attachedRef: Ref? = null + /** + * Called when the component is rendered for the first time. + */ + open fun onFirstRender(context: LifecycleContext) {} + /** * Called when the component is updated. */ diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ContainerComponent.kt similarity index 84% rename from surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt rename to surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ContainerComponent.kt index 5ea0f16e..6f098d78 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ContainerComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ContainerComponent.kt @@ -1,8 +1,10 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.component +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 /** @@ -24,7 +26,7 @@ abstract class ContainerComponent( * 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/DynamicComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/DynamicComponent.kt similarity index 84% rename from surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt rename to surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/DynamicComponent.kt index 94252796..26c120f4 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/DynamicComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/DynamicComponent.kt @@ -1,9 +1,11 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.component +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 diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ItemComponent.kt similarity index 83% rename from surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt rename to surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ItemComponent.kt index 34b5842b..e5bd208a 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/ItemComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/ItemComponent.kt @@ -1,9 +1,11 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.component +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 diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/PaginationComponent.kt similarity index 97% rename from surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt rename to surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/PaginationComponent.kt index 8edf50be..0a89f0da 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/PaginationComponent.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/component/components/PaginationComponent.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.component +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 @@ -7,6 +7,7 @@ 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 @@ -102,7 +103,6 @@ class PaginationComponent( line { gray("Click to go to the previous page") } } }), - priority = ComponentPriority.HIGH // High priority to render above items ) { onClick = { previousPage(player) @@ -122,7 +122,6 @@ class PaginationComponent( } }) }, - priority = ComponentPriority.HIGH // High priority to render above items ) private fun createNextButtonComponent() = component( @@ -133,7 +132,6 @@ class PaginationComponent( line { gray("Click to go to the next page") } } }), - priority = ComponentPriority.HIGH // High priority to render above items ) { onClick = { nextPage(player) 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 index e66f5ebb..dd5ad7fb 100644 --- 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 @@ -4,8 +4,8 @@ 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.DynamicComponent -import dev.slne.surf.surfapi.bukkit.api.gui.component.ItemComponent +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.LifecycleContext import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext @@ -15,6 +15,8 @@ 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 import kotlin.time.Duration /** @@ -31,16 +33,19 @@ class ComponentBuilder { var updateInterval: Duration? = null var ref: Ref? = null var priority: ComponentPriority = ComponentPriority.NORMAL + 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 = mutableMapOf>() + private val _props = mutableObjectListOf>() /** * Add a prop to this component. */ - fun prop(name: String, prop: Prop) { - _props[name] = prop + fun prop(prop: Prop) { + _props.add(prop) } /** @@ -49,7 +54,12 @@ class ComponentBuilder { internal fun build(slot: Slot, renderer: (ViewContext) -> GuiItem?): Component { return object : DynamicComponent(slot, renderer, priority, onClick) { override val updateInterval: Duration? = this@ComponentBuilder.updateInterval - override val props: Map> = _props + override val props: ObjectList> = _props + + override fun onFirstRender(context: LifecycleContext) { + super.onFirstRender(context) + this@ComponentBuilder.onFirstRender?.invoke(context) + } override fun onUpdate(context: LifecycleContext) { super.onUpdate(context) @@ -60,6 +70,9 @@ class ComponentBuilder { return "DSLGeneratedDynamicComponent(updateInterval=$updateInterval, props=$props) ${super.toString()}" } }.also { component -> + component.disabled = this.disabled + component.hidden = this.hidden + ref?.set(component) component.attachedRef = ref } @@ -72,13 +85,9 @@ class ComponentBuilder { fun component( slot: Slot, item: GuiItem, - priority: ComponentPriority = ComponentPriority.NORMAL, - onUpdate: (LifecycleContext.() -> Unit)? = null, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() - componentBuilder.priority = priority - componentBuilder.onUpdate = onUpdate componentBuilder.builder() return componentBuilder.build(slot) { item } } @@ -89,13 +98,9 @@ fun component( fun dynamicComponent( slot: Slot, renderer: (ViewContext) -> GuiItem?, - priority: ComponentPriority = ComponentPriority.NORMAL, - onUpdate: (LifecycleContext.() -> Unit)? = null, builder: ComponentBuilder.() -> Unit = {} ): Component { val componentBuilder = ComponentBuilder() - componentBuilder.priority = priority - componentBuilder.onUpdate = onUpdate componentBuilder.builder() return componentBuilder.build(slot, renderer) } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index aa3fa373..59b29308 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -42,7 +42,7 @@ open class AbstractGuiView : GuiView() { // Register with listener ViewManager.registerView(inventory, this) - renderAllSlots(player, inventory) + renderAllSlots(player, inventory, true) // Open inventory player.openInventory(inventory) @@ -93,15 +93,24 @@ open class AbstractGuiView : GuiView() { super.close(player) } - private fun renderAllSlots(player: Player, inventory: Inventory) { + private fun renderAllSlots( + player: Player, + inventory: Inventory, + firstRender: Boolean + ) { val allSlots = (0 until inventory.size).map { Slot.of(it) } allSlots.forEach { slot -> - renderSlot(player, inventory, slot) + renderSlot(player, inventory, slot, firstRender) } } - private fun renderSlot(player: Player, inventory: Inventory, slot: Slot) { + private fun renderSlot( + player: Player, + inventory: Inventory, + slot: Slot, + firstRender: Boolean = false + ) { if (slot.index >= inventory.size) return val componentsAtSlot = findComponentsBySlot(slot) @@ -113,6 +122,15 @@ open class AbstractGuiView : GuiView() { val component = componentsAtSlot.first() val context = createViewContext(player) + if (firstRender) { + component.onFirstRender( + createLifecycleContext( + player, + LifecycleEventType.FIRST_RENDER + ) + ) + } + // Check if it's a container component val slotsToRender = component.renderSlots(context) @@ -151,7 +169,7 @@ open class AbstractGuiView : GuiView() { inventory.clear() // Re-render all slots - renderAllSlots(player, inventory) + renderAllSlots(player, inventory, false) } /** @@ -174,7 +192,7 @@ open class AbstractGuiView : GuiView() { } collectAllSlots(component).forEach { slot -> - renderSlot(player, inventory, slot) + renderSlot(player, inventory, slot, false) } } 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 index f16535a3..4c767294 100644 --- 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 @@ -123,12 +123,11 @@ abstract class GuiView { ensureInitialized() viewers[player.uniqueId] = player - val context = createViewContext(player) - onOpen(context) + onOpen(createViewContext(player)) if (player.uniqueId !in firstRenderPerViewer) { - val renderContext = createRenderContext(player) - onFirstRender(renderContext) + onFirstRender(createRenderContext(player)) + firstRenderPerViewer.add(player.uniqueId) } } @@ -147,8 +146,7 @@ abstract class GuiView { */ fun update() { viewers.values.forEach { player -> - val context = createViewContext(player) - onUpdate(context) + onUpdate(createViewContext(player)) } } @@ -179,7 +177,7 @@ abstract class GuiView { updateChildrenRecursively(child) } } - + updateChildrenRecursively(component) // Refresh the component's slots to update the visual display 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 index 323931ce..dc3444d0 100644 --- 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 @@ -6,7 +6,7 @@ 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.PaginationComponent +import dev.slne.surf.surfapi.bukkit.api.gui.component.components.PaginationComponent import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext import dev.slne.surf.surfapi.bukkit.api.gui.dsl.dynamicComponent import dev.slne.surf.surfapi.bukkit.api.gui.dsl.slot From b382e2faa387c666717cbe9a2763d3f76965d78d Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 17:47:34 +0100 Subject: [PATCH 77/92] refactor: remove unnecessary line break in slot component rendering logic --- .../dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 59b29308..0353e640 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -114,7 +114,6 @@ open class AbstractGuiView : GuiView() { if (slot.index >= inventory.size) return val componentsAtSlot = findComponentsBySlot(slot) - var rendered = false if (componentsAtSlot.isNotEmpty()) { From 0ba552ae1925032be47730f77ec17a462695a7d3 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 17:47:46 +0100 Subject: [PATCH 78/92] refactor: add onFirstRender logic to PaginationComponent for improved button visibility --- .../gui/component/components/PaginationComponent.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 index 0a89f0da..887933ad 100644 --- 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 @@ -104,6 +104,12 @@ class PaginationComponent( } }), ) { + onFirstRender = { ctx -> + val hasPrev = hasPreviousPage(ctx.player) + + this.hidden = !hasPrev + this.disabled = !hasPrev + } onClick = { previousPage(player) } @@ -133,6 +139,12 @@ class PaginationComponent( } }), ) { + onFirstRender = { ctx -> + val hasNext = hasNextPage(ctx.player) + + this.hidden = !hasNext + this.disabled = !hasNext + } onClick = { nextPage(player) } From 02e7c081e198f264a39903bf967f5c56a6032b8b Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 18:08:27 +0100 Subject: [PATCH 79/92] refactor: implement slot resolution logic for improved rendering and click handling --- .../bukkit/api/gui/dsl/ComponentDsl.kt | 3 +- .../bukkit/api/gui/view/AbstractGuiView.kt | 100 +++++++++--------- .../surfapi/bukkit/api/gui/view/GuiView.kt | 10 ++ .../bukkit/api/gui/view/ResolvedSlot.kt | 13 +++ 4 files changed, 76 insertions(+), 50 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/ResolvedSlot.kt 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 index dd5ad7fb..95e5abff 100644 --- 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 @@ -58,6 +58,7 @@ class ComponentBuilder { override fun onFirstRender(context: LifecycleContext) { super.onFirstRender(context) + println("First render invoked for component at slot $slot hidden: $hidden disabled: $disabled") this@ComponentBuilder.onFirstRender?.invoke(context) } @@ -72,7 +73,7 @@ class ComponentBuilder { }.also { component -> component.disabled = this.disabled component.hidden = this.hidden - + ref?.set(component) component.attachedRef = ref } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 0353e640..66c8944b 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -113,49 +113,18 @@ open class AbstractGuiView : GuiView() { ) { if (slot.index >= inventory.size) return - val componentsAtSlot = findComponentsBySlot(slot) - var rendered = false - - if (componentsAtSlot.isNotEmpty()) { - // Get highest priority component - val component = componentsAtSlot.first() - val context = createViewContext(player) - - if (firstRender) { - component.onFirstRender( - createLifecycleContext( - player, - LifecycleEventType.FIRST_RENDER - ) - ) - } + val resolved = resolveSlot(player, slot) - // Check if it's a container component - val slotsToRender = component.renderSlots(context) - - if (slotsToRender.isNotEmpty()) { - // Only render this slot if it's in the container's output - slotsToRender[slot]?.let { guiItem -> - if (slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) - rendered = true - } - } - } else { - // Regular component - only render at its start slot - if (slot == component.area.first()) { - val guiItem = component.render(context) - if (guiItem != null && slot.index < inventory.size) { - inventory.setItem(slot.index, guiItem.toItemStack()) - rendered = true - } - } - } + if (firstRender && resolved != null) { + resolved.component.onFirstRender( + createLifecycleContext(player, LifecycleEventType.FIRST_RENDER) + ) } - if (!rendered) { - inventory.setItem(slot.index, null) - } + inventory.setItem( + slot.index, + resolved?.guiItem?.toItemStack() + ) } /** @@ -202,6 +171,38 @@ open class AbstractGuiView : GuiView() { 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) { + 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. */ @@ -211,18 +212,19 @@ open class AbstractGuiView : GuiView() { } val slot = Slot.of(event.slot) + val resolved = resolveSlot(player, slot) ?: return - // Find all components at this slot, sorted by priority (highest first) - val componentsAtSlot = findComponentsBySlot(slot) - - // Only the highest priority component handles the click, and only if not disabled - val component = componentsAtSlot.firstOrNull() + if (resolved.component.disabled) return - if (component != null && !component.disabled) { - val clickContext = AbstractClickContext(this, player, event, component) + resolved.component.onClick(createClickContext(player, event, resolved.component)) + } - component.onClick(clickContext) - } + override fun createClickContext( + player: Player, + event: InventoryClickEvent, + component: Component + ): ClickContext { + return AbstractClickContext(this, player, event, component) } override fun createViewContext(player: Player): ViewContext { 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 index 4c767294..0b95f7df 100644 --- 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 @@ -4,6 +4,7 @@ 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 org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent import java.util.* import kotlin.time.Duration @@ -216,6 +217,15 @@ abstract class GuiView { return this } + /** + * Create a click context for a player and click event. + */ + abstract fun createClickContext( + player: Player, + event: InventoryClickEvent, + component: Component, + ): ClickContext + /** * Create a view context for a player. */ 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 00000000..b111497d --- /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)" + } +} From 6737fa0336c6cb584e750d18f81bad14ae4b7c72 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 18:17:06 +0100 Subject: [PATCH 80/92] refactor: update prop handling to use mutableObjectList for improved performance and consistency --- .../components/PaginationComponent.kt | 12 ----------- .../bukkit/api/gui/dsl/ComponentDsl.kt | 21 +++++++++---------- 2 files changed, 10 insertions(+), 23 deletions(-) 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 index 887933ad..0a89f0da 100644 --- 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 @@ -104,12 +104,6 @@ class PaginationComponent( } }), ) { - onFirstRender = { ctx -> - val hasPrev = hasPreviousPage(ctx.player) - - this.hidden = !hasPrev - this.disabled = !hasPrev - } onClick = { previousPage(player) } @@ -139,12 +133,6 @@ class PaginationComponent( } }), ) { - onFirstRender = { ctx -> - val hasNext = hasNextPage(ctx.player) - - this.hidden = !hasNext - this.disabled = !hasNext - } onClick = { nextPage(player) } 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 index 95e5abff..b4594e1c 100644 --- 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 @@ -58,7 +58,6 @@ class ComponentBuilder { override fun onFirstRender(context: LifecycleContext) { super.onFirstRender(context) - println("First render invoked for component at slot $slot hidden: $hidden disabled: $disabled") this@ComponentBuilder.onFirstRender?.invoke(context) } @@ -111,67 +110,67 @@ fun dynamicComponent( */ @ComponentDsl class PropsBuilder { - private val _props = mutableMapOf>() + private val _props = mutableObjectListOf>() /** * Create an immutable prop. */ fun immutable(name: String, value: T): Prop.Immutable { - return Prop.Immutable(name, value).also { _props[name] = it } + 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[name] = it } + 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[name] = it } + 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[name] = it } + 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[name] = it } + 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[name] = it } + 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[name] = it } + return LazyProp.Mutable(name, initializer).also { _props.add(it) } } /** * Get all props. */ - internal fun build(): Map> = _props.toMap() + internal fun build(): ObjectList> = mutableObjectListOf(_props) } /** * Create props using DSL. */ -fun props(builder: PropsBuilder.() -> Unit): Map> { +fun props(builder: PropsBuilder.() -> Unit): ObjectList> { val propsBuilder = PropsBuilder() propsBuilder.builder() return propsBuilder.build() From ea4b26cd8fe3827afec6178570899e676de0488f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:20:49 +0000 Subject: [PATCH 81/92] Add onInit lifecycle hook to components - called before first render - Added onInit() lifecycle hook to Component class - Added INIT to LifecycleEventType enum - onInit called before onFirstRender for components and all children recursively - Allows components to modify state/properties before first render - Tracked via initializedComponents set to ensure onInit called only once per component Co-authored-by: ammodev <32338899+ammodev@users.noreply.github.com> --- .../bukkit/api/gui/component/Component.kt | 6 ++++ .../api/gui/context/LifecycleEventType.kt | 1 + .../bukkit/api/gui/view/AbstractGuiView.kt | 28 +++++++++++++++++++ .../surfapi/bukkit/api/gui/view/GuiView.kt | 5 ++++ 4 files changed, 40 insertions(+) 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 index ebbdd1cd..46b4afe6 100644 --- 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 @@ -99,6 +99,12 @@ abstract class Component { */ internal var attachedRef: Ref? = null + /** + * Called when the component is initialized, before the first render. + * Use this to set up initial state or modify properties before rendering. + */ + open fun onInit(context: LifecycleContext) {} + /** * Called when the component is rendered for the first time. */ 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 index 0596a7ec..20747bf0 100644 --- 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 @@ -4,6 +4,7 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context * Types of lifecycle events. */ enum class LifecycleEventType { + INIT, UPDATE, FIRST_RENDER, OPEN, diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 66c8944b..feddf5af 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -116,6 +116,11 @@ open class AbstractGuiView : GuiView() { val resolved = resolveSlot(player, slot) if (firstRender && resolved != null) { + // Call onInit before onFirstRender if component hasn't been initialized yet + if (!isComponentInitialized(resolved.component)) { + initializeComponentRecursively(resolved.component, player) + } + resolved.component.onFirstRender( createLifecycleContext(player, LifecycleEventType.FIRST_RENDER) ) @@ -126,6 +131,29 @@ open class AbstractGuiView : GuiView() { resolved?.guiItem?.toItemStack() ) } + + /** + * Check if a component has been initialized. + */ + private fun isComponentInitialized(component: Component): Boolean { + return initializedComponents.contains(component) + } + + /** + * Initialize a component and all its children recursively. + */ + private fun initializeComponentRecursively(component: Component, player: Player) { + if (!initializedComponents.contains(component)) { + val lifecycleContext = createLifecycleContext(player, LifecycleEventType.INIT) + component.onInit(lifecycleContext) + initializedComponents.add(component) + + // Initialize all children + component.children.forEach { child -> + initializeComponentRecursively(child, player) + } + } + } /** * Refresh the inventory for a player. 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 index 0b95f7df..57422367 100644 --- 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 @@ -65,6 +65,11 @@ abstract class GuiView { * Whether this view has been rendered for the first time. */ private val firstRenderPerViewer = mutableSetOf() + + /** + * Track which components have been initialized. + */ + private val initializedComponents = mutableSetOf() /** * Current viewers of this view. From 5b3b4f5fc75e993f500136c73f1fd3306f3933f0 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 18:21:33 +0100 Subject: [PATCH 82/92] Revert "Add onInit lifecycle hook to components - called before first render" This reverts commit ea4b26cd8fe3827afec6178570899e676de0488f. --- .../bukkit/api/gui/component/Component.kt | 6 ---- .../api/gui/context/LifecycleEventType.kt | 1 - .../bukkit/api/gui/view/AbstractGuiView.kt | 28 ------------------- .../surfapi/bukkit/api/gui/view/GuiView.kt | 5 ---- 4 files changed, 40 deletions(-) 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 index 46b4afe6..ebbdd1cd 100644 --- 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 @@ -99,12 +99,6 @@ abstract class Component { */ internal var attachedRef: Ref? = null - /** - * Called when the component is initialized, before the first render. - * Use this to set up initial state or modify properties before rendering. - */ - open fun onInit(context: LifecycleContext) {} - /** * Called when the component is rendered for the first time. */ 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 index 20747bf0..0596a7ec 100644 --- 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 @@ -4,7 +4,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context * Types of lifecycle events. */ enum class LifecycleEventType { - INIT, UPDATE, FIRST_RENDER, OPEN, diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index feddf5af..66c8944b 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -116,11 +116,6 @@ open class AbstractGuiView : GuiView() { val resolved = resolveSlot(player, slot) if (firstRender && resolved != null) { - // Call onInit before onFirstRender if component hasn't been initialized yet - if (!isComponentInitialized(resolved.component)) { - initializeComponentRecursively(resolved.component, player) - } - resolved.component.onFirstRender( createLifecycleContext(player, LifecycleEventType.FIRST_RENDER) ) @@ -131,29 +126,6 @@ open class AbstractGuiView : GuiView() { resolved?.guiItem?.toItemStack() ) } - - /** - * Check if a component has been initialized. - */ - private fun isComponentInitialized(component: Component): Boolean { - return initializedComponents.contains(component) - } - - /** - * Initialize a component and all its children recursively. - */ - private fun initializeComponentRecursively(component: Component, player: Player) { - if (!initializedComponents.contains(component)) { - val lifecycleContext = createLifecycleContext(player, LifecycleEventType.INIT) - component.onInit(lifecycleContext) - initializedComponents.add(component) - - // Initialize all children - component.children.forEach { child -> - initializeComponentRecursively(child, player) - } - } - } /** * Refresh the inventory for a player. 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 index 57422367..0b95f7df 100644 --- 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 @@ -65,11 +65,6 @@ abstract class GuiView { * Whether this view has been rendered for the first time. */ private val firstRenderPerViewer = mutableSetOf() - - /** - * Track which components have been initialized. - */ - private val initializedComponents = mutableSetOf() /** * Current viewers of this view. From 487194f7022a84a62dd028e9fe4b1e834d52f34f Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 18:29:31 +0100 Subject: [PATCH 83/92] refactor: replace mutable collections with fastutil equivalents for improved performance --- .../surfapi/bukkit/api/gui/area/CircularArea.kt | 7 +++++-- .../bukkit/api/gui/area/ComponentArea.kt | 3 ++- .../surfapi/bukkit/api/gui/area/CuboidArea.kt | 8 ++++++-- .../bukkit/api/gui/area/SingleSlotArea.kt | 6 ++++-- .../bukkit/api/gui/component/Component.kt | 13 ++++++++----- .../component/components/ContainerComponent.kt | 3 ++- .../component/components/PaginationComponent.kt | 17 +++++++++++------ .../context/abstract/AbstractRenderContext.kt | 1 + .../surf/surfapi/bukkit/api/gui/view/GuiView.kt | 15 +++++++++------ .../bukkit/test/gui/PaginatedShopGuiView.kt | 3 ++- 10 files changed, 50 insertions(+), 26 deletions(-) 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 index b7547c3e..624ef979 100644 --- 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 @@ -1,6 +1,8 @@ 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 @@ -12,8 +14,8 @@ data class CircularArea( val center: Slot, val radius: Double ) : ComponentArea { - override fun slots(): Set { - val slots = mutableSetOf() + override fun slots(): ObjectSet { + val slots = mutableObjectSetOf() val radiusCeil = ceil(radius).toInt() // Check all slots within the bounding box @@ -33,6 +35,7 @@ data class CircularArea( val dx = (slot.column - center.column).toDouble() val dy = (slot.row - center.row).toDouble() val distance = sqrt(dx * dx + dy * dy) + return distance <= radius } 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 index bd45e447..087fbd9e 100644 --- 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 @@ -1,6 +1,7 @@ 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. @@ -10,7 +11,7 @@ interface ComponentArea { /** * Get all slots that are part of this area. */ - fun slots(): Set + fun slots(): ObjectSet /** * Check if a slot is within this area. 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 index 112be87b..e77eea45 100644 --- 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 @@ -1,6 +1,8 @@ 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. @@ -10,13 +12,15 @@ data class CuboidArea( val startSlot: Slot, val endSlot: Slot ) : ComponentArea { - override fun slots(): Set { - val slots = mutableSetOf() + 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 } 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 index 2cc82e6f..f5e33c54 100644 --- 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 @@ -1,6 +1,8 @@ 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. @@ -9,10 +11,10 @@ import dev.slne.surf.surfapi.bukkit.api.gui.Slot data class SingleSlotArea( val slot: Slot ) : ComponentArea { - override fun slots(): Set = setOf(slot) + 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)" } 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 index ebbdd1cd..a4545088 100644 --- 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 @@ -9,6 +9,9 @@ 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 kotlin.time.Duration @@ -58,8 +61,8 @@ abstract class Component { /** * The children of this component. */ - private val _children = mutableListOf() - val children: List get() = _children.toList() + private val _children = mutableObjectListOf() + val children: ObjectList get() = _children.freeze() /** * The view this component belongs to. @@ -92,7 +95,7 @@ abstract class Component { * Props accessible by this component. * Children can access parent props. */ - protected open val props: MutableList> = mutableListOf() + protected open val props: ObjectList> = mutableObjectListOf() /** * Ref attached to this component, if any. @@ -147,8 +150,8 @@ abstract class Component { /** * Get all props including parent props. */ - fun getAllProps(): List> { - val allProps = mutableListOf>() + fun getAllProps(): ObjectList> { + val allProps = mutableObjectListOf>() parent?.getAllProps()?.let { allProps.addAll(it) } allProps.addAll(props) 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 index 6f098d78..2ee37645 100644 --- 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 @@ -6,6 +6,7 @@ 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. @@ -20,7 +21,7 @@ abstract class ContainerComponent( * Render multiple items at their respective slots. * Override this to provide the slot-to-item mapping. */ - abstract override fun renderSlots(context: ViewContext): Map + abstract override fun renderSlots(context: ViewContext): Object2ObjectMap /** * Container doesn't render a single item. 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 index 0a89f0da..ed819e7e 100644 --- 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 @@ -14,6 +14,11 @@ 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 @@ -27,7 +32,7 @@ import org.bukkit.entity.Player class PaginationComponent( startSlot: Slot, endSlot: Slot, - private val items: () -> List, + private val items: () -> ObjectList, private val itemRenderer: (T, ViewContext) -> GuiItem?, override val priority: ComponentPriority = ComponentPriority.NORMAL, private val onItemClick: ((T, ClickContext) -> Unit)? = null, @@ -157,16 +162,16 @@ class PaginationComponent( /** * Get the items for the current page of a viewer. */ - fun getPageItems(viewer: Player): List { + 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) + allItems.subList(startIndex, endIndex).toObjectList() } else { - emptyList() + objectListOf() } } @@ -244,9 +249,9 @@ class PaginationComponent( updateNavigationButtonsState(context.player) } - override fun renderSlots(context: ViewContext): Map { + override fun renderSlots(context: ViewContext): Object2ObjectMap { val pageItems = getPageItems(context.player) - val renderedSlots = mutableMapOf() + val renderedSlots = mutableObject2ObjectMapOf() pageItems.forEachIndexed { index, item -> val guiItem = itemRenderer(item, context) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt index d88e1b92..a64b4e9f 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt @@ -30,6 +30,7 @@ class AbstractRenderContext( override fun setItem(slot: Slot, item: GuiItem) { val inventory = player.openInventory.topInventory + if (slot.index in 0 until inventory.size) { inventory.setItem(slot.index, item.toItemStack()) } 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 index 0b95f7df..874f2a6c 100644 --- 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 @@ -3,6 +3,8 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view 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.core.api.util.* +import it.unimi.dsi.fastutil.objects.ObjectList import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent import java.util.* @@ -28,15 +30,15 @@ abstract class GuiView { * Components in this view. * Components can overlap, and priority determines rendering and click handling order. */ - private val _components = mutableListOf() - val components: List get() = _components.toList() + 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): List { - val allComponents = mutableListOf() + fun findComponentsBySlot(slot: Slot): ObjectList { + val allComponents = mutableObjectListOf() // Helper function to recursively collect components and their children fun collectComponents(component: Component) { @@ -54,6 +56,7 @@ abstract class GuiView { return allComponents .filter { it.contains(slot) && !it.hidden } .sortedByDescending { it.priority.value } + .toObjectList() } /** @@ -64,12 +67,12 @@ abstract class GuiView { /** * Whether this view has been rendered for the first time. */ - private val firstRenderPerViewer = mutableSetOf() + private val firstRenderPerViewer = mutableObjectSetOf() /** * Current viewers of this view. */ - private val viewers = mutableMapOf() + private val viewers = mutableObject2ObjectMapOf() /** * Configuration for this view. 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 index dc3444d0..bcc4c26c 100644 --- 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 @@ -16,6 +16,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig 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.event.inventory.InventoryType @@ -31,7 +32,7 @@ class PaginatedShopGuiView : AbstractGuiView() { private val shopItems by lazy { Material.entries.filter { it.isItem && !it.isAir - }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled() + }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled().toObjectList() } // Pagination component - includes built-in navigation buttons From bda196f8b2dc2d2e8308d2d3ca6ca02230f42b8a Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 18:31:49 +0100 Subject: [PATCH 84/92] refactor: comment out Hytale API modules in settings.gradle.kts --- settings.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 49471d0f..313c4f68 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") From 1d88f391a2f4598b63f802cec2657edae36b8d18 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 18:31:58 +0100 Subject: [PATCH 85/92] refactor: remove unused update interval logic and related jobs from GUI classes --- .../bukkit/api/gui/component/Component.kt | 8 +--- .../bukkit/api/gui/dsl/ComponentDsl.kt | 5 +-- .../bukkit/api/gui/view/AbstractGuiView.kt | 42 +------------------ .../surfapi/bukkit/api/gui/view/GuiView.kt | 8 +--- 4 files changed, 4 insertions(+), 59 deletions(-) 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 index a4545088..f3a13ba3 100644 --- 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 @@ -13,7 +13,6 @@ 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 kotlin.time.Duration /** * Base interface for all GUI components. @@ -76,11 +75,6 @@ abstract class Component { } } - /** - * Update interval for this component, if any. - */ - open val updateInterval: Duration? = null - /** * Whether this component is hidden (not rendered at all). */ @@ -167,6 +161,6 @@ abstract class Component { } override fun toString(): String { - return "Component(area=$area, priority=$priority, width=$width, height=$height, children=$children, view=$view, updateInterval=$updateInterval, hidden=$hidden, disabled=$disabled, props=$props, attachedRef=${attachedRef.hashCode()})" + return "Component(area=$area, priority=$priority, width=$width, height=$height, children=$children, view=$view, hidden=$hidden, disabled=$disabled, props=$props, attachedRef=${attachedRef.hashCode()})" } } 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 index b4594e1c..804da140 100644 --- 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 @@ -17,7 +17,6 @@ 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 -import kotlin.time.Duration /** * DSL marker for component building. @@ -30,7 +29,6 @@ annotation class ComponentDsl */ @ComponentDsl class ComponentBuilder { - var updateInterval: Duration? = null var ref: Ref? = null var priority: ComponentPriority = ComponentPriority.NORMAL var onFirstRender: (LifecycleContext.() -> Unit)? = null @@ -53,7 +51,6 @@ class ComponentBuilder { */ internal fun build(slot: Slot, renderer: (ViewContext) -> GuiItem?): Component { return object : DynamicComponent(slot, renderer, priority, onClick) { - override val updateInterval: Duration? = this@ComponentBuilder.updateInterval override val props: ObjectList> = _props override fun onFirstRender(context: LifecycleContext) { @@ -67,7 +64,7 @@ class ComponentBuilder { } override fun toString(): String { - return "DSLGeneratedDynamicComponent(updateInterval=$updateInterval, props=$props) ${super.toString()}" + return "DSLGeneratedDynamicComponent(props=$props) ${super.toString()}" } }.also { component -> component.disabled = this.disabled diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 66c8944b..c7cc49cf 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -5,10 +5,7 @@ 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.bukkit.api.surfBukkitApi import dev.slne.surf.surfapi.core.api.util.InternalSurfApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import org.bukkit.Bukkit import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent @@ -20,8 +17,6 @@ import java.util.concurrent.ConcurrentHashMap @InternalSurfApi open class AbstractGuiView : GuiView() { private val inventories = ConcurrentHashMap() - private val updateJobs = ConcurrentHashMap() - private val componentJobs = ConcurrentHashMap>() override fun open(player: Player) { super.open(player) @@ -46,44 +41,9 @@ open class AbstractGuiView : GuiView() { // Open inventory player.openInventory(inventory) - - // Start update task if configured (Folia-compatible) - updateInterval?.let { interval -> - val job = surfBukkitApi.launch(surfBukkitApi.entityDispatcher(player)) { - while (true) { - delay(interval) - update() - } - } - updateJobs[player.uniqueId] = job - } - - // Start component update tasks (Folia-compatible) - val playerComponentJobs = mutableListOf() - components.forEach { component -> - component.updateInterval?.let { interval -> - val job = surfBukkitApi.launch(surfBukkitApi.entityDispatcher(player)) { - while (true) { - delay(interval) - val lifecycleContext = - createLifecycleContext(player, LifecycleEventType.UPDATE) - component.onUpdate(lifecycleContext) - refreshComponentSlots(player, component) - } - } - playerComponentJobs.add(job) - } - } - if (playerComponentJobs.isNotEmpty()) { - componentJobs[player.uniqueId] = playerComponentJobs - } } override fun close(player: Player) { - // Cancel all update jobs - updateJobs.remove(player.uniqueId)?.cancel() - componentJobs.remove(player.uniqueId)?.forEach { it.cancel() } - // Unregister inventory // TODO: Fix, this currently can cause issues if multiple inventories are open inventories.remove(player.uniqueId)?.let { inventory -> @@ -247,6 +207,6 @@ open class AbstractGuiView : GuiView() { } override fun toString(): String { - return "AbstractGuiView(inventories=$inventories, updateJobs=$updateJobs, componentJobs=$componentJobs)" + return "AbstractGuiView(inventories=$inventories)" } } \ 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/GuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt index 874f2a6c..1f9d7540 100644 --- 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 @@ -8,7 +8,6 @@ import it.unimi.dsi.fastutil.objects.ObjectList import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent import java.util.* -import kotlin.time.Duration /** * Base class for all GUI views. @@ -21,11 +20,6 @@ abstract class GuiView { var parent: GuiView? = null internal set - /** - * Update interval for the entire view. - */ - open val updateInterval: Duration? = null - /** * Components in this view. * Components can overlap, and priority determines rendering and click handling order. @@ -256,7 +250,7 @@ abstract class GuiView { ): ResumeContext override fun toString(): String { - return "GuiView(parent=$parent, updateInterval=$updateInterval, components=$components, initialized=$initialized, firstRenderPerViewer=$firstRenderPerViewer, viewers=$viewers, config=$config)" + return "GuiView(parent=$parent, components=$components, initialized=$initialized, firstRenderPerViewer=$firstRenderPerViewer, viewers=$viewers, config=$config)" } } From 43e5ce3f7a90a179c648caf949bd2fe6fdb96e1f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 17:42:25 +0000 Subject: [PATCH 86/92] chore: update ABI and bump version [skip ci] --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 690b873b..1d392cd5 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 From 372f3305f9cdeb50e96e510954c717a2f228befc Mon Sep 17 00:00:00 2001 From: twisti Date: Wed, 4 Feb 2026 19:05:52 +0100 Subject: [PATCH 87/92] feat: implement component initialization logic and update imports for shared API --- .../surfapi/bukkit/api/gui/component/Component.kt | 6 ++++-- .../gui/component/components/PaginationComponent.kt | 13 +++++++++++++ .../bukkit/api/gui/context/LifecycleEventType.kt | 1 + .../gui/context/abstract/AbstractClickContext.kt | 2 +- .../context/abstract/AbstractLifecycleContext.kt | 2 +- .../gui/context/abstract/AbstractRenderContext.kt | 2 +- .../gui/context/abstract/AbstractResumeContext.kt | 2 +- .../api/gui/context/abstract/AbstractViewContext.kt | 2 +- .../api/gui/context/abstract/NavigationHelper.kt | 2 +- .../surf/surfapi/bukkit/api/gui/dsl/ComponentDsl.kt | 11 ++++++++--- .../surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 3 ++- 11 files changed, 34 insertions(+), 12 deletions(-) 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 index f3a13ba3..2b0b140d 100644 --- 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 @@ -78,12 +78,12 @@ abstract class Component { /** * Whether this component is hidden (not rendered at all). */ - var hidden: Boolean = false + open var hidden: Boolean = false /** * Whether this component is disabled (rendered but onClick is blocked). */ - var disabled: Boolean = false + open var disabled: Boolean = false /** * Props accessible by this component. @@ -96,6 +96,8 @@ abstract class Component { */ internal var attachedRef: Ref? = null + open fun initComponent(context: LifecycleContext) {} + /** * Called when the component is rendered for the first time. */ 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 index ed819e7e..4ebb3ed8 100644 --- 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 @@ -109,6 +109,13 @@ class PaginationComponent( } }), ) { + initComponent = { + if (!hasPreviousPage(player)) { + disabled = true + hidden = true + } + } + onClick = { previousPage(player) } @@ -138,6 +145,12 @@ class PaginationComponent( } }), ) { + initComponent = { + if (!hasNextPage(player)) { + disabled = true + hidden = true + } + } onClick = { nextPage(player) } 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 index 0596a7ec..49991849 100644 --- 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 @@ -5,6 +5,7 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context */ enum class LifecycleEventType { UPDATE, + INIT_COMPONENT, FIRST_RENDER, OPEN, CLOSE, 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 index 56939924..29f8b6dc 100644 --- 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 @@ -3,7 +3,7 @@ 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.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent 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 index 3874f349..80dc3cf3 100644 --- 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 @@ -3,7 +3,7 @@ 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.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player @InternalSurfApi diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt index a64b4e9f..a87d395f 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt @@ -7,7 +7,7 @@ import dev.slne.surf.surfapi.bukkit.api.gui.context.RenderContext import dev.slne.surf.surfapi.bukkit.api.gui.toItemStack import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player @InternalSurfApi 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 index cf357f65..08f95413 100644 --- 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 @@ -3,7 +3,7 @@ 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.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player @InternalSurfApi 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 index 2a026267..a0a14b94 100644 --- 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 @@ -2,7 +2,7 @@ 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.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player @InternalSurfApi 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 index ec643b9d..00f574e9 100644 --- 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 @@ -2,7 +2,7 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView -import dev.slne.surf.surfapi.core.api.util.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player @InternalSurfApi 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 index 804da140..4c97c0eb 100644 --- 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 @@ -31,6 +31,7 @@ annotation class ComponentDsl class ComponentBuilder { var ref: Ref? = null var priority: ComponentPriority = ComponentPriority.NORMAL + var initComponent: (LifecycleContext.() -> Unit)? = null var onFirstRender: (LifecycleContext.() -> Unit)? = null var onUpdate: (LifecycleContext.() -> Unit)? = null var onClick: (ClickContext.() -> Unit)? = null @@ -52,6 +53,13 @@ class ComponentBuilder { 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 fun initComponent(context: LifecycleContext) { + super.initComponent(context) + this@ComponentBuilder.initComponent?.invoke(context) + } override fun onFirstRender(context: LifecycleContext) { super.onFirstRender(context) @@ -67,9 +75,6 @@ class ComponentBuilder { return "DSLGeneratedDynamicComponent(props=$props) ${super.toString()}" } }.also { component -> - component.disabled = this.disabled - component.hidden = this.hidden - ref?.set(component) component.attachedRef = ref } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index c7cc49cf..13c38edc 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -5,7 +5,7 @@ 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.InternalSurfApi +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.Bukkit import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryClickEvent @@ -145,6 +145,7 @@ open class AbstractGuiView : GuiView() { val viewContext = createViewContext(player) for (component in candidates) { + component.initComponent(createLifecycleContext(player, LifecycleEventType.INIT_COMPONENT)) if (component.hidden) continue val slots = component.renderSlots(viewContext) From c1daf1e08630773735efc6f02d87f318c8bd31e7 Mon Sep 17 00:00:00 2001 From: twisti Date: Wed, 4 Feb 2026 19:08:02 +0100 Subject: [PATCH 88/92] refactor: remove unused launch and entityDispatcher methods from SurfBukkitApi --- .../dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt | 11 ----------- .../surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt index 2f9e246d..69b929c1 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/SurfBukkitApi.kt @@ -5,15 +5,11 @@ import dev.slne.surf.surfapi.bukkit.api.scoreboard.SurfScoreboardBuilder import dev.slne.surf.surfapi.bukkit.api.time.SkipOperations.SkipOperation import dev.slne.surf.surfapi.bukkit.api.time.TimeSkipResult import dev.slne.surf.surfapi.core.api.SurfCoreApi -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import net.kyori.adventure.text.Component import net.megavex.scoreboardlibrary.api.ScoreboardLibrary import org.bukkit.World -import org.bukkit.entity.Entity import org.bukkit.entity.Player import org.jetbrains.annotations.ApiStatus -import kotlin.coroutines.CoroutineContext /** * Represents the API for SurfBukkit. @@ -108,13 +104,6 @@ interface SurfBukkitApi : SurfCoreApi { */ suspend fun skipTimeSmoothly(skipOperation: SkipOperation): Map - fun launch( - context: CoroutineContext, - block: suspend CoroutineScope.() -> Unit - ): Job - - fun entityDispatcher(entity: Entity): CoroutineContext - companion object { @JvmStatic val instance get() = SurfCoreApi.instance as SurfBukkitApi diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt index 148fcec5..2bd4b342 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt +++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/SurfBukkitApiImpl.kt @@ -1,7 +1,5 @@ package dev.slne.surf.surfapi.bukkit.server.impl -import com.github.shynixn.mccoroutine.folia.entityDispatcher -import com.github.shynixn.mccoroutine.folia.launch import com.google.auto.service.AutoService import com.google.common.io.ByteStreams import dev.slne.surf.surfapi.bukkit.api.SurfBukkitApi @@ -16,15 +14,12 @@ import dev.slne.surf.surfapi.core.api.SurfCoreApi import dev.slne.surf.surfapi.core.api.util.checkInstantiationByServiceLoader import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf import dev.slne.surf.surfapi.core.server.impl.SurfCoreApiImpl -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.World -import org.bukkit.entity.Entity import java.util.* -import kotlin.coroutines.CoroutineContext @AutoService(SurfCoreApi::class) class SurfBukkitApiImpl : SurfCoreApiImpl(), SurfBukkitApi { @@ -108,10 +103,4 @@ class SurfBukkitApiImpl : SurfCoreApiImpl(), SurfBukkitApi { } }.mapValuesTo(mutableObject2ObjectMapOf(worlds.size)) { (_, def) -> def.await() } } - - override fun launch(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit) = - plugin.launch(context = context, block = block) - - override fun entityDispatcher(entity: Entity) = - plugin.entityDispatcher(entity) } From b75d213f7610f5df2b77ad36d5245d14ab02475d Mon Sep 17 00:00:00 2001 From: twisti Date: Wed, 4 Feb 2026 19:59:17 +0100 Subject: [PATCH 89/92] feat: implement new GUI framework with improved view management and initialization context --- .../bukkit/api/gui/component/Component.kt | 15 ++++ ...{RenderContext.kt => InitializeContext.kt} | 8 +- .../abstract/AbstractInitializeContext.kt | 40 +++++++++ .../context/abstract/AbstractRenderContext.kt | 58 ------------- .../bukkit/api/gui/dsl/ComponentDsl.kt | 6 +- .../bukkit/api/gui/view/AbstractGuiView.kt | 66 +++----------- .../surfapi/bukkit/api/gui/view/GuiView.kt | 87 +++++++++++++------ .../bukkit/api/gui/view/ViewManager.kt | 17 +--- .../surfapi/bukkit/test/gui/CounterGuiView.kt | 13 ++- .../bukkit/test/gui/PaginatedShopGuiView.kt | 14 ++- .../bukkit/server/gui/view/GuiViewListener.kt | 7 +- .../bukkit/server/gui/view/ViewManagerImpl.kt | 32 +++---- 12 files changed, 168 insertions(+), 195 deletions(-) rename surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/{RenderContext.kt => InitializeContext.kt} (78%) create mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractInitializeContext.kt delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt 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 index 2b0b140d..01985c8f 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -13,6 +14,8 @@ 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. @@ -57,6 +60,18 @@ abstract class Component { var parent: Component? = null internal set + 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. */ diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/InitializeContext.kt similarity index 78% rename from surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt rename to surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/InitializeContext.kt index 7b3c25fb..60b0738e 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/RenderContext.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/InitializeContext.kt @@ -3,11 +3,17 @@ 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 RenderContext : ViewContext { +interface InitializeContext { + /** + * Get the view configuration. + */ + fun config(): ViewConfig + /** * Render a component. * The component contains its own slot information (startSlot/endSlot). 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 00000000..f380b286 --- /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/AbstractRenderContext.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt deleted file mode 100644 index a87d395f..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/context/abstract/AbstractRenderContext.kt +++ /dev/null @@ -1,58 +0,0 @@ -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.RenderContext -import dev.slne.surf.surfapi.bukkit.api.gui.toItemStack -import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView -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 AbstractRenderContext( - override val view: GuiView, - override val player: Player, - private val bukkitView: AbstractGuiView -) : RenderContext { - override fun renderComponent(component: Component) { - bukkitView.addComponent(component) - } - - override fun clearSlot(slot: Slot) { - // Find and remove all components that occupy this slot - val componentsToRemove = bukkitView.findComponentsBySlot(slot) - componentsToRemove.forEach { component -> - bukkitView.removeComponent(component) - } - } - - override fun setItem(slot: Slot, item: GuiItem) { - val inventory = player.openInventory.topInventory - - if (slot.index in 0 until inventory.size) { - inventory.setItem(slot.index, item.toItemStack()) - } - } - - 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 "AbstractRenderContext(view=$view, player=$player, bukkitView=$bukkitView)" - } -} \ 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 index 4c97c0eb..0c458cb4 100644 --- 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 @@ -7,8 +7,8 @@ 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.RenderContext 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 @@ -182,7 +182,7 @@ fun props(builder: PropsBuilder.() -> Unit): ObjectList> { * DSL for rendering components in a view. */ @ComponentDsl -fun RenderContext.slot(component: Component) { +fun InitializeContext.slot(component: Component) { renderComponent(component) } @@ -190,7 +190,7 @@ fun RenderContext.slot(component: Component) { * DSL for rendering components in a view with item at a specific slot. */ @ComponentDsl -fun RenderContext.slot( +fun InitializeContext.slot( slot: Slot, item: GuiItem, priority: ComponentPriority = ComponentPriority.NORMAL, diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 13c38edc..8e4a8be7 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -6,80 +6,42 @@ 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.shared.api.util.InternalSurfApi -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 java.util.* -import java.util.concurrent.ConcurrentHashMap @InternalSurfApi open class AbstractGuiView : GuiView() { - private val inventories = ConcurrentHashMap() - override fun open(player: Player) { super.open(player) - // Create inventory based on type - val inventory = when (config.type) { - InventoryType.CHEST -> { - Bukkit.createInventory(null, config.size, config.title) - } - - else -> { - Bukkit.createInventory(null, config.type, config.title) - } - } - - inventories[player.uniqueId] = inventory - - // Register with listener - ViewManager.registerView(inventory, this) - - renderAllSlots(player, inventory, true) + ViewManager.setActiveView(player, this) + renderAllSlots(player) // Open inventory player.openInventory(inventory) } override fun close(player: Player) { - // Unregister inventory - // TODO: Fix, this currently can cause issues if multiple inventories are open - inventories.remove(player.uniqueId)?.let { inventory -> - ViewManager.unregisterView(inventory) - } - + ViewManager.removeActiveView(player) super.close(player) } - private fun renderAllSlots( - player: Player, - inventory: Inventory, - firstRender: Boolean - ) { + private fun renderAllSlots(player: Player) { val allSlots = (0 until inventory.size).map { Slot.of(it) } allSlots.forEach { slot -> - renderSlot(player, inventory, slot, firstRender) + renderSlot(player, slot) } } private fun renderSlot( player: Player, - inventory: Inventory, - slot: Slot, - firstRender: Boolean = false + slot: Slot ) { if (slot.index >= inventory.size) return val resolved = resolveSlot(player, slot) - - if (firstRender && resolved != null) { - resolved.component.onFirstRender( - createLifecycleContext(player, LifecycleEventType.FIRST_RENDER) - ) - } + resolved?.component?.renderFirstRenderPerPlayer(player) inventory.setItem( slot.index, @@ -91,21 +53,17 @@ open class AbstractGuiView : GuiView() { * Refresh the inventory for a player. */ internal fun refreshInventory(player: Player) { - val inventory = inventories[player.uniqueId] ?: return - // Clear inventory first inventory.clear() // Re-render all slots - renderAllSlots(player, inventory, false) + renderAllSlots(player) } /** * Refresh all slots occupied by a specific component and its children. */ internal fun refreshComponentSlots(player: Player, component: Component) { - val inventory = inventories[player.uniqueId] ?: return - // 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 @@ -120,7 +78,7 @@ open class AbstractGuiView : GuiView() { } collectAllSlots(component).forEach { slot -> - renderSlot(player, inventory, slot, false) + renderSlot(player, slot) } } @@ -192,8 +150,8 @@ open class AbstractGuiView : GuiView() { return AbstractViewContext(this, player) } - override fun createRenderContext(player: Player): RenderContext { - return AbstractRenderContext(this, player, this) + override fun createInitializeContext(): InitializeContext { + return AbstractInitializeContext(config, this) } override fun createLifecycleContext( @@ -208,6 +166,6 @@ open class AbstractGuiView : GuiView() { } override fun toString(): String { - return "AbstractGuiView(inventories=$inventories)" + return "AbstractGuiView() ${super.toString()}" } } \ 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/GuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt index 1f9d7540..31870fc3 100644 --- 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 @@ -1,13 +1,21 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view +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.core.api.util.* +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 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 java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.properties.Delegates /** * Base class for all GUI views. @@ -20,6 +28,9 @@ abstract class GuiView { 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. @@ -56,39 +67,44 @@ abstract class GuiView { /** * Whether this view has been initialized. */ + @Volatile private var initialized = false - /** - * Whether this view has been rendered for the first time. - */ - private val firstRenderPerViewer = mutableObjectSetOf() - /** * Current viewers of this view. */ - private val viewers = mutableObject2ObjectMapOf() + private val viewers = ConcurrentHashMap.newKeySet() /** * Configuration for this view. */ - val config = ViewConfig() + internal val config = ViewConfig() + + protected fun init() { + onInit(createInitializeContext()) + + this.inventory = when (config.type) { + InventoryType.CHEST -> { + Bukkit.createInventory(null, config.size, config.title) + } + + else -> { + Bukkit.createInventory(null, config.type, config.title) + } + } + } /** * Initialize the view configuration. * Called once when the view is first created. */ - open fun onInit(config: ViewConfig) {} + open fun onInit(context: InitializeContext) {} /** * Called when the view is opened for a player. */ open fun onOpen(context: ViewContext) {} - /** - * Called the first time the view is rendered for a player. - */ - open fun onFirstRender(context: RenderContext) {} - /** * Called when the view is updated. */ @@ -109,9 +125,30 @@ abstract class GuiView { */ internal fun ensureInitialized() { if (!initialized) { - onInit(config) - initialized = true + 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 } /** @@ -119,15 +156,9 @@ abstract class GuiView { */ open fun open(player: Player) { ensureInitialized() - viewers[player.uniqueId] = player + viewers.add(player.uniqueId) onOpen(createViewContext(player)) - - if (player.uniqueId !in firstRenderPerViewer) { - onFirstRender(createRenderContext(player)) - - firstRenderPerViewer.add(player.uniqueId) - } } /** @@ -143,7 +174,7 @@ abstract class GuiView { * Update the view for all viewers. */ fun update() { - viewers.values.forEach { player -> + viewerPlayers().forEach { player -> onUpdate(createViewContext(player)) } } @@ -156,9 +187,9 @@ abstract class GuiView { */ internal fun updateComponent(component: Component, viewer: Player? = null) { val viewersToUpdate = if (viewer != null) { - listOfNotNull(viewers[viewer.uniqueId]) + listOf(viewer) } else { - viewers.values.toList() + viewerPlayers() } viewersToUpdate.forEach { player -> @@ -231,7 +262,7 @@ abstract class GuiView { /** * Create a render context for a player. */ - abstract fun createRenderContext(player: Player): RenderContext + abstract fun createInitializeContext(): InitializeContext /** * Create a lifecycle context for a player. @@ -250,7 +281,7 @@ abstract class GuiView { ): ResumeContext override fun toString(): String { - return "GuiView(parent=$parent, components=$components, initialized=$initialized, firstRenderPerViewer=$firstRenderPerViewer, viewers=$viewers, config=$config)" + return "GuiView(parent=$parent, components=$components, initialized=$initialized, viewers=$viewers, config=$config)" } } 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 index 295489fd..ce827672 100644 --- 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 @@ -1,23 +1,14 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view import dev.slne.surf.surfapi.core.api.util.requiredService -import it.unimi.dsi.fastutil.objects.Object2ObjectMap -import org.bukkit.inventory.Inventory -import org.jetbrains.annotations.Unmodifiable +import org.bukkit.entity.Player private val viewManager = requiredService() interface ViewManager { - val views: @Unmodifiable Object2ObjectMap - - fun registerView(inventory: Inventory, view: GuiView) - - fun unregisterView(view: GuiView) - fun unregisterView(inventory: Inventory) - - fun findByInventory(inventory: Inventory): GuiView? { - return views[inventory] - } + fun getActiveView(player: Player): GuiView? + fun setActiveView(player: Player, view: GuiView) + fun removeActiveView(player: Player) companion object : ViewManager by viewManager { val INSTANCE get() = viewManager 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 index efe66bc9..44bc1036 100644 --- 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 @@ -7,14 +7,13 @@ 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.RenderContext +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.AbstractGuiView -import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig import dev.slne.surf.surfapi.bukkit.test.plugin import dev.slne.surf.surfapi.core.api.messages.adventure.text import kotlinx.coroutines.runBlocking @@ -29,13 +28,11 @@ object CounterGuiView : AbstractGuiView() { private val counterProp = Prop.Mutable("counter", 0) private val counterDisplayRef = Ref() - override fun onInit(config: ViewConfig) { - config.type = InventoryType.CHEST - config.rows = 3 - config.title = text("Counter Gui") - } + override fun onInit(context: InitializeContext) { + context.config().type = InventoryType.CHEST + context.config().rows = 3 + context.config().title = text("Counter Gui") - override fun onFirstRender(context: RenderContext) { // Counter display at top center context.slot( dynamicComponent( 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 index bcc4c26c..85251e2a 100644 --- 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 @@ -7,13 +7,12 @@ 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.RenderContext +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.AbstractGuiView -import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewConfig 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 @@ -80,14 +79,11 @@ class PaginatedShopGuiView : AbstractGuiView() { // Navigation buttons will be automatically created and centered in the last row ) - override fun onInit(config: ViewConfig) { - config.type = InventoryType.CHEST - config.rows = 6 - config.title = text("Shop") - } + override fun onInit(context: InitializeContext) { + context.config().type = InventoryType.CHEST + context.config().rows = 6 + context.config().title = text("Shop") - override fun onFirstRender(context: RenderContext) { - // Coins display at top center context.slot( dynamicComponent( Slot.at(4, 0), 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 index 554f7ca1..8f110084 100644 --- 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 @@ -14,9 +14,8 @@ import org.bukkit.event.inventory.InventoryCloseEvent object GuiViewListener : Listener { @EventHandler fun onInventoryClick(event: InventoryClickEvent) { - val inventory = event.clickedInventory ?: return - val view = ViewManager.findByInventory(inventory) ?: return val player = event.whoClicked as? Player ?: return + val view = ViewManager.getActiveView(player) ?: return if (view is AbstractGuiView) { view.handleClick(player, event) @@ -25,10 +24,8 @@ object GuiViewListener : Listener { @EventHandler fun onInventoryClose(event: InventoryCloseEvent) { - val inventory = event.inventory - val view = ViewManager.findByInventory(inventory) ?: return 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 index 0014ccc9..078f85ab 100644 --- 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 @@ -1,34 +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 dev.slne.surf.surfapi.core.api.util.freeze -import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf -import org.bukkit.inventory.Inventory +import org.bukkit.entity.Player +import java.util.* @AutoService(ViewManager::class) class ViewManagerImpl : ViewManager { - private val _views = mutableObject2ObjectMapOf() - override val views get() = _views.freeze() + private val activePerPlayer = Caffeine.newBuilder() + .weakValues() + .build() - override fun registerView( - inventory: Inventory, - view: GuiView - ) { - _views[inventory] = view + override fun getActiveView(player: Player): GuiView? { + return activePerPlayer.getIfPresent(player.uniqueId) } - override fun unregisterView(view: GuiView) { - val entry = _views.entries.firstOrNull { it.value == view } ?: return - _views.remove(entry.key) + override fun setActiveView( + player: Player, + view: GuiView + ) { + activePerPlayer.put(player.uniqueId, view) } - override fun unregisterView(inventory: Inventory) { - _views.remove(inventory) + override fun removeActiveView(player: Player) { + activePerPlayer.invalidate(player.uniqueId) } override fun toString(): String { - return "ViewManagerImpl(views=$views)" + return "ViewManagerImpl(activePerPlayer=$activePerPlayer)" } } \ No newline at end of file From 0857823d8f456c993be9164f9a1db27d6324b858 Mon Sep 17 00:00:00 2001 From: twisti Date: Wed, 4 Feb 2026 20:20:51 +0100 Subject: [PATCH 90/92] feat: update GUI framework with improved slot resolution and view management --- .../surfapi/bukkit/api/gui/component/Component.kt | 2 +- .../surfapi/bukkit/api/gui/view/AbstractGuiView.kt | 6 +++--- .../surf/surfapi/bukkit/api/gui/view/GuiView.kt | 2 ++ .../surfapi/bukkit/test/gui/PaginatedShopGuiView.kt | 13 ++++++++----- .../bukkit/server/gui/view/ViewManagerImpl.kt | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) 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 index 01985c8f..6ceeee72 100644 --- 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 @@ -178,6 +178,6 @@ abstract class Component { } override fun toString(): String { - return "Component(area=$area, priority=$priority, width=$width, height=$height, children=$children, view=$view, hidden=$hidden, disabled=$disabled, props=$props, attachedRef=${attachedRef.hashCode()})" + 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/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt index 8e4a8be7..b0a2f16b 100644 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt @@ -1,5 +1,6 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view +import dev.slne.surf.surfapi.bukkit.api.event.cancel 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.* @@ -14,7 +15,6 @@ open class AbstractGuiView : GuiView() { override fun open(player: Player) { super.open(player) - ViewManager.setActiveView(player, this) renderAllSlots(player) // Open inventory @@ -130,10 +130,10 @@ open class AbstractGuiView : GuiView() { event.isCancelled = true } - val slot = Slot.of(event.slot) + val slot = Slot.of(event.rawSlot) val resolved = resolveSlot(player, slot) ?: return - if (resolved.component.disabled) return + if (resolved.component.disabled) return event.cancel() resolved.component.onClick(createClickContext(player, event, resolved.component)) } 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 index 31870fc3..e867eda4 100644 --- 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 @@ -156,6 +156,8 @@ abstract class GuiView { */ open fun open(player: Player) { ensureInitialized() + + ViewManager.setActiveView(player, this) viewers.add(player.uniqueId) onOpen(createViewContext(player)) 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 index 85251e2a..15ebf392 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -17,7 +18,9 @@ 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. @@ -29,9 +32,9 @@ class PaginatedShopGuiView : AbstractGuiView() { // Shop items - lazy to avoid initialization issues private val shopItems by lazy { - Material.entries.filter { - it.isItem && !it.isAir - }.map { ShopItem(it, it.name, (10..500).random()) }.shuffled().toObjectList() + Registry.ITEM.map { + ShopItem(it, it.key.asString(), (10..500).random()) + }.shuffled().toObjectList() } // Pagination component - includes built-in navigation buttons @@ -42,7 +45,7 @@ class PaginatedShopGuiView : AbstractGuiView() { endSlot = Slot.at(7, 4), // Column 8, Row 4 (9 cols x 4 rows = 36 slots total) items = { shopItems }, itemRenderer = { item, ctx -> - GuiItem.of(ItemStack(item.material) { + GuiItem.of(buildItem(item.type) { displayName { gold(item.name) } @@ -106,7 +109,7 @@ class PaginatedShopGuiView : AbstractGuiView() { } private data class ShopItem( - val material: Material, + val type: ItemType, val name: String, val price: Int ) 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 index 078f85ab..878d35c2 100644 --- 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 @@ -10,7 +10,7 @@ import java.util.* @AutoService(ViewManager::class) class ViewManagerImpl : ViewManager { private val activePerPlayer = Caffeine.newBuilder() - .weakValues() + .maximumSize(10_000) .build() override fun getActiveView(player: Player): GuiView? { From dbb9922af0aa729f1327a8c82852e8e8348c1be4 Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 20:51:02 +0100 Subject: [PATCH 91/92] feat: add cancelOnClick property to components and update GUI handling logic --- .../bukkit/api/gui/component/Component.kt | 18 +- .../components/PaginationComponent.kt | 8 +- .../gui/context/abstract/NavigationHelper.kt | 15 +- .../bukkit/api/gui/dsl/ComponentDsl.kt | 2 + .../surfapi/bukkit/api/gui/dsl/ViewDsl.kt | 12 +- .../bukkit/api/gui/view/AbstractGuiView.kt | 171 ------------------ .../surfapi/bukkit/api/gui/view/GuiView.kt | 158 ++++++++++++++-- .../surfapi/bukkit/api/gui/view/ViewConfig.kt | 3 +- .../surfapi/bukkit/test/gui/CounterGuiView.kt | 30 ++- .../bukkit/test/gui/PaginatedShopGuiView.kt | 4 +- .../bukkit/server/gui/view/GuiViewListener.kt | 6 +- 11 files changed, 213 insertions(+), 214 deletions(-) delete mode 100644 surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt 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 index 6ceeee72..b53a9efc 100644 --- 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 @@ -60,12 +60,24 @@ abstract class Component { 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)) } + if (!firstRenderPerPlayer.add(player.uniqueId)) return false + + view?.let { + onFirstRender( + it.createLifecycleContext( + player, + LifecycleEventType.FIRST_RENDER + ) + ) + } return true } 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 index 4ebb3ed8..9c0d4d90 100644 --- 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 @@ -109,6 +109,8 @@ class PaginationComponent( } }), ) { + this.cancelOnClick = true + initComponent = { if (!hasPreviousPage(player)) { disabled = true @@ -134,7 +136,9 @@ class PaginationComponent( } }) }, - ) + ) { + this.cancelOnClick = true + } private fun createNextButtonComponent() = component( slot = calculatedNextButtonSlot, @@ -145,6 +149,8 @@ class PaginationComponent( } }), ) { + this.cancelOnClick = true + initComponent = { if (!hasNextPage(player)) { disabled = true 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 index 00f574e9..37d0ba62 100644 --- 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 @@ -1,6 +1,5 @@ package dev.slne.surf.surfapi.bukkit.api.gui.context.abstract -import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.GuiView import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import org.bukkit.entity.Player @@ -8,17 +7,15 @@ import org.bukkit.entity.Player @InternalSurfApi object NavigationHelper { fun navigateTo(currentView: GuiView, targetView: GuiView, player: Player, passProps: Boolean) { - if (targetView is AbstractGuiView) { - targetView.withParent(currentView) - player.closeInventory() - targetView.open(player) - } + targetView.withParent(currentView) + player.closeInventory() + targetView.open(player) } fun navigateBack(currentView: GuiView, player: Player) { val parent = currentView.parent - if (parent is AbstractGuiView) { + if (parent is GuiView) { player.closeInventory() val resumeContext = parent.createResumeContext(player, currentView) @@ -35,8 +32,6 @@ object NavigationHelper { } fun update(view: GuiView, player: Player) { - if (view is AbstractGuiView) { - view.refreshInventory(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 index 0c458cb4..2aebf867 100644 --- 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 @@ -31,6 +31,7 @@ annotation class 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 @@ -55,6 +56,7 @@ class ComponentBuilder { 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) 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 index ef460cd8..bb8615a8 100644 --- 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 @@ -2,8 +2,8 @@ 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 net.kyori.adventure.text.Component as AdventureComponent import org.bukkit.event.inventory.InventoryType +import net.kyori.adventure.text.Component as AdventureComponent /** * DSL marker for view building. @@ -22,15 +22,15 @@ class ViewConfigBuilder { 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(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 @@ -41,7 +41,6 @@ class ViewConfigBuilder { this.type.defaultSize } this.cancelOnClick = this@ViewConfigBuilder.cancelOnClick - this.closeOnClickOutside = this@ViewConfigBuilder.closeOnClickOutside } } } @@ -54,9 +53,8 @@ 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 - config.closeOnClickOutside = builtConfig.closeOnClickOutside } diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt deleted file mode 100644 index b0a2f16b..00000000 --- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/AbstractGuiView.kt +++ /dev/null @@ -1,171 +0,0 @@ -package dev.slne.surf.surfapi.bukkit.api.gui.view - -import dev.slne.surf.surfapi.bukkit.api.event.cancel -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.shared.api.util.InternalSurfApi -import org.bukkit.entity.Player -import org.bukkit.event.inventory.InventoryClickEvent - -@InternalSurfApi -open class AbstractGuiView : GuiView() { - override fun open(player: Player) { - super.open(player) - - renderAllSlots(player) - - // Open inventory - player.openInventory(inventory) - } - - override fun close(player: Player) { - ViewManager.removeActiveView(player) - super.close(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) - } - } - - /** - * Implementation of refreshComponentSlotsInternal for GuiView. - */ - override fun refreshComponentSlotsInternal(player: Player, component: Component) { - 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() - - resolved.component.onClick(createClickContext(player, event, resolved.component)) - } - - override fun createClickContext( - player: Player, - event: InventoryClickEvent, - component: Component - ): ClickContext { - return AbstractClickContext(this, player, event, component) - } - - override fun createViewContext(player: Player): ViewContext { - return AbstractViewContext(this, player) - } - - override fun createInitializeContext(): InitializeContext { - return AbstractInitializeContext(config, this) - } - - override fun createLifecycleContext( - player: Player, - eventType: LifecycleEventType - ): LifecycleContext { - return AbstractLifecycleContext(this, player, eventType) - } - - override fun createResumeContext(player: Player, origin: GuiView?): ResumeContext { - return AbstractResumeContext(this, player, origin) - } - - override fun toString(): String { - return "AbstractGuiView() ${super.toString()}" - } -} \ 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/GuiView.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/gui/view/GuiView.kt index e867eda4..95833d5a 100644 --- 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 @@ -1,9 +1,12 @@ package dev.slne.surf.surfapi.bukkit.api.gui.view +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 @@ -80,9 +83,17 @@ abstract class GuiView { */ 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) @@ -94,6 +105,17 @@ abstract class GuiView { } } + fun modifyConfig(modifier: ViewConfig.() -> Unit) { + config.modifier() + initialized = false + + init() + + viewerPlayers().forEach { viewer -> + open(viewer) + } + } + /** * Initialize the view configuration. * Called once when the view is first created. @@ -161,6 +183,11 @@ abstract class GuiView { viewers.add(player.uniqueId) onOpen(createViewContext(player)) + + renderAllSlots(player) + + // Open inventory + player.openInventory(inventory) } /** @@ -170,6 +197,7 @@ abstract class GuiView { val context = createViewContext(player) onClose(context) viewers.remove(player.uniqueId) + ViewManager.removeActiveView(player) } /** @@ -181,6 +209,63 @@ abstract class GuiView { } } + 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. @@ -213,15 +298,64 @@ abstract class GuiView { // Refresh the component's slots to update the visual display // This now includes all children's slots, so everything updates together - refreshComponentSlotsInternal(player, component) + 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 + } + /** - * Internal method to refresh component slots. - * Must be implemented by concrete view implementations. + * Handle click event. */ - protected abstract fun refreshComponentSlotsInternal(player: Player, component: Component) + 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. @@ -250,37 +384,37 @@ abstract class GuiView { /** * Create a click context for a player and click event. */ - abstract fun createClickContext( + fun createClickContext( player: Player, event: InventoryClickEvent, component: Component, - ): ClickContext + ): ClickContext = AbstractClickContext(this, player, event, component) /** * Create a view context for a player. */ - abstract fun createViewContext(player: Player): ViewContext + fun createViewContext(player: Player): ViewContext = AbstractViewContext(this, player) /** * Create a render context for a player. */ - abstract fun createInitializeContext(): InitializeContext + fun createInitializeContext(): InitializeContext = AbstractInitializeContext(config, this) /** * Create a lifecycle context for a player. */ - abstract fun createLifecycleContext( + fun createLifecycleContext( player: Player, eventType: LifecycleEventType - ): LifecycleContext + ): LifecycleContext = AbstractLifecycleContext(this, player, eventType) /** * Create a resume context for a player. */ - abstract fun createResumeContext( + fun createResumeContext( player: Player, origin: GuiView? - ): ResumeContext + ): ResumeContext = AbstractResumeContext(this, player, origin) override fun toString(): String { return "GuiView(parent=$parent, components=$components, initialized=$initialized, viewers=$viewers, config=$config)" 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 index 386dff2d..72dc3acc 100644 --- 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 @@ -11,7 +11,6 @@ data class ViewConfig( var size: Int = 54, // 6 rows by default for CHEST var type: InventoryType = InventoryType.CHEST, var cancelOnClick: Boolean = true, - var closeOnClickOutside: Boolean = false ) { /** * Set rows (only for CHEST type). @@ -28,6 +27,6 @@ data class ViewConfig( } override fun toString(): String { - return "ViewConfig(title=$title, size=$size, type=$type, cancelOnClick=$cancelOnClick, closeOnClickOutside=$closeOnClickOutside, rows=$rows)" + 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-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 index 44bc1036..bb546502 100644 --- 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 @@ -13,10 +13,12 @@ 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.AbstractGuiView +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.TextColor import org.bukkit.Material import org.bukkit.event.inventory.InventoryType @@ -24,7 +26,7 @@ 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 : AbstractGuiView() { +object CounterGuiView : GuiView() { private val counterProp = Prop.Mutable("counter", 0) private val counterDisplayRef = Ref() @@ -33,6 +35,30 @@ object CounterGuiView : AbstractGuiView() { context.config().rows = 3 context.config().title = text("Counter Gui") + context.slot( + component( + Slot.at(0, 0), + GuiItem.of(ItemStack(Material.NAME_TAG) { + displayName { + success("Randomize Title") + } + }) + ) { + onClick = { + val randomHex = List(6) { + ('0'..'9') + ('A'..'F') + }.flatten().shuffled().take(6).joinToString("") + + val textColor = TextColor.fromHexString(randomHex) + val oldTitle = view.title.plain() + + view.modifyConfig { + title = text(oldTitle, textColor) + } + } + } + ) + // Counter display at top center context.slot( dynamicComponent( 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 index 15ebf392..f71b5916 100644 --- 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 @@ -13,7 +13,7 @@ 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.AbstractGuiView +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 @@ -26,7 +26,7 @@ import org.bukkit.inventory.ItemType * Example GUI demonstrating ViewerMutableProp and PaginationComponent. * Each player has their own coins (viewer-specific state). */ -class PaginatedShopGuiView : AbstractGuiView() { +class PaginatedShopGuiView : GuiView() { private val coinsProp = ViewerProp.Mutable("coins", 1000) private val coinsDisplayRef = Ref() 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 index 8f110084..3e277270 100644 --- 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 @@ -1,6 +1,5 @@ package dev.slne.surf.surfapi.bukkit.server.gui.view -import dev.slne.surf.surfapi.bukkit.api.gui.view.AbstractGuiView import dev.slne.surf.surfapi.bukkit.api.gui.view.ViewManager import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -17,15 +16,14 @@ object GuiViewListener : Listener { val player = event.whoClicked as? Player ?: return val view = ViewManager.getActiveView(player) ?: return - if (view is AbstractGuiView) { - view.handleClick(player, event) - } + 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 From d9adef4dce5851d045a34f62042c5356bbe3579c Mon Sep 17 00:00:00 2001 From: Ammo Date: Wed, 4 Feb 2026 21:33:18 +0100 Subject: [PATCH 92/92] feat: enhance CounterGuiView with dynamic title color cycling and refactor modifyConfig method --- .../surfapi/bukkit/api/gui/view/GuiView.kt | 23 +++++++++++--- .../surfapi/bukkit/test/gui/CounterGuiView.kt | 31 +++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) 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 index 95833d5a..f982cba7 100644 --- 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 @@ -1,5 +1,8 @@ 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 @@ -11,11 +14,13 @@ 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 @@ -105,14 +110,22 @@ abstract class GuiView { } } - fun modifyConfig(modifier: ViewConfig.() -> Unit) { - config.modifier() + fun modifyConfig(modifier: (config: ViewConfig) -> Unit) { + modifier(config) + initialized = false - init() + ensureInitialized() + + val plugin = JavaPlugin.getProvidingPlugin(GuiView::class.java) as SuspendingJavaPlugin - viewerPlayers().forEach { viewer -> - open(viewer) + plugin.launch { + viewerPlayers().forEach { viewer -> + withContext(plugin.entityDispatcher(viewer)) { + viewer.closeInventory() + open(viewer) + } + } } } 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 index bb546502..2cbe5450 100644 --- 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 @@ -18,7 +18,7 @@ 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.TextColor +import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.Material import org.bukkit.event.inventory.InventoryType @@ -30,10 +30,26 @@ 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") + context.config().title = text("Counter Gui", titleColor.color) context.slot( component( @@ -45,15 +61,12 @@ object CounterGuiView : GuiView() { }) ) { onClick = { - val randomHex = List(6) { - ('0'..'9') + ('A'..'F') - }.flatten().shuffled().take(6).joinToString("") - - val textColor = TextColor.fromHexString(randomHex) val oldTitle = view.title.plain() + val nextColor = titleColor.next() + titleColor = nextColor - view.modifyConfig { - title = text(oldTitle, textColor) + view.modifyConfig { config -> + config.title = text(oldTitle, titleColor.color) } } }