diff --git a/gradle.properties b/gradle.properties index c2d823f..b267552 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.3.3.1 +mod_version=0.3.3.2 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java index a4564c0..d4b6425 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/NotebookScreen.java @@ -1,7 +1,11 @@ package net.dnorberg.rivencraft.client.screen; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; import net.dnorberg.rivencraft.menu.NotebookMenu; import net.dnorberg.rivencraft.network.RenameNotebookPagePayload; +import net.dnorberg.rivencraft.registry.ModItems; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; @@ -10,106 +14,165 @@ import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.ClickType; -import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.network.PacketDistributor; +/** A page surface modelled on Legacy's folder GUI, backed by the Notebook menu. */ public final class NotebookScreen extends AbstractContainerScreen { private static final int SURFACE_TOP = 20; private static final int SURFACE_BOTTOM = 132; - private static final int PAGE_WIDTH = 17; - private static final int PAGE_HEIGHT = 31; - private static final int PAGE_ROW_HEIGHT = 34; - private EditBox nameInput; - private Button previousButton; - private Button nextButton; + private static final int SURFACE_RIGHT = 156; + private static final int PAGE_WIDTH = 30; + private static final int PAGE_HEIGHT = 40; + private static final int PAGE_STEP_X = 31; + private static final int PAGE_STEP_Y = 41; + private static final int PAGE_COLUMNS = 5; + private EditBox textInput; + private boolean renaming; + private boolean draggingScrollbar; + private boolean surfaceClickPendingRelease; private int selectedSlot = -1; + private int scroll; public NotebookScreen(NotebookMenu menu, Inventory inventory, Component title) { super(menu, inventory, title); imageWidth = 176; imageHeight = 213; inventoryLabelY = 123; - titleLabelY = 7; } @Override protected void init() { super.init(); - nameInput = new EditBox(font, leftPos + 47, topPos + 7, 86, 18, - Component.translatable("screen.rivencraft.notebook.rename")); - nameInput.setMaxLength(32); - nameInput.setResponder(ignored -> updateButtons()); - addRenderableWidget(nameInput); - - previousButton = Button.builder(Component.literal("←"), ignored -> moveSelected(false)) - .bounds(leftPos + 136, topPos + 7, 16, 18) - .tooltip(Tooltip.create(Component.translatable("screen.rivencraft.notebook.move_previous"))) - .build(); - nextButton = Button.builder(Component.literal("→"), ignored -> moveSelected(true)) - .bounds(leftPos + 153, topPos + 7, 16, 18) - .tooltip(Tooltip.create(Component.translatable("screen.rivencraft.notebook.move_next"))) - .build(); - addRenderableWidget(previousButton); - addRenderableWidget(nextButton); - updateButtons(); + Button sortButton = Button.builder(Component.literal("AZ"), button -> {}) + .bounds(leftPos, topPos, 18, 18).build(); + Button allButton = Button.builder(Component.literal("ALL"), button -> {}) + .bounds(leftPos + 20, topPos, 18, 18).build(); + sortButton.active = false; + allButton.active = false; + addRenderableWidget(sortButton); + addRenderableWidget(allButton); + textInput = new EditBox(font, leftPos + 40, topPos, 94, 18, + Component.translatable("screen.rivencraft.notebook.search")); + textInput.setMaxLength(32); + textInput.setHint(Component.translatable("screen.rivencraft.notebook.search")); + textInput.setResponder(value -> { + if (!renaming) scroll = 0; + }); + addRenderableWidget(textInput); + addRenderableWidget(Button.builder(Component.literal("R"), button -> toggleRename()) + .bounds(leftPos + 136, topPos, 18, 18) + .tooltip(Tooltip.create(Component.translatable("screen.rivencraft.notebook.rename_toggle"))) + .build()); } @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { super.render(graphics, mouseX, mouseY, partialTick); - if (selectedSlot >= 0) { - Slot slot = menu.slots.get(selectedSlot); - graphics.renderOutline(leftPos + slot.x - 1, topPos + slot.y - 1, 18, 18, 0xFFE0C36A); + List pages = visiblePages(); + scroll = Math.min(scroll, maxScroll(pages.size())); + graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT, + topPos + SURFACE_BOTTOM); + for (int position = 0; position < pages.size(); position++) { + int x = leftPos + (position % PAGE_COLUMNS) * PAGE_STEP_X; + int y = topPos + SURFACE_TOP + (position / PAGE_COLUMNS) * PAGE_STEP_Y - scroll; + if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) { + drawPage(graphics, pages.get(position), x, y); + } + } + graphics.disableScissor(); + drawScrollbar(graphics, pages.size()); + int hovered = pageAt(mouseX, mouseY, pages); + if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) { + graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8); + } else if (hovered >= 0) { + ItemStack page = menu.getSlot(hovered).getItem(); + if (!page.isEmpty() && matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY); + } else { + renderTooltip(graphics, mouseX, mouseY); } - renderTooltip(graphics, mouseX, mouseY); } @Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { - drawBeveledPanel(graphics, leftPos, topPos, imageWidth, SURFACE_BOTTOM); - graphics.fill(leftPos, topPos + SURFACE_TOP, leftPos + imageWidth - 1, topPos + SURFACE_BOTTOM - 1, - 0xFF1D2633); - drawPageCards(graphics); - + graphics.fill(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT, + topPos + SURFACE_BOTTOM, 0xB0000000); drawBeveledPanel(graphics, leftPos, topPos + 132, imageWidth, 81); for (int row = 0; row < 3; row++) { - for (int column = 0; column < 9; column++) { - drawSlot(graphics, 7 + column * 18, 134 + row * 18); - } - } - for (int column = 0; column < 9; column++) { - drawSlot(graphics, 7 + column * 18, 192); + for (int column = 0; column < 9; column++) drawSlot(graphics, 7 + column * 18, 134 + row * 18); } + for (int column = 0; column < 9; column++) drawSlot(graphics, 7 + column * 18, 192); } @Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { - graphics.drawString(font, title, titleLabelX, titleLabelY, 0x3F2A18, false); graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false); } @Override - protected void slotClicked(Slot slot, int slotId, int mouseButton, ClickType type) { - if (slotId >= 0 && slotId < NotebookMenu.NOTEBOOK_SLOTS) { - selectedSlot = slotId; + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (inScrollbar(mouseX, mouseY) && button == 0) { + draggingScrollbar = true; + setScrollFromMouse(mouseY, visiblePages().size()); + return true; } - super.slotClicked(slot, slotId, mouseButton, type); - refreshName(); + int index = pageAt(mouseX, mouseY, visiblePages()); + if (index >= 0 && (button == 0 || button == 1)) { + ItemStack page = menu.getSlot(index).getItem(); + if (!page.isEmpty() && !matchesSearch(page)) return true; + if (renaming) finishRename(); + setFocused(null); + textInput.setFocused(false); + selectedSlot = index; + slotClicked(menu.getSlot(index), index, button, ClickType.PICKUP); + surfaceClickPendingRelease = true; + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (draggingScrollbar && button == 0) { + setScrollFromMouse(mouseY, visiblePages().size()); + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (draggingScrollbar || surfaceClickPendingRelease) { + draggingScrollbar = false; + surfaceClickPendingRelease = false; + return true; + } + draggingScrollbar = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) { + scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20, + 0, maxScroll(visiblePages().size())); + return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (nameInput.isFocused() && (keyCode == 257 || keyCode == 335)) { - renameSelected(); + if (renaming && textInput.isFocused() && (keyCode == 257 || keyCode == 335)) { + finishRename(); return true; } - if (!nameInput.isFocused() && keyCode == 263) { - moveSelected(false); + if (!textInput.isFocused() && keyCode == 82 && selectedSlot >= 0) { + toggleRename(); return true; } - if (!nameInput.isFocused() && keyCode == 262) { - moveSelected(true); + if (!textInput.isFocused() && selectedSlot >= 0 && (keyCode == 263 || keyCode == 262)) { + moveSelected(keyCode == 262); return true; } return super.keyPressed(keyCode, scanCode, modifiers); @@ -117,61 +180,133 @@ public final class NotebookScreen extends AbstractContainerScreen @Override public void onClose() { - renameSelected(); + commitRename(); super.onClose(); } - private void refreshName() { - ItemStack selected = selectedPage(); - nameInput.setValue(selected.isEmpty() ? "" : selected.getHoverName().getString()); - updateButtons(); + private List visiblePages() { + List pages = new ArrayList<>(); + int firstEmpty = -1; + for (int index = 0; index < NotebookMenu.NOTEBOOK_SLOTS; index++) { + ItemStack stack = menu.getSlot(index).getItem(); + if (stack.isEmpty()) { + if (firstEmpty < 0) firstEmpty = index; + } else { + pages.add(index); + } + } + if (firstEmpty >= 0 && menu.getCarried().is(ModItems.PAGE.get())) { + pages.add(firstEmpty); + } + return pages; } - private void renameSelected() { - if (selectedSlot < 0 || selectedPage().isEmpty()) { - return; + private boolean matchesSearch(ItemStack page) { + return renaming || textInput.getValue().isBlank() + || page.getHoverName().getString().toLowerCase(Locale.ROOT) + .contains(textInput.getValue().toLowerCase(Locale.ROOT)); + } + + private int pageAt(double mouseX, double mouseY, List pages) { + if (!inSurface(mouseX, mouseY)) return -1; + int localX = (int)mouseX - leftPos; + int localY = (int)mouseY - topPos - SURFACE_TOP + scroll; + int column = localX / PAGE_STEP_X; + int position = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column; + return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH + && localY % PAGE_STEP_Y < PAGE_HEIGHT && position < pages.size() + ? pages.get(position) : -1; + } + + private boolean inSurface(double mouseX, double mouseY) { + return mouseX >= leftPos && mouseX < leftPos + SURFACE_RIGHT + && mouseY >= topPos + SURFACE_TOP && mouseY < topPos + SURFACE_BOTTOM; + } + + private boolean inScrollbar(double mouseX, double mouseY) { + return mouseX >= leftPos + SURFACE_RIGHT && mouseX < leftPos + imageWidth + && mouseY >= topPos + SURFACE_TOP && mouseY < topPos + SURFACE_BOTTOM; + } + + private int maxScroll(int pageCount) { + int rows = (pageCount + PAGE_COLUMNS - 1) / PAGE_COLUMNS; + return Math.max(0, rows * PAGE_STEP_Y - (SURFACE_BOTTOM - SURFACE_TOP)); + } + + private void setScrollFromMouse(double mouseY, int pageCount) { + int maximum = maxScroll(pageCount); + double fraction = (mouseY - topPos - SURFACE_TOP - 10.0) + / (SURFACE_BOTTOM - SURFACE_TOP - 20.0); + scroll = Math.clamp((int)(Math.clamp(fraction, 0.0, 1.0) * maximum), 0, maximum); + } + + private void drawScrollbar(GuiGraphics graphics, int pageCount) { + int x = leftPos + SURFACE_RIGHT; + int y = topPos + SURFACE_TOP; + int height = SURFACE_BOTTOM - SURFACE_TOP; + graphics.fill(x, y, x + 20, y + height, 0xFF858585); + graphics.renderOutline(x, y, 20, height, 0xFF262626); + for (int line = y + 5; line < y + height - 4; line += 5) { + graphics.fill(x + 4, line, x + 16, line + 1, 0xFFB7B7B7); + } + int maximum = maxScroll(pageCount); + int thumbY = y + 4 + (maximum == 0 ? 0 : scroll * (height - 23) / maximum); + graphics.fill(x + 4, thumbY, x + 16, thumbY + 15, 0xFFC9C9C9); + graphics.renderOutline(x + 4, thumbY, 12, 15, 0xFF414141); + } + + private void drawPage(GuiGraphics graphics, int index, int x, int y) { + ItemStack page = menu.getSlot(index).getItem(); + boolean visible = !page.isEmpty() && matchesSearch(page); + graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, + visible ? 0xFFE5D4A7 : 0x704B4945); + graphics.fill(x + 2, y + 2, x + PAGE_WIDTH - 2, y + PAGE_HEIGHT - 2, + visible ? 0xFFF4E5C3 : 0x603B3934); + graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, + index == selectedSlot ? 0xFFFFD061 : 0xFF564A34); + if (visible) { + graphics.pose().pushPose(); + graphics.pose().translate(x + 3, y + 8, 0); + graphics.pose().scale(1.5F, 1.5F, 1.0F); + graphics.renderItem(page, 0, 0); + graphics.pose().popPose(); + } + } + + private void toggleRename() { + if (renaming) { + finishRename(); + } else if (selectedSlot >= 0 && !menu.getSlot(selectedSlot).getItem().isEmpty()) { + renaming = true; + textInput.setValue(menu.getSlot(selectedSlot).getItem().getHoverName().getString()); + textInput.setHint(Component.translatable("screen.rivencraft.notebook.rename")); + setFocused(textInput); + textInput.setFocused(true); + } + } + + private void finishRename() { + commitRename(); + renaming = false; + textInput.setValue(""); + textInput.setHint(Component.translatable("screen.rivencraft.notebook.search")); + } + + private void commitRename() { + if (renaming && selectedSlot >= 0 && !menu.getSlot(selectedSlot).getItem().isEmpty()) { + PacketDistributor.sendToServer(new RenameNotebookPagePayload( + menu.containerId, selectedSlot, textInput.getValue())); } - PacketDistributor.sendToServer(new RenameNotebookPagePayload( - menu.containerId, selectedSlot, nameInput.getValue())); } private void moveSelected(boolean next) { int target = selectedSlot + (next ? 1 : -1); - if (selectedSlot < 0 || selectedPage().isEmpty() - || target < 0 || target >= NotebookMenu.NOTEBOOK_SLOTS) { - return; - } - renameSelected(); + if (selectedSlot < 0 || target < 0 || target >= NotebookMenu.NOTEBOOK_SLOTS + || menu.getSlot(selectedSlot).getItem().isEmpty()) return; int buttonId = (next ? NotebookMenu.MOVE_NEXT_BASE : NotebookMenu.MOVE_PREVIOUS_BASE) + selectedSlot; if (menu.clickMenuButton(minecraft.player, buttonId)) { minecraft.gameMode.handleInventoryButtonClick(menu.containerId, buttonId); selectedSlot = target; - refreshName(); - } - } - - private ItemStack selectedPage() { - return selectedSlot >= 0 && selectedSlot < NotebookMenu.NOTEBOOK_SLOTS - ? menu.getSlot(selectedSlot).getItem() - : ItemStack.EMPTY; - } - - private void updateButtons() { - if (previousButton == null || nextButton == null) { - return; - } - boolean hasPage = !selectedPage().isEmpty(); - previousButton.active = hasPage && selectedSlot > 0; - nextButton.active = hasPage && selectedSlot >= 0 && selectedSlot < NotebookMenu.NOTEBOOK_SLOTS - 1; - } - - private void drawPageCards(GuiGraphics graphics) { - for (int index = 0; index < NotebookMenu.NOTEBOOK_SLOTS; index++) { - int row = index / NotebookMenu.PAGE_COLUMNS; - int x = leftPos + 7 + (index % NotebookMenu.PAGE_COLUMNS) * 18; - int y = topPos + 21 + row * PAGE_ROW_HEIGHT; - graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xFFB8AD8D); - graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFF594D39); } } diff --git a/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java index 750dd05..8e17066 100644 --- a/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java +++ b/src/main/java/net/dnorberg/rivencraft/menu/NotebookMenu.java @@ -21,8 +21,6 @@ import net.minecraft.world.item.ItemStack; public final class NotebookMenu extends AbstractContainerMenu { public static final int NOTEBOOK_SLOTS = NotebookContents.MAX_PAGES; - public static final int PAGE_COLUMNS = 9; - public static final int PAGE_ROWS = 3; public static final int MOVE_PREVIOUS_BASE = 0; public static final int MOVE_NEXT_BASE = NOTEBOOK_SLOTS; private static final int PLAYER_INVENTORY_START = NOTEBOOK_SLOTS; @@ -44,7 +42,8 @@ public final class NotebookMenu extends AbstractContainerMenu { for (int row = 0; row < 3; row++) { for (int column = 0; column < 9; column++) { - addSlot(new PageSlot(contents, column + row * 9, 8 + column * 18, 28 + row * 34)); + // The client draws these pages on a filtered, scrollable surface instead. + addSlot(new PageSlot(contents, column + row * 9, -1000, -1000)); } } for (int row = 0; row < 3; row++) { diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index d337180..427418d 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -16,6 +16,8 @@ "item.rivencraft.notebook.pages": "Pages: %s/%s", "item.rivencraft.notebook.unresolved": "Unresolved pages: %s", "screen.rivencraft.notebook.rename": "Rename selected page", + "screen.rivencraft.notebook.search": "Search pages", + "screen.rivencraft.notebook.rename_toggle": "Rename selected page (R); use left/right arrows to reorder", "screen.rivencraft.notebook.move_previous": "Move selected page left", "screen.rivencraft.notebook.move_next": "Move selected page right", "message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",