Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
javaVersion=21
mcVersion=1.21.8
group=dev.slne.surf
version=1.21.8-2.38.0
version=1.21.8-2.39.0
relocationPrefix=dev.slne.surf.surfapi.libs
snapshot=true
37 changes: 37 additions & 0 deletions surf-api-bukkit/surf-api-bukkit-api/api/surf-api-bukkit-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,43 @@ public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogInputBu
public static final fun dialogInput (Lkotlin/jvm/functions/Function1;)Lit/unimi/dsi/fastutil/objects/ObjectList;
}

public abstract interface class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction {
public abstract fun newPage (II)I
}

public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder {
public fun <init> ()V
public final fun addElement (Ljava/lang/Object;)V
public final fun addElements (Ljava/util/Collection;)V
public final fun addElements ([Ljava/lang/Object;)V
public final fun base (Lkotlin/jvm/functions/Function3;)V
public final fun build ()Lio/papermc/paper/dialog/Dialog;
public final fun buttonBuilder (Lkotlin/jvm/functions/Function2;)V
public final fun currentPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V
public static synthetic fun currentPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun elementsPerPage (I)V
public final fun exitAction (Lkotlin/jvm/functions/Function1;)V
public final fun firstPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V
public static synthetic fun firstPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun getCurrentPage ()I
public final fun getHasNextPage ()Z
public final fun getHasPreviousPage ()Z
public final fun getMaxPages ()I
public final fun lastPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V
public static synthetic fun lastPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun minElementButtonWidth (I)V
public final fun nextPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V
public static synthetic fun nextPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
public final fun paginationButtonWidth (I)V
public final fun previousPageButton (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;)V
public static synthetic fun previousPageButton$default (Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilder;Ldev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPageAction;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)V
}

public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogPaginationBuilderKt {
public static final fun PaginatedDialog (Lkotlin/jvm/functions/Function1;)Lio/papermc/paper/dialog/Dialog;
public static final fun paginatedDialog (Lkotlin/jvm/functions/Function1;)Lio/papermc/paper/dialog/Dialog;
}

public final class dev/slne/surf/surfapi/bukkit/api/dialog/builder/DialogTypeBuilder {
public fun <init> ()V
public final fun confirmation (Lio/papermc/paper/registry/data/dialog/ActionButton;Lio/papermc/paper/registry/data/dialog/ActionButton;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
@file:Suppress("UnstableApiUsage")

package dev.slne.surf.surfapi.bukkit.api.dialog.builder

import dev.slne.surf.surfapi.bukkit.api.dialog.base
import dev.slne.surf.surfapi.bukkit.api.dialog.dialog
import dev.slne.surf.surfapi.bukkit.api.dialog.type
import dev.slne.surf.surfapi.core.api.messages.adventure.buildText
import dev.slne.surf.surfapi.core.api.messages.adventure.text
import io.papermc.paper.registry.data.dialog.ActionButton
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import net.kyori.adventure.text.Component

private const val PAGINATION_BUTTON_DEFAULT_WIDTH = 50

fun <T> PaginatedDialog(
block: DialogPaginationBuilder<T>.() -> Unit,
) = DialogPaginationBuilder<T>().apply(block).build()

fun <T> paginatedDialog(
block: DialogPaginationBuilder<T>.() -> Unit,
) = PaginatedDialog(block)

fun interface DialogPageAction {
fun newPage(currentPage: Int, maxPages: Int): Int
}

private enum class DialogPaginationBaseAction(
val label: (Int, Int) -> Component,
val tooltip: (Int, Int) -> Component,
val pageAction: DialogPageAction,
) {
FIRST(
label = { _, _ -> text("«") },
tooltip = { _, _ -> buildText { info("Erste Seite") } },
pageAction = { _, _ -> 0 }
),
BACK(
label = { _, _ -> text("‹") },
tooltip = { _, _ -> buildText { info("Vorherige Seite") } },
pageAction = { currentPage, _ -> (currentPage - 1).coerceAtLeast(0) }
),
CURRENT(
label = { currentPage, maxPages -> text("${currentPage + 1} / $maxPages") },
tooltip = { currentPage, maxPages -> buildText { info("Seite ${currentPage + 1} von $maxPages") } },
pageAction = { currentPage, _ -> currentPage },
),
NEXT(
label = { _, _ -> text("›") },
tooltip = { _, _ -> buildText { info("Nächste Seite") } },
pageAction = { currentPage, maxPages -> (currentPage + 1).coerceAtMost(maxPages - 1) }
),
LAST(
label = { _, _ -> text("»") },
tooltip = { _, _ -> buildText { info("Letzte Seite") } },
pageAction = { _, maxPages -> maxPages - 1 }
);

fun actionButton(
currentPage: Int,
maxPages: Int,
width: Int,
) = actionButton {
label(this@DialogPaginationBaseAction.label(currentPage, maxPages))
tooltip(this@DialogPaginationBaseAction.tooltip(currentPage, maxPages))
width(width)
}
}

class DialogPaginationBuilder<T> {

private var base: (DialogBaseBuilder.() -> Unit)? = null
private var exitAction: ActionButton? = null
private var buttonBuilder: (DialogActionButtonBuilder.(T) -> Unit)? = null

private val elements = ObjectLinkedOpenHashSet<T>()
private var elementsPerPage = 10
private val currentPageElements
get() = elements.drop(currentPage * elementsPerPage).take(elementsPerPage)

private var paginationButtonWidth = PAGINATION_BUTTON_DEFAULT_WIDTH

@Suppress("SuspiciousVarProperty")
private var minElementButtonWidth = 200

private fun calculateElementButtonWidth(): Int {
var width = 0

if (hasPreviousPage) {
width += paginationButtonWidth * 2 // first + back
}

if (hasNextPage) {
width += paginationButtonWidth * 2 // next + last
}

width += paginationButtonWidth // current page button

return width.coerceAtLeast(minElementButtonWidth)
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns a calculated width but this value is never used to update the button widths. The variable width accumulates pagination button widths but doesn't account for the actual element button width calculation logic.

Suggested change
return width.coerceAtLeast(minElementButtonWidth)
val calculatedWidth = width.coerceAtLeast(minElementButtonWidth)
minElementButtonWidth = calculatedWidth
return calculatedWidth

Copilot uses AI. Check for mistakes.
}

var currentPage = 0
private set

val maxPages: Int
get() = (elements.size + elementsPerPage - 1) / elementsPerPage

val hasNextPage: Boolean
get() = currentPage < maxPages - 1

val hasPreviousPage: Boolean
get() = currentPage > 0

private var firstPageButton = DialogPaginationBaseAction.FIRST.actionButton(
currentPage, maxPages, paginationButtonWidth
)

private var backButton = DialogPaginationBaseAction.BACK.actionButton(
currentPage, maxPages, paginationButtonWidth
)

private var currentPageButton = DialogPaginationBaseAction.CURRENT.actionButton(
currentPage, maxPages, paginationButtonWidth
)

private var nextButton = DialogPaginationBaseAction.NEXT.actionButton(
currentPage, maxPages, paginationButtonWidth
)

private var lastPageButton = DialogPaginationBaseAction.LAST.actionButton(
Comment on lines +114 to +130
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These buttons are initialized with currentPage and maxPages values that will be stale. When currentPage changes or elements are added/removed, these buttons will still display outdated page information since they're not regenerated.

Suggested change
private var firstPageButton = DialogPaginationBaseAction.FIRST.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private var backButton = DialogPaginationBaseAction.BACK.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private var currentPageButton = DialogPaginationBaseAction.CURRENT.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private var nextButton = DialogPaginationBaseAction.NEXT.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private var lastPageButton = DialogPaginationBaseAction.LAST.actionButton(
private val firstPageButton get() = DialogPaginationBaseAction.FIRST.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private val backButton get() = DialogPaginationBaseAction.BACK.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private val currentPageButton get() = DialogPaginationBaseAction.CURRENT.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private val nextButton get() = DialogPaginationBaseAction.NEXT.actionButton(
currentPage, maxPages, paginationButtonWidth
)
private val lastPageButton get() = DialogPaginationBaseAction.LAST.actionButton(

Copilot uses AI. Check for mistakes.
currentPage, maxPages, paginationButtonWidth
)

fun base(block: DialogBaseBuilder.(Int, Int) -> Unit) {
base = {
block(currentPage, maxPages)
}
}

private fun buildPageButtonAction(pageAction: DialogPageAction) = dialogAction {
playerCallback { player ->
val newPage = pageAction.newPage(currentPage, maxPages)

if (newPage == currentPage) return@playerCallback

currentPage = newPage

player.showDialog(this@DialogPaginationBuilder.build())
}
}

fun firstPageButton(
pageAction: DialogPageAction = DialogPaginationBaseAction.FIRST.pageAction,
block: DialogActionButtonBuilder.(Int, Int) -> Unit,
) {
firstPageButton = actionButton {
block(currentPage, maxPages)

width(paginationButtonWidth)
action(buildPageButtonAction(pageAction))
}
}

fun previousPageButton(
pageAction: DialogPageAction = DialogPaginationBaseAction.BACK.pageAction,
block: DialogActionButtonBuilder.(Int, Int) -> Unit,
) {
backButton = actionButton {
block(currentPage, maxPages)

width(paginationButtonWidth)
action(buildPageButtonAction(pageAction))
}
}

fun currentPageButton(
pageAction: DialogPageAction = DialogPaginationBaseAction.CURRENT.pageAction,
block: DialogActionButtonBuilder.(Int, Int) -> Unit,
) {
currentPageButton = actionButton {
block(currentPage, maxPages)

width(paginationButtonWidth)
action(buildPageButtonAction(pageAction))
}
}

fun nextPageButton(
pageAction: DialogPageAction = DialogPaginationBaseAction.NEXT.pageAction,
block: DialogActionButtonBuilder.(Int, Int) -> Unit,
) {
nextButton = actionButton {
block(currentPage, maxPages)

width(paginationButtonWidth)
action(buildPageButtonAction(pageAction))
}
}

fun lastPageButton(
pageAction: DialogPageAction = DialogPaginationBaseAction.LAST.pageAction,
block: DialogActionButtonBuilder.(Int, Int) -> Unit,
) {
lastPageButton = actionButton {
block(currentPage, maxPages)

width(paginationButtonWidth)
action(buildPageButtonAction(pageAction))
}
}

fun elementsPerPage(elementsPerPage: Int) {
require(elementsPerPage > 0) { "elementsPerPage must be greater than 0" }

this.elementsPerPage = elementsPerPage
}

fun addElement(element: T) {
elements.add(element)
}

fun addElements(elements: Collection<T>) {
this.elements.addAll(elements)
}

fun addElements(vararg elements: T) {
this.elements.addAll(elements)
}

fun paginationButtonWidth(width: Int) {
require(width in 1..1024) {
"Pagination button width must be between 1 and 1024"
}

paginationButtonWidth = width
}

fun minElementButtonWidth(width: Int) {
require(width in 1..1024) {
"Element button minimum width must be between 1 and 1024"
}

minElementButtonWidth = width
}

fun exitAction(block: DialogActionButtonBuilder.() -> Unit) {
exitAction = actionButton(block)
}

fun buttonBuilder(builder: DialogActionButtonBuilder.(T) -> Unit) {
buttonBuilder = builder
}

fun build() = dialog {
val baseBuilder = base
val buttonBuilder = buttonBuilder
val exitAction = exitAction

require(baseBuilder != null) {
"Dialog base must be set before building the paginated dialog"
}

require(buttonBuilder != null) {
"Dialog button builder must be set before building the paginated dialog"
}

base(baseBuilder)

val elementButtonWidth = calculateElementButtonWidth()
val elementButtons = currentPageElements.map {
actionButton {
buttonBuilder(it)

width(elementButtonWidth)
}
}

type {
multiAction {
columns(1)

elementButtons.forEach { action(it) }

if (hasPreviousPage) {
action(firstPageButton)
action(backButton)
}

action(currentPageButton)

if (hasNextPage) {
action(nextButton)
action(lastPageButton)
}

if (exitAction != null) {
exitAction(exitAction)
}
}
}
}

}