Skip to content
Merged
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 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=25
mcVersion=26.2
group=dev.slne.surf.api
version=3.38.0
version=3.39.0
relocationPrefix=dev.slne.surf.api.libs
snapshot=false
43 changes: 43 additions & 0 deletions surf-api-minestom/api/surf-api-minestom.api
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,49 @@ public final class dev/slne/surf/api/minestom/dialog/callback/DialogResponseView
public final fun of-VLTPHq8 (Lnet/kyori/adventure/nbt/BinaryTag;)Lnet/kyori/adventure/nbt/CompoundBinaryTag;
}

public final class dev/slne/surf/api/minestom/dialog/search/SearchDialogAction {
public fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public final fun component1 ()Lkotlin/jvm/functions/Function1;
public final fun component2 ()Lkotlin/jvm/functions/Function1;
public final fun copy (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction;
public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/search/SearchDialogAction;
public fun equals (Ljava/lang/Object;)Z
public final fun getLabel ()Lkotlin/jvm/functions/Function1;
public final fun getTooltip ()Lkotlin/jvm/functions/Function1;
public fun hashCode ()I
public final fun setLabel (Lkotlin/jvm/functions/Function1;)V
public final fun setTooltip (Lkotlin/jvm/functions/Function1;)V
public fun toString ()Ljava/lang/String;
}

public final class dev/slne/surf/api/minestom/dialog/search/SearchDialogKt {
public static final fun searchDialog (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLnet/minestom/server/dialog/DialogAfterAction;)Lnet/minestom/server/dialog/Dialog;
public static synthetic fun searchDialog$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLnet/minestom/server/dialog/DialogAfterAction;ILjava/lang/Object;)Lnet/minestom/server/dialog/Dialog;
}

public final class dev/slne/surf/api/minestom/dialog/search/SearchInput {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lkotlin/jvm/functions/Function1;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lkotlin/jvm/functions/Function1;
public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/minestom/dialog/search/SearchInput;
public static synthetic fun copy$default (Ldev/slne/surf/api/minestom/dialog/search/SearchInput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/api/minestom/dialog/search/SearchInput;
public fun equals (Ljava/lang/Object;)Z
public final fun getInitialValue ()Ljava/lang/String;
public final fun getInputModifier ()Lkotlin/jvm/functions/Function1;
public final fun getKey ()Ljava/lang/String;
public final fun getLabel ()Lkotlin/jvm/functions/Function1;
public fun hashCode ()I
public final fun setInitialValue (Ljava/lang/String;)V
public final fun setInputModifier (Lkotlin/jvm/functions/Function1;)V
public final fun setKey (Ljava/lang/String;)V
public final fun setLabel (Lkotlin/jvm/functions/Function1;)V
public fun toString ()Ljava/lang/String;
}

public final class dev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader : dev/slne/minestom/lobby/api/event/EventRegistrar {
public static final field Companion Ldev/slne/surf/api/minestom/impl/inventory/framework/MinestomInventoryLoader$Companion;
public fun <init> ()V
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.slne.surf.api.minestom.dialog.builder

import dev.slne.surf.api.core.util.mutableObjectListOf
import dev.slne.surf.api.core.util.mutableObjectSetOf
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import net.minestom.server.dialog.Dialog
import net.minestom.server.dialog.DialogActionButton
import net.minestom.server.dialog.DialogMetadata
Expand Down Expand Up @@ -114,7 +114,8 @@ class DialogTypeBuilder {
* Collects the dialogs a dialog list offers and how they are laid out.
*/
class DialogListTypeBuilder {
private val dialogs = mutableObjectSetOf<Dialog>()
/** Keeps the order the dialogs were added in, which is the order the client shows them in. */
private val dialogs = ObjectLinkedOpenHashSet<Dialog>()
var exitAction: DialogActionButton? = null
var columns: @Range(from = 1, to = Int.MAX_VALUE.toLong()) Int = DEFAULT_COLUMNS
var buttonWidth: @Range(from = 1, to = 1024) Int = DEFAULT_BUTTON_WIDTH
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package dev.slne.surf.api.minestom.dialog.search

import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder
import dev.slne.surf.api.minestom.dialog.base
import dev.slne.surf.api.minestom.dialog.builder.DialogBodyBuilder
import dev.slne.surf.api.minestom.dialog.builder.DialogInputBuilder
import dev.slne.surf.api.minestom.dialog.dialog
import dev.slne.surf.api.minestom.dialog.type
import net.minestom.server.dialog.Dialog
import net.minestom.server.dialog.DialogAfterAction
import net.minestom.server.entity.Player

/**
* The label and tooltip of one of a search dialog's buttons.
*/
data class SearchDialogAction(
var label: SurfComponentBuilder.() -> Unit,
var tooltip: SurfComponentBuilder.() -> Unit
)

/**
* The text field a search dialog collects its query in.
*
* @property key the key the query is reported back under
* @property label the label shown next to the field
* @property initialValue the query the field starts out with
* @property inputModifier applied to the field last, for anything the other properties do not cover
*/
data class SearchInput(
var key: String = "search",
var label: SurfComponentBuilder.() -> Unit = {
text("Suche")
},
var initialValue: String = "",
var inputModifier: DialogInputBuilder.TextInputBuilder.() -> Unit = {}
)

/**
* Builds a dialog that asks for a single query and hands it to [onSearch] or [onClose].
*
* The dialog closes itself before either callback runs, so a callback is free to open an inventory
* or another dialog straight away.
*
* ```
* player.showDialog(
* searchDialog(
* title = { primary("Search a player...") },
* onSearch = { player, query -> showResults(player, query) },
* onClose = { player, _ -> showOverview(player) },
* searchInput = { initialValue = lastQuery },
* )
* )
* ```
*
* @param title the title shown above the dialog
* @param externalTitle the title shown wherever the dialog is listed, defaulting to [title]
* @param onSearch called with the query once the search button is pressed
* @param onClose called with the query once the cancel button is pressed
* @param body anything shown above the search field
* @param searchButton the button that runs the search
* @param cancelButton the button that abandons the search
* @param searchInput the text field the query is collected in
* @param canCloseWithEscape whether the dialog can be dismissed without pressing a button
* @param afterAction what the client does with the dialog once a button was pressed
*/
fun searchDialog(
title: SurfComponentBuilder.() -> Unit,
externalTitle: SurfComponentBuilder.() -> Unit = title,
onSearch: (player: Player, query: String) -> Unit,
onClose: (player: Player, query: String) -> Unit,
body: DialogBodyBuilder.() -> Unit = { },
searchButton: SearchDialogAction.() -> Unit = {
label = {
text("Suchen")
}
tooltip = {
text("Klicke hier, um die Suche zu starten.")
}
},
cancelButton: SearchDialogAction.() -> Unit = {
label = {
text("Abbrechen")
}
tooltip = {
text("Klicke hier, um die Suche abzubrechen.")
}
},
searchInput: SearchInput.() -> Unit,
canCloseWithEscape: Boolean = false,
afterAction: DialogAfterAction = DialogAfterAction.NONE
): Dialog = dialog {
val input = SearchInput().apply(searchInput)

val search = SearchDialogAction(
label = { },
tooltip = { }
Comment on lines +94 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve defaults when customizing button blocks

When a caller supplies either button block to customize only one property—for example, changing the cancel tooltip—the corresponding default-argument block is not executed, and these empty initializers erase the default label and tooltip. The resulting button can therefore have a blank label; initialize each SearchDialogAction with its documented default values before applying the caller's block, as is already done for SearchInput.

Useful? React with 👍 / 👎.

).apply(searchButton)

val cancel = SearchDialogAction(
label = { },
tooltip = { }
).apply(cancelButton)

base {
title(title)
externalTitle(externalTitle)
canCloseWithEscape(canCloseWithEscape)
afterAction(afterAction)

body(body)

input {
text(input.key) {
label(input.label)
initial(input.initialValue)

input.inputModifier(this)
}
}
}

type {
confirmation {
no {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bind cancellation to the no button

When canCloseWithEscape is enabled, confirmation-dialog cancellation delegates to the no button, but this block assigns onSearch to that button and onClose to yes. Pressing Escape therefore submits the current query and invokes onSearch instead of treating the dismissal as cancellation; swap the search and cancel button mappings.

Useful? React with 👍 / 👎.

label(search.label)
tooltip(search.tooltip)

action {
customPlayerClick { response, player ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow returned dialogs to handle repeated submissions

When the returned Dialog is cached, revisited from a dialog list, or shown to multiple players, each button stops working after its first use because customPlayerClick defaults to DialogCallbackOptions.DEFAULT, whose registration permits only one global use. Since searchDialog neither exposes callback options nor documents the returned dialog as single-use, subsequent clicks are silently ignored; register reusable callbacks or expose lifecycle options to callers.

Useful? React with 👍 / 👎.

player.closeDialog()

onSearch(player, response.getText(input.key) ?: "")
}
}
}

yes {
label(cancel.label)
tooltip(cancel.tooltip)

action {
customPlayerClick { response, player ->
player.closeDialog()

onClose(player, response.getText(input.key) ?: "")
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dev.slne.surf.api.minestom.dialog.search

import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
import net.minestom.server.dialog.Dialog
import net.minestom.server.dialog.DialogAfterAction
import net.minestom.server.dialog.DialogBody
import net.minestom.server.dialog.DialogInput
import net.minestom.testing.EnvTest
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs

@EnvTest
class SearchDialogTest {

@Test
fun `a search dialog asks for a single query`() {
val dialog = searchDialog(
title = { text("Search") },
onSearch = { _, _ -> },
onClose = { _, _ -> },
searchInput = { initialValue = "red" },
)

val confirmation = assertIs<Dialog.Confirmation>(dialog)
val input = assertIs<DialogInput.Text>(confirmation.metadata().inputs().single())

assertEquals("search", input.key())
assertEquals("red", input.initial())
}

@Test
fun `a search dialog keeps its title and body`() {
val dialog = searchDialog(
title = { text("Search") },
onSearch = { _, _ -> },
onClose = { _, _ -> },
body = { plainMessage(Component.text("Who are you looking for?")) },
searchInput = { },
)

assertEquals("Search", plain(dialog.metadata().title()))
assertEquals(
listOf(DialogBody.PlainMessage(Component.text("Who are you looking for?"), 200)),
dialog.metadata().body()
)
}

private fun plain(component: Component) =
PlainTextComponentSerializer.plainText().serialize(component)

@Test
fun `a search dialog stays open until a button is pressed`() {
val dialog = searchDialog(
title = { text("Search") },
onSearch = { _, _ -> },
onClose = { _, _ -> },
searchInput = { },
)

assertFalse(dialog.metadata().canCloseWithEscape())
assertEquals(DialogAfterAction.NONE, dialog.metadata().afterAction())
}

@Test
fun `the query is collected under the key it was asked for`() {
val dialog = searchDialog(
title = { text("Search") },
onSearch = { _, _ -> },
onClose = { _, _ -> },
searchInput = {
key = "player"
inputModifier = { maxLength(16) }
},
)

val input = assertIs<DialogInput.Text>(dialog.metadata().inputs().single())

assertEquals("player", input.key())
assertEquals(16, input.maxLength())
}
}