diff options
Diffstat (limited to 'common/src/main')
18 files changed, 1118 insertions, 363 deletions
diff --git a/common/src/main/java/dev/isxander/yacl/api/Option.java b/common/src/main/java/dev/isxander/yacl/api/Option.java index a6c0311..5f66c19 100644 --- a/common/src/main/java/dev/isxander/yacl/api/Option.java +++ b/common/src/main/java/dev/isxander/yacl/api/Option.java @@ -17,10 +17,13 @@ public interface Option<T> { */ @NotNull Component name(); + @NotNull OptionDescription description(); + /** * Tooltip (or description) of the option. * Rendered on hover. */ + @Deprecated @NotNull Component tooltip(); /** @@ -126,12 +129,17 @@ public interface Option<T> { */ Builder<T> name(@NotNull Component name); + Builder<T> description(@NotNull OptionDescription description); + + Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction); + /** * Sets the tooltip to be used by the option. * No need to wrap the text yourself, the gui does this itself. * * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. */ + @Deprecated Builder<T> tooltip(@NotNull Function<T, Component> tooltipGetter); /** @@ -150,6 +158,7 @@ public interface Option<T> { * * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. */ + @Deprecated Builder<T> tooltip(@NotNull Component... tooltips); /** diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionDescription.java b/common/src/main/java/dev/isxander/yacl/api/OptionDescription.java new file mode 100644 index 0000000..3b28a65 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionDescription.java @@ -0,0 +1,39 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.ImageRenderer; +import dev.isxander.yacl.impl.OptionDescriptionImpl; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public interface OptionDescription { + Component descriptiveName(); + + Component description(); + + CompletableFuture<Optional<ImageRenderer>> image(); + + static Builder createBuilder() { + return new OptionDescriptionImpl.BuilderImpl(); + } + + interface Builder { + Builder name(Component name); + + Builder description(Component description); + + Builder image(ResourceLocation image, int width, int height); + Builder image(Path path, ResourceLocation uniqueLocation); + + Builder gifImage(ResourceLocation image); + Builder gifImage(Path path, ResourceLocation uniqueLocation); + + Builder webpImage(ResourceLocation image, int frameDelayMS); + Builder webpImage(Path path, ResourceLocation uniqueLocation, int frameDelayMS); + + OptionDescription build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java deleted file mode 100644 index 6668584..0000000 --- a/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java +++ /dev/null @@ -1,100 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.ConfigCategory; -import dev.isxander.yacl.gui.utils.GuiUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; - -import java.util.List; - -public class CategoryListWidget extends ElementListWidgetExt<CategoryListWidget.CategoryEntry> { - private final YACLScreen yaclScreen; - - public CategoryListWidget(Minecraft client, YACLScreen yaclScreen, int screenWidth, int screenHeight) { - super(client, 0, 0, screenWidth / 3, yaclScreen.searchFieldWidget.getY() - 5, true); - this.yaclScreen = yaclScreen; - setRenderBackground(false); - setRenderTopAndBottom(false); - - for (ConfigCategory category : yaclScreen.config.categories()) { - addEntry(new CategoryEntry(category)); - } - } - - @Override - public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - GuiUtils.enableScissor(0, 0, width, height); - super.render(graphics, mouseX, mouseY, delta); - RenderSystem.disableScissor(); - } - - @Override - public int getRowWidth() { - return Math.min(width - width / 10, 396); - } - - @Override - public int getRowLeft() { - return super.getRowLeft() - 2; - } - - @Override - protected int getScrollbarPosition() { - return width - 2; - } - - @Override - protected void renderBackground(GuiGraphics graphics) { - - } - - public class CategoryEntry extends Entry<CategoryEntry> { - private final CategoryWidget categoryButton; - public final int categoryIndex; - - public CategoryEntry(ConfigCategory category) { - this.categoryIndex = yaclScreen.config.categories().indexOf(category); - categoryButton = new CategoryWidget( - yaclScreen, - category, - categoryIndex, - getRowLeft(), 0, - getRowWidth(), 20 - ); - } - - @Override - public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (mouseY > y1) { - mouseY = -20; - } - - categoryButton.setY(y); - categoryButton.render(graphics, mouseX, mouseY, tickDelta); - } - - public void postRender(GuiGraphics graphics, int mouseX, int mouseY, float tickDelta) { - categoryButton.renderHoveredTooltip(graphics); - } - - @Override - public int getItemHeight() { - return 21; - } - - @Override - public List<? extends GuiEventListener> children() { - return ImmutableList.of(categoryButton); - } - - @Override - public List<? extends NarratableEntry> narratables() { - return ImmutableList.of(categoryButton); - } - } -} diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java deleted file mode 100644 index 60817a2..0000000 --- a/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.isxander.yacl.gui; - -import dev.isxander.yacl.api.ConfigCategory; -import net.minecraft.client.sounds.SoundManager; - -public class CategoryWidget extends TooltipButtonWidget { - private final int categoryIndex; - - public CategoryWidget(YACLScreen screen, ConfigCategory category, int categoryIndex, int x, int y, int width, int height) { - super(screen, x, y, width, height, category.name(), category.tooltip(), btn -> { - screen.searchFieldWidget.setValue(""); - screen.changeCategory(categoryIndex); - }); - this.categoryIndex = categoryIndex; - } - - private boolean isCurrentCategory() { - return ((YACLScreen) screen).getCurrentCategoryIdx() == categoryIndex; - } - - @Override - protected int getTextureY() { - int i = 1; - if (!this.active) { - i = 0; - } else if (this.isHoveredOrFocused() || isCurrentCategory()) { - i = 2; - } - - return 46 + i * 20; - } - - @Override - public void playDownSound(SoundManager soundManager) { - if (!isCurrentCategory()) - super.playDownSound(soundManager); - } -} diff --git a/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java index cb9fc87..ffeffbf 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java +++ b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java @@ -3,13 +3,18 @@ package dev.isxander.yacl.gui; import com.mojang.blaze3d.platform.InputConstants; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.ContainerObjectSelectionList; import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; -public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> extends ContainerObjectSelectionList<E> { - protected final int x, y; +import java.util.function.Consumer; + +public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> extends ContainerObjectSelectionList<E> implements LayoutElement { + protected int x, y; private double smoothScrollAmount = getScrollAmount(); private boolean returnSmoothAmount = false; @@ -33,9 +38,8 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten @Override protected void renderBackground(GuiGraphics graphics) { // render transparent background if in-game. - setRenderBackground(minecraft.level == null); - if (minecraft.level != null) - graphics.fill(x0, y0, x1, y1, 0x6B000000); + setRenderBackground(true); + setRenderTopAndBottom(false); } @Override @@ -48,10 +52,23 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { smoothScrollAmount = Mth.lerp(Minecraft.getInstance().getDeltaFrameTime() * 0.5, smoothScrollAmount, getScrollAmount()); returnSmoothAmount = true; + + graphics.enableScissor(x0, y0, x1, y1); + super.render(graphics, mouseX, mouseY, delta); + + graphics.disableScissor(); + returnSmoothAmount = false; } + public void updateDimensions(ScreenRectangle rectangle) { + this.x0 = rectangle.left(); + this.y0 = rectangle.top(); + this.x1 = rectangle.right(); + this.y1 = rectangle.bottom(); + } + /** * awful code to only use smooth scroll state when rendering, * not other code that needs target scroll amount @@ -141,6 +158,42 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten /* END cloth config code */ + @Override + public void setX(int i) { + this.x = x0 = i; + this.x1 = x0 + width; + } + + @Override + public void setY(int i) { + this.y = y0 = i; + this.y1 = y0 + height; + } + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void visitWidgets(Consumer<AbstractWidget> consumer) { + } + public abstract static class Entry<E extends Entry<E>> extends ContainerObjectSelectionList.Entry<E> { @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { diff --git a/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java b/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java new file mode 100644 index 0000000..8ea8ba3 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java @@ -0,0 +1,258 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.Blaze3D; +import com.mojang.blaze3d.platform.NativeImage; +import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.FastColor; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.IntStream; + +public interface ImageRenderer extends AutoCloseable { + int render(GuiGraphics graphics, int x, int y, int width); + + class TextureBacked implements ImageRenderer { + private final ResourceLocation location; + private final int width, height; + + public TextureBacked(ResourceLocation location, int width, int height) { + this.location = location; + this.width = width; + this.height = height; + } + + @Override + public int render(GuiGraphics graphics, int x, int y, int renderWidth) { + float ratio = renderWidth / (float)this.width; + int targetHeight = (int) (this.height * ratio); + + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(ratio, ratio, 1); + graphics.blit(location, 0, 0, 0, 0, this.width, this.height, this.width, this.height); + graphics.pose().popPose(); + + return targetHeight; + } + + @Override + public void close() { + + } + } + + class NativeImageBacked implements ImageRenderer { + protected static final TextureManager textureManager = Minecraft.getInstance().getTextureManager(); + + protected NativeImage image; + protected DynamicTexture texture; + protected final ResourceLocation uniqueLocation; + protected int width, height; + + public NativeImageBacked(NativeImage image, ResourceLocation uniqueLocation) { + this.image = image; + this.texture = new DynamicTexture(image); + this.uniqueLocation = uniqueLocation; + textureManager.register(this.uniqueLocation, this.texture); + this.width = image.getWidth(); + this.height = image.getHeight(); + } + + private NativeImageBacked(Path imagePath, ResourceLocation uniqueLocation) throws IOException { + this.uniqueLocation = uniqueLocation; + this.image = NativeImage.read(new FileInputStream(imagePath.toFile())); + this.width = image.getWidth(); + this.height = image.getHeight(); + this.texture = new DynamicTexture(image); + textureManager.register(this.uniqueLocation, this.texture); + } + + public static Optional<ImageRenderer> createFromPath(Path path, ResourceLocation uniqueLocation) { + try { + return Optional.of(new NativeImageBacked(path, uniqueLocation)); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + @Override + public int render(GuiGraphics graphics, int x, int y, int renderWidth) { + if (image == null) return 0; + + float ratio = renderWidth / (float)this.width; + int targetHeight = (int) (this.height * ratio); + + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(ratio, ratio, 1); + graphics.blit(uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height); + graphics.pose().popPose(); + + return targetHeight; + } + + @Override + public void close() { + image.close(); + image = null; + texture = null; + textureManager.release(uniqueLocation); + } + } + + class AnimatedNativeImageBacked extends NativeImageBacked { + private int currentFrame; + private double lastFrameTime; + + private double frameDelay; + private int frameCount; + + private int packCols, packRows; + private int frameWidth, frameHeight; + + public AnimatedNativeImageBacked(NativeImage image, int frameWidth, int frameHeight, int frameCount, double frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) { + super(image, uniqueLocation); + this.frameWidth = frameWidth; + this.frameHeight = frameHeight; + this.frameCount = frameCount; + this.frameDelay = frameDelayMS; + this.packCols = packCols; + this.packRows = packRows; + } + + public static AnimatedNativeImageBacked createGIFFromTexture(ResourceLocation textureLocation) throws IOException { + ResourceManager resourceManager = Minecraft.getInstance().getResourceManager(); + Resource resource = resourceManager.getResource(textureLocation).orElseThrow(); + + return createGIF(resource.open(), textureLocation); + } + + public static AnimatedNativeImageBacked createWEBPFromTexture(ResourceLocation textureLocation, int frameDelayMS) throws IOException { + ResourceManager resourceManager = Minecraft.getInstance().getResourceManager(); + Resource resource = resourceManager.getResource(textureLocation).orElseThrow(); + + return createWEBP(resource.open(), textureLocation, frameDelayMS); + } + + public static AnimatedNativeImageBacked createGIF(InputStream is, ResourceLocation uniqueLocation) { + try (is) { + ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next(); + reader.setInput(ImageIO.createImageInputStream(is)); + + IIOMetadata metadata = reader.getImageMetadata(0); + String metaFormatName = metadata.getNativeMetadataFormatName(); + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName); + IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0); + int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10; + + return createFromImageReader(reader, delay, uniqueLocation); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static AnimatedNativeImageBacked createWEBP(InputStream is, ResourceLocation uniqueLocation, int frameDelayMS) { + try (is) { + ImageReader reader = ImageIO.getImageReadersBySuffix("webp").next(); + reader.setInput(ImageIO.createImageInputStream(is)); + return createFromImageReader(reader, frameDelayMS, uniqueLocation); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static AnimatedNativeImageBacked createFromImageReader(ImageReader reader, int frameDelayMS, ResourceLocation uniqueLocation) throws IOException { + int frameCount = reader.getNumImages(true); + + int frameWidth = reader.getWidth(0); + int frameHeight = reader.getHeight(0); + + // Packs the frames into an optimal 1:1 texture. + // OpenGL can only have texture axis with a max of 32768 pixels, + // and packing them to that length is not efficient, apparently. + double ratio = frameWidth / (double)frameHeight; + int cols = (int)Math.ceil(Math.sqrt(frameCount) / Math.sqrt(ratio)); + int rows = (int)Math.ceil(frameCount / (double)cols); + + NativeImage image = new NativeImage(frameWidth * cols, frameHeight * rows, true); + for (int i = reader.getMinIndex(); i < frameCount - 1; i++) { + BufferedImage bi = reader.read(i); + for (int w = 0; w < bi.getWidth(); w++) { + for (int h = 0; h < bi.getHeight(); h++) { + int rgb = bi.getRGB(w, h); + int r = FastColor.ARGB32.red(rgb); + int g = FastColor.ARGB32.green(rgb); + int b = FastColor.ARGB32.blue(rgb); + + int col = i % cols; + int row = (int) Math.floor(i / (double)cols); + + image.setPixelRGBA( + bi.getWidth() * col + w, + bi.getHeight() * row + h, + FastColor.ABGR32.color(255, b, g, r) // NativeImage uses ABGR for some reason + ); + } + } + } + image.upload(0, 0, 0, false); + + return new AnimatedNativeImageBacked(image, frameWidth, frameHeight, frameCount, frameDelayMS, cols, rows, uniqueLocation); + } + + @Override + public int render(GuiGraphics graphics, int x, int y, int renderWidth) { + if (image == null) return 0; + + float ratio = renderWidth / (float)frameWidth; + int targetHeight = (int) (frameHeight * ratio); + + int currentCol = currentFrame % packCols; + int currentRow = (int) Math.floor(currentFrame / (double)packCols); + + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(ratio, ratio, 1); + graphics.blit( + uniqueLocation, + 0, 0, + frameWidth * currentCol, frameHeight * currentRow, + frameWidth, frameHeight, + this.width, this.height + ); + graphics.pose().popPose(); + + double timeMS = Blaze3D.getTime() * 1000; + if (lastFrameTime == 0) lastFrameTime = timeMS; + if (timeMS - lastFrameTime >= frameDelay) { + currentFrame++; + lastFrameTime = timeMS; + } + if (currentFrame >= frameCount) currentFrame = 0; + + return targetHeight; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java new file mode 100644 index 0000000..d9ad045 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java @@ -0,0 +1,156 @@ +package dev.isxander.yacl.gui; + +import dev.isxander.yacl.api.OptionDescription; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Supplier; + +public class OptionDescriptionWidget extends AbstractWidget implements AutoCloseable { + private @Nullable OptionDescription description; + private List<FormattedCharSequence> wrappedText; + + private static final Minecraft minecraft = Minecraft.getInstance(); + private static final Font font = minecraft.font; + + private Supplier<ScreenRectangle> dimensions; + + private int scrollAmount; + private int maxScrollAmount; + private int descriptionY; + + public OptionDescriptionWidget(Supplier<ScreenRectangle> dimensions, @Nullable OptionDescription description) { + super(0, 0, 0, 0, description == null ? Component.empty() : description.descriptiveName()); + this.dimensions = dimensions; + this.setOptionDescription(description); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (description == null) return; + + ScreenRectangle dimensions = this.dimensions.get(); + this.setX(dimensions.left()); + this.setY(dimensions.top()); + this.width = dimensions.width(); + this.height = dimensions.height(); + + int y = getY(); + + int nameWidth = font.width(description.descriptiveName()); + if (nameWidth > getWidth()) { + renderScrollingString(graphics, font, description.descriptiveName(), getX(), y, getX() + getWidth(), y + font.lineHeight, -1); + } else { + graphics.drawString(font, description.descriptiveName(), getX(), y, 0xFFFFFF); + } + + y += 5 + font.lineHeight; + + graphics.enableScissor(getX(), y, getX() + getWidth(), getY() + getHeight()); + + y -= scrollAmount; + + if (description.image().isDone()) { + var image = description.image().join(); + if (image.isPresent()) { + image.get().render(graphics, getX(), y, getWidth()); + y += image.get().render(graphics, getX(), y, getWidth()) + 5; + } + } + + if (wrappedText == null && description.description() != null) + wrappedText = font.split(description.description(), getWidth()); + + descriptionY = y; + for (var line : wrappedText) { + graphics.drawString(font, line, getX(), y, 0xFFFFFF); + y += font.lineHeight; + } + + graphics.disableScissor(); + + maxScrollAmount = Math.max(0, y + scrollAmount - getY() - getHeight()); + + Style hoveredStyle = getDescStyle(mouseX, mouseY); + if (hoveredStyle != null && hoveredStyle.getHoverEvent() != null) { + graphics.renderComponentHoverEffect(font, hoveredStyle, mouseX, mouseY); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + Style clickedStyle = getDescStyle((int) mouseX, (int) mouseY); + if (clickedStyle != null && clickedStyle.getClickEvent() != null) { + if (minecraft.screen.handleComponentClicked(clickedStyle)) { + playDownSound(minecraft.getSoundManager()); + return true; + } + return false; + } + + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + if (isMouseOver(mouseX, mouseY)) { + scrollAmount = Mth.clamp(scrollAmount - (int) amount * 10, 0, maxScrollAmount); + return true; + } + return false; + } + + private Style getDescStyle(int mouseX, int mouseY) { + if (!clicked(mouseX, mouseY)) + return null; + + int x = mouseX - getX(); + int y = mouseY - descriptionY; + + if (x < 0 || x > getX() + getWidth()) return null; + if (y < 0 || y > getY() + getHeight()) return null; + + int line = y / font.lineHeight; + + if (line >= wrappedText.size()) return null; + + return font.getSplitter().componentStyleAtWidth(wrappedText.get(line), x); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) { + + } + + public void setOptionDescription(OptionDescription description) { + //this.close(); + this.description = description; + this.wrappedText = null; + } + + @Override + public void close() { + if (description != null) { + description.image().thenAccept(image -> { + if (image.isPresent()) { + try { + image.get().close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java index fc7c317..390e6c0 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java +++ b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -1,7 +1,6 @@ package dev.isxander.yacl.gui; import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.vertex.PoseStack; import dev.isxander.yacl.api.*; import dev.isxander.yacl.api.utils.Dimension; import dev.isxander.yacl.impl.utils.YACLConstants; @@ -21,24 +20,27 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entry> { private final YACLScreen yaclScreen; - private boolean singleCategory = false; - + private final ConfigCategory category; private ImmutableList<Entry> viewableChildren; + private String searchQuery = ""; + private final Consumer<Option<?>> hoverEvent; + private Option<?> lastHoveredOption; - public OptionListWidget(YACLScreen screen, Minecraft client, int width, int height) { - super(client, width / 3, 0, width / 3 * 2 + 1, height, true); + public OptionListWidget(YACLScreen screen, ConfigCategory category, Minecraft client, int x, int y, int width, int height, Consumer<Option<?>> hoverEvent) { + super(client, x, y, width, height, true); this.yaclScreen = screen; + this.category = category; + this.hoverEvent = hoverEvent; refreshOptions(); - for (ConfigCategory category : screen.config.categories()) { - for (OptionGroup group : category.groups()) { - if (group instanceof ListOption<?> listOption) { - listOption.addRefreshListener(() -> refreshListEntries(listOption, category)); - } + for (OptionGroup group : category.groups()) { + if (group instanceof ListOption<?> listOption) { + listOption.addRefreshListener(() -> refreshListEntries(listOption, category)); } } } @@ -46,47 +48,36 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr public void refreshOptions() { clearEntries(); - List<ConfigCategory> categories = new ArrayList<>(); - if (yaclScreen.getCurrentCategoryIdx() == -1) { - // -1 = no category, search in progress, so use all categories for search - categories.addAll(yaclScreen.config.categories()); - } else { - categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx())); - } - singleCategory = categories.size() == 1; - - for (ConfigCategory category : categories) { - for (OptionGroup group : category.groups()) { - GroupSeparatorEntry groupSeparatorEntry; - if (!group.isRoot()) { - groupSeparatorEntry = group instanceof ListOption<?> listOption - ? new ListGroupSeparatorEntry(listOption, yaclScreen) - : new GroupSeparatorEntry(group, yaclScreen); - addEntry(groupSeparatorEntry); - } else { - groupSeparatorEntry = null; - } + for (OptionGroup group : category.groups()) { + GroupSeparatorEntry groupSeparatorEntry; + if (!group.isRoot()) { + groupSeparatorEntry = group instanceof ListOption<?> listOption + ? new ListGroupSeparatorEntry(listOption, yaclScreen) + : new GroupSeparatorEntry(group, yaclScreen); + addEntry(groupSeparatorEntry); + } else { + groupSeparatorEntry = null; + } - List<Entry> optionEntries = new ArrayList<>(); + List<Entry> optionEntries = new ArrayList<>(); - // add empty entry to make sure users know it's empty not just bugging out - if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) { - if (listGroupSeparatorEntry.listOption.options().isEmpty()) { - EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category); - addEntry(emptyListLabel); - optionEntries.add(emptyListLabel); - } + // add empty entry to make sure users know it's empty not just bugging out + if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) { + if (listGroupSeparatorEntry.listOption.options().isEmpty()) { + EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category); + addEntry(emptyListLabel); + optionEntries.add(emptyListLabel); } + } - for (Option<?> option : group.options()) { - OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); - addEntry(entry); - optionEntries.add(entry); - } + for (Option<?> option : group.options()) { + OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); + addEntry(entry); + optionEntries.add(entry); + } - if (groupSeparatorEntry != null) { - groupSeparatorEntry.setChildEntries(optionEntries); - } + if (groupSeparatorEntry != null) { + groupSeparatorEntry.setChildEntries(optionEntries); } } @@ -137,6 +128,12 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr } } + public void updateSearchQuery(String query) { + this.searchQuery = query; + expandAllGroups(); + recacheViewableChildren(); + } + @Override public int getRowWidth() { return Math.min(396, (int)(width / 1.3f)); @@ -294,6 +291,13 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr resetButton.setY(y); resetButton.render(graphics, mouseX, mouseY, tickDelta); } + + if (isHovered()) { + if (lastHoveredOption != option) { + lastHoveredOption = option; + hoverEvent.accept(option); + } + } } @Override @@ -318,12 +322,10 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr @Override public boolean isViewable() { - String query = yaclScreen.searchFieldWidget.getQuery(); return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded()) - && (yaclScreen.searchFieldWidget.isEmpty() - || (!singleCategory && categoryName.contains(query)) - || groupName.contains(query) - || widget.matchesSearch(query)); + && (searchQuery.isEmpty() + || groupName.contains(searchQuery) + || widget.matchesSearch(searchQuery)); } @Override @@ -427,7 +429,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr @Override public boolean isViewable() { - return yaclScreen.searchFieldWidget.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable); + return searchQuery.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable); } @Override @@ -524,6 +526,11 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr } @Override + public void setExpanded(boolean expanded) { + super.setExpanded(listOption.available() && expanded); + } + + @Override public List<? extends GuiEventListener> children() { return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton); } @@ -547,10 +554,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr @Override public boolean isViewable() { - String query = yaclScreen.searchFieldWidget.getQuery(); - return parent.isExpanded() && (yaclScreen.searchFieldWidget.isEmpty() - || (!singleCategory && categoryName.contains(query)) - || groupName.contains(query)); + return parent.isExpanded() && (searchQuery.isEmpty() || groupName.contains(searchQuery)); } @Override diff --git a/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java index fb098a9..24db7ab 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java +++ b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java @@ -6,20 +6,24 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; import net.minecraft.network.chat.Component; +import java.util.function.Consumer; + public class SearchFieldWidget extends EditBox { private Component emptyText; private final YACLScreen yaclScreen; private final Font font; + private final Consumer<String> updateConsumer; private boolean isEmpty = true; - public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText) { + public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText, Consumer<String> updateConsumer) { super(font, x, y, width, height, text); - setResponder(string -> update()); + setResponder(this::update); setFilter(string -> !string.endsWith(" ") && !string.startsWith(" ")); this.yaclScreen = yaclScreen; this.font = font; this.emptyText = emptyText; + this.updateConsumer = updateConsumer; } @Override @@ -30,23 +34,14 @@ public class SearchFieldWidget extends EditBox { } } - private void update() { + private void update(String query) { boolean wasEmpty = isEmpty; - isEmpty = getValue().isEmpty(); + isEmpty = query.isEmpty(); if (isEmpty && wasEmpty) return; - if (!isEmpty && yaclScreen.getCurrentCategoryIdx() != -1) - yaclScreen.changeCategory(-1); - if (isEmpty && yaclScreen.getCurrentCategoryIdx() == -1) - yaclScreen.changeCategory(0); - - yaclScreen.optionList.expandAllGroups(); - yaclScreen.optionList.recacheViewableChildren(); - - yaclScreen.optionList.setScrollAmount(0); - yaclScreen.categoryList.setScrollAmount(0); + updateConsumer.accept(query); } public String getQuery() { diff --git a/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java new file mode 100644 index 0000000..b9e756c --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java @@ -0,0 +1,105 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.network.chat.CommonComponents; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Supplier; + +/** + * Author: MrCrayfish + */ +public class TabListWidget<T extends ElementListWidgetExt<?>> extends AbstractWidget implements ContainerEventHandler +{ + private final Supplier<ScreenRectangle> dimensions; + private final T list; + + public TabListWidget(Supplier<ScreenRectangle> dimensions, T list) { + super(0, 0, 100, 0, CommonComponents.EMPTY); + this.dimensions = dimensions; + this.list = list; + } + + @Override + public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float deltaTick) { + ScreenRectangle dimensions = this.dimensions.get(); + this.setX(dimensions.left()); + this.setY(dimensions.top()); + this.width = dimensions.width(); + this.height = dimensions.height(); + this.list.updateDimensions(dimensions); + this.list.render(guiGraphics, mouseX, mouseY, deltaTick); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + this.list.updateNarration(output); + } + + @Override + public List<? extends GuiEventListener> children() { + return ImmutableList.of(this.list); + } + + public T getList() { + return list; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return this.list.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + return this.list.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return this.list.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + return this.list.mouseScrolled(mouseX, mouseY, amount); + } + + @Override + public boolean keyPressed(int i, int j, int k) { + return this.list.keyPressed(i, j, k); + } + + @Override + public boolean charTyped(char c, int i) { + return this.list.charTyped(c, i); + } + + @Override + public boolean isDragging() { + return this.list.isDragging(); + } + + @Override + public void setDragging(boolean dragging) { + this.list.setDragging(dragging); + } + + @Nullable + @Override + public GuiEventListener getFocused() { + return this.list.getFocused(); + } + + @Override + public void setFocused(@Nullable GuiEventListener listener) { + this.list.setFocused(listener); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java index b3e614f..77090c8 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java +++ b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java @@ -2,10 +2,7 @@ package dev.isxander.yacl.gui; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.*; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.OptionFlag; -import dev.isxander.yacl.api.PlaceholderCategory; -import dev.isxander.yacl.api.YetAnotherConfigLib; +import dev.isxander.yacl.api.*; import dev.isxander.yacl.api.utils.Dimension; import dev.isxander.yacl.api.utils.MutableDimension; import dev.isxander.yacl.api.utils.OptionUtils; @@ -14,117 +11,81 @@ import dev.isxander.yacl.impl.utils.YACLConstants; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.components.tabs.Tab; +import net.minecraft.client.gui.components.tabs.TabManager; +import net.minecraft.client.gui.components.tabs.TabNavigationBar; +import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import java.io.*; import java.util.HashSet; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; public class YACLScreen extends Screen { public final YetAnotherConfigLib config; - private int currentCategoryIdx; private final Screen parent; - public OptionListWidget optionList; - public CategoryListWidget categoryList; - public TooltipButtonWidget finishedSaveButton, cancelResetButton, undoButton; - public SearchFieldWidget searchFieldWidget; + public final TabManager tabManager = new TabManager(this::addRenderableWidget, this::removeWidget); + public TabNavigationBar tabNavigationBar; + public Tab[] tabs; + public ScreenRectangle tabArea; public Component saveButtonMessage, saveButtonTooltipMessage; private int saveButtonMessageTime; - public YACLScreen(YetAnotherConfigLib config, Screen parent) { super(config.title()); this.config = config; this.parent = parent; - this.currentCategoryIdx = 0; } @Override protected void init() { - int columnWidth = width / 3; - int padding = columnWidth / 20; - columnWidth = Math.min(columnWidth, 400); - int paddedWidth = columnWidth - padding * 2; - - MutableDimension<Integer> actionDim = Dimension.ofInt(width / 3 / 2, height - padding - 20, paddedWidth, 20); - finishedSaveButton = new TooltipButtonWidget( - this, - actionDim.x() - actionDim.width() / 2, - actionDim.y(), - actionDim.width(), - actionDim.height(), - Component.empty(), - Component.empty(), - btn -> finishOrSave() - ); - actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22); - cancelResetButton = new TooltipButtonWidget( - this, - actionDim.x() - actionDim.width() / 2, - actionDim.y(), - actionDim.width(), - actionDim.height(), - Component.empty(), - Component.empty(), - btn -> cancelOrReset() - ); - actionDim.move(actionDim.width() + 4, 0); - undoButton = new TooltipButtonWidget( - this, - actionDim.x() - actionDim.width() / 2, - actionDim.y(), - actionDim.width(), - actionDim.height(), - Component.translatable("yacl.gui.undo"), - Component.translatable("yacl.gui.undo.tooltip"), - btn -> undo() - ); - - searchFieldWidget = new SearchFieldWidget( - this, - font, - width / 3 / 2 - paddedWidth / 2 + 1, - undoButton.getY() - 22, - paddedWidth - 2, 18, - Component.translatable("gui.recipebook.search_hint"), - Component.translatable("gui.recipebook.search_hint") - ); - - categoryList = new CategoryListWidget(minecraft, this, width, height); - addWidget(categoryList); - - updateActionAvailability(); - addRenderableWidget(searchFieldWidget); - addRenderableWidget(cancelResetButton); - addRenderableWidget(undoButton); - addRenderableWidget(finishedSaveButton); - - optionList = new OptionListWidget(this, minecraft, width, height); - addWidget(optionList); + if (tabs != null) { + closeTabs(); + } + + tabNavigationBar = TabNavigationBar.builder(tabManager, this.width) + .addTabs(tabs = config.categories() + .stream() + .map(category -> { + if (category instanceof PlaceholderCategory placeholder) + return new PlaceholderTab(placeholder); + return new CategoryTab(category); + }) + .toArray(Tab[]::new) + ) + .build(); + tabNavigationBar.selectTab(0, false); + tabNavigationBar.arrangeElements(); + ScreenRectangle navBarArea = tabNavigationBar.getRectangle(); + tabArea = new ScreenRectangle(0, navBarArea.height() - 1, this.width, this.height - navBarArea.height() + 1); + tabManager.setTabArea(tabArea); + addRenderableWidget(tabNavigationBar); config.initConsumer().accept(this); } @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - renderBackground(graphics); + renderDirtBackground(graphics); super.render(graphics, mouseX, mouseY, delta); - categoryList.render(graphics, mouseX, mouseY, delta); - searchFieldWidget.render(graphics, mouseX, mouseY, delta); - optionList.render(graphics, mouseX, mouseY, delta); - - categoryList.postRender(graphics, mouseX, mouseY, delta); - optionList.postRender(graphics, mouseX, mouseY, delta); for (GuiEventListener child : children()) { if (child instanceof TooltipButtonWidget tooltipButtonWidget) { @@ -171,54 +132,8 @@ public class YACLScreen extends Screen { } @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (optionList.keyPressed(keyCode, scanCode, modifiers)) { - return true; - } - - return super.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean charTyped(char chr, int modifiers) { - if (optionList.charTyped(chr, modifiers)) { - return true; - } - - return super.charTyped(chr, modifiers); - } - - public void changeCategory(int idx) { - if (idx == currentCategoryIdx) - return; - - if (idx != -1 && config.categories().get(idx) instanceof PlaceholderCategory placeholderCategory) { - minecraft.setScreen(placeholderCategory.screen().apply(minecraft, this)); - } else { - currentCategoryIdx = idx; - optionList.refreshOptions(); - } - } - - public int getCurrentCategoryIdx() { - return currentCategoryIdx; - } - - private void updateActionAvailability() { - boolean pendingChanges = pendingChanges(); - - undoButton.active = pendingChanges; - finishedSaveButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE)); - finishedSaveButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip")); - cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset")); - cancelResetButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip")); - } - - @Override public void tick() { - searchFieldWidget.tick(); - - updateActionAvailability(); + tabManager.tickCurrent(); if (saveButtonMessage != null) { if (saveButtonMessageTime > 140) { @@ -227,9 +142,9 @@ public class YACLScreen extends Screen { saveButtonMessageTime = 0; } else { saveButtonMessageTime++; - finishedSaveButton.setMessage(saveButtonMessage); + //finishedSaveButton.setMessage(saveButtonMessage); if (saveButtonTooltipMessage != null) { - finishedSaveButton.setTooltip(saveButtonTooltipMessage); + //finishedSaveButton.setTooltip(saveButtonTooltipMessage); } } } @@ -266,6 +181,19 @@ public class YACLScreen extends Screen { @Override public void onClose() { minecraft.setScreen(parent); + closeTabs(); + } + + private void closeTabs() { + for (Tab tab : tabs) { + if (tab instanceof Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } } public static void renderMultilineTooltip(GuiGraphics graphics, Font font, MultiLineLabel text, int centerX, int yAbove, int yBelow, int screenWidth, int screenHeight) { @@ -312,4 +240,137 @@ public class YACLScreen extends Screen { graphics.pose().popPose(); } } + + private class CategoryTab implements Tab, Closeable { + private final ConfigCategory category; + + private final TabListWidget<OptionListWidget> optionList; + private final Button saveFinishedButton; + private final Button cancelResetButton; + private final Button undoButton; + private final SearchFieldWidget searchField; + private OptionDescriptionWidget descriptionWidget; + + public CategoryTab(ConfigCategory category) { + this.category = category; + + this.optionList = new TabListWidget<>( + () -> new ScreenRectangle(tabArea.position(), tabArea.width() / 3 * 2 + 1, tabArea.height()), + new OptionListWidget(YACLScreen.this, category, minecraft, 0, 0, width / 3 * 2 + 1, height, hoveredOption -> { + descriptionWidget.setOptionDescription(hoveredOption.description()); + }) + ); + + int columnWidth = width / 3; + int padding = columnWidth / 20; + columnWidth = Math.min(columnWidth, 400); + int paddedWidth = columnWidth - padding * 2; + MutableDimension<Integer> actionDim = Dimension.ofInt(width / 3 * 2 + width / 6, height - padding - 20, paddedWidth, 20); + + saveFinishedButton = Button.builder(Component.literal("Done"), btn -> finishOrSave()) + .pos(actionDim.x() - actionDim.width() / 2, actionDim.y()) + .size(actionDim.width(), actionDim.height()) + .build(); + + actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22); + cancelResetButton = Button.builder(Component.literal("Cancel"), btn -> cancelOrReset()) + .pos(actionDim.x() - actionDim.width() / 2, actionDim.y()) + .size(actionDim.width(), actionDim.height()) + .build(); + + actionDim.move(actionDim.width() + 4, 0); + undoButton = Button.builder(Component.translatable("yacl.gui.undo"), btn -> undo()) + .pos(actionDim.x() - actionDim.width() / 2, actionDim.y()) + .size(actionDim.width(), actionDim.height()) + .tooltip(Tooltip.create(Component.translatable("yacl.gui.undo.tooltip"))) + .build(); + + searchField = new SearchFieldWidget( + YACLScreen.this, + font, + width / 3 * 2 + width / 6 - paddedWidth / 2 + 1, + undoButton.getY() - 22, + paddedWidth - 2, 18, + Component.translatable("gui.recipebook.search_hint"), + Component.translatable("gui.recipebook.search_hint"), + searchQuery -> optionList.getList().updateSearchQuery(searchQuery) + ); + + descriptionWidget = new OptionDescriptionWidget( + () -> new ScreenRectangle( + width / 3 * 2 + padding, + tabArea.top() + padding, + paddedWidth, + searchField.getY() - 1 - tabArea.top() - padding * 2 + ), + null + ); + + updateButtons(); + } + + @Override + public Component getTabTitle() { + return category.name(); + } + + @Override + public void visitChildren(Consumer<AbstractWidget> consumer) { + consumer.accept(optionList); + consumer.accept(saveFinishedButton); + consumer.accept(cancelResetButton); + consumer.accept(undoButton); + consumer.accept(searchField); + consumer.accept(descriptionWidget); + } + + @Override + public void doLayout(ScreenRectangle screenRectangle) { + + } + + @Override + public void tick() { + updateButtons(); + searchField.tick(); + } + + private void updateButtons() { + boolean pendingChanges = pendingChanges(); + + undoButton.active = pendingChanges; + saveFinishedButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE)); + saveFinishedButton.setTooltip(Tooltip.create(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip"))); + cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset")); + cancelResetButton.setTooltip(Tooltip.create(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip"))); + } + + @Override + public void close() { + descriptionWidget.close(); + } + } + + private class PlaceholderTab implements Tab { + private final PlaceholderCategory category; + + public PlaceholderTab(PlaceholderCategory category) { + this.category = category; + } + + @Override + public Component getTabTitle() { + return category.name(); + } + + @Override + public void visitChildren(Consumer<AbstractWidget> consumer) { + + } + + @Override + public void doLayout(ScreenRectangle screenRectangle) { + minecraft.setScreen(category.screen().apply(minecraft, YACLScreen.this)); + } + } } diff --git a/common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java b/common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java new file mode 100644 index 0000000..d6e6220 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java @@ -0,0 +1,48 @@ +package dev.isxander.yacl.gui; + +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import net.minecraft.util.Mth; +import org.joml.Vector2i; +import org.joml.Vector2ic; + +import java.util.function.Supplier; + +public class YACLTooltipPositioner implements ClientTooltipPositioner { + private final Supplier<ScreenRectangle> buttonDimensions; + + public YACLTooltipPositioner(net.minecraft.client.gui.components.AbstractWidget widget) { + this.buttonDimensions = widget::getRectangle; + } + + public YACLTooltipPositioner(dev.isxander.yacl.gui.AbstractWidget widget) { + this.buttonDimensions = () -> { + var dim = widget.getDimension(); + return new ScreenRectangle(dim.x(), dim.y(), dim.width(), dim.height()); + }; + } + + public YACLTooltipPositioner(Supplier<ScreenRectangle> buttonDimensions) { + this.buttonDimensions = buttonDimensions; + } + + @Override + public Vector2ic positionTooltip(int guiWidth, int guiHeight, int x, int y, int width, int height) { + ScreenRectangle buttonDimensions = this.buttonDimensions.get(); + + int centerX = buttonDimensions.left() + buttonDimensions.width() / 2; + int aboveY = buttonDimensions.top() - height - 4; + int belowY = buttonDimensions.top() + buttonDimensions.height() + 4; + + int maxBelow = guiHeight - (belowY + height); + int minAbove = aboveY - height; + + int yResult = aboveY; + if (minAbove < 8) + yResult = maxBelow > minAbove ? belowY : aboveY; + + int xResult = Mth.clamp(centerX - width / 2, -4, guiWidth - width - 4); + + return new Vector2i(xResult, yResult); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java index 11da99e..f0e0bdd 100644 --- a/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java +++ b/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java @@ -19,7 +19,7 @@ import java.util.function.Function; @ApiStatus.Internal public final class ButtonOptionImpl implements ButtonOption { private final Component name; - private final Component tooltip; + private final OptionDescription description; private final BiConsumer<YACLScreen, ButtonOption> action; private boolean available; private final Controller<BiConsumer<YACLScreen, ButtonOption>> controller; @@ -27,13 +27,13 @@ public final class ButtonOptionImpl implements ButtonOption { public ButtonOptionImpl( @NotNull Component name, - @Nullable Component tooltip, + @Nullable OptionDescription description, @NotNull BiConsumer<YACLScreen, ButtonOption> action, boolean available, @NotNull Function<ButtonOption, Controller<BiConsumer<YACLScreen, ButtonOption>>> controlGetter ) { this.name = name; - this.tooltip = tooltip; + this.description = description; this.action = action; this.available = available; this.controller = controlGetter.apply(this); @@ -46,8 +46,13 @@ public final class ButtonOptionImpl implements ButtonOption { } @Override + public @NotNull OptionDescription description() { + return description; + } + + @Override public @NotNull Component tooltip() { - return tooltip; + return description().description(); } @Override @@ -162,7 +167,7 @@ public final class ButtonOptionImpl implements ButtonOption { public Builder tooltip(@NotNull Component... tooltips) { Validate.notNull(tooltips, "`tooltips` cannot be empty"); - tooltipLines.addAll(List.of(tooltips)); + //tooltipLines.addAll(List.of(tooltips)); return this; } @@ -212,7 +217,7 @@ public final class ButtonOptionImpl implements ButtonOption { concatenatedTooltip.append(line); } - return new ButtonOptionImpl(name, concatenatedTooltip, action, available, controlGetter); + return new ButtonOptionImpl(name, OptionDescription.createBuilder().name(name).description(concatenatedTooltip).build(), action, available, controlGetter); } } } diff --git a/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java index 732a373..2a7759c 100644 --- a/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java +++ b/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java @@ -19,6 +19,7 @@ import java.util.function.BiConsumer; public final class LabelOptionImpl implements LabelOption { private final Component label; private final Component name = Component.literal("Label Option"); + private final OptionDescription description; private final Component tooltip = Component.empty(); private final LabelController labelController; private final Binding<Component> binding; @@ -27,6 +28,10 @@ public final class LabelOptionImpl implements LabelOption { this.label = label; this.labelController = new LabelController(this); this.binding = Binding.immutable(label); + this.description = OptionDescription.createBuilder() + .name(this.name) + .description(this.label) + .build(); } @Override @@ -40,6 +45,11 @@ public final class LabelOptionImpl implements LabelOption { } @Override + public @NotNull OptionDescription description() { + return description; + } + + @Override public @NotNull Component tooltip() { return tooltip; } diff --git a/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java index c15efe6..d02259e 100644 --- a/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java +++ b/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java @@ -34,6 +34,11 @@ public final class ListOptionEntryImpl<T> implements ListOptionEntry<T> { } @Override + public @NotNull OptionDescription description() { + return group.description(); + } + + @Override public @NotNull Component tooltip() { return Component.empty(); } diff --git a/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java index f47493c..24fe2b1 100644 --- a/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java +++ b/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java @@ -19,7 +19,7 @@ import java.util.stream.Collectors; @ApiStatus.Internal public final class ListOptionImpl<T> implements ListOption<T> { private final Component name; - private final Component tooltip; + private final OptionDescription description; private final Binding<List<T>> binding; private final T initialValue; private final List<ListOptionEntry<T>> entries; @@ -31,9 +31,9 @@ public final class ListOptionImpl<T> implements ListOption<T> { private final List<BiConsumer<Option<List<T>>, List<T>>> listeners; private final List<Runnable> refreshListeners; - public ListOptionImpl(@NotNull Component name, @NotNull Component tooltip, @NotNull Binding<List<T>> binding, @NotNull T initialValue, @NotNull Class<T> typeClass, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) { + public ListOptionImpl(@NotNull Component name, @NotNull OptionDescription description, @NotNull Binding<List<T>> binding, @NotNull T initialValue, @NotNull Class<T> typeClass, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) { this.name = name; - this.tooltip = tooltip; + this.description = description; this.binding = binding; this.initialValue = initialValue; this.entryFactory = new EntryFactory(controllerFunction); @@ -54,8 +54,13 @@ public final class ListOptionImpl<T> implements ListOption<T> { } @Override + public @NotNull OptionDescription description() { + return this.description; + } + + @Override public @NotNull Component tooltip() { - return this.tooltip; + return description().description(); } @Override @@ -332,7 +337,7 @@ public final class ListOptionImpl<T> implements ListOption<T> { concatenatedTooltip.append(line); } - return new ListOptionImpl<>(name, concatenatedTooltip, binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, listeners); + return new ListOptionImpl<>(name, OptionDescription.createBuilder().name(name).description(concatenatedTooltip).build(), binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, listeners); } } } diff --git a/common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java new file mode 100644 index 0000000..f57a410 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java @@ -0,0 +1,124 @@ +package dev.isxander.yacl.impl; + +import dev.isxander.yacl.api.OptionDescription; +import dev.isxander.yacl.gui.ImageRenderer; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public record OptionDescriptionImpl(Component descriptiveName, Component description, CompletableFuture<Optional<ImageRenderer>> image) implements OptionDescription { + public static class BuilderImpl implements Builder { + private Component name; + private Component description; + private CompletableFuture<Optional<ImageRenderer>> image = CompletableFuture.completedFuture(Optional.empty()); + private boolean imageUnset = true; + + @Override + public Builder name(Component name) { + this.name = name; + return this; + } + + @Override + public Builder description(Component description) { + this.description = description; + return this; + } + + @Override + public Builder image(ResourceLocation image, int width, int height) { + Validate.isTrue(imageUnset, "Image already set!"); + Validate.isTrue(width > 0, "Width must be greater than 0!"); + Validate.isTrue(height > 0, "Height must be greater than 0!"); + + this.image = CompletableFuture.completedFuture(Optional.of(new ImageRenderer.TextureBacked(image, width, height))); + imageUnset = false; + return this; + } + + @Override + public Builder image(Path path, ResourceLocation uniqueLocation) { + Validate.isTrue(imageUnset, "Image already set!"); + this.image = CompletableFuture.supplyAsync(() -> ImageRenderer.NativeImageBacked.createFromPath(path, uniqueLocation)); + imageUnset = false; + return this; + } + + @Override + public Builder gifImage(ResourceLocation image) { + Validate.isTrue(imageUnset, "Image already set!"); + this.image = CompletableFuture.supplyAsync(() -> { + try { + return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createGIFFromTexture(image)); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + }); + imageUnset = false; + return this; + } + + @Override + public Builder gifImage(Path path, ResourceLocation uniqueLocation) { + Validate.isTrue(imageUnset, "Image already set!"); + this.image = CompletableFuture.supplyAsync(() -> { + try { + return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createGIF(new FileInputStream(path.toFile()), uniqueLocation)); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + }); + imageUnset = false; + return this; + } + + @Override + public Builder webpImage(ResourceLocation image, int frameDelayMS) { + Validate.isTrue(imageUnset, "Image already set!"); + this.image = CompletableFuture.supplyAsync(() -> { + try { + return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createWEBPFromTexture(image, frameDelayMS)); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + }); + imageUnset = false; + return this; + } + + @Override + public Builder webpImage(Path path, ResourceLocation uniqueLocation, int frameDelayMS) { + Validate.isTrue(imageUnset, "Image already set!"); + this.image = CompletableFuture.supplyAsync(() -> { + try { + return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createWEBP(new FileInputStream(path.toFile()), uniqueLocation, frameDelayMS)); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + }); + imageUnset = false; + return this; + } + + @Override + public OptionDescription build() { + Validate.notNull(name, "Name must be set!"); + + if (description == null) + description = Component.empty(); + + return new OptionDescriptionImpl(name.copy().withStyle(ChatFormatting.BOLD), description, image); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java index 4b65d56..ef4d13b 100644 --- a/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java +++ b/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java @@ -1,11 +1,9 @@ package dev.isxander.yacl.impl; import com.google.common.collect.ImmutableSet; -import dev.isxander.yacl.api.Binding; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.OptionFlag; +import dev.isxander.yacl.api.*; import net.minecraft.ChatFormatting; +import net.minecraft.Util; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentContents; import net.minecraft.network.chat.MutableComponent; @@ -23,7 +21,7 @@ import java.util.function.Supplier; @ApiStatus.Internal public final class OptionImpl<T> implements Option<T> { private final Component name; - private Component tooltip; + private OptionDescription description; private final Controller<T> controller; private final Binding<T> binding; private boolean available; @@ -38,7 +36,7 @@ public final class OptionImpl<T> implements Option<T> { public OptionImpl( @NotNull Component name, - @Nullable Function<T, Component> tooltipGetter, + @NotNull Function<T, OptionDescription> descriptionFunction, @NotNull Function<Option<T>, Controller<T>> controlGetter, @NotNull Binding<T> binding, boolean available, @@ -54,7 +52,9 @@ public final class OptionImpl<T> implements Option<T> { this.listeners = new ArrayList<>(listeners); this.controller = controlGetter.apply(this); - addListener((opt, pending) -> tooltip = tooltipGetter.apply(pending)); + var memoizedDescriptionFunction = Util.memoize(descriptionFunction); + addListener((opt, pending) -> description = memoizedDescriptionFunction.apply(pending)); + requestSet(binding().getValue()); } @@ -64,8 +64,13 @@ public final class OptionImpl<T> implements Option<T> { } @Override + public @NotNull OptionDescription description() { + return this.description; + } + + @Override public @NotNull Component tooltip() { - return tooltip; + return description.description(); } @Override @@ -147,6 +152,7 @@ public final class OptionImpl<T> implements Option<T> { public static class BuilderImpl<T> implements Builder<T> { private Component name = Component.literal("Name not specified!").withStyle(ChatFormatting.RED); + private Function<T, OptionDescription> descriptionFunction = null; private final List<Function<T, Component>> tooltipGetters = new ArrayList<>(); private Function<Option<T>, Controller<T>> controlGetter; @@ -176,6 +182,17 @@ public final class OptionImpl<T> implements Option<T> { } @Override + public Builder<T> description(@NotNull OptionDescription description) { + return description(opt -> description); + } + + @Override + public Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction) { + this.descriptionFunction = descriptionFunction; + return this; + } + + @Override public Builder<T> tooltip(@NotNull Function<T, Component> tooltipGetter) { Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null"); @@ -292,12 +309,11 @@ public final class OptionImpl<T> implements Option<T> { return concatenatedTooltip; }; - - if (instant) { - listeners.add((opt, pendingValue) -> opt.applyValue()); + if (descriptionFunction == null) { + descriptionFunction = opt -> OptionDescription.createBuilder().name(name).description(concatenatedTooltipGetter.apply(opt)).build(); } - return new OptionImpl<>(name, concatenatedTooltipGetter, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners); + return new OptionImpl<>(name, descriptionFunction, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners); } } } |