From b82fe661c897daefdb5f3b3b8f94de9679796091 Mon Sep 17 00:00:00 2001 From: Natan Date: Thu, 30 Jul 2026 00:56:09 -0300 Subject: [PATCH 1/6] feat: InlineView class --- .../inventoryframework/InlineView.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/InlineView.java diff --git a/inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/InlineView.java b/inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/InlineView.java new file mode 100644 index 00000000..0e4ee473 --- /dev/null +++ b/inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/InlineView.java @@ -0,0 +1,98 @@ +package me.devnatan.inventoryframework; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import me.devnatan.inventoryframework.context.CloseContext; +import me.devnatan.inventoryframework.context.Context; +import me.devnatan.inventoryframework.context.OpenContext; +import me.devnatan.inventoryframework.context.RenderContext; +import me.devnatan.inventoryframework.context.SlotClickContext; +import org.jetbrains.annotations.NotNull; + +/** + * A {@link View} whose lifecycle handlers are supplied as lambdas rather than by overriding methods + * on a subclass. Instances are created and configured through {@link ViewBuilder}, never directly. + *

+ * Unlike a regular {@link View} subclass, handlers aren't known upfront in a constructor, so they're + * accumulated into these fields as {@link ViewBuilder}'s fluent methods are called, in the same + * "add another handler" fashion regardless of call order. + */ +final class InlineView extends View { + + private final ViewConfigBuilder configBuilder; + private Consumer initializer; + private Consumer openHandler; + private Consumer firstRenderHandler; + private Consumer updateHandler; + private Consumer closeHandler; + private Consumer clickHandler; + private BiConsumer resumeHandler; + + InlineView(@NotNull ViewConfigBuilder configBuilder) { + this.configBuilder = configBuilder; + } + + void addInitializer(@NotNull Consumer handler) { + initializer = initializer == null ? handler : initializer.andThen(handler); + } + + void addOpenHandler(@NotNull Consumer handler) { + openHandler = openHandler == null ? handler : openHandler.andThen(handler); + } + + void addFirstRenderHandler(@NotNull Consumer handler) { + firstRenderHandler = firstRenderHandler == null ? handler : firstRenderHandler.andThen(handler); + } + + void addUpdateHandler(@NotNull Consumer handler) { + updateHandler = updateHandler == null ? handler : updateHandler.andThen(handler); + } + + void addCloseHandler(@NotNull Consumer handler) { + closeHandler = closeHandler == null ? handler : closeHandler.andThen(handler); + } + + void addClickHandler(@NotNull Consumer handler) { + clickHandler = clickHandler == null ? handler : clickHandler.andThen(handler); + } + + void addResumeHandler(@NotNull BiConsumer handler) { + resumeHandler = resumeHandler == null ? handler : resumeHandler.andThen(handler); + } + + @Override + public void onInit(@NotNull ViewConfigBuilder config) { + config.inheritFrom(configBuilder); + if (initializer != null) initializer.accept(this); + } + + @Override + public void onOpen(@NotNull OpenContext open) { + if (openHandler != null) openHandler.accept(open); + } + + @Override + public void onFirstRender(@NotNull RenderContext render) { + if (firstRenderHandler != null) firstRenderHandler.accept(render); + } + + @Override + public void onUpdate(@NotNull Context update) { + if (updateHandler != null) updateHandler.accept(update); + } + + @Override + public void onClose(@NotNull CloseContext close) { + if (closeHandler != null) closeHandler.accept(close); + } + + @Override + public void onClick(@NotNull SlotClickContext click) { + if (clickHandler != null) clickHandler.accept(click); + } + + @Override + public void onResume(@NotNull Context origin, @NotNull Context target) { + if (resumeHandler != null) resumeHandler.accept(origin, target); + } +} From d7d5bb9cefb46dfbd588ecb514467b46cc450882 Mon Sep 17 00:00:00 2001 From: Natan Date: Thu, 30 Jul 2026 00:57:25 -0300 Subject: [PATCH 2/6] chore: create inventory-framework-inline module --- inventory-framework-inline/build.gradle.kts | 15 +++++++++ .../me/devnatan/inventoryframework/View.java | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 inventory-framework-inline/build.gradle.kts diff --git a/inventory-framework-inline/build.gradle.kts b/inventory-framework-inline/build.gradle.kts new file mode 100644 index 00000000..b42a0737 --- /dev/null +++ b/inventory-framework-inline/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("me.devnatan.inventoryframework.library") +} + +inventoryFramework { + publish = true +} + +dependencies { + compileOnly(libs.spigot) + testCompileOnly(libs.spigot) + testRuntimeOnly(libs.spigot) + compileOnlyApi(projects.inventoryFrameworkPlatformBukkit) + testImplementation(projects.inventoryFrameworkPlatformBukkit) +} diff --git a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java index 358486eb..22ef6b6a 100644 --- a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java +++ b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java @@ -1,5 +1,6 @@ package me.devnatan.inventoryframework; +import java.util.Collections; import me.devnatan.inventoryframework.component.BukkitItemComponentBuilder; import me.devnatan.inventoryframework.context.CloseContext; import me.devnatan.inventoryframework.context.Context; @@ -51,4 +52,35 @@ public final void registerPlatformInterceptors() { public final void nextTick(Runnable task) { Bukkit.getServer().getScheduler().runTask(getFramework().getOwner(), task); } + + /** + * Opens this view instance directly to a player. + *

+ * Unlike {@link ViewFrame#open(Class, Player)}, this does not require the view to be looked up + * by its class, so it works for views built with the inline builder API as well as regular + * class-based views. + * + * @param player The player that'll see this view. + * @return The id of the newly created context. + */ + @ApiStatus.Experimental + public final String open(@NotNull Player player) { + return open(player, null); + } + + /** + * Opens this view instance directly to a player with initial data. + *

+ * Unlike {@link ViewFrame#open(Class, Player, Object)}, this does not require the view to be + * looked up by its class, so it works for views built with the inline builder API as well as + * regular class-based views. + * + * @param player The player that'll see this view. + * @param initialData The initial data. + * @return The id of the newly created context. + */ + @ApiStatus.Experimental + public final String open(@NotNull Player player, Object initialData) { + return open(Collections.singletonList(getElementFactory().createViewer(player, null)), initialData); + } } From 462a512595a0dee22733cd46e778f2a853a3aa10 Mon Sep 17 00:00:00 2001 From: Natan Date: Thu, 30 Jul 2026 00:57:41 -0300 Subject: [PATCH 3/6] chore: include inline module as Bukkit runtime dependency --- inventory-framework-platform-bukkit/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/inventory-framework-platform-bukkit/build.gradle.kts b/inventory-framework-platform-bukkit/build.gradle.kts index 723e7a89..752bdc5c 100644 --- a/inventory-framework-platform-bukkit/build.gradle.kts +++ b/inventory-framework-platform-bukkit/build.gradle.kts @@ -12,6 +12,7 @@ inventoryFramework { dependencies { api(projects.inventoryFrameworkPlatform) runtimeOnly(projects.inventoryFrameworkAnvilInput) + runtimeOnly(projects.inventoryFrameworkInline) compileOnly(libs.spigot) testCompileOnly(libs.spigot) testRuntimeOnly(libs.spigot) From 1c6dda22632f3fb37c39d16c69ee47c7587450ad Mon Sep 17 00:00:00 2001 From: Natan Date: Thu, 30 Jul 2026 00:57:49 -0300 Subject: [PATCH 4/6] chore: register module --- settings.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cf6875d..14305cf5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,7 @@ include( "inventory-framework-platform-bukkit", "inventory-framework-platform-minestom", "inventory-framework-anvil-input", + "inventory-framework-inline", "example-paper", "example-minestom" ) From eaad4d2c184b244751775fe5d352f3163a965ffa Mon Sep 17 00:00:00 2001 From: Natan Date: Thu, 30 Jul 2026 00:58:55 -0300 Subject: [PATCH 5/6] feat: automatic plugin detection --- .../inventoryframework/PluginDetector.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/PluginDetector.java diff --git a/inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/PluginDetector.java b/inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/PluginDetector.java new file mode 100644 index 00000000..a073e864 --- /dev/null +++ b/inventory-framework-inline/src/main/java/me/devnatan/inventoryframework/PluginDetector.java @@ -0,0 +1,42 @@ +package me.devnatan.inventoryframework; + +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Best-effort detection of the plugin that is calling into the inline builder API, so that users + * don't have to explicitly pass their plugin instance around. + *

+ * Walks the current call stack looking for the first frame that doesn't belong to this library + * (identified by classloader identity rather than package name, since this library is commonly + * shaded and relocated) and resolves it to a {@link Plugin} via {@link JavaPlugin#getProvidingPlugin(Class)}. + */ +final class PluginDetector { + + private PluginDetector() {} + + static Plugin detectCallingPlugin() { + final ClassLoader ownLoader = PluginDetector.class.getClassLoader(); + final StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + + for (final StackTraceElement element : stack) { + final Class frameClass; + try { + frameClass = Class.forName(element.getClassName(), false, ownLoader); + } catch (final Throwable ignored) { + continue; + } + + if (frameClass.getClassLoader() == ownLoader) continue; + + try { + return JavaPlugin.getProvidingPlugin(frameClass); + } catch (final Throwable ignored) { + // frame doesn't belong to a plugin class loader, keep looking up the stack + } + } + + throw new IllegalStateException("Could not automatically determine the plugin that owns this view. " + + "Set it explicitly with ViewBuilder#plugin(Plugin)."); + } +} From adc174375e767c74f0daa4490330cee97c01f303 Mon Sep 17 00:00:00 2001 From: Natan Date: Thu, 30 Jul 2026 13:05:56 -0300 Subject: [PATCH 6/6] feat: ItemPlacement common interface --- .../me/devnatan/inventoryframework/View.java | 32 ---------- .../context/ItemPlacement.java | 62 +++++++++++++++++++ .../context/RenderContext.java | 7 ++- 3 files changed, 68 insertions(+), 33 deletions(-) create mode 100644 inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/ItemPlacement.java diff --git a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java index 22ef6b6a..358486eb 100644 --- a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java +++ b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/View.java @@ -1,6 +1,5 @@ package me.devnatan.inventoryframework; -import java.util.Collections; import me.devnatan.inventoryframework.component.BukkitItemComponentBuilder; import me.devnatan.inventoryframework.context.CloseContext; import me.devnatan.inventoryframework.context.Context; @@ -52,35 +51,4 @@ public final void registerPlatformInterceptors() { public final void nextTick(Runnable task) { Bukkit.getServer().getScheduler().runTask(getFramework().getOwner(), task); } - - /** - * Opens this view instance directly to a player. - *

- * Unlike {@link ViewFrame#open(Class, Player)}, this does not require the view to be looked up - * by its class, so it works for views built with the inline builder API as well as regular - * class-based views. - * - * @param player The player that'll see this view. - * @return The id of the newly created context. - */ - @ApiStatus.Experimental - public final String open(@NotNull Player player) { - return open(player, null); - } - - /** - * Opens this view instance directly to a player with initial data. - *

- * Unlike {@link ViewFrame#open(Class, Player, Object)}, this does not require the view to be - * looked up by its class, so it works for views built with the inline builder API as well as - * regular class-based views. - * - * @param player The player that'll see this view. - * @param initialData The initial data. - * @return The id of the newly created context. - */ - @ApiStatus.Experimental - public final String open(@NotNull Player player, Object initialData) { - return open(Collections.singletonList(getElementFactory().createViewer(player, null)), initialData); - } } diff --git a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/ItemPlacement.java b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/ItemPlacement.java new file mode 100644 index 00000000..8b410178 --- /dev/null +++ b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/ItemPlacement.java @@ -0,0 +1,62 @@ +package me.devnatan.inventoryframework.context; + +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; + +/** + * Common contract for placing items at specific slots. + *

+ * Implemented by {@link RenderContext}, for immediate placement during rendering, and by + * {@code ViewBuilder} (in the {@code inventory-framework-inline} module), for placement declared + * ahead of render time. What placing an item returns differs per implementation - the item builder + * itself for further per-item configuration on {@link RenderContext}, versus the implementer for + * continued top-level chaining on a builder. + * + * @param What placing an item returns. + */ +public interface ItemPlacement { + + /** + * Places an item in a specific slot. + * + * @param slot The slot in which the item will be positioned. + * @param item The item. + * @return See implementation. + */ + R slot(int slot, @Nullable ItemStack item); + + /** + * Places an item at a specific row and column. + * + * @param row The row (Y) in which the item will be positioned. + * @param column The column (X) in which the item will be positioned. + * @param item The item. + * @return See implementation. + */ + R slot(int row, int column, @Nullable ItemStack item); + + /** + * Places an item in the first slot of the container. + * + * @param item The item. + * @return See implementation. + */ + R firstSlot(@Nullable ItemStack item); + + /** + * Places an item in the last slot of the container. + * + * @param item The item. + * @return See implementation. + */ + R lastSlot(@Nullable ItemStack item); + + /** + * Places an item in every slot matching a layout character. + * + * @param character The layout character target. + * @param item The item. + * @return See implementation. + */ + R layoutSlot(char character, @Nullable ItemStack item); +} diff --git a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/RenderContext.java b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/RenderContext.java index b89eae09..6229a1a9 100644 --- a/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/RenderContext.java +++ b/inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/RenderContext.java @@ -21,7 +21,7 @@ import org.jetbrains.annotations.Nullable; public final class RenderContext extends PlatformRenderContext - implements Context, InventoryHolder { + implements Context, InventoryHolder, ItemPlacement { private final Player player; @@ -77,6 +77,7 @@ public void resetTitleForPlayer(@NotNull Player player) { * @param slot The slot in which the item will be positioned. * @return An item builder to configure the item. */ + @Override public @NotNull BukkitItemComponentBuilder slot(int slot, @Nullable ItemStack item) { return slot(slot).withItem(item); } @@ -88,6 +89,7 @@ public void resetTitleForPlayer(@NotNull Player player) { * @param column The column (X) in which the item will be positioned. * @return An item builder to configure the item. */ + @Override @NotNull public BukkitItemComponentBuilder slot(int row, int column, @Nullable ItemStack item) { return slot(row, column).withItem(item); @@ -99,6 +101,7 @@ public BukkitItemComponentBuilder slot(int row, int column, @Nullable ItemStack * @param item The item that'll be set. * @return An item builder to configure the item. */ + @Override public @NotNull BukkitItemComponentBuilder firstSlot(@Nullable ItemStack item) { return firstSlot().withItem(item); } @@ -109,6 +112,7 @@ public BukkitItemComponentBuilder slot(int row, int column, @Nullable ItemStack * @param item The item that'll be set. * @return An item builder to configure the item. */ + @Override public @NotNull BukkitItemComponentBuilder lastSlot(@Nullable ItemStack item) { return lastSlot().withItem(item); } @@ -130,6 +134,7 @@ public BukkitItemComponentBuilder slot(int row, int column, @Nullable ItemStack * @param item The item that'll represent the layout character. * @return An item builder to configure the item. */ + @Override public @NotNull BukkitItemComponentBuilder layoutSlot(char character, @Nullable ItemStack item) { return layoutSlot(character).withItem(item); }