diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ComponentBuilder.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ComponentBuilder.java index 03217b9f4..32954a5ca 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ComponentBuilder.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ComponentBuilder.java @@ -1,7 +1,9 @@ package me.devnatan.inventoryframework.component; import java.util.function.BooleanSupplier; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import me.devnatan.inventoryframework.Ref; import me.devnatan.inventoryframework.context.IFContext; import me.devnatan.inventoryframework.state.State; @@ -162,4 +164,49 @@ public interface ComponentBuilder, C extends IF * @see #displayIf(Predicate) */ S hideIf(Predicate condition); + + /** + * Identifies this component with a constant key. + *

+ * Components with explicit keys are only re-rendered when their key changes. + * This can be used to prevent unnecessary re-renders during updates. + * + *

This API is experimental and is not subject to the general compatibility guarantees. + * It may be changed or removed completely in any further release. + * + * @param key The constant key to identify this component + * @return This component builder + */ + @ApiStatus.Experimental + S identifiedBy(String key); + + /** + * Identifies this component with a key provided by a {@link Supplier}. + *

+ * Components with explicit keys are only re-rendered when their key changes. + * This can be used to prevent unnecessary re-renders during scheduled updates. + * + *

This API is experimental and is not subject to the general compatibility guarantees. + * It may be changed or removed completely in any further release. + * + * @param keyProvider A supplier that provides the key to identify this component. + * @return This component builder. + */ + @ApiStatus.Experimental + S identifiedBy(Supplier keyProvider); + + /** + * Identifies this component with a key provided by a {@link Function} based on the context. + *

+ * Components with explicit keys are only re-rendered when their key changes. + * This can be used to prevent unnecessary re-renders during scheduled updates. + * + *

This API is experimental and is not subject to the general compatibility guarantees. + * It may be changed or removed completely in any further release. + * + * @param keyProvider A function that provides the key to identify this component based on the context. + * @return This component builder. + */ + @ApiStatus.Experimental + S identifiedBy(Function keyProvider); } diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java index 85cac589c..d09772cac 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/component/ItemComponent.java @@ -3,8 +3,8 @@ import java.util.Collections; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import me.devnatan.inventoryframework.InventoryFrameworkException; import me.devnatan.inventoryframework.Ref; @@ -17,7 +17,7 @@ // TODO Make this render abstract and remove `getResult` (Object) from IFSlotRenderContext public class ItemComponent implements Component, InteractionHandler { - private final String key = UUID.randomUUID().toString(); + private final Function keyFactory; private final VirtualView root; private int position; private final Object stack; @@ -30,10 +30,13 @@ public class ItemComponent implements Component, InteractionHandler { private final Set> watching; private final boolean isManagedExternally; private final boolean updateOnClick; - private boolean isVisible; private final Ref reference; + private boolean isVisible; + private volatile String lastKey; + public ItemComponent( + Function keyFactory, VirtualView root, int position, Object stack, @@ -48,6 +51,8 @@ public ItemComponent( boolean updateOnClick, boolean isVisible, Ref reference) { + //noinspection unchecked + this.keyFactory = (Function) keyFactory; this.root = root; this.position = position; this.stack = stack; @@ -66,7 +71,7 @@ public ItemComponent( @Override public String getKey() { - return key; + return lastKey; } @NotNull @@ -135,6 +140,8 @@ public boolean intersects(@NotNull Component other) { @Override public void render(@NotNull IFSlotRenderContext context) { + lastKey = keyFactory.apply(context); + if (getRenderHandler() != null) { final int initialSlot = getPosition(); @@ -181,10 +188,15 @@ public void render(@NotNull IFSlotRenderContext context) { @Override public void updated(@NotNull IFSlotRenderContext context) { if (context.isCancelled()) return; + // Key-based skip optimization should always take precedence + if (keyFactory != null + && lastKey != null) { + String currentKey = keyFactory.apply(context); + if (Objects.equals(lastKey, currentKey)) return; + } - // Static item with no `displayIf` must not even reach the update handler - if (!context.isForceUpdate() && displayCondition == null && getRenderHandler() == null) return; - + boolean isWatchingAnyState = + getWatchingStates() != null && !getWatching().isEmpty(); if (isVisible() && getUpdateHandler() != null) { getUpdateHandler().accept(context); if (context.isCancelled()) return; diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotClickContext.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotClickContext.java index 78a390b63..b95ae3883 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotClickContext.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotClickContext.java @@ -1,7 +1,6 @@ package me.devnatan.inventoryframework.context; import me.devnatan.inventoryframework.ViewContainer; -import me.devnatan.inventoryframework.component.Component; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -17,8 +16,6 @@ public interface IFSlotClickContext extends IFSlotContext, IFConfinedContext { @NotNull ViewContainer getClickedContainer(); - Component getComponent(); - int getClickedSlot(); /** diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotContext.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotContext.java index 86c338d13..46182c6d7 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotContext.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFSlotContext.java @@ -1,8 +1,10 @@ package me.devnatan.inventoryframework.context; import me.devnatan.inventoryframework.ViewContainer; +import me.devnatan.inventoryframework.component.Component; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; /** * Represents a context in which there is a specific slot related to it, the main context @@ -59,4 +61,16 @@ public interface IFSlotContext extends IFContext { */ @NotNull ViewContainer getContainer(); + + /** + * The component associated with this slot context. + * + *

This is an internal inventory-framework API that should not be used from outside of + * this library. No compatibility guarantees are provided. + * + * @return The component associated with this slot context. + */ + @ApiStatus.Internal + @UnknownNullability + Component getComponent(); } diff --git a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/internal/ElementFactory.java b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/internal/ElementFactory.java index 97de9a70a..d5aad0dbc 100644 --- a/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/internal/ElementFactory.java +++ b/inventory-framework-api/src/main/java/me/devnatan/inventoryframework/internal/ElementFactory.java @@ -68,7 +68,7 @@ public abstract IFSlotClickContext createSlotClickContext( boolean combined); public abstract IFSlotRenderContext createSlotRenderContext( - int slot, @NotNull IFRenderContext parent, @Nullable Viewer viewer); + int slot, @NotNull IFRenderContext parent, @Nullable Viewer viewer, Component component); /** * Creates a new close context for the current platform. diff --git a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java index 818ba2c84..f6fc74da4 100644 --- a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java +++ b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/component/DefaultComponentBuilder.java @@ -5,8 +5,11 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.function.BooleanSupplier; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import me.devnatan.inventoryframework.Ref; import me.devnatan.inventoryframework.context.IFContext; import me.devnatan.inventoryframework.state.State; @@ -16,6 +19,10 @@ public abstract class DefaultComponentBuilder, C extends IFContext> implements ComponentBuilder { + protected static final Function RANDOM_KEY_FACTORY = + __ -> UUID.randomUUID().toString(); + + protected Function keyFactory; protected Ref reference; protected Map data; protected boolean cancelOnClick, closeOnClick, updateOnClick; @@ -24,6 +31,7 @@ public abstract class DefaultComponentBuilder, protected Predicate displayCondition; protected DefaultComponentBuilder( + Function keyFactory, Ref reference, Map data, boolean cancelOnClick, @@ -32,6 +40,7 @@ protected DefaultComponentBuilder( Set> watchingStates, boolean isManagedExternally, Predicate displayCondition) { + this.keyFactory = keyFactory; this.reference = reference; this.data = data; this.cancelOnClick = cancelOnClick; @@ -122,4 +131,20 @@ public S hideIf(Predicate condition) { public S hideIf(BooleanSupplier condition) { return displayIf(condition == null ? null : () -> !condition.getAsBoolean()); } + + @Override + public S identifiedBy(String key) { + return identifiedBy(() -> key); + } + + @Override + public S identifiedBy(Supplier key) { + return identifiedBy(__ -> key.get()); + } + + @Override + public S identifiedBy(Function keyFactory) { + this.keyFactory = keyFactory; + return (S) this; + } } diff --git a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java index aa576eb6b..3d9b3494c 100644 --- a/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java +++ b/inventory-framework-core/src/main/java/me/devnatan/inventoryframework/context/AbstractIFContext.java @@ -114,7 +114,7 @@ private IFSlotRenderContext createSlotRenderContext(@NotNull Component component final IFRenderContext renderContext = (IFRenderContext) this; final IFSlotRenderContext slotRender = getRoot() .getElementFactory() - .createSlotRenderContext(component.getPosition(), renderContext, renderContext.getViewer()); + .createSlotRenderContext(component.getPosition(), renderContext, renderContext.getViewer(), component); slotRender.setForceUpdate(force); return slotRender; } diff --git a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/component/BukkitItemComponentBuilder.java b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/component/BukkitItemComponentBuilder.java index 81ea7741b..dddd629b4 100644 --- a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/component/BukkitItemComponentBuilder.java +++ b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/component/BukkitItemComponentBuilder.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import me.devnatan.inventoryframework.Ref; @@ -29,6 +30,7 @@ public final class BukkitItemComponentBuilder extends DefaultComponentBuilder keyFactory, VirtualView root, int slot, ItemStack item, @@ -61,6 +64,7 @@ private BukkitItemComponentBuilder( boolean isManagedExternally, Predicate displayCondition) { super( + keyFactory, reference, data, cancelOnClick, @@ -188,7 +192,10 @@ public BukkitItemComponentBuilder onUpdate(@Nullable Consumer componentKeyProvider = + keyFactory == null ? RANDOM_KEY_FACTORY : keyFactory; return new ItemComponent( + componentKeyProvider, root, slot, item, @@ -208,6 +215,7 @@ public BukkitItemComponentBuilder onUpdate(@Nullable Consumer?, slot: Int, item: ItemStack?, renderHandler: Consumer?, @@ -35,6 +38,7 @@ class MinestomItemComponentBuilder isManagedExternally: Boolean, displayCondition: Predicate?, ) : DefaultComponentBuilder( + keyFactory, reference, data, cancelOnClick, @@ -55,20 +59,21 @@ class MinestomItemComponentBuilder constructor( root: VirtualView, ) : this( - root, - -1, - null, - null, - null, - null, - null, - HashMap(), - false, - false, - false, - LinkedHashSet>(), - false, - null, + root = root, + keyFactory = null, + slot = -1, + item = null, + renderHandler = null, + clickHandler = null, + updateHandler = null, + reference = null, + data = HashMap(), + cancelOnClick = false, + closeOnClick = false, + updateOnClick = false, + watchingStates = LinkedHashSet>(), + isManagedExternally = false, + displayCondition = null, ) init { @@ -203,39 +208,59 @@ class MinestomItemComponentBuilder return this } - override fun create(): Component = - ItemComponent( + override fun create(): Component { + val componentKeyProvider = if (keyFactory == null) RANDOM_KEY_FACTORY else keyFactory + + return ItemComponent( + // keyFactory = + componentKeyProvider, + // root = root, + // position = slot, + // stack = item, + // cancelOnClick = cancelOnClick, + // closeOnClick = closeOnClick, + // displayCondition = displayCondition, + // renderHandler = renderHandler, + // updateHandler = updateHandler, + // clickHandler = clickHandler, + // watching = watchingStates, + // isManagedExternally = isManagedExternally, + // updateOnClick = updateOnClick, + // isVisible = false, + // reference = reference, ) + } override fun copy(): MinestomItemComponentBuilder = MinestomItemComponentBuilder( - root, - slot, - item, - renderHandler, - clickHandler, - updateHandler, - reference, - data, - cancelOnClick, - closeOnClick, - updateOnClick, - watchingStates, - isManagedExternally, - displayCondition, + root = root, + keyFactory = keyFactory, + slot = slot, + item = item, + renderHandler = renderHandler, + clickHandler = clickHandler, + updateHandler = updateHandler, + reference = reference, + data = data, + cancelOnClick = cancelOnClick, + closeOnClick = closeOnClick, + updateOnClick = updateOnClick, + watchingStates = watchingStates, + isManagedExternally = isManagedExternally, + displayCondition = displayCondition, ) } diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt index 6ad529c50..166ee180f 100644 --- a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/context/SlotRenderContext.kt @@ -3,9 +3,11 @@ package me.devnatan.inventoryframework.context import me.devnatan.inventoryframework.MinestomViewer import me.devnatan.inventoryframework.RootView import me.devnatan.inventoryframework.Viewer +import me.devnatan.inventoryframework.component.Component import net.minestom.server.entity.Player import net.minestom.server.item.ItemStack import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.UnknownNullability class SlotRenderContext @ApiStatus.Internal @@ -13,6 +15,7 @@ class SlotRenderContext slot: Int, parent: IFRenderContext, private val viewer: Viewer?, + private val _component: Component, ) : SlotContext(slot, parent), IFSlotRenderContext { override val player: Player = (viewer as MinestomViewer).player @@ -48,6 +51,8 @@ class SlotRenderContext override fun isOnEntityContainer(): Boolean = container.isEntityContainer + override fun getComponent(): @UnknownNullability Component = _component + override fun getViewer(): Viewer? = viewer override fun closeForPlayer() { diff --git a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt index 11e290abf..3de131cf1 100644 --- a/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt +++ b/inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/internal/MinestomElementFactory.kt @@ -164,7 +164,8 @@ class MinestomElementFactory : ElementFactory() { slot: Int, parent: IFRenderContext, viewer: Viewer?, - ): IFSlotRenderContext = SlotRenderContext(slot, parent, viewer) + component: Component, + ): IFSlotRenderContext = SlotRenderContext(slot, parent, viewer, component) override fun createCloseContext( viewer: Viewer, diff --git a/inventory-framework-platform/src/test/java/me/devnatan/inventoryframework/component/TestItemComponentBuilder.java b/inventory-framework-platform/src/test/java/me/devnatan/inventoryframework/component/TestItemComponentBuilder.java index 663600e34..200e5dc80 100644 --- a/inventory-framework-platform/src/test/java/me/devnatan/inventoryframework/component/TestItemComponentBuilder.java +++ b/inventory-framework-platform/src/test/java/me/devnatan/inventoryframework/component/TestItemComponentBuilder.java @@ -2,6 +2,7 @@ import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; import me.devnatan.inventoryframework.Ref; import me.devnatan.inventoryframework.context.IFContext; @@ -14,10 +15,11 @@ public class TestItemComponentBuilder extends DefaultComponentBuilder keyFactory, Ref referenceKey, Map data, boolean cancelOnClick, @@ -27,6 +29,7 @@ protected TestItemComponentBuilder( boolean isManagedExternally, Predicate displayCondition) { super( + keyFactory, referenceKey, data, cancelOnClick, diff --git a/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/internal/MockElementFactory.java b/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/internal/MockElementFactory.java index d1d311faa..9728c80f2 100644 --- a/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/internal/MockElementFactory.java +++ b/inventory-framework-test/src/main/java/me/devnatan/inventoryframework/internal/MockElementFactory.java @@ -86,12 +86,13 @@ public IFSlotClickContext createSlotClickContext( @Override public IFSlotRenderContext createSlotRenderContext( - int slot, @NotNull IFRenderContext parent, @Nullable Viewer viewer) { + int slot, @NotNull IFRenderContext parent, @Nullable Viewer viewer, Component component) { IFSlotRenderContext mock = mock(IFSlotRenderContext.class); when(mock.getSlot()).thenReturn(slot); when(mock.getParent()).thenReturn(parent); when(mock.getViewer()).thenReturn(viewer); when(mock.getContainer()).then(ignored -> parent.getContainer()); + when(mock.getComponent()).thenReturn(component); return mock; }