Add API to control creative inventory screen (#3814)

* Add API to control creative inventory screen

* Rename methods

* Apply suggestions from code review

Co-authored-by: haykam821 <24855774+haykam821@users.noreply.github.com>

* Update fabric-item-group-api-v1/src/client/java/net/fabricmc/fabric/api/client/itemgroup/v1/FabricCreativeInventoryScreen.java

Co-authored-by: haykam821 <24855774+haykam821@users.noreply.github.com>

---------

Co-authored-by: haykam821 <24855774+haykam821@users.noreply.github.com>

(cherry picked from commit 00ab0a636c)
This commit is contained in:
modmuss 2024-06-08 14:06:14 +01:00 committed by modmuss50
parent bd772e8311
commit e0fdb01756
8 changed files with 264 additions and 134 deletions

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.client.itemgroup.v1;
import java.util.List;
import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
import net.minecraft.item.ItemGroup;
/**
* Fabric provided extensions to {@link CreativeInventoryScreen}.
* This interface is automatically implemented on all creative inventory screens via Mixin and interface injection.
*/
public interface FabricCreativeInventoryScreen {
/**
* Switches to the page with the given index if it exists.
*
* @param page the index of the page to switch to
* @return Returns true when the page was changed
*/
default boolean switchToPage(int page) {
throw new AssertionError("Implemented by mixin");
}
/**
* Switches to the next page if it exists.
*
* @return Returns true when the page was changed
*/
default boolean switchToNextPage() {
return switchToPage(getCurrentPage() + 1);
}
/**
* Switches to the previous page if it exists.
*
* @return Returns true when the page was changed
*/
default boolean switchToPreviousPage() {
return switchToPage(getCurrentPage() - 1);
}
/**
* Returns the index of the current page.
*/
default int getCurrentPage() {
throw new AssertionError("Implemented by mixin");
}
/**
* Returns the total number of pages.
*/
default int getPageCount() {
throw new AssertionError("Implemented by mixin");
}
/**
* Returns an ordered list containing the item groups on the requested page.
*/
default List<ItemGroup> getItemGroupsOnPage(int page) {
throw new AssertionError("Implemented by mixin");
}
/**
* Returns the page index of the given item group.
*
* <p>Item groups appearing on every page always return the current page index.
*
* @param itemGroup the item group to get the page index for
* @return the page index of the item group
*/
default int getPage(ItemGroup itemGroup) {
throw new AssertionError("Implemented by mixin");
}
/**
* Returns whether there are additional pages to show on top of the default vanilla pages.
*
* @return true if there are additional pages
*/
default boolean hasAdditionalPages() {
throw new AssertionError("Implemented by mixin");
}
/**
* Returns the {@link ItemGroup} that is associated with the currently selected tab.
*
* @return the currently selected {@link ItemGroup}
*/
default ItemGroup getSelectedItemGroup() {
throw new AssertionError("Implemented by mixin");
}
/**
* Sets the currently selected tab to the given {@link ItemGroup}.
*
* @param itemGroup the {@link ItemGroup} to select
* @return true if the tab was successfully selected
*/
default boolean setSelectedItemGroup(ItemGroup itemGroup) {
throw new AssertionError("Implemented by mixin");
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.client.itemgroup;
public interface CreativeGuiExtensions {
void fabric_nextPage();
void fabric_previousPage();
int fabric_currentPage();
boolean fabric_isButtonVisible(FabricCreativeGuiComponents.Type type);
boolean fabric_isButtonEnabled(FabricCreativeGuiComponents.Type type);
}

View file

@ -18,6 +18,7 @@ package net.fabricmc.fabric.impl.client.itemgroup;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.client.MinecraftClient;
@ -30,31 +31,33 @@ import net.minecraft.registry.Registries;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroup;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroupImpl;
public class FabricCreativeGuiComponents {
private static final Identifier BUTTON_TEX = new Identifier("fabric", "textures/gui/creative_buttons.png");
private static final double TABS_PER_PAGE = FabricItemGroup.TABS_PER_PAGE;
private static final double TABS_PER_PAGE = FabricItemGroupImpl.TABS_PER_PAGE;
public static final Set<ItemGroup> COMMON_GROUPS = Set.of(ItemGroups.SEARCH, ItemGroups.INVENTORY, ItemGroups.HOTBAR).stream()
.map(Registries.ITEM_GROUP::getOrThrow)
.collect(Collectors.toSet());
public static int getPageCount() {
return (int) Math.ceil((ItemGroups.getGroupsToDisplay().size() - COMMON_GROUPS.size()) / TABS_PER_PAGE);
}
public static class ItemGroupButtonWidget extends ButtonWidget {
final CreativeGuiExtensions extensions;
final CreativeInventoryScreen gui;
final CreativeInventoryScreen screen;
final Type type;
public ItemGroupButtonWidget(int x, int y, Type type, CreativeGuiExtensions extensions) {
super(x, y, 11, 12, type.text, (bw) -> type.clickConsumer.accept(extensions), ButtonWidget.DEFAULT_NARRATION_SUPPLIER);
this.extensions = extensions;
public ItemGroupButtonWidget(int x, int y, Type type, CreativeInventoryScreen screen) {
super(x, y, 11, 12, type.text, (bw) -> type.clickConsumer.accept(screen), ButtonWidget.DEFAULT_NARRATION_SUPPLIER);
this.type = type;
this.gui = (CreativeInventoryScreen) extensions;
this.screen = screen;
}
@Override
protected void renderWidget(DrawContext drawContext, int mouseX, int mouseY, float delta) {
this.active = extensions.fabric_isButtonEnabled(type);
this.visible = extensions.fabric_isButtonVisible(type);
this.active = type.isEnabled.test(screen);
this.visible = screen.hasAdditionalPages();
if (!this.visible) {
return;
@ -65,22 +68,23 @@ public class FabricCreativeGuiComponents {
drawContext.drawTexture(BUTTON_TEX, this.getX(), this.getY(), u + (type == Type.NEXT ? 11 : 0), v, 11, 12);
if (this.isHovered()) {
int pageCount = (int) Math.ceil((ItemGroups.getGroupsToDisplay().size() - COMMON_GROUPS.size()) / TABS_PER_PAGE);
drawContext.drawTooltip(MinecraftClient.getInstance().textRenderer, Text.translatable("fabric.gui.creativeTabPage", extensions.fabric_currentPage() + 1, pageCount), mouseX, mouseY);
drawContext.drawTooltip(MinecraftClient.getInstance().textRenderer, Text.translatable("fabric.gui.creativeTabPage", screen.getCurrentPage() + 1, getPageCount()), mouseX, mouseY);
}
}
}
public enum Type {
NEXT(Text.literal(">"), CreativeGuiExtensions::fabric_nextPage),
PREVIOUS(Text.literal("<"), CreativeGuiExtensions::fabric_previousPage);
NEXT(Text.literal(">"), CreativeInventoryScreen::switchToNextPage, screen -> screen.getCurrentPage() + 1 < screen.getPageCount()),
PREVIOUS(Text.literal("<"), CreativeInventoryScreen::switchToPreviousPage, screen -> screen.getCurrentPage() != 0);
final Text text;
final Consumer<CreativeGuiExtensions> clickConsumer;
final Consumer<CreativeInventoryScreen> clickConsumer;
final Predicate<CreativeInventoryScreen> isEnabled;
Type(Text text, Consumer<CreativeGuiExtensions> clickConsumer) {
Type(Text text, Consumer<CreativeInventoryScreen> clickConsumer, Predicate<CreativeInventoryScreen> isEnabled) {
this.text = text;
this.clickConsumer = clickConsumer;
this.isEnabled = isEnabled;
}
}
}

View file

@ -16,10 +16,13 @@
package net.fabricmc.fabric.mixin.itemgroup.client;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@ -34,12 +37,12 @@ import net.minecraft.item.ItemGroups;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.text.Text;
import net.fabricmc.fabric.impl.client.itemgroup.CreativeGuiExtensions;
import net.fabricmc.fabric.api.client.itemgroup.v1.FabricCreativeInventoryScreen;
import net.fabricmc.fabric.impl.client.itemgroup.FabricCreativeGuiComponents;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroup;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroupImpl;
@Mixin(CreativeInventoryScreen.class)
public abstract class CreativeInventoryScreenMixin<T extends ScreenHandler> extends AbstractInventoryScreen<T> implements CreativeGuiExtensions {
public abstract class CreativeInventoryScreenMixin<T extends ScreenHandler> extends AbstractInventoryScreen<T> implements FabricCreativeInventoryScreen {
public CreativeInventoryScreenMixin(T screenHandler, PlayerInventory playerInventory, Text text) {
super(screenHandler, playerInventory, text);
}
@ -51,51 +54,15 @@ public abstract class CreativeInventoryScreenMixin<T extends ScreenHandler> exte
private static ItemGroup selectedTab;
// "static" matches selectedTab
private static int fabric_currentPage = 0;
@Unique
private static int currentPage = 0;
@Override
public void fabric_nextPage() {
if (!fabric_hasGroupForPage(fabric_currentPage + 1)) {
return;
}
fabric_currentPage++;
fabric_updateSelection();
}
@Override
public void fabric_previousPage() {
if (fabric_currentPage == 0) {
return;
}
fabric_currentPage--;
fabric_updateSelection();
}
@Override
public boolean fabric_isButtonVisible(FabricCreativeGuiComponents.Type type) {
return ItemGroups.getGroupsToDisplay().size() > (Objects.requireNonNull(ItemGroups.displayContext).hasPermissions() ? 14 : 13);
}
@Override
public boolean fabric_isButtonEnabled(FabricCreativeGuiComponents.Type type) {
if (type == FabricCreativeGuiComponents.Type.NEXT) {
return fabric_hasGroupForPage(fabric_currentPage + 1);
}
if (type == FabricCreativeGuiComponents.Type.PREVIOUS) {
return fabric_currentPage != 0;
}
return false;
}
private void fabric_updateSelection() {
if (!fabric_isGroupVisible(selectedTab)) {
@Unique
private void updateSelection() {
if (!isGroupVisible(selectedTab)) {
ItemGroups.getGroups()
.stream()
.filter(this::fabric_isGroupVisible)
.filter(this::isGroupVisible)
.min((a, b) -> Boolean.compare(a.isSpecial(), b.isSpecial()))
.ifPresent(this::setSelectedTab);
}
@ -103,63 +70,131 @@ public abstract class CreativeInventoryScreenMixin<T extends ScreenHandler> exte
@Inject(method = "init", at = @At("RETURN"))
private void init(CallbackInfo info) {
fabric_currentPage = fabric_getPage(selectedTab);
currentPage = getPage(selectedTab);
int xpos = x + 170;
int ypos = y + 4;
addDrawableChild(new FabricCreativeGuiComponents.ItemGroupButtonWidget(xpos + 11, ypos, FabricCreativeGuiComponents.Type.NEXT, this));
addDrawableChild(new FabricCreativeGuiComponents.ItemGroupButtonWidget(xpos, ypos, FabricCreativeGuiComponents.Type.PREVIOUS, this));
CreativeInventoryScreen self = (CreativeInventoryScreen) (Object) this;
addDrawableChild(new FabricCreativeGuiComponents.ItemGroupButtonWidget(xpos + 11, ypos, FabricCreativeGuiComponents.Type.NEXT, self));
addDrawableChild(new FabricCreativeGuiComponents.ItemGroupButtonWidget(xpos, ypos, FabricCreativeGuiComponents.Type.PREVIOUS, self));
}
@Inject(method = "setSelectedTab", at = @At("HEAD"), cancellable = true)
private void setSelectedTab(ItemGroup itemGroup, CallbackInfo info) {
if (!fabric_isGroupVisible(itemGroup)) {
if (!isGroupVisible(itemGroup)) {
info.cancel();
}
}
@Inject(method = "renderTabTooltipIfHovered", at = @At("HEAD"), cancellable = true)
private void renderTabTooltipIfHovered(DrawContext drawContext, ItemGroup itemGroup, int mx, int my, CallbackInfoReturnable<Boolean> info) {
if (!fabric_isGroupVisible(itemGroup)) {
if (!isGroupVisible(itemGroup)) {
info.setReturnValue(false);
}
}
@Inject(method = "isClickInTab", at = @At("HEAD"), cancellable = true)
private void isClickInTab(ItemGroup itemGroup, double mx, double my, CallbackInfoReturnable<Boolean> info) {
if (!fabric_isGroupVisible(itemGroup)) {
if (!isGroupVisible(itemGroup)) {
info.setReturnValue(false);
}
}
@Inject(method = "renderTabIcon", at = @At("HEAD"), cancellable = true)
private void renderTabIcon(DrawContext drawContext, ItemGroup itemGroup, CallbackInfo info) {
if (!fabric_isGroupVisible(itemGroup)) {
if (!isGroupVisible(itemGroup)) {
info.cancel();
}
}
private boolean fabric_isGroupVisible(ItemGroup itemGroup) {
return itemGroup.shouldDisplay() && fabric_currentPage == fabric_getPage(itemGroup);
}
private static int fabric_getPage(ItemGroup itemGroup) {
if (FabricCreativeGuiComponents.COMMON_GROUPS.contains(itemGroup)) {
return fabric_currentPage;
}
final FabricItemGroup fabricItemGroup = (FabricItemGroup) itemGroup;
return fabricItemGroup.getPage();
}
private static boolean fabric_hasGroupForPage(int page) {
return ItemGroups.getGroupsToDisplay().stream()
.anyMatch(itemGroup -> fabric_getPage(itemGroup) == page);
@Unique
private boolean isGroupVisible(ItemGroup itemGroup) {
return itemGroup.shouldDisplay() && currentPage == getPage(itemGroup);
}
@Override
public int fabric_currentPage() {
return fabric_currentPage;
public int getPage(ItemGroup itemGroup) {
if (FabricCreativeGuiComponents.COMMON_GROUPS.contains(itemGroup)) {
return currentPage;
}
final FabricItemGroupImpl fabricItemGroup = (FabricItemGroupImpl) itemGroup;
return fabricItemGroup.fabric_getPage();
}
@Unique
private boolean hasGroupForPage(int page) {
return ItemGroups.getGroupsToDisplay()
.stream()
.anyMatch(itemGroup -> getPage(itemGroup) == page);
}
@Override
public boolean switchToPage(int page) {
if (!hasGroupForPage(page)) {
return false;
}
if (currentPage == page) {
return false;
}
currentPage = page;
updateSelection();
return true;
}
@Override
public int getCurrentPage() {
return currentPage;
}
@Override
public int getPageCount() {
return FabricCreativeGuiComponents.getPageCount();
}
@Override
public List<ItemGroup> getItemGroupsOnPage(int page) {
return ItemGroups.getGroupsToDisplay()
.stream()
.filter(itemGroup -> getPage(itemGroup) == page)
// Thanks to isXander for the sorting
.sorted(Comparator.comparing(ItemGroup::getRow).thenComparingInt(ItemGroup::getColumn))
.sorted((a, b) -> {
if (a.isSpecial() && !b.isSpecial()) return 1;
if (!a.isSpecial() && b.isSpecial()) return -1;
return 0;
})
.toList();
}
@Override
public boolean hasAdditionalPages() {
return ItemGroups.getGroupsToDisplay().size() > (Objects.requireNonNull(ItemGroups.displayContext).hasPermissions() ? 14 : 13);
}
@Override
public ItemGroup getSelectedItemGroup() {
return selectedTab;
}
@Override
public boolean setSelectedItemGroup(ItemGroup itemGroup) {
Objects.requireNonNull(itemGroup, "itemGroup");
if (selectedTab == itemGroup) {
return false;
}
if (currentPage != getPage(itemGroup)) {
if (!switchToPage(getPage(itemGroup))) {
return false;
}
}
setSelectedTab(itemGroup);
return true;
}
}

View file

@ -16,10 +16,10 @@
package net.fabricmc.fabric.impl.itemgroup;
public interface FabricItemGroup {
public interface FabricItemGroupImpl {
int TABS_PER_PAGE = 10;
int getPage();
int fabric_getPage();
void setPage(int page);
void fabric_setPage(int page);
}

View file

@ -37,11 +37,11 @@ import net.minecraft.registry.RegistryKey;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroupEntries;
import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroup;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroupImpl;
import net.fabricmc.fabric.impl.itemgroup.ItemGroupEventsImpl;
@Mixin(ItemGroup.class)
abstract class ItemGroupMixin implements FabricItemGroup {
abstract class ItemGroupMixin implements FabricItemGroupImpl {
@Shadow
private Collection<ItemStack> displayStacks;
@ -49,7 +49,7 @@ abstract class ItemGroupMixin implements FabricItemGroup {
private Set<ItemStack> searchTabStacks;
@Unique
private int fabric_page = -1;
private int page = -1;
@SuppressWarnings("ConstantConditions")
@Inject(method = "updateEntries", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemGroup;reloadSearchProvider()V"))
@ -91,16 +91,16 @@ abstract class ItemGroupMixin implements FabricItemGroup {
}
@Override
public int getPage() {
if (fabric_page < 0) {
public int fabric_getPage() {
if (page < 0) {
throw new IllegalStateException("Item group has no page");
}
return fabric_page;
return page;
}
@Override
public void setPage(int page) {
this.fabric_page = page;
public void fabric_setPage(int page) {
this.page = page;
}
}

View file

@ -47,12 +47,12 @@ import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.entry.RegistryEntry;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroup;
import net.fabricmc.fabric.impl.itemgroup.FabricItemGroupImpl;
@Mixin(ItemGroups.class)
public class ItemGroupsMixin {
@Unique
private static final int TABS_PER_PAGE = FabricItemGroup.TABS_PER_PAGE;
private static final int TABS_PER_PAGE = FabricItemGroupImpl.TABS_PER_PAGE;
@Inject(method = "collect", at = @At("HEAD"), cancellable = true)
private static void deferDuplicateCheck(CallbackInfo ci) {
@ -86,16 +86,16 @@ public class ItemGroupsMixin {
for (RegistryEntry.Reference<ItemGroup> reference : sortedItemGroups) {
final ItemGroup itemGroup = reference.value();
final FabricItemGroup fabricItemGroup = (FabricItemGroup) itemGroup;
final FabricItemGroupImpl fabricItemGroup = (FabricItemGroupImpl) itemGroup;
if (vanillaGroups.contains(reference.registryKey())) {
// Vanilla group goes on the first page.
fabricItemGroup.setPage(0);
fabricItemGroup.fabric_setPage(0);
continue;
}
final ItemGroupAccessor itemGroupAccessor = (ItemGroupAccessor) itemGroup;
fabricItemGroup.setPage((count / TABS_PER_PAGE) + 1);
fabricItemGroup.fabric_setPage((count / TABS_PER_PAGE) + 1);
int pageIndex = count % TABS_PER_PAGE;
ItemGroup.Row row = pageIndex < (TABS_PER_PAGE / 2) ? ItemGroup.Row.TOP : ItemGroup.Row.BOTTOM;
itemGroupAccessor.setRow(row);
@ -110,9 +110,9 @@ public class ItemGroupsMixin {
for (RegistryKey<ItemGroup> registryKey : Registries.ITEM_GROUP.getKeys()) {
final ItemGroup itemGroup = Registries.ITEM_GROUP.getOrThrow(registryKey);
final FabricItemGroup fabricItemGroup = (FabricItemGroup) itemGroup;
final FabricItemGroupImpl fabricItemGroup = (FabricItemGroupImpl) itemGroup;
final String displayName = itemGroup.getDisplayName().getString();
final var position = new ItemGroupPosition(itemGroup.getRow(), itemGroup.getColumn(), fabricItemGroup.getPage());
final var position = new ItemGroupPosition(itemGroup.getRow(), itemGroup.getColumn(), fabricItemGroup.fabric_getPage());
final String existingName = map.put(position, displayName);
if (existingName != null) {

View file

@ -30,6 +30,9 @@
],
"accessWidener": "fabric-item-group-api-v1.accesswidener",
"custom": {
"fabric-api:module-lifecycle": "stable"
"fabric-api:module-lifecycle": "stable",
"loom:injected_interfaces": {
"net/minecraft/class_481": ["net/fabricmc/fabric/api/client/itemgroup/v1/FabricCreativeInventoryScreen"]
}
}
}