diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7aa7375..ac96698 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ cd guiAPI ./gradlew build ``` -The built jar is at `build/libs/guiapi-1.0.5.jar`. +The built jar is at `build/libs/guiapi-1.0.6.jar`. To run in a local Minecraft instance, use Fabric's `runServer` task or drop the jar into a test server's `mods/` folder. diff --git a/README.md b/README.md index 9fb0eb7..15126e3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ No client mod required. No macros. No external dependencies beyond Fabric API. ## Installation -1. Drop `guiapi-1.0.5.jar` into your `mods/` folder. +1. Drop `guiapi-1.0.6.jar` into your `mods/` folder. 2. Drop your datapack into `world/datapacks/`. 3. Run `/reload` or `/guiapi reload`. @@ -220,7 +220,7 @@ Please refer to the updated `example-datapack` directory in the repository sourc ```bash chmod +x gradlew ./gradlew build -# Output: build/libs/guiapi-1.0.5.jar +# Output: build/libs/guiapi-1.0.6.jar ``` Requires **Java 21**. diff --git a/build.gradle b/build.gradle index 0ba7f72..d4cd5da 100644 --- a/build.gradle +++ b/build.gradle @@ -24,8 +24,6 @@ dependencies { modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - // Optional Mod Menu integration. The classes are compiled against Mod Menu, - // but Mod Menu is only suggested at runtime in fabric.mod.json. modCompileOnly "maven.modrinth:modmenu:${project.modmenu_version}" modApi "me.shedaniel.cloth:cloth-config-fabric:19.0.147" @@ -38,16 +36,16 @@ test { } processResources { - inputs.property "version", project.version - inputs.property "minecraft_version", project.minecraft_version - inputs.property "loader_version", project.loader_version + def props = [ + version: version, + minecraft_version: project.minecraft_version, + loader_version: project.loader_version + ] + + inputs.properties(props) filesMatching("fabric.mod.json") { - expand( - "version": project.version, - "minecraft_version": project.minecraft_version, - "loader_version": project.loader_version - ) + expand(props) } } @@ -57,15 +55,17 @@ tasks.withType(JavaCompile).configureEach { java { withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } jar { - inputs.property "archivesName", project.base.archivesName - from("LICENSE") { - rename { "${it}_${inputs.properties.archivesName}" } - } + // LICENSE dosyası ekleme kaldırıldı (deprecated hatası buradan kaynaklanıyordu) +} + +tasks.named('build') { + dependsOn 'clean' } publishing { @@ -91,4 +91,4 @@ publishing { repositories { mavenLocal() } -} +} \ No newline at end of file diff --git a/example-datapack/data/example/gui/macro_and_input_demo.json b/example-datapack/data/example/gui/macro_and_input_demo.json index 3f7c181..139939f 100644 --- a/example-datapack/data/example/gui/macro_and_input_demo.json +++ b/example-datapack/data/example/gui/macro_and_input_demo.json @@ -21,7 +21,14 @@ "§7Saves to {input} and {var:myVar}" ], "actions": [ - { "type": "anvil_input", "var": "myVar", "value": "Enter Name|{var:myVar}" } + { + "type": "anvil_input", + "var": "myVar", + "value": "Enter Name|{var:myVar}", + "actions": [ + { "type": "message", "value": "§aSaved input: §f{input}" } + ] + } ] }, { diff --git a/gradle.properties b/gradle.properties index 141adf2..9354c0a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.workers.max=1 org.gradle.daemon=false # Project -mod_version=1.0.5+1.21.8 +mod_version=1.0.6+1.21.8 maven_group=dev.toolkitmc archives_base_name=guiapi diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java index 16b40a2..e9835c3 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/BarrelGuiHandler.java @@ -703,12 +703,18 @@ static boolean executeAction(ServerPlayerEntity player, GuiDefinition def, } final Identifier previousGuiId = def.getId(); final int previousPage = currentPage; + final java.util.List afterInputActions = action.actions(); AnvilGuiHandler.openInput(player, anvilTitle, defaultText, (sp, text) -> { GuiVarStore.INSTANCE.set(sp.getUuid(), varKey, text); GuiInputStore.INSTANCE.set(sp.getUuid(), text); dev.toolkitmc.guiapi.loader.GuiRegistry.INSTANCE.get(previousGuiId) - .ifPresent(target -> open(sp, target, previousPage)); + .ifPresent(target -> { + if (!afterInputActions.isEmpty()) { + executeDelayedActionChain(sp, target, previousPage, afterInputActions, 0, false); + } + open(sp, target, previousPage); + }); }); } case RUN_FUNCTION -> { diff --git a/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java b/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java index c4f0314..657cf04 100644 --- a/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java +++ b/src/main/java/dev/toolkitmc/guiapi/gui/GuiDefinition.java @@ -131,12 +131,15 @@ public static ConditionType fromString(String s) { * @param var Variable key for set_var / add_var / sub_var / reset_var actions * @param delay Action execution delay in ticks */ - public record ButtonAction(ActionType type, String value, RunWith runWith, String var, int delay) { + public record ButtonAction(ActionType type, String value, RunWith runWith, String var, int delay, List actions) { public ButtonAction(ActionType type, String value) { - this(type, value, RunWith.PLAYER, "", 0); + this(type, value, RunWith.PLAYER, "", 0, List.of()); } public ButtonAction(ActionType type, String value, RunWith runWith) { - this(type, value, runWith, "", 0); + this(type, value, runWith, "", 0, List.of()); + } + public ButtonAction(ActionType type, String value, RunWith runWith, String var, int delay) { + this(type, value, runWith, var, delay, List.of()); } } @@ -527,7 +530,17 @@ private static ButtonAction parseAction(JsonObject a) { ? RunWith.fromString(a.get("run_with").getAsString()) : RunWith.PLAYER; int delay = a.has("delay") ? Math.max(0, a.get("delay").getAsInt()) : 0; - return new ButtonAction(type, value, runWith, var, delay); + + List nestedActions = new ArrayList<>(); + if (a.has("actions") && a.get("actions").isJsonArray()) { + for (JsonElement el : a.getAsJsonArray("actions")) { + if (el.isJsonObject()) { + nestedActions.add(parseAction(el.getAsJsonObject())); + } + } + } + + return new ButtonAction(type, value, runWith, var, delay, nestedActions); } // ── Getters ────────────────────────────────────────────────────────────── diff --git a/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java b/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java index b9d8f4a..99fdc9b 100644 --- a/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java +++ b/src/main/java/dev/toolkitmc/guiapi/loader/GuiRegistry.java @@ -198,21 +198,13 @@ private static String serializeDefinition(GuiDefinition def) { com.google.gson.JsonArray actionsOnArr = new com.google.gson.JsonArray(); for (GuiDefinition.ButtonAction act : tgl.actionsOn()) { - JsonObject aObj = new JsonObject(); - aObj.addProperty("type", act.type().name().toLowerCase()); - aObj.addProperty("value", act.value()); - if (!act.var().isEmpty()) aObj.addProperty("var", act.var()); - actionsOnArr.add(aObj); + actionsOnArr.add(serializeAction(act)); } tObj.add("actions_on", actionsOnArr); com.google.gson.JsonArray actionsOffArr = new com.google.gson.JsonArray(); for (GuiDefinition.ButtonAction act : tgl.actionsOff()) { - JsonObject aObj = new JsonObject(); - aObj.addProperty("type", act.type().name().toLowerCase()); - aObj.addProperty("value", act.value()); - if (!act.var().isEmpty()) aObj.addProperty("var", act.var()); - actionsOffArr.add(aObj); + actionsOffArr.add(serializeAction(act)); } tObj.add("actions_off", actionsOffArr); @@ -220,11 +212,7 @@ private static String serializeDefinition(GuiDefinition def) { } else { com.google.gson.JsonArray actionsArr = new com.google.gson.JsonArray(); for (GuiDefinition.ButtonAction act : b.actions()) { - JsonObject aObj = new JsonObject(); - aObj.addProperty("type", act.type().name().toLowerCase()); - aObj.addProperty("value", act.value()); - if (!act.var().isEmpty()) aObj.addProperty("var", act.var()); - actionsArr.add(aObj); + actionsArr.add(serializeAction(act)); } bObj.add("actions", actionsArr); } @@ -236,6 +224,23 @@ private static String serializeDefinition(GuiDefinition def) { return GSON.toJson(obj); } + private static JsonObject serializeAction(GuiDefinition.ButtonAction act) { + JsonObject aObj = new JsonObject(); + aObj.addProperty("type", act.type().name().toLowerCase()); + if (!act.value().isEmpty()) aObj.addProperty("value", act.value()); + if (act.runWith() != GuiDefinition.RunWith.PLAYER) aObj.addProperty("run_with", act.runWith().name().toLowerCase()); + if (!act.var().isEmpty()) aObj.addProperty("var", act.var()); + if (act.delay() > 0) aObj.addProperty("delay", act.delay()); + if (!act.actions().isEmpty()) { + com.google.gson.JsonArray nested = new com.google.gson.JsonArray(); + for (GuiDefinition.ButtonAction nestedAction : act.actions()) { + nested.add(serializeAction(nestedAction)); + } + aObj.add("actions", nested); + } + return aObj; + } + /** Addon API — register a GUI definition from Java code */ public void registerAddon(GuiDefinition definition) { Identifier id = definition.getId(); diff --git a/src/main/java/dev/toolkitmc/guiapi/modmenu/GuiApiModMenuEntry.java b/src/main/java/dev/toolkitmc/guiapi/modmenu/GuiApiModMenuEntry.java index 7531fe8..40245f8 100644 --- a/src/main/java/dev/toolkitmc/guiapi/modmenu/GuiApiModMenuEntry.java +++ b/src/main/java/dev/toolkitmc/guiapi/modmenu/GuiApiModMenuEntry.java @@ -27,135 +27,9 @@ public class GuiApiModMenuEntry implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return parent -> { - me.shedaniel.clothconfig2.api.ConfigBuilder builder = me.shedaniel.clothconfig2.api.ConfigBuilder.create() - .setParentScreen(parent) - .setTitle(Text.literal("GUI API Settings")); - - me.shedaniel.clothconfig2.api.ConfigCategory generalCategory = builder.getOrCreateCategory(Text.literal("Settings")); - me.shedaniel.clothconfig2.api.ConfigCategory otherCategory = builder.getOrCreateCategory(Text.literal("Other")); - me.shedaniel.clothconfig2.api.ConfigCategory guisCategory = builder.getOrCreateCategory(Text.literal("Loaded GUIs")); - - me.shedaniel.clothconfig2.api.ConfigEntryBuilder entryBuilder = builder.entryBuilder(); - - GuiApiConfig cfg = GuiApiConfig.INSTANCE; - - // --- CATEGORY 1: Settings --- - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Allow run_with: console"), cfg.isAllowConsoleRunWith()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setAllowConsoleRunWith) - .setTooltip(Text.literal("Permit buttons to run commands with console (OP-level) permission.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Log unknown item IDs"), cfg.isLogUnknownItems()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setLogUnknownItems) - .setTooltip(Text.literal("Print a WARN to the log when a button uses an unrecognized item ID.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Log unknown sound IDs"), cfg.isLogUnknownSounds()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setLogUnknownSounds) - .setTooltip(Text.literal("Print a WARN to the log when a sound action uses an unrecognized sound ID.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Debug mode"), cfg.isDebugMode()) - .setDefaultValue(false) - .setSaveConsumer(cfg::setDebugMode) - .setTooltip(Text.literal("Log GUI open/close, action execution and placeholder resolution to console.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Allow close_on_move"), cfg.isAllowCloseOnMove()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setAllowCloseOnMove) - .setTooltip(Text.literal("Globally permit menus to close automatically when players walk away.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Allow action delays"), cfg.isAllowDelayedActions()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setAllowDelayedActions) - .setTooltip(Text.literal("Globally permit action chains to execute with tick delays.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Allow status effects"), cfg.isAllowStatusEffects()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setAllowStatusEffects) - .setTooltip(Text.literal("Globally permit buttons and click actions to manage player potion effects.")) - .build()); - - generalCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Log command executions"), cfg.isLogCommands()) - .setDefaultValue(false) - .setSaveConsumer(cfg::setLogCommands) - .setTooltip(Text.literal("Write a message to log console every time a GUI button runs a command.")) - .build()); - - generalCategory.addEntry(entryBuilder.startIntSlider(Text.literal("Default Tick Rate"), cfg.getDefaultTickRate(), 1, 100) - .setDefaultValue(20) - .setSaveConsumer(cfg::setDefaultTickRate) - .setTooltip(Text.literal("Default tick rate for auto-refreshing menus.")) - .build()); - - generalCategory.addEntry(entryBuilder.startIntSlider(Text.literal("Command permission level"), cfg.getPermissionLevel(), 0, 4) - .setDefaultValue(2) - .setSaveConsumer(cfg::setPermissionLevel) - .setTooltip(Text.literal("Permission level for running standard commands.")) - .build()); - - - // --- CATEGORY 2: Other --- - otherCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Enable button glint"), cfg.isEnableButtonGlint()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setEnableButtonGlint) - .setTooltip(Text.literal("Toggles whether to show the glowing enchantment shine on buttons.")) - .build()); - - otherCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Show developer item IDs"), cfg.isShowItemIdsDeveloper()) - .setDefaultValue(false) - .setSaveConsumer(cfg::setShowItemIdsDeveloper) - .setTooltip(Text.literal("Show technical item IDs in tooltips for designers.")) - .build()); - - otherCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Mute click errors"), cfg.isMuteClickErrors()) - .setDefaultValue(false) - .setSaveConsumer(cfg::setMuteClickErrors) - .setTooltip(Text.literal("Silence warn messages when clicks fail to meet conditions or permissions.")) - .build()); - - otherCategory.addEntry(entryBuilder.startBooleanToggle(Text.literal("Play close sound"), cfg.isEnableCloseSound()) - .setDefaultValue(true) - .setSaveConsumer(cfg::setEnableCloseSound) - .setTooltip(Text.literal("Play a clean chest close sound when closing virtual GUIs.")) - .build()); - - otherCategory.addEntry(entryBuilder.startTextField(Text.literal("Chat Prefix"), cfg.getChatPrefix()) - .setDefaultValue("§8[§6GuiAPI§8] §f") - .setSaveConsumer(cfg::setChatPrefix) - .setTooltip(Text.literal("Custom prefix for all chat messages sent by GuiAPI.")) - .build()); - - otherCategory.addEntry(entryBuilder.startIntSlider(Text.literal("Sound Volume (%)"), cfg.getSoundVolume(), 0, 100) - .setDefaultValue(100) - .setSaveConsumer(cfg::setSoundVolume) - .setTooltip(Text.literal("Global multiplier for mod UI sound volumes.")) - .build()); - - otherCategory.addEntry(entryBuilder.startSelector(Text.literal("Message Execute Mode"), new String[]{"CHAT", "SYSTEM", "SILENT"}, cfg.getCommandExecuteMode()) - .setDefaultValue("CHAT") - .setSaveConsumer(cfg::setCommandExecuteMode) - .setTooltip(Text.literal("Where button message feedback is routed.")) - .build()); - - - // --- CATEGORY 3: Loaded GUIs --- - guisCategory.addEntry(entryBuilder.startTextDescription(Text.literal("§eClick [Loaded GUIs List] below to open the Visual Dashboard!")) - .build()); - - builder.setSavingRunnable(() -> { - cfg.save(); - }); - - return builder.build(); - }; + // Use GuiAPI's custom dashboard instead of a static Cloth Config category. + // The custom screen contains the real Loaded GUIs list with Edit / Info buttons. + return GuiApiConfigScreen::new; } // ── Helper methods for Actions string parsing & serialization ──────────── @@ -234,7 +108,7 @@ public ScrollableElement(net.minecraft.client.gui.Drawable drawable, net.minecra private boolean logCommands; private int defaultTickRate; - private Tab currentTab = Tab.CONFIG; + private Tab currentTab = Tab.GUIS; private final List scrollableElements = new ArrayList<>(); private double scrollY = 0; private int maxScrollY = 0; @@ -304,9 +178,12 @@ protected void init() { scrollableElements.clear(); // 1. Add persistent Tab Selectors (Fixed at Top) - ButtonWidget configTabBtn = ButtonWidget.builder(Text.literal("§eSettings Screen"), btn -> { - MinecraftClient.getInstance().setScreen(new GuiApiModMenuEntry().getModConfigScreenFactory().create(parent)); + ButtonWidget configTabBtn = ButtonWidget.builder(Text.literal(currentTab == Tab.CONFIG ? "§aSettings" : "§7Settings"), btn -> { + currentTab = Tab.CONFIG; + scrollY = 0; + this.init(); }).dimensions(cx - 125, 22, 80, 18).build(); + configTabBtn.active = (currentTab != Tab.CONFIG); addDrawableChild(configTabBtn); ButtonWidget guisTabBtn = ButtonWidget.builder(Text.literal(currentTab == Tab.GUIS ? "§aLoaded GUIs" : "§7Loaded GUIs"), btn -> { @@ -484,13 +361,23 @@ protected void init() { var id = entry.getKey(); var def = entry.getValue(); - ButtonWidget btnWidget = ButtonWidget.builder( - Text.literal("Edit: " + id.getPath() + " (" + def.getRows() + " rows)"), - btn -> MinecraftClient.getInstance().setScreen(new GuiEditorScreen(this, id, def)) - ).dimensions(cx - 150, y, 300, 18).build(); - - addDrawableChild(btnWidget); - scrollableElements.add(new ScrollableElement(btnWidget, btnWidget, y, 18)); + TextWidget nameWidget = new TextWidget(cx - 150, y + 4, 120, 10, + Text.literal("§f" + id.toString()), textRenderer); + ButtonWidget editWidget = ButtonWidget.builder( + Text.literal("Edit"), + btn -> MinecraftClient.getInstance().setScreen(new GuiLoadingScreen(this, id, def, GuiLoadingScreen.Mode.OPEN_EDITOR)) + ).dimensions(cx - 25, y, 85, 18).build(); + ButtonWidget infoWidget = ButtonWidget.builder( + Text.literal("Info"), + btn -> MinecraftClient.getInstance().setScreen(new GuiInfoScreen(this, id, def)) + ).dimensions(cx + 65, y, 85, 18).build(); + + addDrawableChild(nameWidget); + addDrawableChild(editWidget); + addDrawableChild(infoWidget); + scrollableElements.add(new ScrollableElement(nameWidget, nameWidget, y, 10)); + scrollableElements.add(new ScrollableElement(editWidget, editWidget, y, 18)); + scrollableElements.add(new ScrollableElement(infoWidget, infoWidget, y, 18)); y += 22; } } @@ -621,6 +508,95 @@ private static Text permLevelText(int level) { return Text.literal(color + level); } } + + static class GuiLoadingScreen extends Screen { + enum Mode { OPEN_EDITOR, CHECK_SLOTS } + + private final Screen parent; + private final net.minecraft.util.Identifier id; + private final GuiDefinition def; + private final Mode mode; + private int ticksElapsed = 0; + + GuiLoadingScreen(Screen parent, net.minecraft.util.Identifier id, GuiDefinition def, Mode mode) { + super(Text.literal(mode == Mode.CHECK_SLOTS ? "Checking slots..." : "Loading...")); + this.parent = parent; + this.id = id; + this.def = def; + this.mode = mode; + } + + @Override + public void tick() { + ticksElapsed++; + if (ticksElapsed >= 20) { + if (mode == Mode.CHECK_SLOTS && parent instanceof GuiEditorScreen editor) { + MinecraftClient.getInstance().setScreen(new ButtonListScreen(editor, id, def)); + } else { + MinecraftClient.getInstance().setScreen(new GuiEditorScreen(parent, id, def)); + } + } + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + } + + @Override + public void render(DrawContext ctx, int mouseX, int mouseY, float delta) { + ctx.fill(0, 0, width, height, 0xFF0A0A0A); + int cx = width / 2; + int cy = height / 2; + String msg = mode == Mode.CHECK_SLOTS ? "Checking slots..." : "Loading..."; + int dots = (ticksElapsed / 8) % 4; + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§6" + msg + ".".repeat(dots)), cx, cy - 8, 0xFFFFFF); + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§7" + id.toString()), cx, cy + 8, 0xAAAAAA); + } + } + + static class GuiInfoScreen extends Screen { + private final Screen parent; + private final net.minecraft.util.Identifier id; + private final GuiDefinition def; + + GuiInfoScreen(Screen parent, net.minecraft.util.Identifier id, GuiDefinition def) { + super(Text.literal("GUI Info")); + this.parent = parent; + this.id = id; + this.def = def; + } + + @Override + protected void init() { + int cx = width / 2; + addDrawableChild(ButtonWidget.builder(Text.literal("Back"), btn -> + MinecraftClient.getInstance().setScreen(parent)) + .dimensions(cx - 50, height - 25, 100, 20).build()); + } + + @Override + public void render(DrawContext ctx, int mouseX, int mouseY, float delta) { + super.render(ctx, mouseX, mouseY, delta); + int cx = width / 2; + int y = 30; + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§6GUI Info"), cx, 10, 0xFFFFFF); + ctx.drawTextWithShadow(textRenderer, Text.literal("§eID: §f" + id), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eTitle: §f" + def.getTitle()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eRows: §f" + def.getRows()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§ePages: §f" + def.getPageCount()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eButtons: §f" + def.getButtons().size()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eContainer: §f" + def.getContainerType().name().toLowerCase()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eTick rate: §f" + def.getTickRate()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eclose_on_move: §f" + def.isCloseOnMove()), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eFiller: §f" + (def.getFiller().isPresent() ? "configured" : "none")), cx - 150, y, 0xFFFFFF); y += 14; + ctx.drawTextWithShadow(textRenderer, Text.literal("§eMacros: §f" + def.getMacros().size()), cx - 150, y, 0xFFFFFF); + } + + @Override + public void close() { + MinecraftClient.getInstance().setScreen(parent); + } + } // ── GUI Editor Screen ──────────────────────────────────────────────────── static class GuiEditorScreen extends Screen { @@ -717,15 +693,15 @@ protected void init() { ).dimensions(cx - 150, y, 300, 18).build()); y += 22; - // Edit Buttons Navigation - addDrawableChild(ButtonWidget.builder(Text.literal("§bEdit GUI Buttons"), btn -> - MinecraftClient.getInstance().setScreen(new ButtonListScreen(this, id, def)) + // Edit Slots Navigation + addDrawableChild(ButtonWidget.builder(Text.literal("§bEdit Slots"), btn -> + MinecraftClient.getInstance().setScreen(new GuiLoadingScreen(this, id, def, GuiLoadingScreen.Mode.CHECK_SLOTS)) ).dimensions(cx - 150, y, 300, 18).build()); y += 28; // Action Buttons - // Save & Back — Open loading screen to search datapack and write JSON directly - addDrawableChild(ButtonWidget.builder(Text.literal("Apply & Back"), btn -> { + // Done — Open loading screen to search datapack and write JSON directly + addDrawableChild(ButtonWidget.builder(Text.literal("Done"), btn -> { GuiDefinition newDef = GuiDefinition.create( def.getId(), titleField.getText(), @@ -779,7 +755,7 @@ private static Text toggleText(boolean on) { static class GuiSaveProcessScreen extends Screen { enum State { - CONFIRM_SAVE, + MODIFYING_PROGRESS, SAVING_PROGRESS, CONFIRM_RELOAD, RELOADING_SPINNER @@ -790,7 +766,7 @@ enum State { private final net.minecraft.util.Identifier id; private final GuiDefinition newDef; - private State state = State.CONFIRM_SAVE; + private State state = State.MODIFYING_PROGRESS; private int ticksElapsed = 0; GuiSaveProcessScreen(Screen settingsScreen, Screen editorScreen, net.minecraft.util.Identifier id, GuiDefinition newDef) { @@ -809,16 +785,7 @@ protected void init() { int cx = width / 2; int cy = height / 2; - if (state == State.CONFIRM_SAVE) { - addDrawableChild(ButtonWidget.builder(Text.literal("Yes, Save Changes"), btn -> { - state = State.SAVING_PROGRESS; - this.init(); - }).dimensions(cx - 110, cy + 20, 100, 20).build()); - - addDrawableChild(ButtonWidget.builder(Text.literal("No, Back"), btn -> { - MinecraftClient.getInstance().setScreen(editorScreen); - }).dimensions(cx + 10, cy + 20, 100, 20).build()); - } else if (state == State.CONFIRM_RELOAD) { + if (state == State.CONFIRM_RELOAD) { addDrawableChild(ButtonWidget.builder(Text.literal("Yes, Reload"), btn -> { if (MinecraftClient.getInstance().player != null) { MinecraftClient.getInstance().player.networkHandler.sendChatCommand("guiapi reload"); @@ -835,7 +802,13 @@ protected void init() { @Override public void tick() { - if (state == State.SAVING_PROGRESS) { + if (state == State.MODIFYING_PROGRESS) { + ticksElapsed++; + if (ticksElapsed >= 25) { + state = State.SAVING_PROGRESS; + this.init(); + } + } else if (state == State.SAVING_PROGRESS) { ticksElapsed++; if (ticksElapsed >= 40) { MinecraftServer server = MinecraftClient.getInstance().getServer(); @@ -864,7 +837,7 @@ public void render(DrawContext ctx, int mouseX, int mouseY, float delta) { // Draw a highly professional solid opaque dark background manually ctx.fill(0, 0, width, height, 0xFF0A0A0A); - if (state == State.CONFIRM_SAVE || state == State.CONFIRM_RELOAD) { + if (state == State.CONFIRM_RELOAD) { super.render(ctx, mouseX, mouseY, delta); } @@ -875,17 +848,13 @@ public void render(DrawContext ctx, int mouseX, int mouseY, float delta) { int rainbowColor = java.awt.Color.HSBtoRGB((time % 1500) / 1500f, 0.8f, 0.8f); switch (state) { - case CONFIRM_SAVE -> { - ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§6★ Save Confirmation ★"), cx, cy - 30, 0xFFFFFF); - ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§fAre you sure you want to write changes to disk?"), cx, cy - 10, 0xAAAAAA); + case MODIFYING_PROGRESS -> { + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§6★ Modifying Data Pack... ★"), cx, cy - 35, 0xFFFFFF); + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("Preparing safe write steps..."), cx, cy - 10, rainbowColor); } case SAVING_PROGRESS -> { - String status = "Locating Datapack Folder..."; - if (ticksElapsed >= 20) { - status = "Overwriting Datapack JSON File..."; - } - ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§6★ Writing Datapack ★"), cx, cy - 40, 0xFFFFFF); - ctx.drawCenteredTextWithShadow(textRenderer, Text.literal(status), cx, cy - 10, rainbowColor); + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("§6★ Saving Slots... ★"), cx, cy - 40, 0xFFFFFF); + ctx.drawCenteredTextWithShadow(textRenderer, Text.literal("Saving slots..."), cx, cy - 10, rainbowColor); int barWidth = 160; int progress = Math.min(barWidth, (ticksElapsed * barWidth) / 40); @@ -1068,7 +1037,7 @@ protected void init() { int cx = width / 2; int y = 30; - addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eSelect a button to edit or delete:"), textRenderer)); + addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eSelect a slot to edit, add, or delete:"), textRenderer)); y += 12; int buttonsPerPage = 6; @@ -1076,7 +1045,7 @@ protected void init() { if (listPage >= totalPages) listPage = totalPages - 1; addDrawableChild(new TextWidget(cx - 150, y, 300, 10, - Text.literal("§7Loaded Buttons: §f" + buttonsList.size() + " (Page " + (listPage + 1) + "/" + totalPages + ")"), + Text.literal("§7Loaded Slots: §f" + buttonsList.size() + " (Page " + (listPage + 1) + "/" + totalPages + ")"), textRenderer)); y += 14; @@ -1086,11 +1055,11 @@ protected void init() { for (int i = startIdx; i < endIdx; i++) { final int index = i; GuiDefinition.Button btn = buttonsList.get(index); - String labelText = "Slot " + btn.slot() + ": " + + String labelText = "Slot " + btn.slot() + " / Page " + (btn.page() + 1) + ": " + (btn.name().isEmpty() ? btn.item() : btn.name()); addDrawableChild(ButtonWidget.builder(Text.literal(labelText), b -> { - MinecraftClient.getInstance().setScreen(createButtonEditorScreen(this, index, btn, this)); + MinecraftClient.getInstance().setScreen(new ButtonEditorScreen(this, index, btn)); }).dimensions(cx - 150, y, 300, 18).build()); y += 20; @@ -1119,18 +1088,18 @@ protected void init() { y = height - 30; // Add New Button - addDrawableChild(ButtonWidget.builder(Text.literal("§aAdd New Button"), btn -> { + addDrawableChild(ButtonWidget.builder(Text.literal("§aAdd Slot"), btn -> { GuiDefinition.Button newBtn = new GuiDefinition.Button( 0, listPage, "minecraft:stone", "New Button", List.of(), false, GuiDefinition.ClickType.ANY, Optional.empty(), List.of(), Optional.empty(), Optional.empty(), Optional.empty(), "1", false, false ); buttonsList.add(newBtn); - MinecraftClient.getInstance().setScreen(createButtonEditorScreen(this, buttonsList.size() - 1, newBtn, this)); + MinecraftClient.getInstance().setScreen(new ButtonEditorScreen(this, buttonsList.size() - 1, newBtn)); }).dimensions(cx - 155, y, 100, 20).build()); // Save & Back - addDrawableChild(ButtonWidget.builder(Text.literal("Save Buttons"), btn -> { + addDrawableChild(ButtonWidget.builder(Text.literal("Save Slots"), btn -> { parent.updateButtons(buttonsList); MinecraftClient.getInstance().setScreen(parent); }).dimensions(cx - 50, y, 100, 20).build()); @@ -1145,7 +1114,7 @@ protected void init() { public void render(DrawContext ctx, int mouseX, int mouseY, float delta) { super.render(ctx, mouseX, mouseY, delta); ctx.drawCenteredTextWithShadow(textRenderer, - Text.literal("§6Buttons Editor — " + id.getPath()), width / 2, 10, 0xFFFFFF); + Text.literal("§6Slot Editor — " + id.getPath()), width / 2, 10, 0xFFFFFF); ctx.fill(width / 2 - 150, height - 38, width / 2 + 150, height - 37, 0x44FFFFFF); } @@ -1161,178 +1130,12 @@ public void deleteButton(int index) { } // ── Button Properties Editor Screen ────────────────────────────────────── - public static Screen createButtonEditorScreen(Screen parent, int index, GuiDefinition.Button btn, ButtonListScreen listScreen) { - me.shedaniel.clothconfig2.api.ConfigBuilder builder = me.shedaniel.clothconfig2.api.ConfigBuilder.create() - .setParentScreen(parent) - .setTitle(Text.literal("Edit Button Properties")); - - me.shedaniel.clothconfig2.api.ConfigCategory basicCat = builder.getOrCreateCategory(Text.literal("Basic")); - me.shedaniel.clothconfig2.api.ConfigCategory appearanceCat = builder.getOrCreateCategory(Text.literal("Appearance")); - me.shedaniel.clothconfig2.api.ConfigCategory logicCat = builder.getOrCreateCategory(Text.literal("Logic")); - - me.shedaniel.clothconfig2.api.ConfigEntryBuilder entryBuilder = builder.entryBuilder(); - - final int[] slot = {btn.slot()}; - final int[] amount = {1}; - try { amount[0] = Integer.parseInt(btn.amount()); } catch (Exception ignored) {} - final int[] pageVal = {btn.page()}; - final String[] itemText = {btn.item()}; - final String[] nameText = {btn.name()}; - final boolean[] glint = {btn.glint()}; - final GuiDefinition.ClickType[] clickType = {btn.clickType()}; - final String[] loreText = {String.join(";", btn.lore())}; - final String[] actionsText = {serializeActionsToString(btn.actions())}; - final String[] conditionText = {btn.condition().isPresent() ? btn.condition().get().type().name().toLowerCase() + ":" + btn.condition().get().value() : ""}; - - // --- Basic Category --- - basicCat.addEntry(entryBuilder.startIntSlider(Text.literal("Page (1-10)"), pageVal[0], 0, 9) - .setDefaultValue(0) - .setSaveConsumer(v -> pageVal[0] = v) - .build()); - - basicCat.addEntry(entryBuilder.startIntSlider(Text.literal("Slot Position"), slot[0], 0, 53) - .setDefaultValue(0) - .setSaveConsumer(v -> slot[0] = v) - .build()); - - basicCat.addEntry(entryBuilder.startIntSlider(Text.literal("Amount"), amount[0], 1, 99) - .setDefaultValue(1) - .setSaveConsumer(v -> amount[0] = v) - .build()); - - basicCat.addEntry(entryBuilder.startTextField(Text.literal("Item ID"), itemText[0]) - .setDefaultValue("minecraft:stone") - .setSaveConsumer(v -> itemText[0] = v) - .build()); - - basicCat.addEntry(entryBuilder.startTextField(Text.literal("Display Name"), nameText[0]) - .setDefaultValue("") - .setSaveConsumer(v -> nameText[0] = v) - .build()); - - - // --- Appearance Category --- - appearanceCat.addEntry(entryBuilder.startBooleanToggle(Text.literal("Enable Glint"), glint[0]) - .setDefaultValue(false) - .setSaveConsumer(v -> glint[0] = v) - .build()); - - appearanceCat.addEntry(entryBuilder.startTextField(Text.literal("Lore Lines (separated by ';')"), loreText[0]) - .setDefaultValue("") - .setSaveConsumer(v -> loreText[0] = v) - .build()); - - - // --- Logic Category --- - logicCat.addEntry(entryBuilder.startSelector(Text.literal("Click Type"), new String[]{"ANY", "LEFT", "RIGHT", "SHIFT"}, clickType[0].name()) - .setDefaultValue("ANY") - .setSaveConsumer(v -> clickType[0] = GuiDefinition.ClickType.valueOf(v)) - .build()); - - logicCat.addEntry(entryBuilder.startTextField(Text.literal("Condition (type:value)"), conditionText[0]) - .setDefaultValue("") - .setSaveConsumer(v -> conditionText[0] = v) - .build()); - - logicCat.addEntry(entryBuilder.startTextField(Text.literal("Actions (separated by ';')"), actionsText[0]) - .setDefaultValue("") - .setSaveConsumer(v -> actionsText[0] = v) - .build()); - - builder.setSavingRunnable(() -> { - int targetSlot = slot[0]; - boolean duplicate = false; - for (int i = 0; i < listScreen.buttonsList.size(); i++) { - if (i == index) continue; - GuiDefinition.Button existing = listScreen.buttonsList.get(i); - if (existing.slot() == targetSlot && existing.page() == pageVal[0]) { - duplicate = true; - break; - } - } - if (duplicate) { - int emptySlot = targetSlot; - for (int s = 0; s < 54; s++) { - boolean slotUsed = false; - for (int i = 0; i < listScreen.buttonsList.size(); i++) { - if (i == index) continue; - GuiDefinition.Button existing = listScreen.buttonsList.get(i); - if (existing.slot() == s && existing.page() == pageVal[0]) { - slotUsed = true; - break; - } - } - if (!slotUsed) { - emptySlot = s; - break; - } - } - targetSlot = emptySlot; - } - - List finalLore = new ArrayList<>(); - if (!loreText[0].isEmpty()) { - for (String s : loreText[0].split(";")) { - finalLore.add(s); - } - } - - List finalActions = new ArrayList<>(); - if (!actionsText[0].isEmpty()) { - for (String s : actionsText[0].split(";")) { - finalActions.add(parseActionFromString(s)); - } - } - if (finalActions.isEmpty()) { - finalActions.add(new GuiDefinition.ButtonAction(GuiDefinition.ActionType.NONE, "")); - } - - Optional finalCondition = Optional.empty(); - if (!conditionText[0].isEmpty()) { - String[] condParts = conditionText[0].split(":", 2); - if (condParts.length == 2) { - try { - GuiDefinition.ConditionType ct = GuiDefinition.ConditionType.fromString(condParts[0]); - finalCondition = Optional.of(new GuiDefinition.ButtonCondition(ct, condParts[1])); - } catch (Exception ignored) {} - } - } - - GuiDefinition.Button newBtn = new GuiDefinition.Button( - targetSlot, - pageVal[0], - itemText[0], - nameText[0], - finalLore, - glint[0], - clickType[0], - finalCondition, - finalActions, - btn.toggle(), - btn.customModelData(), - btn.itemModel(), - String.valueOf(amount[0]), - btn.hideTooltip(), - btn.hideAdditionalTooltip() - ); - - listScreen.updateButton(index, newBtn); - }); - - return builder.build(); - } - static class ButtonEditorScreen extends Screen { - private enum Tab { - BASIC, APPEARANCE, LOGIC - } private final ButtonListScreen parent; private final int index; private final GuiDefinition.Button btn; - private Tab currentTab = Tab.BASIC; - private TextFieldWidget itemField; private TextFieldWidget nameField; private TextFieldWidget loreField; @@ -1353,8 +1156,11 @@ private enum Tab { private String actionsText; private String conditionText; + private double scrollY = 0; + private int maxScrollY = 0; + ButtonEditorScreen(ButtonListScreen parent, int index, GuiDefinition.Button btn) { - super(Text.literal("Edit Button")); + super(Text.literal("Edit Slot")); this.parent = parent; this.index = index; this.btn = btn; @@ -1395,37 +1201,33 @@ private void saveCurrentTabFields() { if (conditionField != null) conditionText = conditionField.getText(); } + private int scrollTop() { return 42; } + private int scrollBottom() { return height - 34; } + + private T addScrolled(T widget, int originalY, int widgetHeight) { + int newY = originalY - (int)scrollY; + widget.setY(newY); + boolean visible = newY + widgetHeight > scrollTop() && newY < scrollBottom(); + widget.visible = visible; + widget.active = visible; + addDrawableChild(widget); + return widget; + } + + private void scrollBy(double amount) { + saveCurrentTabFields(); + scrollY += amount; + if (scrollY < 0) scrollY = 0; + if (scrollY > maxScrollY) scrollY = maxScrollY; + this.init(); + } + @Override protected void init() { int cx = width / 2; this.clearChildren(); - // 1. Add Tab buttons - ButtonWidget basicTabBtn = ButtonWidget.builder(Text.literal(currentTab == Tab.BASIC ? "§aBasic" : "§7Basic"), btn -> { - saveCurrentTabFields(); - currentTab = Tab.BASIC; - this.init(); - }).dimensions(cx - 125, 22, 80, 18).build(); - basicTabBtn.active = (currentTab != Tab.BASIC); - addDrawableChild(basicTabBtn); - - ButtonWidget appTabBtn = ButtonWidget.builder(Text.literal(currentTab == Tab.APPEARANCE ? "§aAppearance" : "§7Appearance"), btn -> { - saveCurrentTabFields(); - currentTab = Tab.APPEARANCE; - this.init(); - }).dimensions(cx - 40, 22, 80, 18).build(); - appTabBtn.active = (currentTab != Tab.APPEARANCE); - addDrawableChild(appTabBtn); - - ButtonWidget logicTabBtn = ButtonWidget.builder(Text.literal(currentTab == Tab.LOGIC ? "§aLogic" : "§7Logic"), btn -> { - saveCurrentTabFields(); - currentTab = Tab.LOGIC; - this.init(); - }).dimensions(cx + 45, 22, 80, 18).build(); - logicTabBtn.active = (currentTab != Tab.LOGIC); - addDrawableChild(logicTabBtn); - - // 2. Add bottom buttons + // 1. Add bottom buttons addDrawableChild(ButtonWidget.builder(Text.literal("Apply"), b -> { saveCurrentTabFields(); @@ -1511,7 +1313,7 @@ protected void init() { MinecraftClient.getInstance().setScreen(parent); }).dimensions(cx - 155, height - 25, 100, 20).build()); - addDrawableChild(ButtonWidget.builder(Text.literal("§cDelete Button"), b -> { + addDrawableChild(ButtonWidget.builder(Text.literal("§cDelete Slot"), b -> { parent.deleteButton(index); MinecraftClient.getInstance().setScreen(parent); }).dimensions(cx - 50, height - 25, 100, 20).build()); @@ -1520,103 +1322,127 @@ protected void init() { MinecraftClient.getInstance().setScreen(parent)) .dimensions(cx + 55, height - 25, 100, 20).build()); - // 3. Tab Specific Inputs - int startY = 48; - if (currentTab == Tab.BASIC) { - int y = startY; + ButtonWidget upBtn = ButtonWidget.builder(Text.literal("▲"), b -> scrollBy(-28)) + .dimensions(cx + 160, 48, 24, 20).build(); + upBtn.active = scrollY > 0; + addDrawableChild(upBtn); + ButtonWidget downBtn = ButtonWidget.builder(Text.literal("▼"), b -> scrollBy(28)) + .dimensions(cx + 160, 72, 24, 20).build(); + addDrawableChild(downBtn); + + // 2. Slot fields (single-page editor; no tabs) + int y = 48; // Page selection slider (Page 1-10) IntegerSliderWidget pageSlider = new IntegerSliderWidget(cx - 150, y, 300, 20, "Page (1-10)", 0, 9, pageVal, val -> pageVal = val); - addDrawableChild(pageSlider); + addScrolled(pageSlider, y, 20); y += 24; // Slot selection slider (Slot 0-53) IntegerSliderWidget slotSlider = new IntegerSliderWidget(cx - 150, y, 300, 20, "Slot Position", 0, 53, slot, val -> slot = val); - addDrawableChild(slotSlider); + addScrolled(slotSlider, y, 20); y += 24; // Amount selection slider (Amount 1-99) IntegerSliderWidget amountSlider = new IntegerSliderWidget(cx - 150, y, 300, 20, "Amount", 1, 99, amount, val -> amount = val); - addDrawableChild(amountSlider); + addScrolled(amountSlider, y, 20); y += 24; // Item ID Input (Text Field) - addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eItem ID"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eItem ID"), textRenderer), y, 10); itemField = new TextFieldWidget(textRenderer, cx - 150, y + 12, 300, 18, Text.literal("Item ID")); itemField.setMaxLength(256); itemField.setText(itemText); - addDrawableChild(itemField); + addScrolled(itemField, y + 12, 18); y += 34; // Display Name Input (Text Field) - addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eDisplay Name"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eDisplay Name"), textRenderer), y, 10); nameField = new TextFieldWidget(textRenderer, cx - 150, y + 12, 300, 18, Text.literal("Display Name")); nameField.setMaxLength(128); nameField.setText(nameText); - addDrawableChild(nameField); - - } else if (currentTab == Tab.APPEARANCE) { - int y = startY; + addScrolled(nameField, y + 12, 18); + y += 34; // Glint Toggle - addDrawableChild(new TextWidget(cx - 150, y + 4, 200, 10, Text.literal("§eEnable Glint"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y + 4, 200, 10, Text.literal("§eEnable Glint"), textRenderer), y + 4, 10); ButtonWidget glintBtn = ButtonWidget.builder(toggleText(glint), b -> { glint = !glint; b.setMessage(toggleText(glint)); }).dimensions(cx + 60, y, 40, 18).build(); - addDrawableChild(glintBtn); + addScrolled(glintBtn, y, 18); y += 24; // Toggle Button Navigation - addDrawableChild(new TextWidget(cx - 150, y + 4, 200, 10, Text.literal("§eToggle Properties"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y + 4, 200, 10, Text.literal("§eToggle Properties"), textRenderer), y + 4, 10); ButtonWidget toggleBtn = ButtonWidget.builder(Text.literal(toggle.isPresent() ? "§aCONFIGURED" : "§cDISABLED"), b -> { MinecraftClient.getInstance().setScreen(new ToggleEditorScreen(this, toggle)); }).dimensions(cx + 40, y, 110, 18).build(); - addDrawableChild(toggleBtn); + addScrolled(toggleBtn, y, 18); y += 26; // Lore Input - addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eLore Lines (Separate by semicolon ';')"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eLore Lines (Separate by semicolon ';')"), textRenderer), y, 10); loreField = new TextFieldWidget(textRenderer, cx - 150, y + 12, 300, 18, Text.literal("Lore")); loreField.setMaxLength(512); loreField.setText(loreText); - addDrawableChild(loreField); - - } else if (currentTab == Tab.LOGIC) { - int y = startY; + addScrolled(loreField, y + 12, 18); + y += 34; // Click Type - addDrawableChild(new TextWidget(cx - 150, y + 4, 200, 10, Text.literal("§eClick Type"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y + 4, 200, 10, Text.literal("§eClick Type"), textRenderer), y + 4, 10); ButtonWidget clickTypeBtn = ButtonWidget.builder(Text.literal(clickType.name()), b -> { clickType = nextClickType(clickType); b.setMessage(Text.literal(clickType.name())); }).dimensions(cx + 60, y, 60, 18).build(); - addDrawableChild(clickTypeBtn); + addScrolled(clickTypeBtn, y, 18); y += 24; // Condition - addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eCondition"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eCondition"), textRenderer), y, 10); conditionField = new TextFieldWidget(textRenderer, cx - 150, y + 12, 300, 18, Text.literal("Condition")); conditionField.setMaxLength(128); conditionField.setText(conditionText); - addDrawableChild(conditionField); + addScrolled(conditionField, y + 12, 18); y += 34; // Actions - addDrawableChild(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eActions (Separate by semicolon ';')"), textRenderer)); + addScrolled(new TextWidget(cx - 150, y, 300, 10, Text.literal("§eActions (Separate by semicolon ';')"), textRenderer), y, 10); actionsField = new TextFieldWidget(textRenderer, cx - 150, y + 12, 300, 18, Text.literal("Actions")); actionsField.setMaxLength(512); actionsField.setText(actionsText); - addDrawableChild(actionsField); - } + addScrolled(actionsField, y + 12, 18); + y += 34; + + int viewportHeight = scrollBottom() - scrollTop(); + maxScrollY = Math.max(0, y - 48 - viewportHeight + 16); + if (scrollY > maxScrollY) scrollY = maxScrollY; } @Override public void render(DrawContext ctx, int mouseX, int mouseY, float delta) { super.render(ctx, mouseX, mouseY, delta); ctx.drawCenteredTextWithShadow(textRenderer, - Text.literal("§6Edit Button Properties"), width / 2, 8, 0xFFFFFF); + Text.literal("§6Edit Slot Properties"), width / 2, 8, 0xFFFFFF); + ctx.fill(width / 2 - 150, scrollTop() - 3, width / 2 + 150, scrollTop() - 2, 0x44FFFFFF); ctx.fill(width / 2 - 150, height - 32, width / 2 + 150, height - 31, 0x44FFFFFF); + if (maxScrollY > 0) { + int rx = width / 2 + 150; + int top = scrollTop(); + int bottom = scrollBottom(); + int trackHeight = bottom - top; + int thumbHeight = Math.max(16, (int)((double)trackHeight / (trackHeight + maxScrollY) * trackHeight)); + int thumbY = top + (int)(scrollY / maxScrollY * (trackHeight - thumbHeight)); + ctx.fill(rx + 6, top, rx + 10, bottom, 0x44FFFFFF); + ctx.fill(rx + 6, thumbY, rx + 10, thumbY + thumbHeight, 0xAAFFFFFF); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (maxScrollY <= 0) return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + scrollBy(-verticalAmount * 28); + return true; } @Override