Skip to content

Commit 03cc328

Browse files
Scroll-detection related API
- Bundle select handlers on GUI level - Bundle select handlers on Inventory level - Inventory-level visualizer (cached for VirtualInventory) - Window cursor visualizer
1 parent ce659f6 commit 03cc328

38 files changed

Lines changed: 1090 additions & 132 deletions

invui-kotlin/src/main/kotlin/xyz/xenondevs/invui/dsl/GuiDsl.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
package xyz.xenondevs.invui.dsl
44

5+
import org.bukkit.entity.Player
56
import xyz.xenondevs.commons.provider.Provider
67
import xyz.xenondevs.commons.provider.provider
78
import xyz.xenondevs.invui.ExperimentalReactiveApi
@@ -97,6 +98,18 @@ inline fun IngredientsDsl.gui(vararg structure: String, gui: GuiDsl.() -> Unit):
9798
return NormalGuiDslImpl(structure, (this as IngredientsDslImpl).buildPresets()).apply(gui).build()
9899
}
99100

101+
/**
102+
* DSL scope available inside [GuiDsl.onBundleSelect] handlers, providing information about
103+
* a bundle slot selection event on a GUI slot.
104+
*/
105+
@ExperimentalDslApi
106+
interface GuiBundleSelectDsl : BundleSelectDsl {
107+
108+
/** The GUI slot index where the bundle is located. */
109+
val guiSlot: Int
110+
111+
}
112+
100113
/**
101114
* DSL scope for configuring a [Gui].
102115
*
@@ -146,9 +159,31 @@ sealed interface GuiDsl : IngredientsDsl {
146159
* ```
147160
*/
148161
val ignoreObscuredInventorySlots: ProviderDslProperty<Boolean>
162+
163+
/**
164+
* Registers a bundle selection handler that is called when a player selects a slot
165+
* in a bundle's tooltip on a GUI slot. Multiple handlers can be registered and will all be
166+
* called in order.
167+
*
168+
* ```
169+
* onBundleSelect {
170+
* player.sendMessage("Selected bundle slot $bundleSlot at gui slot $guiSlot")
171+
* }
172+
* ```
173+
*
174+
* @see GuiBundleSelectDsl
175+
*/
176+
fun onBundleSelect(handler: GuiBundleSelectDsl.() -> Unit)
149177

150178
}
151179

180+
@ExperimentalDslApi
181+
internal class GuiBundleSelectDslImpl(
182+
override val player: Player,
183+
override val guiSlot: Int,
184+
override val bundleSlot: Int
185+
) : GuiBundleSelectDsl
186+
152187
@PublishedApi
153188
@ExperimentalDslApi
154189
internal abstract class GuiDslImpl<G : Gui, B : Gui.Builder<G, B>>(
@@ -173,6 +208,12 @@ internal abstract class GuiDslImpl<G : Gui, B : Gui.Builder<G, B>>(
173208
override val ignoreObscuredInventorySlots: ProviderDslProperty<Boolean>
174209
get() = ProviderDslProperty(::_ignoreObscuredInventorySlots)
175210

211+
private val bundleSelectHandlers = mutableListOf<GuiBundleSelectDsl.() -> Unit>()
212+
213+
override fun onBundleSelect(handler: GuiBundleSelectDsl.() -> Unit) {
214+
bundleSelectHandlers += handler
215+
}
216+
176217
fun build(): G {
177218
val gui = createBuilder().apply(::applyToBuilder).build()
178219
_gui = gui
@@ -187,6 +228,12 @@ internal abstract class GuiDslImpl<G : Gui, B : Gui.Builder<G, B>>(
187228
}
188229
applyPreset(ingredients.build())
189230

231+
for (handler in bundleSelectHandlers) {
232+
addBundleSelectHandler { player, guiSlot, bundleSlot ->
233+
GuiBundleSelectDslImpl(player, guiSlot, bundleSlot).handler()
234+
}
235+
}
236+
190237
setBackground(_background)
191238
setFrozen(_frozen)
192239
setIgnoreObscuredInventorySlots(_ignoreObscuredInventorySlots)

invui-kotlin/src/main/kotlin/xyz/xenondevs/invui/dsl/WindowDsl.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import net.kyori.adventure.text.Component
66
import org.bukkit.entity.Player
77
import org.bukkit.event.inventory.ClickType
88
import org.bukkit.event.inventory.InventoryCloseEvent
9+
import org.bukkit.inventory.ItemStack
910
import xyz.xenondevs.commons.provider.Provider
1011
import xyz.xenondevs.commons.provider.mutableProvider
1112
import xyz.xenondevs.commons.provider.provider
1213
import xyz.xenondevs.invui.ClickEvent
1314
import xyz.xenondevs.invui.ExperimentalReactiveApi
1415
import xyz.xenondevs.invui.internal.util.InventoryUtils2
16+
import xyz.xenondevs.invui.item.ItemProvider
1517
import xyz.xenondevs.invui.window.Window
1618
import xyz.xenondevs.invui.window.setCloseable
19+
import xyz.xenondevs.invui.window.setCursorVisualizer
1720
import xyz.xenondevs.invui.window.setTitle
1821
import xyz.xenondevs.invui.window.setWindowState
1922
import kotlin.contracts.InvocationKind
@@ -235,6 +238,18 @@ sealed interface WindowDsl {
235238
*/
236239
val clientWindowState: Provider<Int>
237240

241+
/**
242+
* A function that takes the actual [ItemStack] on the cursor and returns an [ItemProvider]
243+
* to visualize it. May return `null` to display the item stack normally.
244+
*
245+
* Defaults to `{ null }`.
246+
*
247+
* ```
248+
* cursorVisualizer by { it?.let { ItemBuilder(it).setGlint(true) } }
249+
* ```
250+
*/
251+
val cursorVisualizer: ProviderDslProperty<(ItemStack?) -> ItemProvider?>
252+
238253
/**
239254
* Registers a handler that is called when the window is opened.
240255
* Multiple handlers can be registered and will all be called in order.
@@ -427,6 +442,7 @@ internal abstract class AbstractWindowDsl<W : Window, B : Window.Builder<W, B>>(
427442
private var _closeable = provider(true)
428443
private var _fallbackWindow = provider<Window?>(null)
429444
private var _serverWindowState = mutableProvider(0)
445+
private var _cursorVisualizer = provider<(ItemStack?) -> ItemProvider?> { { null } }
430446

431447
override val title: ProviderDslProperty<Component>
432448
get() = ProviderDslProperty(::_title)
@@ -436,6 +452,8 @@ internal abstract class AbstractWindowDsl<W : Window, B : Window.Builder<W, B>>(
436452
get() = ProviderDslProperty(::_fallbackWindow)
437453
override val serverWindowState: MutableProviderDslProperty<Int>
438454
get() = MutableProviderDslProperty(::_serverWindowState)
455+
override val cursorVisualizer: ProviderDslProperty<(ItemStack?) -> ItemProvider?>
456+
get() = ProviderDslProperty(::_cursorVisualizer)
439457
override val clientWindowState = mutableProvider(0)
440458
private val openHandlers = mutableListOf<WindowOpenDsl.() -> Unit>()
441459
private val closeHandlers = mutableListOf<WindowCloseDsl.() -> Unit>()
@@ -476,6 +494,7 @@ internal abstract class AbstractWindowDsl<W : Window, B : Window.Builder<W, B>>(
476494
}
477495
setWindowState(_serverWindowState)
478496
addWindowStateChangeHandler(clientWindowState::set)
497+
setCursorVisualizer(_cursorVisualizer)
479498
}
480499
}
481500

invui-kotlin/src/main/kotlin/xyz/xenondevs/invui/inventory/InventoryExtensions.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package xyz.xenondevs.invui.inventory
22

33
import org.bukkit.inventory.ItemStack
4+
import org.jetbrains.annotations.ApiStatus
5+
import xyz.xenondevs.invui.item.ItemProvider
6+
import java.util.function.Function
47

58
/**
69
* Gets a copy of [ItemStack] placed on that [slot].
@@ -31,4 +34,12 @@ operator fun Inventory.contains(item: ItemStack): Boolean =
3134
* Creates an [ObscuredInventory] of only the slots in the given [range].
3235
*/
3336
operator fun Inventory.get(range: IntRange): ObscuredInventory =
34-
ObscuredInventory(this) { it !in range }
37+
ObscuredInventory(this) { it !in range }
38+
39+
/**
40+
* Sets the visualizer of this inventory.
41+
*/
42+
@ApiStatus.Experimental
43+
fun Inventory.setVisualizer(visualizer: ((ItemStack?) -> ItemProvider?)?) {
44+
this.visualizer = if (visualizer != null) Function { itemStack -> visualizer(itemStack) } else null
45+
}

invui-kotlin/src/main/kotlin/xyz/xenondevs/invui/window/WindowExtensions.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package xyz.xenondevs.invui.window
22

33
import net.kyori.adventure.text.Component
4+
import org.bukkit.inventory.ItemStack
45
import xyz.xenondevs.commons.provider.MutableProvider
56
import xyz.xenondevs.commons.provider.Provider
67
import xyz.xenondevs.invui.ExperimentalReactiveApi
78
import xyz.xenondevs.invui.PropertyAdapter
9+
import xyz.xenondevs.invui.item.ItemProvider
10+
import java.util.function.Function
811

912
/**
1013
* Sets the provider containing the title of the [Window] built by this builder.
@@ -33,4 +36,14 @@ fun <S : Window.Builder<*, *>> S.setCloseable(closeable: Provider<Boolean>): S {
3336
fun <S : Window.Builder<*, *>> S.setWindowState(windowState: MutableProvider<Int>): S {
3437
setWindowState(PropertyAdapter(windowState))
3538
return this
39+
}
40+
41+
/**
42+
* Sets the provider containing the cursor visualizer for the [Window] built by this builder.
43+
* If [visualizer] is not a [MutableProvider], attempting to change the visualizer through other means will throw an exception.
44+
*/
45+
@ExperimentalReactiveApi
46+
fun <S : Window.Builder<*, *>> S.setCursorVisualizer(visualizer: Provider<(ItemStack?) -> ItemProvider?>): S {
47+
setCursorVisualizer(PropertyAdapter(visualizer.map { vis -> Function { stack -> vis(stack) } }))
48+
return this
3649
}

invui-kotlin/src/test/kotlin/xyz/xenondevs/invui/PropertyAccessorsPresenceTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ class PropertyAccessorsPresenceTest {
8787
inventory.postUpdateHandlers
8888
inventory.postUpdateHandlers = emptyList()
8989

90+
inventory.visualizer
91+
inventory.visualizer = null
92+
9093
// -- AnvilWindow --
9194
anvilWindow.renameHandlers
9295
anvilWindow.renameHandlers = emptyList()

invui/src/main/java/xyz/xenondevs/invui/gui/AbstractGui.java

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import xyz.xenondevs.invui.item.Item;
2424
import xyz.xenondevs.invui.item.ItemProvider;
2525
import xyz.xenondevs.invui.state.MutableProperty;
26+
import xyz.xenondevs.invui.state.Property;
27+
import xyz.xenondevs.invui.util.InventoryUtils;
2628
import xyz.xenondevs.invui.util.ItemUtils;
2729
import xyz.xenondevs.invui.util.ObserverAtSlot;
30+
import xyz.xenondevs.invui.util.TriConsumer;
2831
import xyz.xenondevs.invui.window.Window;
2932
import xyz.xenondevs.invui.window.WindowManager;
3033

@@ -50,6 +53,8 @@ non-sealed abstract class AbstractGui implements Gui {
5053
private final MutableProperty<Boolean> ignoreObscuredInventorySlots;
5154
private final MutableProperty<@Nullable ItemProvider> background;
5255

56+
private final List<TriConsumer<? super Player, ? super Integer, ? super Integer>> bundleSelectHandlers = new ArrayList<>(0);
57+
5358
private AnimationImpl.@Nullable StateImpl animation;
5459
private @Nullable SlotElement @Nullable [] animationElements;
5560
private boolean isInAnimationContext;
@@ -112,6 +117,12 @@ public void handleBundleSelect(int slot, Player player, int bundleSlot) {
112117
case SlotElement.InventoryLink ie -> handleInvBundleSelect(player, ie.inventory(), ie.slot(), bundleSlot);
113118
case null -> {}
114119
}
120+
121+
CollectionUtils.forEachCatching(
122+
bundleSelectHandlers,
123+
handler -> handler.accept(player, slot, bundleSlot),
124+
"Failed to handle bundle select"
125+
);
115126
}
116127

117128
//<editor-fold desc="inventories">
@@ -449,6 +460,8 @@ private void handleInvMiddleClick(Click click, Inventory inventory, int slot) {
449460

450461
@SuppressWarnings("UnstableApiUsage")
451462
private void handleInvBundleSelect(Player player, Inventory inventory, int slot, int bundleSlot) {
463+
inventory.callBundleSelectEvent(slot, player, bundleSlot);
464+
452465
var bundle = inventory.getItem(slot);
453466
if (bundle != null && bundle.hasData(DataComponentTypes.BUNDLE_CONTENTS)) {
454467
ItemUtils2.setSelectedBundleSlot(bundle, bundleSlot);
@@ -773,6 +786,7 @@ public void setBackground(@Nullable ItemProvider itemProvider) {
773786
this.background.set(itemProvider);
774787
}
775788

789+
776790
@Override
777791
public void applyStructure(Structure structure) {
778792
if (structure.getWidth() != width)
@@ -815,6 +829,27 @@ public boolean isIgnoreObscuredInventorySlots() {
815829
return FuncUtils.getSafely(ignoreObscuredInventorySlots, DEFAULT_IGNORE_OBSCURED_INVENTORY_SLOTS);
816830
}
817831

832+
@Override
833+
public void setBundleSelectHandlers(List<? extends TriConsumer<Player, Integer, Integer>> bundleSelectHandlers) {
834+
this.bundleSelectHandlers.clear();
835+
this.bundleSelectHandlers.addAll(bundleSelectHandlers);
836+
}
837+
838+
@Override
839+
public List<TriConsumer<Player, Integer, Integer>> getBundleSelectHandlers() {
840+
return CollectionUtils.unmodifiableListUnchecked(bundleSelectHandlers);
841+
}
842+
843+
@Override
844+
public void addBundleSelectHandler(TriConsumer<? super Player, ? super Integer, ? super Integer> bundleSelectHandler) {
845+
bundleSelectHandlers.add(bundleSelectHandler);
846+
}
847+
848+
@Override
849+
public void removeBundleSelectHandler(TriConsumer<? super Player, ? super Integer, ? super Integer> bundleSelectHandler) {
850+
bundleSelectHandlers.remove(bundleSelectHandler);
851+
}
852+
818853
//<editor-fold desc="ingredient-key-based methods">
819854
@Override
820855
public void notifyWindows(char key, char... keys) {
@@ -1068,18 +1103,25 @@ public void fillRectangle(int x, int y, Gui gui, boolean replaceExisting) {
10681103

10691104
@Override
10701105
public void fillRectangle(int x, int y, int width, Inventory inventory, boolean replaceExisting) {
1071-
fillRectangle(x, y, width, inventory, null, replaceExisting);
1106+
fillRectangle(x, y, width, inventory, (ItemProvider) null, replaceExisting);
10721107
}
10731108

10741109
@Override
1075-
public void fillRectangle(int x, int y, int width, Inventory inventory, @Nullable ItemProvider background, boolean replaceExisting) {
1110+
public void fillRectangle(
1111+
int x,
1112+
int y,
1113+
int width,
1114+
Inventory inventory,
1115+
@Nullable ItemProvider background,
1116+
boolean replaceExisting
1117+
) {
10761118
int height = (int) Math.ceil((double) inventory.getSize() / (double) width);
10771119

10781120
int slotIndex = 0;
10791121
for (int slot : SlotUtils.getSlotsRect(x, y, width, height, this.width)) {
10801122
if (slotIndex >= inventory.getSize()) return;
10811123
if (hasSlotElement(slot) && !replaceExisting) continue;
1082-
setSlotElement(slot, new SlotElement.InventoryLink(inventory, slotIndex, background));
1124+
setSlotElement(slot, new SlotElement.InventoryLink(inventory, slotIndex, Property.of(background)));
10831125
slotIndex++;
10841126
}
10851127
}
@@ -1093,6 +1135,7 @@ static sealed abstract class AbstractBuilder<G extends Gui, S extends Builder<G,
10931135

10941136
protected @Nullable Structure structure;
10951137
protected @Nullable List<Consumer<? super G>> modifiers;
1138+
protected List<TriConsumer<? super Player, ? super Integer, ? super Integer>> bundleSelectHandlers = new ArrayList<>(0);
10961139
protected MutableProperty<@Nullable ItemProvider> background = MutableProperty.of(DEFAULT_BACKGROUND);
10971140
protected MutableProperty<Boolean> frozen = MutableProperty.of(DEFAULT_FROZEN);
10981141
protected MutableProperty<Boolean> ignoreObscuredInventorySlots = MutableProperty.of(DEFAULT_IGNORE_OBSCURED_INVENTORY_SLOTS);
@@ -1172,12 +1215,27 @@ public S setIgnoreObscuredInventorySlots(MutableProperty<Boolean> ignoreObscured
11721215
return (S) this;
11731216
}
11741217

1218+
@Override
1219+
public S setBundleSelectHandlers(List<? extends TriConsumer<? super Player, ? super Integer, ? super Integer>> bundleSelectHandlers) {
1220+
this.bundleSelectHandlers.clear();
1221+
this.bundleSelectHandlers.addAll(bundleSelectHandlers);
1222+
return (S) this;
1223+
}
1224+
1225+
@Override
1226+
public S addBundleSelectHandler(TriConsumer<? super Player, ? super Integer, ? super Integer> bundleSelectHandler) {
1227+
bundleSelectHandlers.add(bundleSelectHandler);
1228+
return (S) this;
1229+
}
1230+
11751231
/**
11761232
* Applies the {@link AbstractBuilder#modifiers} to the given {@link AbstractGui}.
11771233
*
11781234
* @param gui The {@link AbstractGui} to apply the modifiers to
11791235
*/
1236+
@SuppressWarnings("rawtypes")
11801237
protected void applyModifiers(G gui) {
1238+
gui.setBundleSelectHandlers((List) bundleSelectHandlers);
11811239
if (modifiers != null) modifiers.forEach(modifier -> modifier.accept(gui));
11821240
}
11831241

@@ -1193,6 +1251,7 @@ public S clone() {
11931251
clone.structure = structure.clone();
11941252
if (modifiers != null)
11951253
clone.modifiers = new ArrayList<>(modifiers);
1254+
clone.bundleSelectHandlers = new ArrayList<>(bundleSelectHandlers);
11961255
return (S) clone;
11971256
} catch (CloneNotSupportedException e) {
11981257
throw new AssertionError();

0 commit comments

Comments
 (0)