diff --git a/build.gradle b/build.gradle index 35fb7af0..6c3b0857 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,7 @@ configurations { repositories { mavenLocal() maven { url "https://maven.covers1624.net/" } + maven { url "https://maven.blamejared.com/" } } dependencies { @@ -81,6 +82,9 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + + compileOnly(fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}")) + compileOnly(fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}")) } test { diff --git a/gradle.properties b/gradle.properties index 37d4aca9..6f3ff3aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,4 @@ org.gradle.daemon=false mc_version=1.20.1 forge_version=47.1.65 mod_version=4.4.0 +jei_version=15.2.0.27 diff --git a/src/main/java/codechicken/lib/colour/Colour.java b/src/main/java/codechicken/lib/colour/Colour.java index 00edece3..61b22b05 100644 --- a/src/main/java/codechicken/lib/colour/Colour.java +++ b/src/main/java/codechicken/lib/colour/Colour.java @@ -134,6 +134,78 @@ public Colour set(float[] floats) { return set(floats[0], floats[1], floats[2], floats[3]); } + public Colour rF(float r) { + this.r = (byte) (255F * r); + return this; + } + + public Colour gF(float g) { + this.g = (byte) (255F * g); + return this; + } + + public Colour bF(float b) { + this.b = (byte) (255F * b); + return this; + } + + public Colour aF(float a) { + this.a = (byte) (255F * a); + return this; + } + + public Colour rF(int r) { + this.r = (byte) r; + return this; + } + + public Colour gF(int g) { + this.g = (byte) g; + return this; + } + + public Colour bF(int b) { + this.b = (byte) b; + return this; + } + + public Colour aF(int a) { + this.a = (byte) a; + return this; + } + + public float rF() { + return r / 255F; + } + + public float gF() { + return g / 255F; + } + + public float bF() { + return b / 255F; + } + + public float aF() { + return a / 255F; + } + + public int r() { + return r & 0xFF; + } + + public int g() { + return g & 0xFF; + } + + public int b() { + return b & 0xFF; + } + + public int a() { + return a & 0xFF; + } + /** * Flips a color between ABGR and RGBA. * diff --git a/src/main/java/codechicken/lib/compat/JEIPlugin.java b/src/main/java/codechicken/lib/compat/JEIPlugin.java new file mode 100644 index 00000000..c9608464 --- /dev/null +++ b/src/main/java/codechicken/lib/compat/JEIPlugin.java @@ -0,0 +1,37 @@ +package codechicken.lib.compat; + +import codechicken.lib.CodeChickenLib; +import codechicken.lib.gui.modular.ModularGuiContainer; +import mezz.jei.api.IModPlugin; +import mezz.jei.api.JeiPlugin; +import mezz.jei.api.gui.handlers.IGuiContainerHandler; +import mezz.jei.api.registration.IGuiHandlerRegistration; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +/** + * Created by brandon3055 on 31/12/2023 + */ +@JeiPlugin +public class JEIPlugin implements IModPlugin { + private static final ResourceLocation ID = new ResourceLocation(CodeChickenLib.MOD_ID, "jei_plugin"); + + public JEIPlugin() {} + + @Override + public ResourceLocation getPluginUid() { + return ID; + } + + @Override + public void registerGuiHandlers(IGuiHandlerRegistration registration) { + registration.addGuiContainerHandler(ModularGuiContainer.class, new IGuiContainerHandler<>() { + @Override + public List getGuiExtraAreas(ModularGuiContainer screen) { + return screen.getModularGui().getJeiExclusions().map(e -> e.getRectangle().toRect2i()).toList(); + } + }); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/ModularGui.java b/src/main/java/codechicken/lib/gui/modular/ModularGui.java new file mode 100644 index 00000000..b0a9d527 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/ModularGui.java @@ -0,0 +1,597 @@ +package codechicken.lib.gui.modular; + +import codechicken.lib.gui.modular.elements.GuiElement; +import codechicken.lib.gui.modular.lib.*; +import codechicken.lib.gui.modular.lib.container.ContainerGuiProvider; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import net.covers1624.quack.collection.FastStream; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.Slot; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.TriConsumer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * The modular gui system is built around "Gui Elements" but those elements need to be rendered by a base parent element. That's what this class is. + * This class is essentially just a container for the root gui element. + *

+ * Created by brandon3055 on 18/08/2023 + * + * @see GuiProvider + * @see ContainerGuiProvider + */ +public class ModularGui implements GuiParent { + private static final Logger LOGGER = LogManager.getLogger(); + + private final GuiProvider provider; + private final GuiElement root; + + private boolean guiBuilt = false; + private boolean pauseScreen = false; + private boolean closeOnEscape = true; + private boolean renderBackground = true; + private boolean vanillaSlotRendering = false; + + private Font font; + private Minecraft mc; + private int screenWidth; + private int screenHeight; + private Screen screen; + private Screen parentScreen; + + private Component guiTitle = Component.empty(); + private ResourceLocation newCursor = null; + + private final Map> slotHandlers = new HashMap<>(); + private final List tickListeners = new ArrayList<>(); + private final List resizeListeners = new ArrayList<>(); + private final List closeListeners = new ArrayList<>(); + private final List> preClickListeners = new ArrayList<>(); + private final List> postClickListeners = new ArrayList<>(); + private final List> preKeyPressListeners = new ArrayList<>(); + private final List> postKeyPressListeners = new ArrayList<>(); + + private final List> jeiExclusions = new ArrayList<>(); + + /** + * @param provider The gui builder that will be used to construct this modular gui when the screen is initialized. + */ + public ModularGui(GuiProvider provider) { + this.provider = provider; + if (provider instanceof DynamicTextures textures) textures.makeTextures(DynamicTextures.DynamicTexture::guiTexturePath); + Minecraft mc = Minecraft.getInstance(); + updateScreenData(mc, mc.font, mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight()); + try { + this.root = provider.createRootElement(this); + } catch (Throwable ex) { + LOGGER.error("An error occurred while constructing a modular gui", ex); + throw ex; + } + } + + public ModularGui(GuiProvider provider, Screen parentScreen) { + this(provider); + this.parentScreen = parentScreen; + } + + public void setScreen(Screen screen) { + this.screen = screen; + } + + public GuiProvider getProvider() { + return provider; + } + + //=== Modular Gui Setup ===// + + public void setGuiTitle(@NotNull Component guiTitle) { + this.guiTitle = guiTitle; + } + + @NotNull + public Component getGuiTitle() { + return guiTitle; + } + + /** + * @param pauseScreen Should a single-player game pause while this screen is open? + */ + public void setPauseScreen(boolean pauseScreen) { + this.pauseScreen = pauseScreen; + } + + public boolean isPauseScreen() { + return pauseScreen; + } + + public void setCloseOnEscape(boolean closeOnEscape) { + this.closeOnEscape = closeOnEscape; + } + + public boolean closeOnEscape() { + return closeOnEscape; + } + + /** + * Enable / disable the default screen background. (Default Enabled) + * This will be the usual darkened background when in-game, or the dirt background when not in game. + */ + public void renderScreenBackground(boolean renderBackground) { + this.renderBackground = renderBackground; + } + + public boolean renderBackground() { + return renderBackground; + } + + /** + * @return the root element to which content elements should be added. + */ + public GuiElement getRoot() { + return root instanceof ContentElement ? ((ContentElement) root).getContentElement() : root; + } + + public GuiElement getDirectRoot() { + return root; + } + + /** + * Sets up this gui to render like any other standards gui with the specified width and height. + * Meaning, the root element (usually the gui background image) will be centered on the screen, and will have the specified width and height. + *

+ * + * @param guiWidth Gui Width + * @param guiHeight Gui Height + * @see #initFullscreenGui() + */ + public ModularGui initStandardGui(int guiWidth, int guiHeight) { + root.constrain(GeoParam.WIDTH, Constraint.literal(guiWidth)); + root.constrain(GeoParam.HEIGHT, Constraint.literal(guiHeight)); + root.constrain(GeoParam.LEFT, Constraint.midPoint(get(GeoParam.LEFT), get(GeoParam.RIGHT), guiWidth / -2D)); + root.constrain(GeoParam.TOP, Constraint.midPoint(get(GeoParam.TOP), get(GeoParam.BOTTOM), guiHeight / -2D)); + return this; + } + + /** + * Sets up this gui to render as a full screen gui. + * Meaning the root element's geometry will match that of the underlying minecraft screen. + *

+ * + * @see #initStandardGui(int, int) + */ + public ModularGui initFullscreenGui() { + root.constrain(GeoParam.WIDTH, Constraint.match(get(GeoParam.WIDTH))); + root.constrain(GeoParam.HEIGHT, Constraint.match(get(GeoParam.HEIGHT))); + root.constrain(GeoParam.TOP, Constraint.match(get(GeoParam.TOP))); + root.constrain(GeoParam.LEFT, Constraint.match(get(GeoParam.LEFT))); + return this; + } + + /** + * By default, modular gui completely overrides vanillas default slot rendering. + * This ensures slots render within the depth constraint of the slot element and avoids situations where + * stacks in slots render on top of other parts of the gui. + * Meaning you can do things like hide slots by disabling the slot element, Or render elements on top of the slots. + *

+ * This method allow you to return full rendering control to vanilla if you need to for whatever reason. + */ + public void setVanillaSlotRendering(boolean vanillaSlotRendering) { + this.vanillaSlotRendering = vanillaSlotRendering; + } + + public boolean vanillaSlotRendering() { + return vanillaSlotRendering; + } + + //=== Modular Gui Passthrough Methods ===// + + /** + * Create a new {@link GuiRender} for the current render call. + * + * @param buffers BufferSource can be retried from {@link GuiGraphics} + * @return A new {@link GuiRender} for the current render call. + */ + public GuiRender createRender(MultiBufferSource.BufferSource buffers) { + return new GuiRender(mc, buffers); + } + + /** + * Primary render method for ModularGui. The screen implementing ModularGui must call this in its render method. + * Followed by the {@link #renderOverlay(GuiRender, float)} method to handle overlay rendering. + * + * @param render A new gui render call should be constructed for each frame via {@link #createRender(MultiBufferSource.BufferSource)} + */ + public void render(GuiRender render, float partialTicks) { + root.clearGeometryCache(); + double mouseX = computeMouseX(); + double mouseY = computeMouseY(); + root.render(render, mouseX, mouseY, partialTicks); + + //Ensure overlay is rendered at a depth of ether 400 or total element depth + 100 (whichever is greater) + double depth = root.getCombinedElementDepth(); + if (depth <= 300) { + render.pose().translate(0, 0, 400 - depth); + } else { + render.pose().translate(0, 0, 100); + } + } + + /** + * Handles gui overlay rendering. This is where things like tool tips are rendered. + * This should be called immediately after {@link #render(GuiRender, float)} + *

+ * The reason this is split out from {@link #render(GuiRender, float)} is to allow + * stack tool tips to override gui overlay rendering in {@link ModularGuiContainer} + * + * @param render This should be the same render instance that was passed to the previous {@link #render(GuiRender, float)} call. + * @return true if an overlay such as a tooltip is currently being drawn. + */ + public boolean renderOverlay(GuiRender render, float partialTicks) { + double mouseX = computeMouseX(); + double mouseY = computeMouseY(); + return root.renderOverlay(render, mouseX, mouseY, partialTicks, false); + } + + /** + * Primary update / tick method. Must be called from the tick method of the implementing screen. + */ + public void tick() { + newCursor = null; + double mouseX = computeMouseX(); + double mouseY = computeMouseY(); + root.updateMouseOver(mouseX, mouseY, false); + tickListeners.forEach(Runnable::run); + root.tick(mouseX, mouseY); + CursorHelper.setCursor(newCursor); + } + + /** + * Pass through for the mouseMoved event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param mouseX new mouse X position + * @param mouseY new mouse Y position + */ + public void mouseMoved(double mouseX, double mouseY) { + root.mouseMoved(mouseX, mouseY); + } + + /** + * Pass through for the mouseClicked event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param button Mouse Button + * @return true if this event has been consumed. + */ + public boolean mouseClicked(double mouseX, double mouseY, int button) { + preClickListeners.forEach(e -> e.accept(mouseX, mouseY, button)); + boolean consumed = root.mouseClicked(mouseX, mouseY, button, false); + if (!consumed) { + postClickListeners.forEach(e -> e.accept(mouseX, mouseY, button)); + } + return consumed; + } + + /** + * Pass through for the mouseReleased event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param button Mouse Button + * @return true if this event has been consumed. + */ + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return root.mouseReleased(mouseX, mouseY, button, false); + } + + /** + * Pass through for the keyPressed event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param key the keyboard key that was pressed. + * @param scancode the system-specific scancode of the key + * @param modifiers bitfield describing which modifier keys were held down. + * @return true if this event has been consumed. + */ + public boolean keyPressed(int key, int scancode, int modifiers) { + preKeyPressListeners.forEach(e -> e.accept(key, scancode, modifiers)); + boolean consumed = root.keyPressed(key, scancode, modifiers, false); + if (!consumed) { + postKeyPressListeners.forEach(e -> e.accept(key, scancode, modifiers)); + } + return consumed; + } + + /** + * Pass through for the keyReleased event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param key the keyboard key that was released. + * @param scancode the system-specific scancode of the key + * @param modifiers bitfield describing which modifier keys were held down. + * @return true if this event has been consumed. + */ + public boolean keyReleased(int key, int scancode, int modifiers) { + return root.keyReleased(key, scancode, modifiers, false); + } + + /** + * Pass through for the charTyped event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param character The character typed. + * @param modifiers bitfield describing which modifier keys were held down. + * @return true if this event has been consumed. + */ + public boolean charTyped(char character, int modifiers) { + return root.charTyped(character, modifiers, false); + } + + /** + * Pass through for the mouseScrolled event. Any screen implementing {@link ModularGui} must pass through this event. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param scroll Scroll direction and amount + * @return true if this event has been consumed. + */ + public boolean mouseScrolled(double mouseX, double mouseY, double scroll) { + return root.mouseScrolled(mouseX, mouseY, scroll, false); + } + + /** + * Must be called by the screen when this gui is closed. + */ + public void onGuiClose() { + CursorHelper.resetCursor(); + closeListeners.forEach(Runnable::run); + } + + //=== Basic Minecraft Stuff ===// + + protected void updateScreenData(Minecraft mc, Font font, int screenWidth, int screenHeight) { + this.mc = mc; + this.font = font; + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + } + + @Override + public void onScreenInit(Minecraft mc, Font font, int screenWidth, int screenHeight) { + updateScreenData(mc, font, screenWidth, screenHeight); + root.clearGeometryCache(); + try { + root.onScreenInit(mc, font, screenWidth, screenHeight); + if (!guiBuilt) { + guiBuilt = true; + provider.buildGui(this); + } else { + resizeListeners.forEach(Runnable::run); + } + } catch (Throwable ex) { + //Because it seems the default behavior is to just silently consume init errors... Not helpful! + LOGGER.error("An error occurred while building a modular gui", ex); + throw ex; + } + } + + @Override + public Minecraft mc() { + return mc; + } + + @Override + public Font font() { + return font; + } + + @Override + public int scaledScreenWidth() { + return screenWidth; + } + + @Override + public int scaledScreenHeight() { + return screenHeight; + } + + @Override + public ModularGui getModularGui() { + return this; + } + + /** + * Returns the Screen housing this {@link ModularGui} + * With custom ModularGui implementations this may be null. + */ + public Screen getScreen() { + return screen; + } + + @Nullable + public Screen getParentScreen() { + return parentScreen; + } + + //=== Child Elements ===// + + @Override + public List> getChildren() { + throw new UnsupportedOperationException("Child elements must be managed via the root gui element not the modular gui itself."); + } + + @Override + public void addChild(GuiElement child) { + if (root == null) { //If child is null, we are adding the root element. + child.initElement(this); + return; + } + throw new UnsupportedOperationException("Child elements must be managed via the root gui element not the modular gui itself."); + } + + @Override + public ModularGui addChild(Consumer createChild) { + throw new UnsupportedOperationException("Child elements must be managed via the root gui element not the modular gui itself."); + } + + @Override + public void adoptChild(GuiElement child) { + throw new UnsupportedOperationException("Child elements must be managed via the root gui element not the modular gui itself."); + } + + @Override + public void removeChild(GuiElement child) { + throw new UnsupportedOperationException("Child elements must be managed via the root gui element not the modular gui itself."); + } + + //=== Geometry ===// + //The geometry of the base ModularGui class should always match the underlying minecraft screen. + + @Override + public double xMin() { + return 0; + } + + @Override + public double xMax() { + return screenWidth; + } + + @Override + public double xSize() { + return screenWidth; + } + + @Override + public double yMin() { + return 0; + } + + @Override + public double yMax() { + return screenHeight; + } + + @Override + public double ySize() { + return screenHeight; + } + + //=== Other ===// + + public double computeMouseX() { + return mc.mouseHandler.xpos() * (double) mc.getWindow().getGuiScaledWidth() / (double) mc.getWindow().getScreenWidth(); + } + + public double computeMouseY() { + return mc.mouseHandler.ypos() * (double) mc.getWindow().getGuiScaledHeight() / (double) mc.getWindow().getScreenHeight(); + } + + /** + * Provides a way to later retrieve the gui element responsible for positioning and rendering a slot. + */ + public void setSlotHandler(Slot slot, GuiElement handler) { + if (slotHandlers.containsKey(slot)) throw new IllegalStateException("A gui slot can only have a single handler!"); + slotHandlers.put(slot, handler); + } + + /** + * Returns the gui element responsible for managing a gui slot. + */ + public GuiElement getSlotHandler(Slot slot) { + return slotHandlers.get(slot); + } + + /** + * Sets the current mouse cursor. + * The cursor is reset at the end of each UI tick so this must be set every tick for as long as you want your custom cursor to be active. + * */ + public void setCursor(ResourceLocation cursor) { + this.newCursor = cursor; + } + + /** + * Add an element to the list of jei exclusions. + * Use this for any elements that render outside the normal gui bounds. + * This will ensure JEI does not try to render on top of these elements. + */ + public void jeiExclude(GuiElement element) { + if (!jeiExclusions.contains(element)) { + jeiExclusions.add(element); + } + } + + /** + * Remove an element from the list of jei exclusions. + */ + public void removeJEIExclude(GuiElement element) { + jeiExclusions.remove(element); + } + + public FastStream> getJeiExclusions() { + return FastStream.of(jeiExclusions).filter(GuiElement::isEnabled); + } + + /** + * Allows you to attach a callback that will be fired at the start of each gui tick. + */ + public void onTick(Runnable onTick) { + tickListeners.add(onTick); + } + + /** + * Allows you to attach a callback that will be fired when the parent screen is resized. + */ + public void onResize(Runnable onResize) { + resizeListeners.add(onResize); + } + + public void onClose(Runnable onClose) { + closeListeners.add(onClose); + } + + /** + * Allows you to attach a callback that will be fired on mouse click, before the click is handled by the rest of the gui. + */ + public void onMouseClickPre(TriConsumer onClick) { + preClickListeners.add(onClick); + } + + /** + * Allows you to attach a callback that will be fired on mouse click, after the click has been handled by the rest of the gui. + * Will only be fired if the event was not consumed by an element. + */ + public void onMouseClickPost(TriConsumer onClick) { + postClickListeners.add(onClick); + } + + + /** + * Allows you to attach a callback that will be fired on key press, before the is handled by the rest of the gui. + */ + public void onKeyPressPre(TriConsumer preKeyPress) { + preKeyPressListeners.add(preKeyPress); + } + + /** + * Allows you to attach a callback that will be fired on key press, after it has been handled by the rest of the gui. + * Will only be fired if the event was not consumed by an element. + */ + public void onKeyPressPost(TriConsumer postKeyPress) { + postKeyPressListeners.add(postKeyPress); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/ModularGuiContainer.java b/src/main/java/codechicken/lib/gui/modular/ModularGuiContainer.java new file mode 100644 index 00000000..0063b6a6 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/ModularGuiContainer.java @@ -0,0 +1,280 @@ +package codechicken.lib.gui.modular; + +import codechicken.lib.gui.modular.elements.GuiElement; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.container.ContainerGuiProvider; +import codechicken.lib.gui.modular.lib.container.ContainerScreenAccess; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Container screen implementation for {@link ModularGui}. + * + *

+ * Created by brandon3055 on 08/09/2023 + */ +public class ModularGuiContainer extends AbstractContainerScreen implements ContainerScreenAccess { + + public final ModularGui modularGui; + + public ModularGuiContainer(T containerMenu, Inventory inventory, ContainerGuiProvider provider) { + super(containerMenu, inventory, Component.empty()); + provider.setMenuAccess(this); + this.modularGui = new ModularGui(provider); + this.modularGui.setScreen(this); + } + + public ModularGui getModularGui() { + return modularGui; + } + + @NotNull + @Override + public Component getTitle() { + return modularGui.getGuiTitle(); + } + + @Override + public boolean shouldCloseOnEsc() { + return modularGui.closeOnEscape(); + } + + @Override + protected void init() { + modularGui.onScreenInit(minecraft, font, width, height); + } + + @Override + public void resize(@NotNull Minecraft minecraft, int width, int height) { + super.resize(minecraft, width, height); + modularGui.onScreenInit(minecraft, font, width, height); + } + + @Override + public void render(@NotNull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + GuiElement root = modularGui.getRoot(); + topPos = (int) root.getValue(GeoParam.TOP); + leftPos = (int) root.getValue(GeoParam.LEFT); + imageWidth = (int) root.getValue(GeoParam.WIDTH); + imageHeight = (int) root.getValue(GeoParam.HEIGHT); + + if (modularGui.renderBackground()) { + renderBackground(graphics); + } + GuiRender render = modularGui.createRender(graphics.bufferSource()); + modularGui.render(render, partialTicks); + + super.render(graphics, mouseX, mouseY, partialTicks); + + if (!handleFloatingItemRender(render, mouseX, mouseY) && !renderHoveredStackToolTip(render, mouseX, mouseY)) { + modularGui.renderOverlay(render, partialTicks); + } + } + + protected boolean handleFloatingItemRender(GuiRender render, int mouseX, int mouseY) { + if (modularGui.vanillaSlotRendering()) return false; + boolean ret = false; + + ItemStack stack = draggingItem.isEmpty() ? menu.getCarried() : draggingItem; + if (!stack.isEmpty()) { + int yOffset = draggingItem.isEmpty() ? 8 : 16; + String countOverride = null; + if (!draggingItem.isEmpty() && isSplittingStack) { + stack = stack.copyWithCount(Mth.ceil((float) stack.getCount() / 2.0F)); + } else if (isQuickCrafting && quickCraftSlots.size() > 1) { + stack = stack.copyWithCount(this.quickCraftingRemainder); + if (stack.isEmpty()) { + countOverride = ChatFormatting.YELLOW + "0"; + } + } + renderFloatingItem(render, stack, mouseX - 8, mouseY - yOffset, countOverride); + ret = true; + } + + if (!this.snapbackItem.isEmpty()) { + float anim = (float) (Util.getMillis() - this.snapbackTime) / 100.0F; + if (anim >= 1.0F) { + anim = 1.0F; + this.snapbackItem = ItemStack.EMPTY; + } + + int xDist = snapbackEnd.x - snapbackStartX; + int yDist = snapbackEnd.y - snapbackStartY; + int xPos = snapbackStartX + (int) ((float) xDist * anim); + int yPos = snapbackStartY + (int) ((float) yDist * anim); + renderFloatingItem(render, snapbackItem, xPos + leftPos, yPos + topPos, null); + ret = true; + } + + return ret; + } + + protected boolean renderHoveredStackToolTip(GuiRender guiGraphics, int mouseX, int mouseY) { + if (this.menu.getCarried().isEmpty() && this.hoveredSlot != null && this.hoveredSlot.hasItem()) { + GuiElement handler = modularGui.getSlotHandler(hoveredSlot); + if (handler != null && handler.blockMouseOver(handler, mouseX, mouseY)) { + return false; + } + ItemStack itemStack = this.hoveredSlot.getItem(); + guiGraphics.toolTipWithImage(this.getTooltipFromContainerItem(itemStack), itemStack.getTooltipImage(), mouseX, mouseY); + return true; + } + return false; + } + + @Override + protected void containerTick() { + modularGui.tick(); + } + + @Override + public void removed() { + super.removed(); + modularGui.onGuiClose(); + } + + //=== Input Pass-though ===// + + @Override + public void mouseMoved(double mouseX, double mouseY) { + modularGui.mouseMoved(mouseX, mouseY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return modularGui.mouseClicked(mouseX, mouseY, button) || super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return modularGui.mouseReleased(mouseX, mouseY, button) || super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scroll) { + return modularGui.mouseScrolled(mouseX, mouseY, scroll) || super.mouseScrolled(mouseX, mouseY, scroll); + } + + @Override + public boolean keyPressed(int key, int scancode, int modifiers) { + return modularGui.keyPressed(key, scancode, modifiers) || super.keyPressed(key, scancode, modifiers); + } + + @Override + public boolean keyReleased(int key, int scancode, int modifiers) { + return modularGui.keyReleased(key, scancode, modifiers) || super.keyReleased(key, scancode, modifiers); + } + + @Override + public boolean charTyped(char character, int modifiers) { + return modularGui.charTyped(character, modifiers) || super.charTyped(character, modifiers); + } + + //=== AbstractContainerMenu Overrides ===// + + @Override + protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { + } + + @Override + public void renderSlot(GuiGraphics guiGraphics, Slot slot) { + if (modularGui.vanillaSlotRendering()) super.renderSlot(guiGraphics, slot); + } + + //Modular gui friendly version of the slot render + @Override + public void renderSlot(GuiRender render, Slot slot) { + if (modularGui.vanillaSlotRendering()) return; + int slotX = slot.x + leftPos; + int slotY = slot.y + topPos; + ItemStack slotStack = slot.getItem(); + boolean dragingToSlot = false; + boolean dontRenderItem = slot == this.clickedSlot && !this.draggingItem.isEmpty() && !this.isSplittingStack; + + ItemStack carriedStack = this.menu.getCarried(); + String countString = null; + if (slot == this.clickedSlot && !this.draggingItem.isEmpty() && this.isSplittingStack && !slotStack.isEmpty()) { + slotStack = slotStack.copyWithCount(slotStack.getCount() / 2); + } else if (this.isQuickCrafting && this.quickCraftSlots.contains(slot) && !carriedStack.isEmpty()) { + if (this.quickCraftSlots.size() == 1) { + return; + } + + if (AbstractContainerMenu.canItemQuickReplace(slot, carriedStack, true) && this.menu.canDragTo(slot)) { + dragingToSlot = true; + int k = Math.min(carriedStack.getMaxStackSize(), slot.getMaxStackSize(carriedStack)); + int l = slot.getItem().isEmpty() ? 0 : slot.getItem().getCount(); + int m = AbstractContainerMenu.getQuickCraftPlaceCount(this.quickCraftSlots, this.quickCraftingType, carriedStack) + l; + if (m > k) { + m = k; + countString = ChatFormatting.YELLOW.toString() + k; + } + + slotStack = carriedStack.copyWithCount(m); + } else { + this.quickCraftSlots.remove(slot); + this.recalculateQuickCraftRemaining(); + } + } + + if (!dontRenderItem) { + if (dragingToSlot) { + //Highlights slots when doing a drag place operation. + render.fill(slotX, slotY, slotX + 16, slotY + 16, 0x80ffffff); + } + render.renderItem(slotStack, slotX, slotY, 16, slot.x + (slot.y * this.imageWidth)); //TODO May want a random that does not change if the slot is moved. + render.renderItemDecorations(slotStack, slotX, slotY, countString); + } + } + + @Override //Disable vanilla title and inventory name rendering + protected void renderLabels(GuiGraphics guiGraphics, int i, int j) { + } + + @Override + public void renderFloatingItem(GuiGraphics guiGraphics, ItemStack itemStack, int i, int j, String string) { + if (modularGui.vanillaSlotRendering()) super.renderFloatingItem(guiGraphics, itemStack, i, j, string); + } + + public void renderFloatingItem(GuiRender render, ItemStack itemStack, int x, int y, String string) { + render.pose().pushPose(); + render.pose().translate(0.0F, 0.0F, 50F); + render.renderItem(itemStack, x, y); + render.renderItemDecorations(itemStack, x, y - (this.draggingItem.isEmpty() ? 0 : 8), string); + render.pose().popPose(); + } + + @Nullable + @Override + public Slot findSlot(double mouseX, double mouseY) { + Slot slot = super.findSlot(mouseX, mouseY); + if (slot == null) return null; + GuiElement handler = modularGui.getSlotHandler(slot); + if (handler != null && (!handler.isEnabled() || !handler.isMouseOver())) { + return null; + } + return slot; + } + + @Override + protected void slotClicked(Slot slot, int i, int j, ClickType clickType) { + if (slot != null) { + GuiElement handler = modularGui.getSlotHandler(slot); + if (handler != null && !handler.isEnabled()) return; + } + super.slotClicked(slot, i, j, clickType); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/ModularGuiScreen.java b/src/main/java/codechicken/lib/gui/modular/ModularGuiScreen.java new file mode 100644 index 00000000..10fd1060 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/ModularGuiScreen.java @@ -0,0 +1,121 @@ +package codechicken.lib.gui.modular; + +import codechicken.lib.gui.modular.lib.GuiProvider; +import codechicken.lib.gui.modular.lib.GuiRender; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +/** + * A simple ModularGui screen implementation. + * This is simply a wrapper for a {@link ModularGui} that takes a {@link GuiProvider} + * This should be suitable for most basic gui screens. + *

+ * Created by brandon3055 on 19/08/2023 + */ +public class ModularGuiScreen extends Screen { + + protected final ModularGui modularGui; + + public ModularGuiScreen(GuiProvider provider) { + super(Component.empty()); + this.modularGui = new ModularGui(provider); + this.modularGui.setScreen(this); + } + + public ModularGuiScreen(GuiProvider builder, Screen parentScreen) { + super(Component.empty()); + this.modularGui = new ModularGui(builder, parentScreen); + this.modularGui.setScreen(this); + } + + public ModularGui getModularGui() { + return modularGui; + } + + @NotNull + @Override + public Component getTitle() { + return modularGui.getGuiTitle(); + } + + @Override + public boolean isPauseScreen() { + return modularGui.isPauseScreen(); + } + + @Override + public boolean shouldCloseOnEsc() { + return modularGui.closeOnEscape(); + } + + @Override + protected void init() { + modularGui.onScreenInit(minecraft, font, width, height); + } + + @Override + public void resize(@NotNull Minecraft minecraft, int width, int height) { + super.resize(minecraft, width, height); + modularGui.onScreenInit(minecraft, font, width, height); + } + + @Override + public void render(@NotNull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + if (modularGui.renderBackground()) { + renderBackground(graphics); + } + GuiRender render = modularGui.createRender(graphics.bufferSource()); + modularGui.render(render, partialTicks); + modularGui.renderOverlay(render, partialTicks); + } + + @Override + public void tick() { + modularGui.tick(); + } + + @Override + public void removed() { + modularGui.onGuiClose(); + } + + //=== Input Pass-though ===// + + @Override + public void mouseMoved(double mouseX, double mouseY) { + modularGui.mouseMoved(mouseX, mouseY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return modularGui.mouseClicked(mouseX, mouseY, button) || super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return modularGui.mouseReleased(mouseX, mouseY, button) || super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scroll) { + return modularGui.mouseScrolled(mouseX, mouseY, scroll) || super.mouseScrolled(mouseX, mouseY, scroll); + } + + @Override + public boolean keyPressed(int key, int scancode, int modifiers) { + return modularGui.keyPressed(key, scancode, modifiers) || super.keyPressed(key, scancode, modifiers); + } + + @Override + public boolean keyReleased(int key, int scancode, int modifiers) { + return modularGui.keyReleased(key, scancode, modifiers) || super.keyReleased(key, scancode, modifiers); + } + + @Override + public boolean charTyped(char character, int modifiers) { + return modularGui.charTyped(character, modifiers) || super.charTyped(character, modifiers); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiButton.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiButton.java new file mode 100644 index 00000000..b8df4d2b --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiButton.java @@ -0,0 +1,335 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.Constraints; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.sprite.CCGuiTextures; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.core.Holder; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * Created by brandon3055 on 28/08/2023 + */ +public class GuiButton extends GuiElement { + public static final int LEFT_CLICK = 0; + public static final int RIGHT_CLICK = 1; + public static final int MIDDLE_CLICK = 2; + + private final Map onClick = new HashMap<>(); + private final Map onPress = new HashMap<>(); + private boolean pressed = false; + private Holder pressSound = SoundEvents.UI_BUTTON_CLICK; + private Holder releaseSound = null; + private Supplier disabled = () -> false; + private Supplier toggleState; + private GuiText label = null; + + /** + * In its default state this is a blank, invisible element that can fire callbacks when pressed. + * To make an actual usable button, ether use one of the builtin static create methods, + * Or add your own elements to make this button look and function in a way that meets your needs. + * + * @param parent parent {@link GuiParent}. + */ + public GuiButton(@NotNull GuiParent parent) { + super(parent); + } + + /** + * Creates a new gui button that looks and acts exactly like a standard vanilla button. + */ + public static GuiButton vanilla(@NotNull GuiParent parent, @Nullable Component label, Runnable onClick) { + return vanilla(parent, label).onClick(onClick); + } + + /** + * Creates a new gui button that looks and acts exactly like a standard vanilla button. + */ + public static GuiButton vanilla(@NotNull GuiParent parent, @Nullable Component label) { + GuiButton button = new GuiButton(parent); + GuiTexture texture = new GuiTexture(button, CCGuiTextures.getter(() -> button.toggleState() ? "dynamic/button_highlight" : "dynamic/button_vanilla")); + texture.dynamicTexture(); + GuiRectangle highlight = new GuiRectangle(button).border(() -> button.hoverTime() > 0 ? 0xFFFFFFFF : 0); + + Constraints.bind(texture, button); + Constraints.bind(highlight, button); + + if (label != null) { + button.setLabel(new GuiText(button, label)); + Constraints.bind(button.getLabel(), button, 0, 2, 0, 2); + } + + return button; + } + + /** + * Creates a vanilla button with a "press" animation. + */ + public static GuiButton vanillaAnimated(@NotNull GuiParent parent, Component label, Runnable onPress) { + return vanillaAnimated(parent, label == null ? null : () -> label, onPress); + } + + /** + * Creates a vanilla button with a "press" animation. + */ + public static GuiButton vanillaAnimated(@NotNull GuiParent parent, @Nullable Supplier label, Runnable onPress) { + return vanillaAnimated(parent, label).onPress(onPress); + } + + //TODO Could use a quad-sliced texture for this. + + /** + * Creates a vanilla button with a "press" animation. + */ + public static GuiButton vanillaAnimated(@NotNull GuiParent parent, Component label) { + return vanillaAnimated(parent, label == null ? null : () -> label); + } + + /** + * Creates a vanilla button with a "press" animation. + */ + public static GuiButton vanillaAnimated(@NotNull GuiParent parent, @Nullable Supplier label) { + GuiButton button = new GuiButton(parent); + GuiTexture texture = new GuiTexture(button, CCGuiTextures.getter(() -> button.toggleState() || button.isPressed() ? "dynamic/button_pressed" : "dynamic/button_vanilla")); + texture.dynamicTexture(); + GuiRectangle highlight = new GuiRectangle(button).border(() -> button.isMouseOver() ? 0xFFFFFFFF : 0); + + Constraints.bind(texture, button); + Constraints.bind(highlight, button); + + if (label != null) { + button.setLabel(new GuiText(button, label) + .constrain(TOP, Constraint.relative(button.get(TOP), () -> button.isPressed() ? -0.5D : 0.5D).precise()) + .constrain(LEFT, Constraint.relative(button.get(LEFT), () -> button.isPressed() ? 1.5D : 2.5D).precise()) + .constrain(WIDTH, Constraint.relative(button.get(WIDTH), -4)) + .constrain(HEIGHT, Constraint.match(button.get(HEIGHT))) + ); + } + + return button; + } + + /** + * Super simple button that is just a coloured rectangle with a label. + */ + public static GuiButton flatColourButton(@NotNull GuiParent parent, @Nullable Supplier label, Function buttonColour) { + return flatColourButton(parent, label, buttonColour, null); + } + + /** + * Super simple button that is just a coloured rectangle with a label. + */ + public static GuiButton flatColourButton(@NotNull GuiParent parent, @Nullable Supplier label, Function buttonColour, @Nullable Function borderColour) { + GuiButton button = new GuiButton(parent); + GuiRectangle background = new GuiRectangle(button) + .fill(() -> buttonColour.apply(button.isMouseOver() || button.toggleState() || button.isPressed())) + .border(borderColour == null ? null : () -> borderColour.apply(button.isMouseOver() || button.toggleState() || button.isPressed())); + Constraints.bind(background, button); + + if (label != null) { + GuiText text = new GuiText(button, label); + button.setLabel(text); + Constraints.bind(text, button, 0, 2, 0, 2); + } + + return button; + } + + /** + * When creating buttons with labels, use this method to store a reference to the label in the button fore easy retrival later. + * + * @param label The button label. + */ + public GuiButton setLabel(GuiText label) { + this.label = label; + return this; + } + + /** + * @return The buttons label element, If it has one. + */ + public GuiText getLabel() { + return label; + } + + /** + * This event is fired immediately when this button is left-clicked. + * This is the logic used by most vanilla gui buttons. + * + * @see #onPress(Runnable) + */ + public GuiButton onClick(Runnable onClick) { + return onClick(onClick, LEFT_CLICK); + } + + /** + * This event is fired immediately when this button is clicked with the specified mouse button. + * This is the logic used by most vanilla gui buttons. + * Note: You can apply one listener per mouse button. + * + * @see #onPress(Runnable, int) + */ + public GuiButton onClick(Runnable onClick, int mouseButton) { + this.onClick.put(mouseButton, onClick); + return this; + } + + /** + * This event is fired when the button is pressed and then released using the left mosue button. + * The event is only fired if the cursor is still over the button when left click is released. + * This is the standard logic for most buttons in the world, but not vanillas. + * Note: You can apply one listener per mouse button. + * + * @see #onPress(Runnable, int) + */ + public GuiButton onPress(Runnable onPress) { + return onPress(onPress, LEFT_CLICK); + } + + /** + * This event is fired when the button is pressed and then released using the specified mouse button. + * The event is only fired if the cursor is still over the button when left click is released. + * This is the standard logic for most buttons in the world, but not vanillas. + * + * @see #onPress(Runnable, int) + */ + public GuiButton onPress(Runnable onPress, int mouseButton) { + this.onPress.put(mouseButton, onPress); + return this; + } + + /** + * Allows set the disabled status of this button + * Note: This is not the same as {@link #setEnabled(boolean)} the "enabled" allows you to completely disable an element. + * This "disabled" status is specific to {@link GuiButton}, + * When disabled via this method a button is still visible but greyed out / not clickable. + */ + public GuiButton setDisabled(boolean disabled) { + this.disabled = () -> disabled; + return this; + } + + /** + * Allows you to install a suppler that controls the disabled state of this button. + * Note: This is not the same as {@link #setEnabled(Supplier)} the "enabled" allows you to completely disable an element. + * This "disabled" status is specific to {@link GuiButton}, + * When disabled via this method a button is still visible but greyed out / not clickable. + */ + public GuiButton setDisabled(Supplier disabled) { + this.disabled = disabled; + return this; + } + + /** + * Allows this button to be used as a toggle or radio button. + * This method allows you to install a suppler that controls the current "selected / toggled" state. + * + * @param toggleState supplier that indicates weather or not this button should currently render as pressed/selected. + */ + public GuiButton setToggleMode(@Nullable Supplier toggleState) { + this.toggleState = toggleState; + return this; + } + + /** + * @return the "disabled" status. + * @see #setDisabled(boolean) + * @see #setDisabled(Supplier) + */ + public boolean isDisabled() { + return disabled.get(); + } + + /** + * @return true if this button is currently pressed by the user (left click held down on button) + */ + public boolean isPressed() { + return pressed && hoverTime() > 0; + } + + public boolean toggleState() { + return toggleState != null && toggleState.get(); + } + + /** + * Sets the sound to be played when this button is pressed. + */ + public GuiButton setPressSound(Holder pressSound) { + this.pressSound = pressSound; + return this; + } + + /** + * Sets the sound to be played when this button is released. + */ + public GuiButton setReleaseSound(Holder releaseSound) { + this.releaseSound = releaseSound; + return this; + } + + public Holder getPressSound() { + return pressSound; + } + + public Holder getReleaseSound() { + return releaseSound; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver() || isDisabled()) return false; + Runnable onClick = this.onClick.get(button); + Runnable onPress = this.onPress.get(button); + if (onClick == null && onPress == null) return false; + pressed = true; + hoverTime = 1; + + boolean consume = false; + if (onClick != null) { + onClick.run(); + consume = true; + } + if (onPress != null) { + consume = true; + } + + if (getPressSound() != null) { + mc().getSoundManager().play(SimpleSoundInstance.forUI(getPressSound(), 1F)); + } + return consume; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button, boolean consumed) { + consumed = super.mouseReleased(mouseX, mouseY, button, consumed); + if (!pressed) return consumed; + Runnable onClick = this.onClick.get(button); + Runnable onPress = this.onPress.get(button); + if (onClick == null && onPress == null) return consumed; + hoverTime = 1; + + if (!isDisabled() && isMouseOver()) { + if (pressed && onPress != null) { + onPress.run(); + consumed = true; + } + if (getReleaseSound() != null && (toggleState == null || !toggleState.get())) { + mc().getSoundManager().play(SimpleSoundInstance.forUI(getReleaseSound(), 1F)); + } + } + pressed = false; + return consumed; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiColourPicker.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiColourPicker.java new file mode 100644 index 00000000..c576a7e8 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiColourPicker.java @@ -0,0 +1,232 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.colour.Colour; +import codechicken.lib.gui.modular.lib.*; +import codechicken.lib.gui.modular.lib.geometry.Axis; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.*; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * Created by brandon3055 on 17/11/2023 + */ +public class GuiColourPicker extends GuiManipulable { + + private ColourState colourState = ColourState.create(); + private GuiButton okButton; + private GuiButton cancelButton; + + public GuiColourPicker(@NotNull GuiParent parent) { + super(parent); + } + + public static GuiColourPicker create(GuiParent guiParent, ColourState colourState) { + return create(guiParent, colourState, true); + } + + public static GuiColourPicker create(GuiParent guiParent, ColourState colourState, boolean hasAlpha) { + Colour initialColour = colourState.getColour(); + GuiColourPicker picker = new GuiColourPicker(guiParent.getModularGui().getRoot()); + picker.setOpaque(true); + picker.setColourState(colourState); + Constraints.size(picker, 80, hasAlpha ? 80 : 68); + + GuiRectangle background = GuiRectangle.toolTipBackground(picker.getContentElement()); + Constraints.bind(background, picker.getContentElement()); + + var hexField = GuiTextField.create(background, 0xFF000000, 0xFF505050, 0xe0e0e0); + hexField.field() + .setTextState(picker.getTextState()) + .setMaxLength(hasAlpha ? 8 : 6) + .setFilter(s -> s.isEmpty() || validHex(s)); + hexField.container() + .setOpaque(true) + .constrain(HEIGHT, literal(12)) + .constrain(TOP, relative(background.get(TOP), 4)) + .constrain(LEFT, relative(background.get(LEFT), 4)) + .constrain(RIGHT, relative(background.get(RIGHT), -4)); + + SliderBG slider = makeSlider(background, 0xFFFF0000, picker.sliderStateRed()) + .constrain(TOP, relative(hexField.container().get(BOTTOM), 2)); + + slider = makeSlider(background, 0xFF00FF00, picker.sliderStateGreen()) + .constrain(TOP, relative(slider.get(BOTTOM), 1)); + + slider = makeSlider(background, 0xFF0000FF, picker.sliderStateBlue()) + .constrain(TOP, relative(slider.get(BOTTOM), 1)); + + if (hasAlpha) { + slider = makeSlider(background, 0xFFFFFFFF, picker.sliderStateAlpha()) + .constrain(TOP, relative(slider.get(BOTTOM), 1)); + } else { + colourState.set(colourState.getColour().aF(0)); + } + + ColourPreview preview = new ColourPreview(background, () -> hasAlpha ? colourState.get() : (colourState.get() | 0xFF000000)) + .setOpaque(true) + .constrain(HEIGHT, literal(6)) + .constrain(TOP, relative(slider.get(BOTTOM), 2)) + .constrain(LEFT, relative(background.get(LEFT), 4)) + .constrain(RIGHT, relative(background.get(RIGHT), -4)); + + picker.cancelButton = GuiButton.flatColourButton(background, () -> Component.translatable("gui.cancel"), e -> 0xFF000000, e -> e ? 0xFF777777 : 0xFF555555) + .setOpaque(true) + .onPress(() -> { + colourState.set(initialColour); + picker.getParent().removeChild(picker); + }) + .constrain(HEIGHT, literal(10)) + .constrain(TOP, relative(preview.get(BOTTOM), 2)) + .constrain(LEFT, midPoint(background.get(LEFT), background.get(RIGHT), -4)) + .constrain(RIGHT, relative(background.get(RIGHT), -4)); + + picker.okButton = GuiButton.flatColourButton(background, () -> Component.translatable("gui.ok"), e -> 0xFF000000, e -> e ? 0xFF777777 : 0xFF555555) + .setOpaque(true) + .onPress(() -> picker.getParent().removeChild(picker)) + .constrain(HEIGHT, literal(10)) + .constrain(TOP, relative(preview.get(BOTTOM), 2)) + .constrain(LEFT, relative(background.get(LEFT), 4)) + .constrain(RIGHT, dynamic(() -> picker.cancelButton.isEnabled() ? picker.cancelButton.xMin() - 2 : background.xMax() - 4)); + + return picker; + } + + public static SliderBG makeSlider(GuiElement background, int colour, SliderState state) { + SliderBG slideBG = new SliderBG(background, 0xFF505050, 0x30FFFFFF) + .setOpaque(true) + .constrain(HEIGHT, literal(9)) + .constrain(LEFT, relative(background.get(LEFT), 4)) + .constrain(RIGHT, relative(background.get(RIGHT), -4)); + + GuiSlider slider = new GuiSlider(slideBG, Axis.X) + .setSliderState(state); + Constraints.bind(slider, slideBG, 0, 1, 0, 1); + slideBG.slider = slider; + + GuiRectangle handle = new GuiRectangle(slider) + .fill(colour) + .border(0xFF000000) + .borderWidth(0.5) + .constrain(WIDTH, literal(4)); + + slider.installSlider(handle) + .bindSliderWidth(); + + return slideBG; + } + + private static boolean validHex(String value) { + try { + Integer.parseUnsignedInt(value, 16); + return true; + } catch (Throwable ignored) { + return false; + } + } + + public static class SliderBG extends GuiElement implements BackgroundRender { + public int colour; + public int highlight; + public GuiSlider slider; + public boolean pressed = false; + + public SliderBG(@NotNull GuiParent parent, int colour, int highlight) { + super(parent); + this.colour = colour; + this.highlight = highlight; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button, boolean consumed) { + pressed = button == 0; + return super.mouseClicked(mouseX, mouseY, button, consumed); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button, boolean consumed) { + if (button == 0) pressed = false; + return super.mouseReleased(mouseX, mouseY, button, consumed); + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + render.fill(xMin(), yMin(), xMin() + 1, yMax(), colour); + render.fill(xMax() - 1, yMin(), xMax(), yMax(), colour); + render.fill(xMin() + 1, yCenter() - 0.5, xMax() - 1, yCenter() + 0.5, colour); + + if ((isMouseOver() && !pressed) || slider.isDragging()) { + render.rect(getRectangle(), highlight); + } + } + } + + public static class ColourPreview extends GuiElement implements BackgroundRender { + private final Supplier colour; + public int colourA = 0xFF999999; + public int colourB = 0xFF666666; + + public ColourPreview(@NotNull GuiParent parent, Supplier colour) { + super(parent); + this.colour = colour; + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + render.pushScissorRect(xMin(), yMin(), xSize(), ySize()); + for (int x = 0; xMin() + (x * 2) < xMax(); x++) { + for (int y = 0; yMin() + (y * 2) < yMax(); y++) { + int col = (y & 1) == 0 ? ((x & 1) == 0 ? colourA : colourB) : ((x & 1) == 0 ? colourB : colourA); + render.rect(xMin() + (x * 2), yMin() + (y * 2), 2, 2, col); + } + } + render.popScissor(); + render.rect(getRectangle(), colour.get()); + } + } + + public GuiColourPicker setColourState(ColourState colourState) { + this.colourState = colourState; + return this; + } + + public ColourState getState() { + return colourState; + } + + public SliderState sliderStateAlpha() { + return SliderState.forSlider(() -> (double) colourState.getColour().aF(), e -> colourState.set(colourState.getColour().aF(e.floatValue())), () -> -1D / (Screen.hasShiftDown() ? 16 : 64)); + } + + public SliderState sliderStateRed() { + return SliderState.forSlider(() -> (double) colourState.getColour().rF(), e -> colourState.set(colourState.getColour().rF(e.floatValue())), () -> -1D / (Screen.hasShiftDown() ? 16 : 64)); + } + + public SliderState sliderStateGreen() { + return SliderState.forSlider(() -> (double) colourState.getColour().gF(), e -> colourState.set(colourState.getColour().gF(e.floatValue())), () -> -1D / (Screen.hasShiftDown() ? 16 : 64)); + } + + public SliderState sliderStateBlue() { + return SliderState.forSlider(() -> (double) colourState.getColour().bF(), e -> colourState.set(colourState.getColour().bF(e.floatValue())), () -> -1D / (Screen.hasShiftDown() ? 16 : 64)); + } + + public TextState getTextState() { + return TextState.create(colourState::getHexColour, colourState::setHexColour); + } + + public GuiButton getOkButton() { + return okButton; + } + + /** + * If cancel button is disabled, ok button will automatically resize. + */ + public GuiButton getCancelButton() { + return cancelButton; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiContextMenu.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiContextMenu.java new file mode 100644 index 00000000..77c2b343 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiContextMenu.java @@ -0,0 +1,135 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.ModularGui; +import codechicken.lib.gui.modular.lib.Constraints; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import net.covers1624.quack.collection.FastStream; +import net.minecraft.network.chat.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.*; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * Context menus get added to the root element when they are created so as long as no new elements are added after the menu is opened, + * the menu will always be on top. + * It will also automatically close when an option is selected, or when the user clicks outside the context menu. + *

+ * Created by brandon3055 on 21/11/2023 + */ +public class GuiContextMenu extends GuiElement { + + private BiFunction, GuiButton> buttonBuilder = (menu, label) -> GuiButton.flatColourButton(menu, label, hover -> hover ? 0xFF475b6a : 0xFF151515).constrain(HEIGHT, literal(12)); + private final Map, Runnable> options = new HashMap<>(); + private final Map, Supplier>> tooltips = new HashMap<>(); + private final List buttons = new ArrayList<>(); + private boolean closeOnItemClicked = true; + private boolean closeOnOutsideClick = true; + + public GuiContextMenu(ModularGui gui) { + super(gui.getRoot()); + } + + public static GuiContextMenu tooltipStyleMenu(GuiParent parent) { + GuiContextMenu menu = new GuiContextMenu(parent.getModularGui()); + Constraints.bind(GuiRectangle.toolTipBackground(menu), menu); + return menu; + } + + public GuiContextMenu setCloseOnItemClicked(boolean closeOnItemClicked) { + this.closeOnItemClicked = closeOnItemClicked; + return this; + } + + public GuiContextMenu setCloseOnOutsideClick(boolean closeOnOutsideClick) { + this.closeOnOutsideClick = closeOnOutsideClick; + return this; + } + + /** + * Only height should be constrained, with will be set automatically to accommodate the provided label. + */ + public GuiContextMenu setButtonBuilder(BiFunction, GuiButton> buttonBuilder) { + this.buttonBuilder = buttonBuilder; + return this; + } + + public GuiContextMenu addOption(Supplier label, Runnable action) { + options.put(label, action); + rebuildButtons(); + return this; + } + + public GuiContextMenu addOption(Supplier label, Supplier> tooltip, Runnable action) { + options.put(label, action); + tooltips.put(label, tooltip); + rebuildButtons(); + return this; + } + + public GuiContextMenu addOption(Supplier label, List tooltip, Runnable action) { + return addOption(label, () -> tooltip, action); + } + + public GuiContextMenu addOption(Supplier label, Runnable action, Component... tooltip) { + return addOption(label, () -> List.of(tooltip), action); + } + + private void rebuildButtons() { + buttons.forEach(this::removeChild); + buttons.clear(); + + //Menu options can be dynamic so the width constraint needs to be dynamic. + //This is probably a little expensive, but its only while a context menu is open. + constrain(WIDTH, dynamic(() -> FastStream.of(options.keySet()).map(Supplier::get).intSum(font()::width) + 6D + 4D)); + + double height = 3; + for (Supplier label : options.keySet()) { + Runnable action = options.get(label); + GuiButton button = buttonBuilder.apply(this, label) + .onPress(action) + .constrain(TOP, relative(get(TOP), height)) + .constrain(LEFT, relative(get(LEFT), 3)) + .constrain(RIGHT, relative(get(RIGHT), -3)); + if (tooltips.containsKey(label)) { + button.setTooltip(tooltips.get(label)); + } + button.getLabel().setScroll(false); + buttons.add(button); + height += button.ySize(); + } + constrain(HEIGHT, literal(height + 3)); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button, boolean consumed) { + consumed = super.mouseClicked(mouseX, mouseY, button, consumed); + if (isMouseOver() || consumed) { + if (consumed && closeOnItemClicked) { + close(); + } + return true; + } else if (closeOnOutsideClick) { + close(); + return true; + } + + return consumed; + } + + public void close() { + getParent().removeChild(this); + } + + public GuiContextMenu setNormalizedPos(double x, double y) { + constrain(LEFT, dynamic(() -> Math.min(Math.max(x, 0), scaledScreenWidth() - xSize()))); + constrain(TOP, dynamic(() -> Math.min(Math.max(y, 0), scaledScreenHeight() - ySize()))); + return this; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiDVD.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiDVD.java new file mode 100644 index 00000000..a3195c62 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiDVD.java @@ -0,0 +1,97 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.ContentElement; +import codechicken.lib.math.MathHelper; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.lib.geometry.Rectangle; +import org.jetbrains.annotations.NotNull; +import org.joml.Vector2d; + +import java.util.Random; +import java.util.function.Consumer; + +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * Just for fun! + * Created by brandon3055 on 10/09/2023 + */ +public class GuiDVD extends GuiElement implements ContentElement> { + private static final Random randy = new Random(); + + private final GuiElement movingElement; + private double xOffset = 0; + private double yOffset = 0; + private Vector2d velocity = null; + private int bounce = 0; + private Consumer onBounce = bounce -> { + }; + + public GuiDVD(@NotNull GuiParent parent) { + super(parent); + this.movingElement = new GuiElement<>(this) + .constrain(TOP, Constraint.relative(get(TOP), () -> yOffset)) + .constrain(LEFT, Constraint.relative(get(LEFT), () -> xOffset)) + .constrain(WIDTH, Constraint.match(get(WIDTH))) + .constrain(HEIGHT, Constraint.match(get(HEIGHT))); + } + + @Override + public GuiElement getContentElement() { + return movingElement; + } + + public void start() { + if (velocity == null) { + velocity = new Vector2d(randy.nextBoolean() ? 1 : -1, randy.nextBoolean() ? 1 : -1); + velocity.normalize(); + } else { + velocity = null; + xOffset = yOffset = 0; + bounce = 0; + } + } + + public void onBounce(Consumer onBounce) { + this.onBounce = onBounce; + } + + @Override + public void tick(double mouseX, double mouseY) { + super.tick(mouseX, mouseY); + } + + @Override + public void render(GuiRender render, double mouseX, double mouseY, float partialTicks) { + super.render(render, mouseX, mouseY, partialTicks); + if (velocity == null) return; + + double speed = 5 * partialTicks; + xOffset += velocity.x * speed; + yOffset += velocity.y * speed; + Rectangle rect = movingElement.getRectangle(); + + int bounces = 0; + if ((velocity.y < 0 && rect.y() < 0) || (velocity.y > 0 && rect.yMax() > scaledScreenHeight())) { + velocity.y *= -1; + onBounce.accept(bounce++); + bounces++; + } + + if ((velocity.x < 0 && rect.x() < 0) || (velocity.x > 0 && rect.xMax() > scaledScreenWidth())) { + velocity.x *= -1; + onBounce.accept(bounce++); + bounces++; + } + + if (bounce > 0) { + velocity.y += -0.05 + (randy.nextGaussian() * 0.1); + velocity.x += -0.05 + (randy.nextGaussian() * 0.1); + velocity.x = velocity.x > 0 ? MathHelper.clip(velocity.x, 0.4, 0.6) : MathHelper.clip(velocity.x, -0.4, -0.6); + velocity.y = velocity.y > 0 ? MathHelper.clip(velocity.y, 0.4, 0.6) : MathHelper.clip(velocity.y, -0.4, -0.6); + velocity.normalize(); + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiDialog.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiDialog.java new file mode 100644 index 00000000..064e1f9f --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiDialog.java @@ -0,0 +1,241 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.ModularGui; +import codechicken.lib.gui.modular.lib.Constraints; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import net.covers1624.quack.collection.FastStream; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.*; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * This class is designed to assist with the easy creation of a number of standard dialog windows. + *

+ * Created by brandon3055 on 14/12/2023 + */ +public class GuiDialog extends GuiElement { + + private boolean blockKeyInput = true; + private boolean blockMouseInput = true; + + protected GuiDialog(@NotNull GuiParent parent) { + super(parent); + } + + /** + * Option dialog builder with pre-configured background and button builders. + * + * @param parent Can be any gui element (Will just be used to get the root element) + * @param title Sets a separate title that will be displayed above the main dialog text. (Optional) + * @param dialogText The main dialog text. + * @param width The dialog width, (Height will automatically adjust based on content.) + * @param options The list of options for this dialog. + */ + public static GuiDialog optionsDialog(@NotNull GuiParent parent, @Nullable Component title, Component dialogText, int width, Option... options) { + return optionsDialog(parent, title, dialogText, GuiRectangle::toolTipBackground, GuiDialog::defaultButton, width, options); + } + + /** + * Option dialog builder with pre-configured background and button builders. + * + * @param parent Can be any gui element (Will just be used to get the root element) + * @param dialogText The main dialog text. + * @param width The dialog width, (Height will automatically adjust based on content.) + * @param options The list of options for this dialog. + */ + public static GuiDialog optionsDialog(@NotNull GuiParent parent, Component dialogText, int width, Option... options) { + return optionsDialog(parent, null, dialogText, width, options); + } + + /** + * Creates a simple info dialog for displaying information to the user. + * The dialog has a single "Ok" button that will close the dialog + * + * @param parent Can be any gui element (Will just be used to get the root element) + * @param title Sets a separate title that will be displayed above the main dialog text. (Optional) + * @param dialogText The main dialog text. + * @param width The dialog width, (Height will automatically adjust based on content.) + */ + public static GuiDialog infoDialog(@NotNull GuiParent parent, @Nullable Component title, Component dialogText, int width, @Nullable Runnable okAction) { + return optionsDialog(parent, title, dialogText, width, neutral(Component.translatable("gui.ok"), okAction)); + } + + /** + * Creates a simple info dialog for displaying information to the user. + * The dialog has a single "Ok" button that will close the dialog + * + * @param parent Can be any gui element (Will just be used to get the root element) + * @param title Sets a separate title that will be displayed above the main dialog text. (Optional) + * @param dialogText The main dialog text. + * @param width The dialog width, (Height will automatically adjust based on content.) + */ + public static GuiDialog infoDialog(@NotNull GuiParent parent, @Nullable Component title, Component dialogText, int width) { + return infoDialog(parent, title, dialogText, width, null); + } + + /** + * Creates a simple info dialog for displaying information to the user. + * The dialog has a single "Ok" button that will close the dialog + * + * @param parent Can be any gui element (Will just be used to get the root element) + * @param dialogText The main dialog text. + * @param width The dialog width, (Height will automatically adjust based on content.) + */ + public static GuiDialog infoDialog(@NotNull GuiParent parent, Component dialogText, int width) { + return infoDialog(parent, null, dialogText, width); + } + + /** + * Create a green "Primary" button option. + */ + public static Option primary(Component text, @Nullable Runnable action) { + return new Option(text, action, hovered -> hovered ? 0xFF44AA44 : 0xFF118811); + } + + /** + * Create a grey "Neutral" button option. + */ + public static Option neutral(Component text, @Nullable Runnable action) { + return new Option(text, action, hovered -> hovered ? 0xFF909090 : 0xFF505050); + } + + /** + * Create a red "Caution" button option. + */ + public static Option caution(Component text, @Nullable Runnable action) { + return new Option(text, action, hovered -> hovered ? 0xFFAA4444 : 0xFF881111); + } + + /** + * @param blockKeyInput Prevent keyboard inputs from being sent to the rest of the gui while this dialog is open. + * Default: true. + */ + public GuiDialog setBlockKeyInput(boolean blockKeyInput) { + this.blockKeyInput = blockKeyInput; + return this; + } + + /** + * @param blockMouseInput Prevent mouse inputs from being sent to the rest of the gui while this dialog is open. + * Default: true. + */ + public GuiDialog setBlockMouseInput(boolean blockMouseInput) { + this.blockMouseInput = blockMouseInput; + return this; + } + + public void close() { + getParent().removeChild(this); + } + + @Override + public boolean keyPressed(int key, int scancode, int modifiers) { + return blockKeyInput; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return blockMouseInput; + } + + /** + * This is the core dialog builder method. + * It takes a title text component and a map of Component>Runnable map that is used to define the options. + * Dialog will automatically be centered on the screen. + * + * @param parent Can be any gui element (Will just be used to get the root element) + * @param title Sets a separate title that will be displayed above the main dialog text. (Optional) + * @param dialogText The main dialog text. + * @param backgroundBuilder A function that is used to create the background of the dialog. + * @param buttonBuilder A function that is used to create the dialog buttons. + * @param width The dialog width, (Height will automatically adjust based on content.) + * @param options The list of options for this dialog. + */ + public static GuiDialog optionsDialog(@NotNull GuiParent parent, @Nullable Component title, Component dialogText, Function> backgroundBuilder, BiFunction buttonBuilder, int width, Option... options) { + if (options.length == 0) throw new IllegalStateException("Can not create gui dialog with no options!"); + ModularGui gui = parent.getModularGui(); + + GuiDialog dialog = new GuiDialog(gui.getRoot()) + .constrain(WIDTH, literal(width)) + .setOpaque(true); + Constraints.bind(backgroundBuilder.apply(dialog), dialog); + + Constraint left = relative(dialog.get(LEFT), 5); + Constraint right = relative(dialog.get(RIGHT), -5); + + if (title != null) { + GuiText titleText = new GuiText(dialog, title) + .setWrap(true) + .constrain(TOP, relative(dialog.get(TOP), 5)) + .constrain(LEFT, left) + .constrain(RIGHT, right) + .autoHeight(); + + GuiText bodyText = new GuiText(dialog, dialogText) + .setWrap(true) + .constrain(TOP, relative(titleText.get(BOTTOM), 5)) + .constrain(LEFT, left) + .constrain(RIGHT, right) + .autoHeight(); + dialog.constrain(HEIGHT, dynamic(() -> 5 + titleText.ySize() + 5 + bodyText.ySize() + 5 + 14 + 5)); + } else { + GuiText bodyText = new GuiText(dialog, dialogText) + .setWrap(true) + .constrain(TOP, relative(dialog.get(TOP), 5)) + .constrain(LEFT, left) + .constrain(RIGHT, right) + .autoHeight(); + dialog.constrain(HEIGHT, dynamic(() -> 5 + bodyText.ySize() + 5 + 14 + 5)); + } + + double totalWidth = FastStream.of(options).doubleSum(e -> dialog.font().width(e.text)); + double pos = 5; + int spacing = 2; + for (Option option : options) { + double fraction = dialog.font().width(option.text) / totalWidth; + double opWidth = (dialog.xSize() - 10 - ((options.length - 1) * spacing)) * fraction; + buttonBuilder.apply(dialog, option) + .constrain(BOTTOM, relative(dialog.get(BOTTOM), -5)) + .constrain(HEIGHT, literal(14)) + .constrain(LEFT, relative(dialog.get(LEFT), pos).precise()) + .constrain(WIDTH, literal(opWidth).precise()); + pos += opWidth + spacing; + } + + dialog.constrain(TOP, midPoint(gui.get(TOP), gui.get(BOTTOM), () -> -(dialog.ySize() / 2D))); + dialog.constrain(LEFT, midPoint(gui.get(LEFT), gui.get(RIGHT), -(width / 2D))); + return dialog; + } + + private static GuiButton defaultButton(GuiDialog dialog, Option option) { + GuiButton button = new GuiButton(dialog); + + GuiRectangle background = new GuiRectangle(button) + .fill(() -> option.colour.apply(button.isMouseOver())); + Constraints.bind(background, button); + + GuiText text = new GuiText(button, option.text()); + button.setLabel(text); + Constraints.bind(text, button, 0, 2, 0, 2); + + button.onPress(() -> { + if (option.action != null) { + option.action.run(); + } + dialog.close(); + }); + + return button; + } + + public record Option(Component text, @Nullable Runnable action, Function colour) {} +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiElement.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiElement.java new file mode 100644 index 00000000..8942e51f --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiElement.java @@ -0,0 +1,491 @@ +package codechicken.lib.gui.modular.elements; + +import com.google.common.collect.Lists; +import com.mojang.blaze3d.vertex.PoseStack; +import codechicken.lib.gui.modular.ModularGui; +import codechicken.lib.gui.modular.lib.*; +import codechicken.lib.gui.modular.lib.geometry.ConstrainedGeometry; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.lib.geometry.Position; +import codechicken.lib.gui.modular.lib.geometry.Rectangle; +import net.covers1624.quack.util.SneakyUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * This is the Base class for all gui elements in Modular Gui Version 3. + *

+ * In v2 this vas a massive monolithic class that had way too much crammed into it. + * The primary goals of v3 are the following: + * - Build a new, Extremely flexible system for handling element geometry, including relative positions, anchoring, etc. + * This was archived using the new Geometry system. For details see {@link GuiParent} and {@link ConstrainedGeometry} + * - Implement a system to properly handle element z offsets. + * This was archived by giving all elements a 'depth' property which defines an elements size on the z axis. + * This is then used to properly layer elements and child elements when they are rendered. + * - Switch everything over to the new RenderType system. (This is mostly handled behind the scenes. You don't need to mess with it when creating a GUI) + * - Consolidate all the various rendering helper methods into one convenient utility class. + * The new {@link GuiGraphics} system showed me a good way to implement this. + * - Reduce the amount of ambiguity when building GUIs. (Whether I succeeded here is up for debate xD) + * - Cut out a lot of random bloat that was never used in v2. + *

+ *

+ * Created by brandon3055 on 04/07/2023 + */ +public class GuiElement> extends ConstrainedGeometry implements ElementEvents, TooltipHandler { + + @NotNull + private GuiParent parent; + + private final List> addedQueue = new ArrayList<>(); + private final List> removeQueue = new ArrayList<>(); + private final List> childElements = new ArrayList<>(); + private final List> childElementsUnmodifiable = Collections.unmodifiableList(childElements); + public boolean initialized = false; + + private Minecraft mc; + private Font font; + private int screenWidth; + private int screenHeight; + + protected int hoverTime = 0; + private int hoverTextDelay = 10; + private boolean isMouseOver = false; + private boolean opaque = false; + private boolean removed = true; + private boolean zStacking = true; + private Supplier enabled = () -> true; + private Supplier enableToolTip = () -> true; + private Supplier> toolTip = null; + private Rectangle renderCull = Rectangle.create(Position.create(0, 0), () -> (double) screenWidth, () -> (double) screenHeight); + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiElement(@NotNull GuiParent parent) { + this.parent = parent; + this.parent.addChild(this); + } + + @NotNull + @Override + public GuiParent getParent() { + return parent; + } + + //=== Child Element Handling ===// + + @Override + public List> getChildren() { + return childElementsUnmodifiable; + } + + /** + * In Modular GUI v3, The add child method is primarily for internal use, + * Child elements are automatically added to their parent on construction. + * + * @param child The child element to be added. + */ + @Override + public void addChild(GuiElement child) { + if (!initialized) throw new IllegalStateException("Attempted to add a child to an element before that element has been initialised!"); + if (child == this) throw new InvalidParameterException("Attempted to add element to itself as a child element."); + if (child.getParent() != this) throw new UnsupportedOperationException("Attempted to add an already initialized element to a different parent element."); + if (removeQueue.contains(child)) { + removeQueue.remove(child); + if (!childElements.contains(child)) { + addedQueue.add(child); + } + child.initElement(this); + } else if (!childElements.contains(child)) { + addedQueue.add(child); + child.initElement(this); + } + } + + protected void applyQueuedChildUpdates() { + if (!removeQueue.isEmpty()) { + childElements.removeAll(removeQueue); + removeQueue.clear(); + } + + if (!addedQueue.isEmpty()) { + childElements.addAll(addedQueue); + addedQueue.clear(); + } + } + + /** + * Called immediately after an element is added to its parent, use to initialize the child element. + */ + public void initElement(GuiParent parent) { + removed = false; + updateScreenData(parent.mc(), parent.font(), parent.scaledScreenWidth(), parent.scaledScreenHeight()); + initialized = true; + } + + @Override + public void adoptChild(GuiElement child) { + child.getParent().removeChild(child); + child.parent = this; + addChild(child); + } + + @Override + public void removeChild(GuiElement child) { + if (childElements.contains(child)) { + child.removed = true; + removeQueue.add(child); + } + addedQueue.remove(child); + } + + @Override + public boolean isDescendantOf(GuiElement ancestor) { + return ancestor == parent || parent.isDescendantOf(ancestor); + } + + //=== Minecraft Properties / Initialisation ===// + //TODO I can probably just pass these calls all the way up to the root parent... + + @Override + public Minecraft mc() { + return mc; + } + + @Override + public Font font() { + return font; + } + + @Override + public int scaledScreenWidth() { + return screenWidth; + } + + @Override + public int scaledScreenHeight() { + return screenHeight; + } + + @Override + public ModularGui getModularGui() { + return getParent().getModularGui(); + } + + @Override + public void onScreenInit(Minecraft mc, Font font, int screenWidth, int screenHeight) { + updateScreenData(mc, font, screenWidth, screenHeight); + super.onScreenInit(mc, font, screenWidth, screenHeight); + } + + protected void updateScreenData(Minecraft mc, Font font, int screenWidth, int screenHeight) { + this.mc = mc; + this.font = font; + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + } + + //=== Element Status ===// + + public T setEnabled(boolean enabled) { + this.enabled = () -> enabled; + return SneakyUtils.unsafeCast(this); + } + + public T setEnabled(@Nullable Supplier enabled) { + this.enabled = enabled; + return SneakyUtils.unsafeCast(this); + } + + public boolean isEnabled() { + return !removed && enabled.get(); + } + + public boolean isRemoved() { + return removed; + } + + public T setEnableToolTip(Supplier enableToolTip) { + this.enableToolTip = enableToolTip; + return SneakyUtils.unsafeCast(this); + } + + @Override + public boolean blockMouseOver(GuiElement element, double mouseX, double mouseY) { + return getParent().blockMouseOver(element, mouseX, mouseY); + } + + @Override + public boolean blockMouseEvents() { + return isMouseOver() && isOpaque(); + } + + /** + * @return True if the cursor is within the bounds of this element, and there is no opaque element above this one obstructing the cursor. + */ + public boolean isMouseOver() { + return isMouseOver; + } + + public boolean isOpaque() { + return opaque; + } + + /** + * If an element is marked as opaque it will consume mouseOver updates, thereby preventing elements bellow from accepting mouseOver input. + * Also prevents mouse events within this element from being passed to elements bellow. + */ + public T setOpaque(boolean opaque) { + this.opaque = opaque; + return SneakyUtils.unsafeCast(this); + } + + /** + * @return the amount of time the cursor has spent inside this element's bounds, + * resets to zero when the cursor leaves this element's bounds. + */ + public int hoverTime() { + return hoverTime; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "geometry=" + getRectangle() + + '}'; + } + + /** + * Add this element to the list of jei exclusions. + * Use this for any elements that render outside the normal gui bounds. + * This will ensure JEI does not try to render on top of these elements. + */ + public T jeiExclude() { + getModularGui().jeiExclude(this); + return SneakyUtils.unsafeCast(this); + } + + /** + * Remove this element from the list of jei exclusions. + */ + public T removeJEIExclude() { + getModularGui().removeJEIExclude(this); + return SneakyUtils.unsafeCast(this); + } + + //=== Render / Update ===// + + /** + * Any child elements completely outside this rectangle will not be rendered at all. + * By default, this is set to the screen bounds (meaning the minecraft window) + * Setting this to null will disable culling. + */ + public T setRenderCull(@Nullable Rectangle renderCull) { + this.renderCull = renderCull; + return SneakyUtils.unsafeCast(this); + } + + /** + * Allows you to disable child z-stacking, Meaning all child elements will be rendered at the same z-level + * rather than being stacked. (Not Recursive, children their sub elements with stacking) + *

+ * This can be useful when rendering a lot of high z depth elements such as ItemStacks. + * As long as you know for sure none of the elements intersect, it should be safe to disable stacking. + * + * @param zStacking Enable z stacking (default true) + */ + public T setZStacking(boolean zStacking) { + this.zStacking = zStacking; + return SneakyUtils.unsafeCast(this); + } + + public boolean zStacking() { + return zStacking; + } + + /** + * Returns the depth of this element plus all of its children (recursively) + * Note: You should almost never need to override this! Depth of background and / or foreground content + * should be specified via {@link BackgroundRender#getBackgroundDepth()} and {@link ForegroundRender#getForegroundDepth()} + * + * @return The depth (z height) of this element plus all of its children. + */ + public double getCombinedElementDepth() { + double depth = 0; + if (this instanceof BackgroundRender bgr) depth += bgr.getBackgroundDepth(); + if (this instanceof ForegroundRender fgr) depth += fgr.getForegroundDepth(); + + double childDepth = 0; + for (GuiElement child : childElements) { + if (!child.isEnabled()) continue; + if (zStacking) { + childDepth += child.getCombinedElementDepth(); + } else { + childDepth = Math.max(childDepth, child.getCombinedElementDepth()); + } + } + + return depth + childDepth; + } + + /** + * This is the main render method that handles rendering this element and any child elements it may have. + * This method almost never needs to be overridden, instead when creating custom elements with custom rendering, + * your element should implement {@link BackgroundRender} and / or {@link ForegroundRender} in or order to implement + * its rendering. + *

+ * Note: After the render is complete, the poseStack's z pos will be offset by the total depth of this element and its children. + * This is intended behavior, + * + * @param render Contains gui context information as well as essential render methods/utils including the PoseStack. + * @param mouseX Current mouse X position + * @param mouseY Current mouse Y position + * @param partialTicks Partial render ticks + */ + public void render(GuiRender render, double mouseX, double mouseY, float partialTicks) { + applyQueuedChildUpdates(); + if (this instanceof BackgroundRender bgr) { + double depth = bgr.getBackgroundDepth(); + bgr.renderBackground(render, mouseX, mouseY, partialTicks); + if (depth > 0) { + render.pose().translate(0, 0, depth); + } + } + + double maxDepth = 0; + for (GuiElement child : childElements) { + if (child.isEnabled()) { + boolean rendered = renderChild(child, render, mouseX, mouseY, partialTicks); + //If z-stacking is disabled, we need to undo the z offset that was applied by the child element. + if (!zStacking && rendered) { + double depth = child.getCombinedElementDepth(); + maxDepth = Math.max(maxDepth, depth); + render.pose().translate(0, 0, -depth); + } + } + } + + if (!zStacking) { + //Now we need to apply the z offset of the tallest child. + render.pose().translate(0, 0, maxDepth); + } + + if (this instanceof ForegroundRender fgr) { + double depth = fgr.getForegroundDepth(); + fgr.renderForeground(render, mouseX, mouseY, partialTicks); + if (depth > 0) { + render.pose().translate(0, 0, depth); + } + } + } + + protected boolean renderChild(GuiElement child, GuiRender render, double mouseX, double mouseY, float partialTicks) { + if (renderCull != null && !renderCull.intersects(child.getRectangle())) return false; + child.render(render, mouseX, mouseY, partialTicks); + return true; + } + + /** + * Used to render overlay's such as hover text. Anything rendered in this method will be rendered on top of everything else on the screen. + * Only one overlay should be rendered at a time, When an element renders content via the overlay method it must return true to indicate the render call has been 'consumed' + * If the render call has already been consumed (Check via the consumed boolean) then this element should avoid rendering its overlay. + *

+ * When rendering overlay content, always use the {@link PoseStack} available via the provided {@link GuiRender} + * This stack will already have the correct Z translation to ensure the overlay renders above everything else on the screen. + *

+ * To check if the cursor is over this element, use 'render.hoveredElement() == this' + * {@link #isMouseOver()} Will also work, but may be problematic when multiple, stacked elements have overlay content. + * + * @param render Contains gui context information as well as essential render methods/utils including the PoseStack. + * @param mouseX Current mouse X position + * @param mouseY Current mouse Y position + * @param partialTicks Partial render ticks + * @param consumed Will be true if the overlay render call has already been consumed by another element. + * @return true if the render call has been consumed. + */ + public boolean renderOverlay(GuiRender render, double mouseX, double mouseY, float partialTicks, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.renderOverlay(render, mouseX, mouseY, partialTicks, consumed); + } + } + return consumed || (showToolTip() && renderTooltip(render, mouseX, mouseY)); + } + + private boolean showToolTip() { + return isMouseOver() && enableToolTip.get() && hoverTime() >= getTooltipDelay(); + } + + /** + * Called every tick to update the element. Note this is called regardless of weather or not the element is actually enabled. + * + * @param mouseX Current mouse X position + * @param mouseY Current mouse Y position + */ + public void tick(double mouseX, double mouseY) { + if (isMouseOver()) { + hoverTime++; + } else { + hoverTime = 0; + } + + for (GuiElement childElement : childElements) { + childElement.tick(mouseX, mouseY); + } + } + + /** + * Called at the start of each tick to update the 'mouseOver' state of each element. + * If the cursor is over an element that is marked as opaque, the update will be consumed. + * This ensures no elements below the opaque element will have their mouseOver flag set to true. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param consumed True if mouseover event has been consumed. + * @return true if this event has been consumed. + */ + public boolean updateMouseOver(double mouseX, double mouseY, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.updateMouseOver(mouseX, mouseY, consumed); + } + } + + isMouseOver = !consumed && GuiRender.isInRect(xMin(), yMin(), xSize(), ySize(), mouseX, mouseY) && !blockMouseOver(this, mouseX, mouseY); + return consumed || (isMouseOver && isOpaque()); + } + + //=== Hover Text ===// + + @Override + public Supplier> getTooltip() { + return toolTip; + } + + @Override + public T setTooltipDelay(int tooltipDelay) { + this.hoverTextDelay = tooltipDelay; + return SneakyUtils.unsafeCast(this); + } + + @Override + public int getTooltipDelay() { + return hoverTextDelay; + } + + @Override + public T setTooltip(@Nullable Supplier> tooltip) { + this.toolTip = tooltip; + return SneakyUtils.unsafeCast(this); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiEnergyBar.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiEnergyBar.java new file mode 100644 index 00000000..478e86a8 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiEnergyBar.java @@ -0,0 +1,143 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.Constraints; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.sprite.CCGuiTextures; +import codechicken.lib.gui.modular.sprite.Material; +import codechicken.lib.util.FormatUtil; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static net.minecraft.ChatFormatting.*; + +/** + * Created by brandon3055 on 10/09/2023 + */ +public class GuiEnergyBar extends GuiElement implements BackgroundRender { + public static final DecimalFormat COMMA_FORMAT = new DecimalFormat("###,###,###,###,###", DecimalFormatSymbols.getInstance(Locale.ROOT)); + public static final Material EMPTY = CCGuiTextures.getUncached("widgets/energy_empty"); + public static final Material FULL = CCGuiTextures.getUncached("widgets/energy_full"); + + private Supplier energy = () -> 0L; + private Supplier capacity = () -> 0L; + private Material emptyTexture = EMPTY; + private Material fullTexture = FULL; + private BiFunction> toolTipFormatter; + + public GuiEnergyBar(@NotNull GuiParent parent) { + super(parent); + setTooltipDelay(0); + setToolTipFormatter(defaultFormatter()); + } + + /** + * Creates a simple energy bar using a simple slot as a background to make it look nice. + */ + public static EnergyBar simpleBar(@NotNull GuiParent parent) { + GuiRectangle container = GuiRectangle.vanillaSlot(parent); + GuiEnergyBar energyBar = new GuiEnergyBar(container); + Constraints.bind(energyBar, container, 1); + return new EnergyBar(container, energyBar); + } + + public static BiFunction> defaultFormatter() { + return (energy, capacity) -> { + List tooltip = new ArrayList<>(); + tooltip.add(Component.translatable("energy_bar.polylib.energy_storage").withStyle(DARK_AQUA)); + boolean shift = Screen.hasShiftDown(); + tooltip.add(Component.translatable("energy_bar.polylib.capacity") + .withStyle(GOLD) + .append(" ") + .append(Component.literal(shift ? FormatUtil.addCommas(capacity) : FormatUtil.formatNumber(capacity)) + .withStyle(GRAY) + .append(" ") + .append(Component.translatable("energy_bar.polylib.rf") + .withStyle(GRAY) + ) + ) + ); + tooltip.add(Component.translatable("energy_bar.polylib.stored") + .withStyle(GOLD) + .append(" ") + .append(Component.literal(shift ? FormatUtil.addCommas(energy) : FormatUtil.formatNumber(energy)) + .withStyle(GRAY) + ) + .append(" ") + .append(Component.translatable("energy_bar.polylib.rf") + .withStyle(GRAY) + ) + .append(Component.literal(String.format(" (%.2f%%)", ((double) energy / (double) capacity) * 100D)) + .withStyle(GRAY) + ) + ); + return tooltip; + }; + } + + public GuiEnergyBar setEmptyTexture(Material emptyTexture) { + this.emptyTexture = emptyTexture; + return this; + } + + public GuiEnergyBar setFullTexture(Material fullTexture) { + this.fullTexture = fullTexture; + return this; + } + + public GuiEnergyBar setCapacity(long capacity) { + return setCapacity(() -> capacity); + } + + public GuiEnergyBar setCapacity(Supplier capacity) { + this.capacity = capacity; + return this; + } + + public GuiEnergyBar setEnergy(long energy) { + return setEnergy(() -> energy); + } + + public GuiEnergyBar setEnergy(Supplier energy) { + this.energy = energy; + return this; + } + + public long getEnergy() { + return energy.get(); + } + + public long getCapacity() { + return capacity.get(); + } + + /** + * Install a custom formatter to control how the energy tool tip renders. + */ + public GuiEnergyBar setToolTipFormatter(BiFunction> toolTipFormatter) { + this.toolTipFormatter = toolTipFormatter; + setTooltip(() -> this.toolTipFormatter.apply(getEnergy(), getCapacity())); + return this; + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + float p = 1 / 128F; + float height = getCapacity() <= 0 ? 0 : (float) ySize() * (getEnergy() / (float) getCapacity()); + float texHeight = height * p; + render.partialSprite(EMPTY.renderType(GuiRender::texColType), xMin(), yMin(), xMax(), yMax(), EMPTY.sprite(), 0F, 1F - (p * (float) ySize()), p * (float) xSize(), 1F, 0xFFFFFFFF); + render.partialSprite(FULL.renderType(GuiRender::texColType), xMin(), yMin() + (ySize() - height), xMax(), yMax(), FULL.sprite(), 0F, 1F - texHeight, p * (float) xSize(), 1F, 0xFFFFFFFF); + } + + public record EnergyBar(GuiRectangle container, GuiEnergyBar bar) {} +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiEntityRenderer.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiEntityRenderer.java new file mode 100644 index 00000000..ac09f9b3 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiEntityRenderer.java @@ -0,0 +1,264 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.lib.geometry.Rectangle; +import codechicken.lib.render.CCRenderEventHandler; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.player.RemotePlayer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraftforge.registries.ForgeRegistries; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Quaternionf; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 15/11/2023 + */ +public class GuiEntityRenderer extends GuiElement implements BackgroundRender { + public static final Logger LOGGER = LogManager.getLogger(); + private static final Map entityCache = new HashMap<>(); + private static final List invalidEntities = new ArrayList<>(); + + private Supplier rotationSpeed = () -> 1F; + private Supplier lockedRotation = () -> 0F; + private Entity entity; + private ResourceLocation entityName; + private boolean invalidEntity = false; + private Supplier rotationLocked = () -> false; + private Supplier trackMouse = () -> false; + private Supplier drawName = () -> false; + public boolean force2dSize = false; + + public GuiEntityRenderer(@NotNull GuiParent parent) { + super(parent); + } + + public GuiEntityRenderer setEntity(Entity entity) { + this.entity = entity; + if (this.entity == null) { + invalidEntity = true; + return this; + } + + this.entityName = ForgeRegistries.ENTITY_TYPES.getKey(entity.getType()); + invalidEntity = invalidEntities.contains(entityName); + return this; + } + + public GuiEntityRenderer setEntity(ResourceLocation entity) { + this.entityName = entity; + this.entity = entityCache.computeIfAbsent(entity, resourceLocation -> { + EntityType type = ForgeRegistries.ENTITY_TYPES.getValue(entity); + return type == null ? null : type.create(mc().level); + }); + + invalidEntity = this.entity == null; + if (invalidEntities.contains(entityName)) { + invalidEntity = true; + } + + return this; + } + + public GuiEntityRenderer setRotationSpeed(float rotationSpeed) { + this.rotationSpeed = () -> rotationSpeed; + return this; + } + + public GuiEntityRenderer setRotationSpeed(Supplier rotationSpeed) { + this.rotationSpeed = rotationSpeed; + return this; + } + + public float getRotationSpeed() { + return rotationSpeed.get(); + } + + public GuiEntityRenderer setLockedRotation(float lockedRotation) { + this.lockedRotation = () -> lockedRotation; + return this; + } + + public GuiEntityRenderer setLockedRotation(Supplier lockedRotation) { + this.lockedRotation = lockedRotation; + return this; + } + + public float getLockedRotation() { + return lockedRotation.get(); + } + + public GuiEntityRenderer setRotationLocked(boolean rotationLocked) { + this.rotationLocked = () -> rotationLocked; + return this; + } + + public GuiEntityRenderer setRotationLocked(Supplier rotationLocked) { + this.rotationLocked = rotationLocked; + return this; + } + + public boolean isRotationLocked() { + return rotationLocked.get(); + } + + public GuiEntityRenderer setTrackMouse(boolean trackMouse) { + this.trackMouse = () -> trackMouse; + return this; + } + + public GuiEntityRenderer setTrackMouse(Supplier trackMouse) { + this.trackMouse = trackMouse; + return this; + } + + public boolean isTrackMouse() { + return trackMouse.get(); + } + + public GuiEntityRenderer setDrawName(boolean drawName) { + this.drawName = () -> drawName; + return this; + } + + public GuiEntityRenderer setDrawName(Supplier drawName) { + this.drawName = drawName; + return this; + } + + public boolean isDrawName() { + return drawName.get(); + } + + public GuiEntityRenderer setForce2dSize(boolean force2dSize) { + this.force2dSize = force2dSize; + return this; + } + + @Override + public double getBackgroundDepth() { + Rectangle rect = getRectangle(); + float scale = (float) (force2dSize ? (Math.min(rect.height() / entity.getBbHeight(), rect.width() / entity.getBbWidth())) : rect.height() / entity.getBbHeight()); + return scale * 2; + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + if (invalidEntity) return; + + try { + if (entity != null) { + Rectangle rect = getRectangle(); + float scale = (float) (force2dSize ? (Math.min(rect.height() / entity.getBbHeight(), rect.width() / entity.getBbWidth())) : rect.height() / entity.getBbHeight()); + float xPos = (float) (rect.x() + (rect.width() / 2D)); + float yPos = (float) ((yMin() + (ySize() / 2)) + (rect.height() / 2)); + float rotation = rotationLocked.get() ? lockedRotation.get() : (CCRenderEventHandler.renderTime + partialTicks) * rotationSpeed.get(); + if (entity instanceof LivingEntity living) { + int eyeOffset = (int) ((entity.getEyeHeight()) * scale); + if (trackMouse.get()) { + renderEntityInInventoryFollowsMouse(render, xPos, yPos, scale, xPos - (float) mouseX, yPos - (float) mouseY - eyeOffset, living); + } else { + renderEntityInInventoryWithRotation(render, xPos, yPos, scale, rotation, living); + } + } + } + } catch (Throwable e) { + invalidEntity = true; + invalidEntities.add(entityName); + LOGGER.error("Failed to render entity in GUI. This is not a bug there are just some entities that can not be rendered like this."); + LOGGER.error("Entity: " + entity, e); + } + } + + public static void renderEntityInInventoryFollowsMouse(GuiRender render, double pX, double pY, double pScale, float offsetX, float offsetY, LivingEntity pEntity) { + float xAngle = (float)Math.atan(offsetX / 40.0F); + float yAngle = (float)Math.atan(offsetY / 40.0F); + renderEntityInInventoryFollowsAngle(render, pX, pY, pScale, xAngle, yAngle, pEntity); + } + + public static void renderEntityInInventoryFollowsAngle(GuiRender render, double pX, double pY, double pScale, float angleX, float angleY, LivingEntity pEntity) { + Quaternionf quaternionf = (new Quaternionf()).rotateZ((float)Math.PI); + Quaternionf quaternionf1 = (new Quaternionf()).rotateX(angleY * 20.0F * ((float)Math.PI / 180F)); + quaternionf.mul(quaternionf1); + float f2 = pEntity.yBodyRot; + float f3 = pEntity.getYRot(); + float f4 = pEntity.getXRot(); + float f5 = pEntity.yHeadRotO; + float f6 = pEntity.yHeadRot; + pEntity.yBodyRot = 180.0F + angleX * 20.0F; + pEntity.setYRot(180.0F + angleX * 40.0F); + pEntity.setXRot(-angleY * 20.0F); + pEntity.yHeadRot = pEntity.getYRot(); + pEntity.yHeadRotO = pEntity.getYRot(); + renderEntityInInventory(render, pX, pY, pScale, quaternionf, quaternionf1, pEntity); + pEntity.yBodyRot = f2; + pEntity.setYRot(f3); + pEntity.setXRot(f4); + pEntity.yHeadRotO = f5; + pEntity.yHeadRot = f6; + } + + public static void renderEntityInInventoryWithRotation(GuiRender render, double xPos, double yPos, double scale, double rotation, LivingEntity living) { + Quaternionf quaternionf = new Quaternionf().rotateZ((float)Math.PI); + Quaternionf quaternionf1 = Axis.YP.rotationDegrees((float) rotation); + quaternionf.mul(quaternionf1); + float f2 = living.yBodyRot; + float f3 = living.getYRot(); + float f4 = living.getXRot(); + float f5 = living.yHeadRotO; + float f6 = living.yHeadRot; + living.yBodyRot = 180.0F; + living.setYRot(180.0F); + living.setXRot(0); + living.yHeadRot = living.getYRot(); + living.yHeadRotO = living.getYRot(); + renderEntityInInventory(render, xPos, yPos, scale, quaternionf, quaternionf1, living); + living.yBodyRot = f2; + living.setYRot(f3); + living.setXRot(f4); + living.yHeadRotO = f5; + living.yHeadRot = f6; + } + + public static void renderEntityInInventory(GuiRender render, double pX, double pY, double pScale, Quaternionf quat, @Nullable Quaternionf pCameraOrientation, LivingEntity pEntity) { + render.pose().pushPose(); + render.pose().translate(pX, pY, 50.0D); + render.pose().mulPoseMatrix((new Matrix4f()).scaling((float)pScale, (float)pScale, (float)(-pScale))); + render.pose().mulPose(quat); + Lighting.setupForEntityInInventory(); + EntityRenderDispatcher entityrenderdispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + if (pCameraOrientation != null) { + pCameraOrientation.conjugate(); + entityrenderdispatcher.overrideCameraOrientation(pCameraOrientation); + } + + entityrenderdispatcher.setRenderShadow(false); + RenderSystem.runAsFancy(() -> entityrenderdispatcher.render(pEntity, 0.0D, 0.0D, 0.0D, 0.0F, 1.0F, render.pose(), render.buffers(), 15728880)); + render.flush(); + entityrenderdispatcher.setRenderShadow(true); + render.pose().popPose(); + Lighting.setupFor3DItems(); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiEventProvider.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiEventProvider.java new file mode 100644 index 00000000..393ba6c3 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiEventProvider.java @@ -0,0 +1,122 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import org.apache.logging.log4j.util.TriConsumer; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * Created by brandon3055 on 15/11/2023 + */ +public class GuiEventProvider extends GuiElement { + + private boolean ignoreConsumed = false; + private final List> clickListeners = new ArrayList<>(); + private final List> releaseListeners = new ArrayList<>(); + private final List> movedListeners = new ArrayList<>(); + private final List> scrollListeners = new ArrayList<>(); + private final List> keyPressListeners = new ArrayList<>(); + private final List> keyReleaseListeners = new ArrayList<>(); + private final List> charTypedListeners = new ArrayList<>(); + + public GuiEventProvider(@NotNull GuiParent parent) { + super(parent); + } + + public GuiEventProvider setIgnoreConsumed(boolean ignoreConsumed) { + this.ignoreConsumed = ignoreConsumed; + return this; + } + + public GuiEventProvider onMouseClick(TriConsumer listener) { + clickListeners.add(listener); + return this; + } + + public GuiEventProvider onMouseRelease(TriConsumer listener) { + releaseListeners.add(listener); + return this; + } + + public GuiEventProvider onMouseMove(BiConsumer listener) { + movedListeners.add(listener); + return this; + } + + public GuiEventProvider onScroll(TriConsumer listener) { + scrollListeners.add(listener); + return this; + } + + public GuiEventProvider onKeyPress(TriConsumer listener) { + keyPressListeners.add(listener); + return this; + } + + public GuiEventProvider onKeyRelease(TriConsumer listener) { + keyReleaseListeners.add(listener); + return this; + } + + public GuiEventProvider onCharTyped(BiConsumer listener) { + charTypedListeners.add(listener); + return this; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button, boolean consumed) { + if (ignoreConsumed || !consumed) { + clickListeners.forEach(e -> e.accept(mouseX, mouseY, button)); + } + return super.mouseClicked(mouseX, mouseY, button, consumed); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button, boolean consumed) { + if (ignoreConsumed || !consumed) { + releaseListeners.forEach(e -> e.accept(mouseX, mouseY, button)); + } + return super.mouseReleased(mouseX, mouseY, button, consumed); + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + movedListeners.forEach(e -> e.accept(mouseX, mouseY)); + super.mouseMoved(mouseX, mouseY); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scroll, boolean consumed) { + if (ignoreConsumed || !consumed) { + scrollListeners.forEach(e -> e.accept(mouseX, mouseY, scroll)); + } + return super.mouseScrolled(mouseX, mouseY, scroll, consumed); + } + + @Override + public boolean keyPressed(int key, int scancode, int modifiers, boolean consumed) { + if (ignoreConsumed || !consumed) { + keyPressListeners.forEach(e -> e.accept(key, scancode, modifiers)); + } + return super.keyPressed(key, scancode, modifiers, consumed); + } + + @Override + public boolean keyReleased(int key, int scancode, int modifiers, boolean consumed) { + if (ignoreConsumed || !consumed) { + keyReleaseListeners.forEach(e -> e.accept(key, scancode, modifiers)); + } + return super.keyReleased(key, scancode, modifiers, consumed); + } + + @Override + public boolean charTyped(char character, int modifiers, boolean consumed) { + if (ignoreConsumed || !consumed) { + charTypedListeners.forEach(e -> e.accept(character, modifiers)); + } + return super.charTyped(character, modifiers, consumed); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiFluidTank.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiFluidTank.java new file mode 100644 index 00000000..a2d4eb9a --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiFluidTank.java @@ -0,0 +1,252 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.Constraints; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.sprite.CCGuiTextures; +import codechicken.lib.gui.modular.sprite.Material; +import codechicken.lib.util.FormatUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static net.minecraft.ChatFormatting.*; + +/** + * When implementing this tank, you must specify the tank capacity in mb, + * And then you have two options for specifying the tank contents. + * You can set the fluid and the amount stored, + * Or you can provide a {@link FluidStack} + *

+ * Created by brandon3055 on 11/09/2023 + */ +public class GuiFluidTank extends GuiElement implements BackgroundRender { + //TODO make a better texture, This feels a little too.. cluttered. + public static final Material DEFAULT_WINDOW = CCGuiTextures.getUncached("widgets/tank_window"); + + private int gaugeColour = 0xFF909090; + private boolean drawGauge = true; + private Material window = null; + private Supplier capacity = () -> 10000L; + private Supplier fluidStack = () -> FluidStack.EMPTY; + + private BiFunction> toolTipFormatter; + + public GuiFluidTank(@NotNull GuiParent parent) { + super(parent); + setTooltipDelay(0); + setToolTipFormatter(defaultFormatter()); + } + + /** + * Creates a simple tank using a simple slot as a background to make it look nice. + */ + public static FluidTank simpleTank(@NotNull GuiParent parent) { + GuiRectangle container = GuiRectangle.vanillaSlot(parent); + GuiFluidTank energyBar = new GuiFluidTank(container); + Constraints.bind(energyBar, container, 1); + return new FluidTank(container, energyBar); + } + + /** + * Sets the capacity of this tank in milli-buckets. + */ + public GuiFluidTank setCapacity(long capacity) { + return setCapacity(() -> capacity); + } + + /** + * Supply the capacity of this tank in milli-buckets. + */ + public GuiFluidTank setCapacity(Supplier capacity) { + this.capacity = capacity; + return this; + } + + /** + * Allows you to set the current stored fluid stack. + */ + public GuiFluidTank setFluidStack(FluidStack fluidStack) { + return setFluidStack(() -> fluidStack); + } + + /** + * Allows you to supply the current stored fluid stack. + */ + public GuiFluidTank setFluidStack(Supplier fluidStack) { + this.fluidStack = fluidStack; + return this; + } + + /** + * Install a custom formatter to control how the fluid tool tip renders. + */ + public GuiFluidTank setToolTipFormatter(BiFunction> toolTipFormatter) { + this.toolTipFormatter = toolTipFormatter; + setTooltip(() -> this.toolTipFormatter.apply(getFluidStack(), getCapacity())); + return this; + } + + /** + * Sets the tank window texture, Will be tiled to fit the tank size. + * + * @param window New window texture or null for no window texture. + */ + public GuiFluidTank setWindow(@Nullable Material window) { + this.window = window; + return this; + } + + /** + * Enable the built-in fluid gauge lines. + */ + public GuiFluidTank setDrawGauge(boolean drawGauge) { + this.drawGauge = drawGauge; + return this; + } + + /** + * Sets the colour of the built-in fluid gauge lines + */ + public GuiFluidTank setGaugeColour(int gaugeColour) { + this.gaugeColour = gaugeColour; + return this; + } + + public Long getCapacity() { + return capacity.get(); + } + + public FluidStack getFluidStack() { + return fluidStack.get(); + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + FluidStack stack = getFluidStack(); + Material fluidMat = Material.fromSprite(getStillTexture(stack)); + + if (!stack.isEmpty() && fluidMat != null) { + int fluidColor = getColour(stack); + float height = getCapacity() <= 0 ? 0 : (float) ySize() * (stack.getAmount() / (float) getCapacity()); + render.tileSprite(fluidMat.renderType(GuiRender::texColType), xMin(), yMax() - height, xMax(), yMax(), fluidMat.sprite(), fluidColor); + } + + if (window != null) { + render.tileSprite(window.renderType(GuiRender::texColType), xMin(), yMin(), xMax(), yMax(), window.sprite(), 0xFFFFFFFF); + } + + gaugeColour = 0xFF000000; + if (drawGauge) { + double spacing = computeGaugeSpacing(); + if (spacing == 0) return; + + double pos = spacing; + while (pos + 1 < ySize()) { + double width = xSize() / 4; + double yPos = yMax() - 1 - pos; + render.fill(xMax() - width, yPos, xMax(), yPos + 1, gaugeColour); + pos += spacing; + } + } + } + + private double computeGaugeSpacing() { + double ySize = ySize(); + double capacity = getCapacity(); + if (ySize / (capacity / 100D) > 3) return ySize / (capacity / 100D); + else if (ySize / (capacity / 500D) > 3) return ySize / (capacity / 500D); + else if (ySize / (capacity / 1000D) > 3) return ySize / (capacity / 1000D); + else if (ySize / (capacity / 5000D) > 3) return ySize / (capacity / 5000D); + else if (ySize / (capacity / 10000D) > 3) return ySize / (capacity / 10000D); + else if (ySize / (capacity / 50000D) > 3) return ySize / (capacity / 50000D); + else if (ySize / (capacity / 100000D) > 3) return ySize / (capacity / 100000D); + return 0; + } + + public static BiFunction> defaultFormatter() { + return (fluidStack, capacity) -> { + List tooltip = new ArrayList<>(); + tooltip.add(Component.translatable("fluid_tank.polylib.fluid_storage").withStyle(DARK_AQUA)); + if (!fluidStack.isEmpty()) { + tooltip.add(Component.translatable("fluid_tank.polylib.contains") + .withStyle(GOLD) + .append(" ") + .append(fluidStack.getDisplayName().copy() + .setStyle(Style.EMPTY + .withColor(getColour(fluidStack)) + ) + ) + ); + } + + tooltip.add(Component.translatable("fluid_tank.polylib.capacity") + .withStyle(GOLD) + .append(" ") + .append(Component.literal(FormatUtil.addCommas(capacity)) + .withStyle(GRAY) + .append(" ") + .append(Component.translatable("fluid_tank.polylib.mb") + .withStyle(GRAY) + ) + ) + ); + tooltip.add(Component.translatable("fluid_tank.polylib.stored") + .withStyle(GOLD) + .append(" ") + .append(Component.literal(FormatUtil.addCommas(fluidStack.getAmount())) + .withStyle(GRAY) + ) + .append(" ") + .append(Component.translatable("fluid_tank.polylib.mb") + .withStyle(GRAY) + ) + .append(Component.literal(String.format(" (%.2f%%)", ((double) fluidStack.getAmount() / (double) capacity) * 100D)) + .withStyle(GRAY) + ) + ); + return tooltip; + }; + } + + //TODO These could maybe go in FluidUtils? but they are client side only so... + + public static int getColour(FluidStack fluidStack) { + return fluidStack.getFluid() == Fluids.EMPTY ? -1 : IClientFluidTypeExtensions.of(fluidStack.getFluid()).getTintColor(fluidStack); + } + + public static int getColour(Fluid fluid) { + return fluid == Fluids.EMPTY ? -1 : IClientFluidTypeExtensions.of(fluid).getTintColor(); + } + + @Nullable + public static TextureAtlasSprite getStillTexture(FluidStack stack) { + if (stack.getFluid() == Fluids.EMPTY) return null; + ResourceLocation texture = IClientFluidTypeExtensions.of(stack.getFluid()).getStillTexture(stack); + return Minecraft.getInstance().getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(texture); + } + + @Nullable + public static TextureAtlasSprite getStillTexture(Fluid fluid) { + if (fluid == Fluids.EMPTY) return null; + ResourceLocation texture = IClientFluidTypeExtensions.of(fluid).getStillTexture(); + return Minecraft.getInstance().getTextureAtlas(TextureAtlas.LOCATION_BLOCKS).apply(texture); + } + + public record FluidTank(GuiRectangle container, GuiFluidTank tank) {} +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiItemStack.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiItemStack.java new file mode 100644 index 00000000..2ff85905 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiItemStack.java @@ -0,0 +1,118 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.HEIGHT; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.WIDTH; + +/** + * A simple gui element that renders an item stack. + * This width and height of this element should be constrained to the same value, + * The stack size is based on the element size. + * constrain size to 16x16 for the standard gui stack size. + *

+ * Created by brandon3055 on 03/09/2023 + */ +public class GuiItemStack extends GuiElement implements BackgroundRender { + private Supplier stack; + private Supplier decorate = () -> true; + private Supplier toolTip = () -> true; + + public GuiItemStack(@NotNull GuiParent parent) { + this(parent, () -> ItemStack.EMPTY); + } + + public GuiItemStack(@NotNull GuiParent parent, ItemStack itemStack) { + super(parent); + setStack(itemStack); + } + + public GuiItemStack(@NotNull GuiParent parent, Supplier provider) { + super(parent); + setStack(provider); + } + + public GuiItemStack setStack(Supplier stackProvider) { + this.stack = stackProvider; + return this; + } + + public GuiItemStack setStack(ItemStack stack) { + this.stack = () -> stack; + return this; + } + + /** + * Enable item stack decorations. + * Meaning, Damage bar, Stack size, Item cool down, etc. (Default Enabled) + */ + public GuiItemStack enableStackDecoration(boolean enableDecoration) { + return enableStackDecoration(() -> enableDecoration); + } + + /** + * Enable item stack decorations. + * Meaning, Damage bar, Stack size, Item cool down, etc. (Default Enabled) + */ + public GuiItemStack enableStackDecoration(Supplier enableDecoration) { + this.decorate = enableDecoration; + return this; + } + + /** + * Enable the default item stack tooltip. (Default Enabled) + * Note: If the {@link GuiItemStack} element has a tooltip applied via one of the element #setTooltip methods, + * That will override the item stack tool tip. + */ + public GuiItemStack enableStackToolTip(boolean enableToolTip) { + return enableStackToolTip(() -> enableToolTip); + } + + /** + * Enable the default item stack tooltip. (Default Enabled) + * Note: If the {@link GuiItemStack} element has a tooltip applied via one of the element #setTooltip methods, + * That will override the item stack tool tip. + */ + public GuiItemStack enableStackToolTip(Supplier enableToolTip) { + this.toolTip = enableToolTip; + return this; + } + + //=== Internal methods ===// + + public double getStackSize() { + return Math.max(getValue(WIDTH), getValue(HEIGHT)); + } + + @Override + public double getBackgroundDepth() { + return getStackSize(); + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + ItemStack stack = this.stack.get(); + if (stack.isEmpty()) return; + + render.renderItem(stack, xMin(), yMin(), getStackSize(), (int) (xMin() + (xSize() * yMin()))); + if (decorate.get()) { + render.renderItemDecorations(stack, xMin(), yMin(), getStackSize()); + } + } + + @Override + public boolean renderOverlay(GuiRender render, double mouseX, double mouseY, float partialTicks, boolean consumed) { + if (super.renderOverlay(render, mouseX, mouseY, partialTicks, consumed)) return true; + if (isMouseOver() && !stack.get().isEmpty()) { + render.renderTooltip(stack.get(), mouseX, mouseY); + return true; + } + return false; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiList.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiList.java new file mode 100644 index 00000000..909d5d53 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiList.java @@ -0,0 +1,221 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.SliderState; +import codechicken.lib.gui.modular.lib.geometry.*; +import codechicken.lib.math.MathHelper; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiFunction; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.*; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * GuiList, as the name suggests allows you to display a list of objects. + * The list type can be whatever you want, and you can install a converter to + * map list objects to elements for display. + *

+ * The default converter simply displays the toString() value of the object. + *

+ * Element width will be fixed to the width of the GuiList, element height can be whatever you want. + *

+ * Note on adding child elements to this: + * If any child elements extend beyond the bounds of this element, that part will be culled. + * Also, child elements will always end up bellow the list items when the list is updated. + *

+ * Created by brandon3055 on 21/09/2023 + */ +public class GuiList extends GuiElement> { + + /** + * This is made available primarily for debugging purposes where it can be useful to see what's going on behind the scenes. + */ + public boolean enableScissor = true; + private double yScrollPos = 0; + private double contentHeight = 0; + private double itemSpacing = 1; + private boolean rebuild = true; + private GuiSlider hiddenBar = null; + + private final List listContent = new ArrayList<>(); + private final Map> elementMap = new HashMap<>(); + private final LinkedList> visible = new LinkedList<>(); + + private BiFunction, E, ? extends GuiElement> displayBuilder = (parent, e) -> { + GuiText text = new GuiText(parent, () -> Component.literal(String.valueOf(e))).setWrap(true); + text.constrain(GeoParam.HEIGHT, Constraint.dynamic(() -> (double) font().wordWrapHeight(text.getText(), (int) text.xSize()))); + return text; + }; + + public GuiList(@NotNull GuiParent parent) { + super(parent); + this.setZStacking(false); + this.setRenderCull(getRectangle()); + } + + public boolean add(E e) { + rebuild = true; + return listContent.add(e); + } + + public boolean remove(E e) { + rebuild = true; + return listContent.remove(e); + } + + /** + * You are allowed to modify this list directly, but if you do you must call + * {@link #markDirty()} otherwise the display elements will not get updated. + */ + public List getList() { + return listContent; + } + + public void markDirty() { + this.rebuild = true; + } + + public GuiList setDisplayBuilder(BiFunction, E, ? extends GuiElement> displayBuilder) { + this.displayBuilder = displayBuilder; + return this; + } + + public GuiList setItemSpacing(double itemSpacing) { + this.itemSpacing = itemSpacing; + return this; + } + + public SliderState scrollState() { + return SliderState.forScrollBar(() -> yScrollPos, e -> { + yScrollPos = e; + updateVisible(); + }, () -> MathHelper.clip(ySize() / contentHeight, 0, 1)); + } + + /** + * You can choose to attach a scroll bar to this element the same way you would a {@link GuiScrolling} + * But sometimes you just want to be able to mouse-wheel scroll without an actual scroll bar. + *

+ * This method will add a hidden scroll bar to enable mouse wheel scrolling and middle-click dragging without the need for an actual scroll bar. + * The scroll bar will be an invisible zero width element on the right side of this list. + */ + public GuiList addHiddenScrollBar() { + if (hiddenBar != null) removeChild(hiddenBar); + hiddenBar = new GuiSlider(this, Axis.Y) + .setSliderState(scrollState()) + .setScrollableElement(this) + .constrain(TOP, match(get(TOP))) + .constrain(LEFT, relative(get(RIGHT), -5)) + .constrain(BOTTOM, match(get(BOTTOM))) + .constrain(RIGHT, match(get(RIGHT))); + return this; + } + + public GuiList removeHiddenScrollBar() { + if (hiddenBar != null) removeChild(hiddenBar); + return this; + } + + //=== Internal Logic ===// + + public double hiddenSize() { + return Math.max(contentHeight - ySize(), 0); + } + + @Override + public void tick(double mouseX, double mouseY) { + if (rebuild) { + rebuildElements(); + } + super.tick(mouseX, mouseY); + } + + public void rebuildElements() { + elementMap.values().forEach(this::removeChild); + elementMap.clear(); + + for (E item : listContent) { + GuiElement next = displayBuilder.apply(this, item); + next.constrain(LEFT, match(get(LEFT))); + next.constrain(RIGHT, match(get(RIGHT))); + removeChild(next); + elementMap.put(item, next); + } + rebuild = false; + updateVisible(); + } + + public Map> getElementMap() { + return elementMap; + } + + public void scrollTo(E scrollTo) { + if (rebuild) { + rebuild = false; + rebuildElements(); + } + + if (elementMap.containsKey(scrollTo)) { + scrollState().setPos(0); + double yMax = yMin(); + + for (E item : getList()) { + GuiElement e = elementMap.get(item); + if (e != null) { + yMax += e.ySize() + 1; + if (item.equals(scrollTo)) break; + } + } + + if (yMax > yMax()) { + double move = yMax - yMax(); + scrollState().setPos(move / hiddenSize()); + } + } + } + + private void updateVisible() { + visible.forEach(this::removeChild); + visible.clear(); + contentHeight = 0; + if (listContent.isEmpty()) return; + + for (GuiElement item : elementMap.values()) { + contentHeight += item.ySize() + itemSpacing; + } + contentHeight -= itemSpacing; + + double winTop = yMin(); + double winBottom = yMax(); + + double yPos = winTop + (yScrollPos * -hiddenSize()); + for (E item : listContent) { + GuiElement element = elementMap.get(item); + if (element == null) continue; + double top = yPos; + double bottom = yPos + element.ySize(); + + if ((top >= winTop && top <= winBottom) || (bottom >= winTop && bottom <= winBottom)) { + addChild(element); + visible.add(element); + element.constrain(TOP, literal(top)); + } + yPos = bottom + itemSpacing; + } + } + + @Override + public boolean blockMouseOver(GuiElement element, double mouseX, double mouseY) { + return super.blockMouseOver(element, mouseX, mouseY) || (element.isDescendantOf(this) && !isMouseOver()); + } + + @Override + public void render(GuiRender render, double mouseX, double mouseY, float partialTicks) { + if (enableScissor) render.pushScissorRect(getRectangle()); + super.render(render, mouseX, mouseY, partialTicks); + if (enableScissor) render.popScissor(); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiManipulable.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiManipulable.java new file mode 100644 index 00000000..d95835e4 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiManipulable.java @@ -0,0 +1,401 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.ContentElement; +import codechicken.lib.gui.modular.lib.CursorHelper; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.lib.geometry.Rectangle; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.literal; +import static codechicken.lib.gui.modular.lib.geometry.Constraint.match; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * This element can be used to create movable/resizable guis of gui elements. + * This is achieved via a "contentElement" to which all child elements should eb attached. + * Initially the bounds of the content element will match the parent {@link GuiManipulable} element. + * However, depending on which features are enabled, it is possible for the user to resize the + * content element by clicking and dragging the edges, or move the element by clicking and dragging + * a specified "dragArea". + *

+ * It should be noted that the constraints on the underlying {@link GuiManipulable} should be fairly rigid. + * Things like dynamic constraint changes will not translate through to the contentElement, + *

+ * If a UI resize occurs the content element's bounds will be reset to default. + * You can also trigger a manual reset by calling {@link #resetBounds()} + *

+ * Created by brandon3055 on 13/11/2023 + */ +public class GuiManipulable extends GuiElement implements ContentElement> { + private final GuiElement contentElement; + + private int dragXOffset = 0; + private int dragYOffset = 0; + private boolean isDragging = false; + private boolean dragPos = false; + private boolean dragTop = false; + private boolean dragLeft = false; + private boolean dragBottom = false; + private boolean dragRight = false; + private boolean enableCursors = false; + + //Made available for external position restraints + public int xMin = 0; + public int xMax = 0; + public int yMin = 0; + public int yMax = 0; + + protected Rectangle minSize = Rectangle.create(0, 0, 50, 50); + protected Rectangle maxSize = Rectangle.create(0, 0, 256, 256); + protected Runnable onMovedCallback = null; + protected Runnable onResizedCallback = null; + protected PositionRestraint positionRestraint = draggable -> { + if (xMin < 0) { + int move = -xMin; + xMin += move; + xMax += move; + } else if (xMax > scaledScreenWidth()) { + int move = xMax - scaledScreenWidth(); + xMin -= move; + xMax -= move; + } + if (yMin < 0) { + int move = -yMin; + yMin += move; + yMax += move; + } else if (yMax > scaledScreenHeight()) { + int move = yMax - scaledScreenHeight(); + yMin -= move; + yMax -= move; + } + }; + + private GuiElement moveHandle; + private GuiElement leftHandle; + private GuiElement rightHandle; + private GuiElement topHandle; + private GuiElement bottomHandle; + + public GuiManipulable(@NotNull GuiParent parent) { + super(parent); + this.contentElement = new GuiElement<>(this) + .constrain(LEFT, Constraint.dynamic(() -> (double) xMin)) + .constrain(RIGHT, Constraint.dynamic(() -> (double) xMax)) + .constrain(TOP, Constraint.dynamic(() -> (double) yMin)) + .constrain(BOTTOM, Constraint.dynamic(() -> (double) yMax)); + moveHandle = new GuiRectangle(contentElement); + leftHandle = new GuiRectangle(contentElement); + rightHandle = new GuiRectangle(contentElement); + topHandle = new GuiRectangle(contentElement); + bottomHandle = new GuiRectangle(contentElement); + } + + public GuiManipulable resetBounds() { + xMin = (int)xMin(); + xMax = (int)xMax(); + yMin = (int)yMin(); + yMax = (int)yMax(); + return this; + } + + @Override + public GuiManipulable constrain(GeoParam param, @Nullable Constraint constraint) { + return super.constrain(param, constraint).resetBounds(); //TODO, This will break if strict constraints are enabled... + } + + @Override + public void onScreenInit(Minecraft mc, Font font, int screenWidth, int screenHeight) { + super.onScreenInit(mc, font, screenWidth, screenHeight); + resetBounds(); + } + + @Override + public GuiElement getContentElement() { + return contentElement; + } + + public GuiManipulable addResizeHandles(int handleSize, boolean includeTopHandle) { + if (includeTopHandle) addTopHandle(handleSize); + addLeftHandle(handleSize); + addRightHandle(handleSize); + addBottomHandle(handleSize); + return this; + } + + public GuiManipulable addTopHandle(int handleSize) { + this.topHandle + .constrain(TOP, match(contentElement.get(TOP))) + .constrain(LEFT, match(contentElement.get(LEFT))) + .constrain(RIGHT, match(contentElement.get(RIGHT))) + .constrain(HEIGHT, literal(handleSize)); + return this; + } + + public GuiManipulable addBottomHandle(int handleSize) { + this.bottomHandle + .constrain(BOTTOM, match(contentElement.get(BOTTOM))) + .constrain(LEFT, match(contentElement.get(LEFT))) + .constrain(RIGHT, match(contentElement.get(RIGHT))) + .constrain(HEIGHT, literal(handleSize)); + return this; + } + + public GuiManipulable addLeftHandle(int handleSize) { + this.leftHandle + .constrain(LEFT, match(contentElement.get(LEFT))) + .constrain(TOP, match(contentElement.get(TOP))) + .constrain(BOTTOM, match(contentElement.get(BOTTOM))) + .constrain(WIDTH, literal(handleSize)); + return this; + } + + public GuiManipulable addRightHandle(int handleSize) { + this.rightHandle + .constrain(RIGHT, match(contentElement.get(RIGHT))) + .constrain(TOP, match(contentElement.get(TOP))) + .constrain(BOTTOM, match(contentElement.get(BOTTOM))) + .constrain(WIDTH, literal(handleSize)); + return this; + } + + public GuiManipulable addMoveHandle(int handleSize) { + this.moveHandle + .constrain(TOP, match(contentElement.get(TOP))) + .constrain(LEFT, match(contentElement.get(LEFT))) + .constrain(RIGHT, match(contentElement.get(RIGHT))) + .constrain(HEIGHT, literal(handleSize)); + return this; + } + + /** + * You can use this to retrieve the current move handle. + * You are free to update the constraints on this handle, but it must be constrained relative to the content element. + */ + public GuiElement getMoveHandle() { + return moveHandle; + } + + /** + * You can use this to retrieve the current left resize handle. + * You are free to update the constraints on this handle, but it must be constrained relative to the content element. + */ + public GuiElement getLeftHandle() { + return leftHandle; + } + + /** + * You can use this to retrieve the current right resize handle. + * You are free to update the constraints on this handle, but it must be constrained relative to the content element. + */ + public GuiElement getRightHandle() { + return rightHandle; + } + + /** + * You can use this to retrieve the current top resize handle. + * You are free to update the constraints on this handle, but it must be constrained relative to the content element. + */ + public GuiElement getTopHandle() { + return topHandle; + } + + /** + * You can use this to retrieve the current bottom resize handle. + * You are free to update the constraints on this handle, but it must be constrained relative to the content element. + */ + public GuiElement getBottomHandle() { + return bottomHandle; + } + + /** + * Enables rendering of custom mouse cursors when hovering over a draggable handle. + */ + public GuiManipulable enableCursors(boolean enableCursors) { + this.enableCursors = enableCursors; + return this; + } + + public GuiManipulable setOnMovedCallback(Runnable onMovedCallback) { + this.onMovedCallback = onMovedCallback; + return this; + } + + public GuiManipulable setOnResizedCallback(Runnable onResizedCallback) { + this.onResizedCallback = onResizedCallback; + return this; + } + + public GuiManipulable setPositionRestraint(PositionRestraint positionRestraint) { + this.positionRestraint = positionRestraint; + return this; + } + + public void setMinSize(Rectangle minSize) { + this.minSize = minSize; + } + + public void setMaxSize(Rectangle maxSize) { + this.maxSize = maxSize; + } + + public Rectangle getMinSize() { + return minSize; + } + + public Rectangle getMaxSize() { + return maxSize; + } + + + @Override + public void tick(double mouseX, double mouseY) { + if (enableCursors) { + boolean posFlag = moveHandle != null && moveHandle.isMouseOver(); + boolean topFlag = topHandle != null && topHandle.isMouseOver(); + boolean leftFlag = leftHandle != null && leftHandle.isMouseOver(); + boolean bottomFlag = bottomHandle != null && bottomHandle.isMouseOver(); + boolean rightFlag = rightHandle != null && rightHandle.isMouseOver(); + boolean any = posFlag || topFlag || leftFlag || bottomFlag || rightFlag; + + if (any) { + if (posFlag) { + getModularGui().setCursor(CursorHelper.DRAG); + } else if ((topFlag && leftFlag) || (bottomFlag && rightFlag)) { + getModularGui().setCursor(CursorHelper.RESIZE_TLBR); + } else if ((topFlag && rightFlag) || (bottomFlag && leftFlag)) { + getModularGui().setCursor(CursorHelper.RESIZE_TRBL); + } else if (topFlag || bottomFlag) { + getModularGui().setCursor(CursorHelper.RESIZE_V); + } else { + getModularGui().setCursor(CursorHelper.RESIZE_H); + } + } + } + + super.tick(mouseX, mouseY); + } + + public void startDragging() { + double mouseX = getModularGui().computeMouseX(); + double mouseY = getModularGui().computeMouseY(); + dragXOffset = (int) (mouseX - xMin); + dragYOffset = (int) (mouseY - yMin); + isDragging = true; + dragPos = true; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (super.mouseClicked(mouseX, mouseY, button)) return true; + + boolean posFlag = moveHandle != null && moveHandle.isMouseOver(); + boolean topFlag = topHandle != null && topHandle.isMouseOver(); + boolean leftFlag = leftHandle != null && leftHandle.isMouseOver(); + boolean bottomFlag = bottomHandle != null && bottomHandle.isMouseOver(); + boolean rightFlag = rightHandle != null && rightHandle.isMouseOver(); + + if (posFlag || topFlag || leftFlag || bottomFlag || rightFlag) { + dragXOffset = (int) (mouseX - xMin); + dragYOffset = (int) (mouseY - yMin); + isDragging = true; + if (posFlag) { + dragPos = true; + } else { + dragTop = topFlag; + dragLeft = leftFlag; + dragBottom = bottomFlag; + dragRight = rightFlag; + } + return true; + } + + return false; + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + if (isDragging) { + int xMove = (int) (mouseX - dragXOffset) - xMin; + int yMove = (int) (mouseY - dragYOffset) - yMin; + if (dragPos) { + Rectangle previous = Rectangle.create(xMin, yMin, xMax - xMin, yMax - yMin); + xMin += xMove; + xMax += xMove; + yMin += yMove; + yMax += yMove; + validatePosition(); + onMoved(); + } else { + Rectangle min = getMinSize(); + Rectangle max = getMaxSize(); + if (dragTop) { + yMin += yMove; + if (yMax - yMin < min.height()) yMin = yMax - (int) min.height(); + if (yMax - yMin > max.height()) yMin = yMax - (int) max.height(); + if (yMin < 0) yMin = 0; + } + if (dragLeft) { + xMin += xMove; + if (xMax - xMin < min.width()) xMin = xMax - (int) min.width(); + if (xMax - xMin > max.width()) xMin = xMax - (int) max.width(); + if (xMin < 0) xMin = 0; + } + if (dragBottom) { + yMax = yMin + (dragYOffset + yMove); + if (yMax - yMin < min.height()) yMax = yMin + (int) min.height(); + if (yMax - yMin > max.height()) yMax = yMin + (int) max.height(); + if (yMax > scaledScreenHeight()) yMax = scaledScreenHeight(); + } + if (dragRight) { + xMax = xMin + (dragXOffset + xMove); + if (xMax - xMin < min.width()) xMax = xMin + (int) min.width(); + if (xMax - xMin > max.width()) xMax = xMin + (int) max.width(); + if (xMax > scaledScreenWidth()) xMax = scaledScreenWidth(); + } + validatePosition(); + onResized(); + } + } + super.mouseMoved(mouseX, mouseY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button, boolean consumed) { + if (isDragging) { + validatePosition(); + } + isDragging = dragPos = dragTop = dragLeft = dragBottom = dragRight = false; + return super.mouseReleased(mouseX, mouseY, button, consumed); + } + + protected void validatePosition() { + double x = xMin; + double y = yMin; + positionRestraint.restrainPosition(this); + if ((x != xMin || y != yMin) && onMovedCallback != null) { + onMovedCallback.run(); + } + } + + protected void onMoved() { + if (onMovedCallback != null) { + onMovedCallback.run(); + } + } + + protected void onResized() { + if (onResizedCallback != null) { + onResizedCallback.run(); + } + } + + public interface PositionRestraint { + void restrainPosition(GuiManipulable draggable); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiProgressIcon.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiProgressIcon.java new file mode 100644 index 00000000..3e56ce61 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiProgressIcon.java @@ -0,0 +1,115 @@ +package codechicken.lib.gui.modular.elements; + + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.Axis; +import codechicken.lib.gui.modular.lib.geometry.Direction; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.sprite.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * This can be used to create a simple progress indicator like those used in machines like furnaces. + *

+ * The background texture (if one is used) and the animated texture must be the same shape and size, + * They must be designed so that the animated texture can be rendered directly on top of the background texture with no offset. + * The animated texture should not have any empty space on ether end as the entire width of the texture is used in the animation. + *

+ * Texture must be designed for left to right animation, + *

+ * Created by brandon3055 on 04/09/2023 + */ +public class GuiProgressIcon extends GuiElement implements BackgroundRender { + + private Material background = null; + private Material animated; + private Supplier progress = () -> 0D; + private Direction direction = Direction.RIGHT; + + public GuiProgressIcon(@NotNull GuiParent parent, Material animated) { + super(parent); + this.animated = animated; + } + + public GuiProgressIcon(@NotNull GuiParent parent, Material background, Material animated) { + super(parent); + this.background = background; + this.animated = animated; + } + + public GuiProgressIcon(@NotNull GuiParent parent) { + super(parent); + } + + /** + * Set the direction this progress icon is pointing, Default is RIGHT + */ + public GuiProgressIcon setDirection(Direction direction) { + this.direction = direction; + return this; + } + + /** + * Sets the background texture, aka the "empty" texture. + */ + public GuiProgressIcon setBackground(@Nullable Material background) { + this.background = background; + return this; + } + + /** + * Sets the texture that will be animated. + */ + public GuiProgressIcon setAnimated(Material animated) { + this.animated = animated; + return this; + } + + /** + * Set the current progress to a fixed value. + * + * @see #setProgress(Supplier) + */ + public GuiProgressIcon setProgress(double progress) { + return setProgress(() -> progress); + } + + /** + * Attach a supplier that returns the current progress value for this progress icon (0 to 1) + */ + public GuiProgressIcon setProgress(Supplier progress) { + this.progress = progress; + return this; + } + + public double getProgress() { + return progress.get(); + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + render.pose().pushPose(); + + double width = direction.getAxis() == Axis.X ? xSize() : ySize(); + double height = direction.getAxis() == Axis.X ? ySize() : xSize(); + + render.pose().translate(xMin() + (xSize() / 2), yMin() + (ySize() / 2), 0); + render.pose().mulPose(com.mojang.math.Axis.ZP.rotationDegrees((float) Direction.RIGHT.rotationTo(direction))); + + double halfWidth = width / 2; + double halfHeight = height / 2; + if (background != null) { + render.tex(background, -halfWidth, -halfHeight, halfWidth, halfHeight, 0xFFFFFFFF); + } + + if (animated == null) return; + float progress = (float) getProgress(); + render.partialSprite(animated.renderType(GuiRender::texColType), -halfWidth, -halfHeight, -halfWidth + (width* progress), -halfHeight + height, animated.sprite(), 0F, 0F, progress, 1F, 0xFFFFFFFF); + + render.pose().popPose(); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiRectangle.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiRectangle.java new file mode 100644 index 00000000..0d034702 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiRectangle.java @@ -0,0 +1,165 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +/** + * Used to draw a simple rectangle on the screen. + * Can specify separate (or no) border colours and fill colours. + * Can also render using the "shadedRectangle" render type. + *

+ * Created by brandon3055 on 28/08/2023 + */ +public class GuiRectangle extends GuiElement implements BackgroundRender { + private Supplier fill = null; + private Supplier border = null; + + private Supplier borderWidth = () -> 1D; + + private Supplier shadeTopLeft; + private Supplier shadeBottomRight; + private Supplier shadeCorners; + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiRectangle(@NotNull GuiParent parent) { + super(parent); + } + + /** + * Creates a rectangle that mimics the appearance of a vanilla inventory slot. + * Uses shadedRect to create the 3D "inset" look. + */ + public static GuiRectangle vanillaSlot(@NotNull GuiParent parent) { + return new GuiRectangle(parent).shadedRect(0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); + } + + /** + * Creates a rectangle that mimics the appearance of a vanilla inventory slot, except inverted + * Uses shadedRect to create the 3D "popped out" appearance + */ + public static GuiRectangle invertedSlot(@NotNull GuiParent parent) { + return new GuiRectangle(parent).shadedRect(0xFFffffff, 0xFF373737, 0xFF8b8b8b, 0xFF8b8b8b); + } + + /** + * Creates a rectangle similar in appearance to a vanilla button, but with no texture and no black border. + */ + public static GuiRectangle planeButton(@NotNull GuiParent parent) { + return new GuiRectangle(parent).shadedRect(0xFFaaaaaa, 0xFF545454, 0xFF6f6f6f); + } + + public static GuiRectangle toolTipBackground(@NotNull GuiParent parent) { + return toolTipBackground(parent, 0xF0100010, 0x505000FF, 0x5028007f); + } + + public static GuiRectangle toolTipBackground(@NotNull GuiParent parent, int backgroundColour, int borderColourTop, int borderColourBottom) { + return toolTipBackground(parent, backgroundColour, backgroundColour, borderColourTop, borderColourBottom); + } + + public static GuiRectangle toolTipBackground(@NotNull GuiParent parent, int backgroundColourTop, int backgroundColourBottom, int borderColourTop, int borderColourBottom) { + return new GuiRectangle(parent) { + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + render.toolTipBackground(xMin(), yMin(), xSize(), ySize(), backgroundColourTop, backgroundColourBottom, borderColourTop, borderColourBottom, false); + } + }; + } + + public GuiRectangle border(int border) { + return border(() -> border); + } + + public GuiRectangle border(Supplier border) { + this.border = border; + return this; + } + + public GuiRectangle fill(int fill) { + return fill(() -> fill); + } + + public GuiRectangle fill(Supplier fill) { + this.fill = fill; + return this; + } + + public GuiRectangle rectangle(int fill, int border) { + return rectangle(() -> fill, () -> border); + } + + public GuiRectangle rectangle(Supplier fill, Supplier border) { + this.fill = fill; + this.border = border; + return this; + } + + public GuiRectangle shadedRect(int topLeft, int bottomRight, int fill) { + return shadedRect(() -> topLeft, () -> bottomRight, () -> fill); + } + + public GuiRectangle shadedRect(Supplier topLeft, Supplier bottomRight, Supplier fill) { + return shadedRect(topLeft, bottomRight, () -> GuiRender.midColour(topLeft.get(), bottomRight.get()), fill); + } + + public GuiRectangle shadedRect(int topLeft, int bottomRight, int cornerMix, int fill) { + return shadedRect(() -> topLeft, () -> bottomRight, () -> cornerMix, () -> fill); + } + + public GuiRectangle shadedRect(Supplier topLeft, Supplier bottomRight, Supplier cornerMix, Supplier fill) { + this.fill = fill; + this.shadeTopLeft = topLeft; + this.shadeBottomRight = bottomRight; + this.shadeCorners = cornerMix; + return this; + } + + public GuiRectangle setShadeTopLeft(Supplier shadeTopLeft) { + this.shadeTopLeft = shadeTopLeft; + return this; + } + + public GuiRectangle setShadeBottomRight(Supplier shadeBottomRight) { + this.shadeBottomRight = shadeBottomRight; + return this; + } + + public GuiRectangle setShadeCorners(Supplier shadeCorners) { + this.shadeCorners = shadeCorners; + return this; + } + + public GuiRectangle setShadeCornersAuto() { + this.shadeCorners = () -> GuiRender.midColour(shadeTopLeft.get(), shadeBottomRight.get()); + return this; + } + + public GuiRectangle borderWidth(double borderWidth) { + return borderWidth(() -> borderWidth); + } + + public GuiRectangle borderWidth(Supplier borderWidth) { + this.borderWidth = borderWidth; + return this; + } + + public double getBorderWidth() { + return borderWidth.get(); + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + if (shadeTopLeft != null && shadeBottomRight != null && shadeCorners != null) { + render.shadedRect(getRectangle(), getBorderWidth(), shadeTopLeft.get(), shadeBottomRight.get(), shadeCorners.get(), fill == null ? 0 : fill.get()); + } else if (border != null) { + render.borderRect(getRectangle(), getBorderWidth(), fill == null ? 0 : fill.get(), border.get()); + } else if (fill != null) { + render.rect(getRectangle(), fill.get()); + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiScrolling.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiScrolling.java new file mode 100644 index 00000000..c0c5f9e0 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiScrolling.java @@ -0,0 +1,219 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.Constraints; +import codechicken.lib.gui.modular.lib.ContentElement; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.SliderState; +import codechicken.lib.gui.modular.lib.geometry.Axis; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.math.MathHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.match; +import static codechicken.lib.gui.modular.lib.geometry.Constraint.relative; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * So the logic behind this element is as follows. + * This element contains a base "Content Element" that holds all the scrollable content. + * The content element's position is controlled by the {@link GuiScrolling} + * But its {@link GeoParam#WIDTH} and {@link GeoParam#HEIGHT} constraints can be set by the user, + * Or they can be set to dynamically adjust to the child elements added to it. + *

+ * The bounds of the {@link GuiScrolling} represent the "view window" + * When scrolling up/down, left/right the Content Element is effectively just moving around behind the view window + * and everything outside the view window is scissored off. + * Any events that occur outside the view window are not propagated to scroll element. + * Calls to {@link #isMouseOver()} from an area of an element that is outside the view window will return false. + *

+ * Elements that are completely outside the view window will not be rendered at all for efficiency. + *

+ * Created by brandon3055 on 01/09/2023 + */ +public class GuiScrolling extends GuiElement implements ContentElement> { + + /** + * This is made available primarily for debugging purposes where it can be useful to see what's going on behind the scenes. + */ + public boolean enableScissor = true; + private GuiElement contentElement; + private double xScrollPos = 0; + private double yScrollPos = 0; + private double contentWidth = 0; + private double contentHeight = 0; + private boolean setup = false; + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiScrolling(@NotNull GuiParent parent) { + super(parent); + installContainerElement(new ContentElement(this)); + } + + //=== Scroll element setup ===// + + /** + * Retrieves the content element that holds all the scrolling elements. + * You must add all of your scrolling content to this element. + * Scrolling content must also be constrained relative to this element. + *

+ * The {@link GeoParam#TOP} and {@link GeoParam#LEFT} constraints for this element are set by the {@link GuiScrolling} and must not be overridden. + * These are used to control the 'scrolling' of the element. + *

+ * By default, the {@link GeoParam#WIDTH} and {@link GeoParam#HEIGHT} (and therefor also BOTTOM, RIGHT) are dynamically constrained to match the outer bounds of the scrolling elements. + * So attempting to constrain the content to any of these dynamic parameters would result in a stack overflow. + * You can however override the WIDTH and HEIGHT constraints if you wish. + * This can be useful if you wish to create something like a fixed width scrolling list where the width of each scrolling element is bound to the width of the list. + *

+ * The most important thing to note, Especially when manually constraining the WIDTH and HEIGHT of the content element, + * All scrolling elements must be withing the bounds of the content element. Anything outside the content element's bounds will not be visible. + * + * @return The content element. + */ + @Override + public GuiElement getContentElement() { + return contentElement; + } + + /** + * This allows you to install a custom container element. + * The elements constraints will automatically be set by this method. + *

+ * After calling this method you may override the container element WIDTH and HEIGHT constraints as described in the documentation for {@link #getContentElement()} + * But you must not touch the TOP or LEFT constraints. + *

+ * Important thing to note, By default the container element is preinstalled before any children can be added, meaning any children added to the {@link GuiScrolling} + * will render on top of the scrolling content. + * As this method allows you to set a new child as the container element, any children added before the new content element, will render under the content element. + * + * @param element The new container element. + */ + public void installContainerElement(GuiElement element) { + if (element.getParent() != this) throw new IllegalStateException("Content element must be a child of the GuiScrollingBase it is being installed in"); + if (contentElement != null) removeChild(contentElement); + setup = true; + contentElement = element; + contentElement.setRenderCull(getRectangle()); + contentElement.constrain(TOP, Constraint.relative(get(TOP), () -> yScrollPos * -hiddenSize(Axis.Y))); + contentElement.constrain(LEFT, Constraint.relative(get(LEFT), () -> xScrollPos * -hiddenSize(Axis.X))); + contentElement.constrain(WIDTH, Constraint.dynamic(() -> contentElement.getChildBounds().xMax() - contentElement.xMin())); + contentElement.constrain(HEIGHT, Constraint.dynamic(() -> contentElement.getChildBounds().yMax() - contentElement.yMin())); + setup = false; + } + + /** + * @return a {@link SliderState} that can be used to get or control the scroll position of the specified axis. + */ + public SliderState scrollState(Axis axis) { + return switch (axis) { + case X -> SliderState.forScrollBar(() -> xScrollPos, e -> xScrollPos = e, () -> MathHelper.clip(xSize() / contentElement.xSize(), 0, 1)); + case Y -> SliderState.forScrollBar(() -> yScrollPos, e -> yScrollPos = e, () -> MathHelper.clip(ySize() / contentElement.ySize(), 0, 1)); + }; + } + + //=== Internal logic ===// + + /** + * @return the total content size / length for the given axis + */ + public double totalSize(Axis axis) { + return switch (axis) { + case X -> contentWidth; + case Y -> contentHeight; + }; + } + + /** + * @return the hidden content size / length for the given axis (How much of the content is outside the view area) + */ + public double hiddenSize(Axis axis) { + return switch (axis) { + case X -> Math.max(contentWidth - xSize(), 0); + case Y -> Math.max(contentHeight - ySize(), 0); + }; + } + + @Override + public void tick(double mouseX, double mouseY) { + super.tick(mouseX, mouseY); + //These can not be generated dynamically, Doing so would result in a calculation loop, aka a stack overflow. + contentWidth = contentElement.xSize(); + contentHeight = contentElement.ySize(); + } + + @Override + public boolean blockMouseOver(GuiElement element, double mouseX, double mouseY) { + return super.blockMouseOver(element, mouseX, mouseY) || (element.isDescendantOf(contentElement) && !this.isMouseOver()); + } + + //=== Rendering ===// + + @Override + protected boolean renderChild(GuiElement child, GuiRender render, double mouseX, double mouseY, float partialTicks) { + boolean scissor = child == contentElement && enableScissor; + if (scissor) render.pushScissorRect(getRectangle()); + boolean ret = super.renderChild(child, render, mouseX, mouseY, partialTicks); + if (scissor) render.popScissor(); + return ret; + } + + private class ContentElement extends GuiElement { + /** + * @param parent parent {@link GuiParent}. + */ + public ContentElement(@NotNull GuiParent parent) { + super(parent); + } + + @Override + public ContentElement constrain(GeoParam param, @Nullable Constraint constraint) { + if (!setup && (param == TOP || param == LEFT)) throw new IllegalStateException("Can not override TOP or LEFT constraints on content element, These are used to control the scrolling behavior!"); + return super.constrain(param, constraint); + } + } + + public static ScrollWindow simpleScrollWindow(@NotNull GuiParent parent, boolean verticalScrollBar, boolean horizontalScrollBar) { + GuiElement container = new GuiElement<>(parent); + GuiRectangle background = GuiRectangle.vanillaSlot(container) + .constrain(TOP, match(container.get(TOP))) + .constrain(LEFT, match(container.get(LEFT))) + .constrain(BOTTOM, relative(container.get(BOTTOM), horizontalScrollBar ? -10 : 0)) + .constrain(RIGHT, relative(container.get(RIGHT), verticalScrollBar ? -10 : 0)); + + GuiScrolling scroll = new GuiScrolling(background); + Constraints.bind(scroll, background, 1); + + GuiSlider.ScrollBar verticalBar = null; + if (verticalScrollBar) { + verticalBar = GuiSlider.vanillaScrollBar(container, Axis.Y); + verticalBar.container() + .constrain(TOP, match(container.get(TOP))) + .constrain(BOTTOM, relative(container.get(BOTTOM), horizontalScrollBar ? -10 : 0)) + .constrain(RIGHT, match(container.get(RIGHT))) + .constrain(WIDTH, Constraint.literal(9)); + verticalBar.slider() + .setSliderState(scroll.scrollState(Axis.Y)) + .setScrollableElement(scroll); + } + GuiSlider.ScrollBar horizontalBar = null; + if (horizontalScrollBar) { + horizontalBar = GuiSlider.vanillaScrollBar(container, Axis.X); + horizontalBar.container() + .constrain(BOTTOM, match(container.get(BOTTOM))) + .constrain(LEFT, match(container.get(LEFT))) + .constrain(RIGHT, relative(container.get(RIGHT), verticalScrollBar ? -10 : 0)) + .constrain(HEIGHT, Constraint.literal(9)); + horizontalBar.slider() + .setSliderState(scroll.scrollState(Axis.X)) + .setScrollableElement(scroll); + } + return new ScrollWindow(container, scroll, verticalBar, horizontalBar); + } + + public record ScrollWindow(GuiElement container, GuiScrolling scrolling, @Nullable GuiSlider.ScrollBar verticalBar, @Nullable GuiSlider.ScrollBar horizontalBar) { + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiSlider.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiSlider.java new file mode 100644 index 00000000..2157ccf8 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiSlider.java @@ -0,0 +1,266 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.*; +import codechicken.lib.gui.modular.lib.geometry.*; +import codechicken.lib.math.MathHelper; +import org.jetbrains.annotations.NotNull; + +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * This can be used as the base for anything that requires the linear movement of an element between two position. + * e.g. Scroll bars, Slide controls and slide indicators. + *

+ * Implementation is simple, Simply install a "Slide Element", this will be the moving element, + * The movement of this element is confined to the bounds og the {@link GuiSlider} + *

+ * The position of the slider is managed via the installed {@link SliderState} + *

+ * Created by brandon3055 on 02/09/2023 + */ +public class GuiSlider extends GuiElement { + private final Axis axis; + private SliderState state = SliderState.create(0.1); + private GuiElement slider; + private double outOfBoundsDist = 50; + private GuiElement scrollableElement; + + private int dragButton = GuiButton.LEFT_CLICK; + private int scrollDragButton = GuiButton.MIDDLE_CLICK; + private boolean middleClickScroll = false; + /** + * This should theoretically never be needed, But just in case... + */ + public boolean invertDragScroll = false; + + private boolean dragging = false; + private double slideStartPos = 0; + private Position clickPos = Position.create(0, 0); + private boolean scrollableDragging = false; + + /** + * Creates a basic gui slider that moves along the specified axis. + * This includes a default slider element the width of which is bound to the GuiSlider, + * And the length of which is controlled by {@link SliderState#sliderRatio()} + */ + public GuiSlider(@NotNull GuiParent parent, Axis axis) { + super(parent); + this.axis = axis; + installSlider(new GuiElement<>(this)); + bindSliderLength(); + bindSliderWidth(); + } + + public GuiSlider(@NotNull GuiParent parent, Axis axis, GuiElement slider) { + super(parent); + this.axis = axis; + installSlider(slider); + } + + /** + * Vanilla does not really seem to have a standard for its scroll bars, + * But this is something that should at least fit in to a typical vanilla gui. + */ + public static ScrollBar vanillaScrollBar(GuiElement parent, Axis axis) { + GuiRectangle background = GuiRectangle.vanillaSlot(parent); + + GuiSlider slider = new GuiSlider(background, axis); + Constraints.bind(slider, background, 1); + + slider.installSlider(GuiRectangle.planeButton(slider)) + .bindSliderLength() + .bindSliderWidth(); + + GuiRectangle sliderHighlight = new GuiRectangle(slider.getSlider()) + .fill(0x5000b6FF) + .setEnabled(() -> slider.getSlider().isMouseOver()); + + Constraints.bind(sliderHighlight, slider.getSlider()); + + return new ScrollBar(background, slider, sliderHighlight); + } + + /** + * Set the slider state used by this slider element. + * The slider state is used to get and set the slider position. + * It also controls scroll speed. + */ + public GuiSlider setSliderState(SliderState state) { + this.state = state; + return this; + } + + /** + * For use cases where this slider is controlling something like a scroll element. + * This enables scrolling when the cursor is over the scrollable element. + * It can also enable scrolling via middle-click + drag. + */ + public GuiSlider setScrollableElement(GuiElement scrollableElement) { + return setScrollableElement(scrollableElement, true); + } + + /** + * For use cases where this slider is controlling something like a scroll element. + * This enables scrolling when the cursor is over the scrollable element. + * It can also enable scrolling via middle-click + drag. + */ + public GuiSlider setScrollableElement(GuiElement scrollableElement, boolean middleClickScroll) { + this.scrollableElement = scrollableElement; + this.middleClickScroll = middleClickScroll; + return this; + } + + /** + * Install an element to be used as the sliding element. + * The sliders minimum position (meaning either LEFT or TOP) on the moving axis will be constrained the {@link GuiSlider} + * Attempting to override this constraint after installing the slider element will break the slider. + *

+ * The size constraints, and position constraint for the non-moving axis need to be set by the implementor. + * + * @see #bindSliderLength() + * @see #bindSliderWidth() + */ + public GuiSlider installSlider(GuiElement slider) { + if (slider.getParent() != this) throw new IllegalStateException("slider element must be a child of the GuiSlider it is being installed in"); + if (this.slider != null) removeChild(this.slider); + this.slider = slider; + switch (axis) { + case X -> slider.constrain(LEFT, Constraint.relative(get(LEFT), () -> (getValue(WIDTH) - slider.getValue(WIDTH)) * state.getPos())); + case Y -> slider.constrain(TOP, Constraint.relative(get(TOP), () -> (getValue(HEIGHT) - slider.getValue(HEIGHT)) * state.getPos())); + } + return this; + } + + /** + * Sets up constraints to automatically control the slider element length. + * The slider element length will be controlled by {@link SliderState#sliderRatio()} + * This is used for things like gui scroll bars where the bar length changes based on the ratio of content in view. + */ + public GuiSlider bindSliderLength() { + switch (axis) {//Ensure we don't accidentally over-constrain + case X -> slider.constrain(RIGHT, null).constrain(WIDTH, Constraint.dynamic(() -> getValue(WIDTH) * state.sliderRatio())); + case Y -> slider.constrain(BOTTOM, null).constrain(HEIGHT, Constraint.dynamic(() -> getValue(HEIGHT) * state.sliderRatio())); + } + return this; + } + + /** + * Binds the sliders position and size on the non-moving axis to the width and pos of the {@link GuiSlider} + */ + public GuiSlider bindSliderWidth() { + switch (axis) {//Ensure we don't accidentally over-constrain + case X -> slider.constrain(HEIGHT, null).constrain(TOP, Constraint.match(get(TOP))).constrain(BOTTOM, Constraint.match(get(BOTTOM))); + case Y -> slider.constrain(WIDTH, null).constrain(LEFT, Constraint.match(get(LEFT))).constrain(RIGHT, Constraint.match(get(RIGHT))); + } + return this; + } + + /** + * @return the installed slider element. + */ + public GuiElement getSlider() { + return slider; + } + + /** + * @return True if the slider is currently being dragged by the user. + */ + public boolean isDragging() { + return dragging; + } + + /** + * Set the out-of-bounds distance, + * If the cursor is dragged more than this distance from the slider bounds on the no-moving axis, + * the slider will snap back to its original position until the cursor moves back into bounds. + * Default is 50, -1 will disable the snap-back functionality. + */ + public GuiSlider setOutOfBoundsDist(double outOfBoundsDist) { + this.outOfBoundsDist = outOfBoundsDist; + return this; + } + + /** + * @param dragButton The mouse button used to drag this slider (Default {@link GuiButton#LEFT_CLICK}) + */ + public GuiSlider setDragButton(int dragButton) { + this.dragButton = dragButton; + return this; + } + + /** + * @param scrollDragButton The button used to scroll by clicking and dragging the defined scrollableElement (Default {@link GuiButton#MIDDLE_CLICK}) + * @see #setScrollableElement(GuiElement, boolean) + */ + public GuiSlider setScrollDragButton(int scrollDragButton) { + this.scrollDragButton = scrollDragButton; + return this; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + dragging = false; + clickPos = Position.create(mouseX, mouseY); + slideStartPos = state.getPos(); + if (button == dragButton && isMouseOver()) { + if (!slider.isMouseOver()) { + clickPos = Position.create(slider.xCenter(), slider.yCenter()); + handleDrag(mouseX, mouseY); + } + dragging = true; + return true; + } + if (button == scrollDragButton && scrollableElement != null && scrollableElement.isMouseOver()) { + scrollableDragging = true; + } + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button, boolean consumed) { + dragging = scrollableDragging = false; + return super.mouseReleased(mouseX, mouseY, button, consumed); + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + if (dragging || (scrollableDragging && scrollableElement != null)) { + handleDrag(mouseX, mouseY); + } + super.mouseMoved(mouseX, mouseY); + } + + private void handleDrag(double mouseX, double mouseY) { + Position mousePos = Position.create(mouseX, mouseY); + Rectangle rect = dragging || scrollableElement == null ? getRectangle() : scrollableElement.getRectangle(); + + if (dragging && outOfBoundsDist >= -1 && rect.distance(axis.opposite(), mousePos) > outOfBoundsDist) { + state.setPos(slideStartPos); + return; + } + + double travel = rect.size(axis) - slider.getRectangle().size(axis); + if (travel <= 0) return; + double clickPos = this.clickPos.get(axis); + double currentPos = mousePos.get(axis); + double movement = (currentPos - clickPos) / travel; + + if (scrollableDragging) { + movement *= invertDragScroll ? state.sliderRatio() : -state.sliderRatio(); + } + + state.setPos(MathHelper.clip(slideStartPos + movement, 0, 1)); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scroll) { + if (isMouseOver() || (scrollableElement != null && scrollableElement.isMouseOver())) { + if (!state.canScroll(axis)) return false; + state.setPos(MathHelper.clip(state.getPos() + (state.scrollSpeed() * -scroll), 0, 1)); + return true; + } + return false; + } + + public record ScrollBar(GuiRectangle container, GuiSlider slider, GuiRectangle highlight) {} +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiSlots.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiSlots.java new file mode 100644 index 00000000..9ce861eb --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiSlots.java @@ -0,0 +1,312 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.container.ContainerScreenAccess; +import codechicken.lib.gui.modular.lib.container.SlotGroup; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.sprite.CCGuiTextures; +import codechicken.lib.gui.modular.sprite.Material; +import net.minecraft.world.inventory.Slot; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.match; +import static codechicken.lib.gui.modular.lib.geometry.Constraint.relative; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; +import static net.minecraft.world.inventory.InventoryMenu.BLOCK_ATLAS; + +/** + * This element is used to manage and render a grid of inventory slots in a GUI. + * The width and height of this element are automatically constrained based on the slot configuration. + * However, you can override those constraints, The slot grid will always render in the center of the element nomater the element size. + *

+ * This can be used to render all slots in a {@link SlotGroup} or a sub-set of slots within a group. + *

+ * Created by brandon3055 on 08/09/2023 + */ +public class GuiSlots extends GuiElement implements BackgroundRender { + public static final Material[] ARMOR_SLOTS = new Material[]{Material.fromAtlas(BLOCK_ATLAS, "item/empty_armor_slot_helmet"), Material.fromAtlas(BLOCK_ATLAS, "item/empty_armor_slot_chestplate"), Material.fromAtlas(BLOCK_ATLAS, "item/empty_armor_slot_leggings"), Material.fromAtlas(BLOCK_ATLAS, "item/empty_armor_slot_boots")}; + public static final Material OFF_HAND_SLOT = Material.fromAtlas(BLOCK_ATLAS, "item/empty_armor_slot_shield"); + + private final int firstSlot; + private final int slotCount; + private final int columns; + private final SlotGroup slots; + private final ContainerScreenAccess screenAccess; + + private Material slotTexture = CCGuiTextures.getUncached("widgets/slot"); + private Function slotIcons = slot -> null; + private Function highlightColour = slot -> 0x80ffffff; + private int xSlotSpacing = 0; + private int ySlotSpacing = 0; + + /** + * @param slots The slot group containing the slots that this element will manage. + * @param gridColumns The width of the inventory grid (Typically 9 for standard player or chest inventories) + */ + public GuiSlots(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup slots, int gridColumns) { + this(parent, screenAccess, slots, 0, slots.size(), gridColumns); + } + + /** + * @param slots The slot group containing the slots that this element will manage. + * @param firstSlot Index of the fist slot within the slot group. + * @param slotCount The number of slots that this element will manage. + * @param gridColumns The width of the inventory grid (Typically 9 for standard player or chest inventories) + */ + public GuiSlots(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup slots, int firstSlot, int slotCount, int gridColumns) { + super(parent); + this.screenAccess = screenAccess; + this.slots = slots; + this.firstSlot = firstSlot; + this.slotCount = slotCount; + this.columns = gridColumns; + if (firstSlot + slotCount > slots.size()) { + throw new IllegalStateException("Specified slot range is out of bounds, Last slot in group is at index " + (slots.size() - 1) + " Specified range is from index " + firstSlot + " to " + (firstSlot + slotCount - 1)); + } + int columns = Math.min(gridColumns, slots.size()); + this.constrain(WIDTH, Constraint.dynamic(() -> (double) (columns * 18) + ((columns - 1) * xSlotSpacing))); + int rows = Math.max(1, slots.size() / gridColumns); + this.constrain(GeoParam.HEIGHT, Constraint.dynamic(() -> (double) (rows * 18) + ((rows - 1) * ySlotSpacing))); + for (int index = 0; index < slotCount; index++) { + Slot slot = slots.getSlot(index + firstSlot); + getModularGui().setSlotHandler(slot, this); + } + + updateSlots(parent.getModularGui().getRoot()); + } + + //=== Construction Helpers ===// + + public static GuiSlots singleSlot(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup slots) { + return singleSlot(parent, screenAccess, slots, 0); + } + + public static GuiSlots singleSlot(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup slots, int index) { + return new GuiSlots(parent, screenAccess, slots, index, 1, 1); + } + + public static Player player(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup mainSlots, SlotGroup hotBarSlots) { + return player(parent, screenAccess, mainSlots, hotBarSlots, 3); + } + + public static Player player(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup mainSlots, SlotGroup hotBarSlots, int hotBarSpacing) { + int width = 18 * 9; + int height = 18 * 4 + hotBarSpacing; + GuiElement container = new GuiElement<>(parent) + .setZStacking(false) + .constrain(WIDTH, Constraint.literal(width)) + .constrain(HEIGHT, Constraint.literal(height)); + + GuiSlots main = new GuiSlots(container, screenAccess, mainSlots, 9) + .constrain(TOP, Constraint.midPoint(container.get(TOP), container.get(BOTTOM), height / -2D)) + .constrain(LEFT, Constraint.midPoint(container.get(LEFT), container.get(RIGHT), width / -2D)); + + GuiSlots bar = new GuiSlots(container, screenAccess, hotBarSlots, 9) + .constrain(TOP, relative(main.get(BOTTOM), hotBarSpacing)) + .constrain(LEFT, match(main.get(LEFT))); + return new Player(container, main, bar); + } + + public static PlayerWithArmor playerWithArmor(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup mainSlots, SlotGroup hotBarSlots, SlotGroup armorSlots) { + return playerWithArmor(parent, screenAccess, mainSlots, hotBarSlots, armorSlots, 3, true); + } + + public static PlayerWithArmor playerWithArmor(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup mainSlots, SlotGroup hotBarSlots, SlotGroup armorSlots, int groupSpacing, boolean slotIcons) { + int width = 18 * 10 + groupSpacing; + int height = 18 * 4 + groupSpacing; + GuiElement container = new GuiElement<>(parent) + .setZStacking(false) + .constrain(WIDTH, Constraint.literal(width)) + .constrain(HEIGHT, Constraint.literal(height)); + + GuiSlots armor = new GuiSlots(container, screenAccess, armorSlots, 1) + .setYSlotSpacing(groupSpacing / 3) + .setEmptyIcon(index -> slotIcons ? ARMOR_SLOTS[index] : null) + .constrain(TOP, Constraint.midPoint(container.get(TOP), container.get(BOTTOM), height / -2D)) + .constrain(LEFT, Constraint.midPoint(container.get(LEFT), container.get(RIGHT), width / -2D)); + + GuiSlots main = new GuiSlots(container, screenAccess, mainSlots, 9) + .constrain(TOP, match(armor.get(TOP))) + .constrain(LEFT, relative(armor.get(RIGHT), groupSpacing)); + + GuiSlots bar = new GuiSlots(container, screenAccess, hotBarSlots, 9) + .constrain(TOP, relative(main.get(BOTTOM), groupSpacing)) + .constrain(LEFT, match(main.get(LEFT))); + + return new PlayerWithArmor(container, main, bar, armor); + } + + public static PlayerAll playerAllSlots(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup mainSlots, SlotGroup hotBarSlots, SlotGroup armorSlots, SlotGroup offhandSlots) { + return playerAllSlots(parent, screenAccess, mainSlots, hotBarSlots, armorSlots, offhandSlots, 3, true); + } + + public static PlayerAll playerAllSlots(@NotNull GuiParent parent, ContainerScreenAccess screenAccess, SlotGroup mainSlots, SlotGroup hotBarSlots, SlotGroup armorSlots, SlotGroup offhandSlots, int groupSpacing, boolean slotIcons) { + int width = 18 * 11 + groupSpacing * 2; + int height = 18 * 4 + groupSpacing; + GuiElement container = new GuiElement<>(parent) + .setZStacking(false) + .constrain(WIDTH, Constraint.literal(width)) + .constrain(HEIGHT, Constraint.literal(height)); + + GuiSlots armor = new GuiSlots(container, screenAccess, armorSlots, 1) + .setYSlotSpacing(groupSpacing / 3) + .setEmptyIcon(index -> slotIcons ? ARMOR_SLOTS[index] : null) + .constrain(TOP, Constraint.midPoint(container.get(TOP), container.get(BOTTOM), height / -2D)) + .constrain(LEFT, Constraint.midPoint(container.get(LEFT), container.get(RIGHT), width / -2D)); + + GuiSlots main = new GuiSlots(container, screenAccess, mainSlots, 9) + .constrain(TOP, match(armor.get(TOP))) + .constrain(LEFT, relative(armor.get(RIGHT), groupSpacing)); + + GuiSlots bar = new GuiSlots(container, screenAccess, hotBarSlots, 9) + .constrain(TOP, relative(main.get(BOTTOM), groupSpacing)) + .constrain(LEFT, match(main.get(LEFT))); + + GuiSlots offHand = new GuiSlots(container, screenAccess, offhandSlots, 1) + .setEmptyIcon(index -> slotIcons ? OFF_HAND_SLOT : null) + .constrain(TOP, match(bar.get(TOP))) + .constrain(LEFT, relative(bar.get(RIGHT), groupSpacing)); + + return new PlayerAll(container, main, bar, armor, offHand); + } + + //=== Slots Setup ===// + + /** + * Allows you to use a custom slot texture, The default is the standard vanilla slot. + */ + public GuiSlots setSlotTexture(Material slotTexture) { + this.slotTexture = slotTexture; + return this; + } + + /** + * Sets a custom slot highlight colour (The highlight you get when your cursor is over a slot.) + */ + public GuiSlots setHighlightColour(int highlightColour) { + return setHighlightColour(slot -> highlightColour); + } + + /** + * Allows you to set per-slot highlight colours, The integer passed to the function is the + * index of the slot within the {@link SlotGroup} + */ + public GuiSlots setHighlightColour(Function highlightColour) { + this.highlightColour = highlightColour; + return this; + } + + /** + * Applies a single empty slot icon to all slots. + * Recommended texture size is 16x16 + */ + public GuiSlots setEmptyIcon(Material texture) { + return setEmptyIcon(index -> texture); + } + + /** + * Allows you to provide a texture to be rendered in each slot when the slot is empty. + * Recommended texture size is 16x16 + * + * @param slotIcons A function that is given the slot index within the {@link SlotGroup}, and should return a material or null. + */ + public GuiSlots setEmptyIcon(Function slotIcons) { + this.slotIcons = slotIcons; + return this; + } + + public GuiSlots setXSlotSpacing(int xSlotSpacing) { + this.xSlotSpacing = xSlotSpacing; + return this; + } + + public GuiSlots setYSlotSpacing(int ySlotSpacing) { + this.ySlotSpacing = ySlotSpacing; + return this; + } + + public GuiSlots setSlotSpacing(int xSlotSpacing, int ySlotSpacing) { + this.xSlotSpacing = xSlotSpacing; + this.ySlotSpacing = ySlotSpacing; + return this; + } + + //=== Internal Methods ===// + + @Override + public double getBackgroundDepth() { + return 33; + } + + private void updateSlots(GuiElement root) { + int columns = Math.min(this.columns, slots.size()); + int rows = Math.max(1, slots.size() / columns); + double width = (columns * 18) + (columns - 1) * xSlotSpacing; + double height = (rows * 18) + (rows - 1) * ySlotSpacing; + int top = (int) (yCenter() - (height / 2) - root.yMin()); + int left = (int) (xCenter() - (width / 2) - root.xMin()); + + for (int index = 0; index < slotCount; index++) { + Slot slot = slots.getSlot(index + firstSlot); + int x = index % columns; + int y = index / columns; + slot.x = left + (x * 18) + 1 + (x * xSlotSpacing); + slot.y = top + (y * 18) + 1 + (y * ySlotSpacing); + } + } + + @Override + public void tick(double mouseX, double mouseY) { + super.tick(mouseX, mouseY); + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + GuiElement root = getModularGui().getRoot(); + updateSlots(root); + + Slot highlightSlot = null; + render.pose().pushPose(); + + for (int index = 0; index < slotCount; index++) { + Slot slot = slots.getSlot(index + firstSlot); + render.texRect(slotTexture, slot.x + root.xMin() - 1, slot.y + root.yMin() - 1, 18, 18); + } + + render.pose().translate(0, 0, 0.4); + + for (int index = 0; index < slotCount; index++) { + Slot slot = slots.getSlot(index + firstSlot); + if (!slot.isActive()) continue; + if (!slot.hasItem()) { + Material icon = slotIcons.apply(index + firstSlot); + if (icon != null) { + render.texRect(icon, slot.x + root.xMin(), slot.y + root.yMin(), 16, 16); + } + } + + screenAccess.renderSlot(render, slot); + if (GuiRender.isInRect(slot.x + root.xMin(), slot.y + root.yMin(), 16, 16, mouseX, mouseY) && !blockMouseOver(this, mouseX, mouseY)) { + highlightSlot = slot; + } + } + + if (highlightSlot != null) { + render.pose().translate(0, 0, getBackgroundDepth() - 0.8); + render.rect(highlightSlot.x + root.xMin(), highlightSlot.y + root.yMin(), 16, 16, highlightColour.apply(slots.indexOf(highlightSlot))); + } + + render.pose().popPose(); + } + + public record Player(GuiElement container, GuiSlots main, GuiSlots hotBar) {} + + public record PlayerWithArmor(GuiElement container, GuiSlots main, GuiSlots hotBar, GuiSlots armor) {} + + public record PlayerAll(GuiElement container, GuiSlots main, GuiSlots hotBar, GuiSlots armor, GuiSlots offHand) {} +} diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiText.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiText.java new file mode 100644 index 00000000..2776c2d5 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiText.java @@ -0,0 +1,268 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.ForegroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.Align; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.lib.geometry.Position; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.gui.Font; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.FormattedCharSequence; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.Align.MAX; +import static codechicken.lib.gui.modular.lib.geometry.Align.MIN; +import static codechicken.lib.gui.modular.lib.geometry.Constraint.dynamic; + +/** + * Created by brandon3055 on 31/08/2023 + */ +public class GuiText extends GuiElement implements ForegroundRender { + private Supplier text; + private Supplier shadow = () -> true; + private Supplier textColour = () -> 0xFFFFFFFF; + private Supplier rotation = null; + private Position rotatePoint = Position.create(() -> xSize() / 2, () -> ySize() / 2); + private boolean trim = false; + private boolean wrap = false; + private boolean scroll = true; + private Align alignment = Align.CENTER; + //TODO, Arbitrary rotation is fun, But may want to switch to Axis, with option for "reverse" + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiText(@NotNull GuiParent parent) { + this(parent, () -> null); + } + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiText(@NotNull GuiParent parent, @Nullable Component text) { + this(parent, () -> text); + } + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiText(@NotNull GuiParent parent, @NotNull Supplier<@Nullable Component> text) { + super(parent); + this.text = text; + } + + /** + * Apply a dynamic height constraint that sets the height based on text height (accounting for wrapping) + */ + public GuiText autoHeight() { + constrain(GeoParam.HEIGHT, dynamic(() -> wrap ? (double) font().wordWrapHeight(getText(), (int) xSize()) : font().lineHeight)); + return this; + } + + public GuiText setTextSupplier(@NotNull Supplier<@Nullable Component> textSupplier) { + this.text = textSupplier; + return this; + } + + public GuiText setText(@Nullable Component text) { + this.text = () -> text; + return this; + } + + public GuiText setText(@NotNull String text) { + this.text = () -> Component.literal(text); + return this; + } + + public GuiText setTranslatable(@NotNull String translationKey) { + this.text = () -> Component.translatable(translationKey); + return this; + } + + @Nullable + public Component getText() { + return text.get(); + } + + public GuiText setAlignment(Align alignment) { + this.alignment = alignment; + return this; + } + + public Align getAlignment() { + return alignment; + } + + /** + * If set to true the text will be trimmed if it is too long to fit within the bounds on the element. + * Default disabled. + * Setting this to true will automatically disable wrap and scroll ether are enabled. + */ + public GuiText setTrim(boolean trim) { + this.trim = trim; + if (trim) wrap = scroll = false; + return this; + } + + public boolean getTrim() { + return trim; + } + + /** + * Set to true the text will be wrapped (rendered as multiple lines of text) if it is too long to fit within the size of the element. + * Default disabled. + * Setting this to true will automatically disable trim and scroll ether are enabled. + */ + public GuiText setWrap(boolean wrap) { + this.wrap = wrap; + if (wrap) trim = scroll = false; + return this; + } + + public boolean getWrap() { + return wrap; + } + + /** + * Set to true the text scroll using the same logic as vanillas widgets if the text is too long to fit within the elements bounds + * Default enabled. + * Setting this to true will automatically disable trim and wrap ether are enabled. + */ + public GuiText setScroll(boolean scroll) { + this.scroll = scroll; + if (scroll) trim = wrap = false; + return this; + } + + public boolean getScroll() { + return scroll; + } + + public GuiText setShadow(@NotNull Supplier shadow) { + this.shadow = shadow; + return this; + } + + public GuiText setShadow(boolean shadow) { + this.shadow = () -> shadow; + return this; + } + + public boolean getShadow() { + return shadow.get(); + } + + public GuiText setTextColour(Supplier textColour) { + this.textColour = textColour; + return this; + } + + public GuiText setTextColour(int textColour) { + this.textColour = () -> textColour; + return this; + } + + public int getTextColour() { + return textColour.get(); + } + + /** + * Set rotation angle in degrees + * //TODO, Does not work when text scroll is used + */ + public GuiText setRotation(double rotation) { + return setRotation(() -> rotation); + } + + /** + * Dynamic rotation control, Set to null to disable rotation. + * Rotation angle is in degrees + * //TODO, Does not work when text scroll is used + */ + public GuiText setRotation(@Nullable Supplier rotation) { + this.rotation = rotation; + return this; + } + + /** + * Sets the point around which the text rotates relative to the top left of the element. + * Default rotation point is a dynamic point set to the dead center of the element. + */ + public GuiText setRotatePoint(Position rotatePoint) { + this.rotatePoint = rotatePoint; + return this; + } + + @Override + public double getForegroundDepth() { + return 0.035; + } + + @Override + public void renderForeground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + Component component = getText(); + if (component == null) return; + Font font = render.font(); + + int textHeight = font.lineHeight; + int textWidth = font.width(component); + boolean tooLong = textWidth > xSize(); + double yPos = (yMin() + ySize() / 2 - textHeight / 2D) + 1; //Adding 1 here makes the text look 'visually' centered, Text height includes the height of the optional underline. + + PoseStack stack = render.pose(); + if (rotation != null) { + stack.pushPose(); + stack.translate(xMin() + rotatePoint.x(), yMin() + rotatePoint.y(), 0); + stack.mulPose(Axis.ZP.rotationDegrees(rotation.get().floatValue())); + stack.translate(-xMin() - rotatePoint.x(), -yMin() - rotatePoint.y(), 0); + } + + //Draw Trimmed + if (tooLong && getTrim()) { + Component tail = Component.literal("...").setStyle(getText().getStyle()); + FormattedText head = font.getSplitter().headByWidth(component, (int) xSize() - font.width(tail), getText().getStyle()); + FormattedCharSequence formatted = Language.getInstance().getVisualOrder(FormattedText.composite(head, tail)); + textWidth = font.width(formatted); + + double xPos = alignment == MIN ? xMin() : alignment == MAX ? xMax() - textWidth : xMin() + xSize() / 2 - textWidth / 2D; + render.drawString(formatted, xPos, yPos, getTextColour(), getShadow()); + } + //Draw Wrapped + else if (tooLong && wrap) { + textHeight = font.wordWrapHeight(component, (int) xSize()); + List list = font.split(component, (int) xSize()); + + yPos = yMin() + ySize() / 2 - textHeight / 2D; + for (FormattedCharSequence line : list) { + int lineWidth = font.width(line); + double xPos = alignment == MIN ? xMin() : alignment == MAX ? xMax() - lineWidth : xMin() + xSize() / 2 - lineWidth / 2D; + render.drawString(line, xPos, yPos, getTextColour(), getShadow()); + yPos += font.lineHeight; + } + } + //Draw Scrolling + else if (tooLong && scroll) { + render.pushScissorRect(getRectangle()); + render.drawScrollingString(component, xMin(), yPos, xMax(), getTextColour(), getShadow(), false); + render.popScissor(); + } + //Draw + else { + double xPos = alignment == MIN ? xMin() : alignment == MAX ? xMax() - textWidth : xMin() + xSize() / 2 - textWidth / 2D; + render.drawString(component, xPos, yPos, getTextColour(), getShadow()); + } + + if (rotation != null) { + stack.popPose(); + } + } +} \ No newline at end of file diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiTextField.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiTextField.java new file mode 100644 index 00000000..680b918c --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiTextField.java @@ -0,0 +1,660 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.TextState; +import codechicken.lib.gui.modular.lib.geometry.Constraint; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.SharedConstants; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +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.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * TODO, Re write this, Its currently mostly pulled from the TextField in Gui v2 + *

+ * Created by brandon3055 on 03/09/2023 + */ +public class GuiTextField extends GuiElement implements BackgroundRender { + private static final RenderType HIGHLIGHT_TYPE = RenderType.create("text_field_highlight", DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.QUADS, 256, RenderType.CompositeState.builder() + .setShaderState(new RenderStateShard.ShaderStateShard(GameRenderer::getPositionColorShader)) + .setColorLogicState(RenderStateShard.OR_REVERSE_COLOR_LOGIC) + .createCompositeState(false)); + + private int tick; + private int cursorPos; + private int maxLength = 32; + private int displayPos; + private int highlightPos; + private boolean focused; + private boolean shiftPressed; + + private Supplier isEditable = () -> true; + private Supplier isFocusable = () -> true; + private Supplier canLoseFocus = () -> true; + + private Runnable onEditComplete = null; + private Runnable onEnterPressed = null; + + private TextState textState = TextState.simpleState(""); + private Supplier shadow = () -> true; + private Supplier textColor = () -> 0xe0e0e0; + + private Supplier suggestion = null; + private Supplier suggestionColour = () -> 0x7f7f80; + + private Predicate filter = Objects::nonNull; + private BiFunction formatter = (string, pos) -> FormattedCharSequence.forward(string, Style.EMPTY); + + public GuiTextField(@NotNull GuiParent parent) { + super(parent); + } + + /** + * Creates a simple text box with a simple bordered background. + * Using colours 0xFF000000, 0xFFFFFFFF, 0xE0E0E0 will get you a text box identical to the one in a command block + */ + public static TextField create(GuiElement parent, int backgroundColour, int borderColour, int textColour) { + GuiRectangle background = new GuiRectangle(parent) + .rectangle(backgroundColour, borderColour); + + GuiTextField textField = new GuiTextField(background) + .setTextColor(textColour) + .constrain(TOP, Constraint.relative(background.get(TOP), 1)) + .constrain(BOTTOM, Constraint.relative(background.get(BOTTOM), -1)) + .constrain(LEFT, Constraint.relative(background.get(LEFT), 4)) + .constrain(RIGHT, Constraint.relative(background.get(RIGHT), -4)); + + return new TextField(background, textField); + } + + //=== Text field setup ===// + + /** + * Called when the user clicks outside the text box, or when they press enter + */ + public GuiTextField setOnEditComplete(Runnable onEditComplete) { + this.onEditComplete = onEditComplete; + return this; + } + + /** + * Called when the user presses enter key (Including numpad enter) with the text box ion focus + */ + public GuiTextField setEnterPressed(Runnable onEnterPressed) { + this.onEnterPressed = onEnterPressed; + return this; + } + + /** + * The {@link TextState} is an accessor for the current text value. + * It simply contains string getter and setter methods. + * You can use this to link the text field to some external value. + */ + public GuiTextField setTextState(TextState textState) { + this.textState = textState; + return this; + } + + public TextState getTextState() { + return textState; + } + + public GuiTextField setTextColor(Supplier textColor) { + this.textColor = textColor; + return this; + } + + public GuiTextField setTextColor(int textColor) { + return setTextColor(() -> textColor); + } + + /** + * Should the text be rendered with a shadow? + */ + public GuiTextField setShadow(Supplier shadow) { + this.shadow = shadow; + return this; + } + + /** + * Should the text be rendered with a shadow? + */ + public GuiTextField setShadow(boolean shadow) { + return setShadow(() -> shadow); + } + + /** + * Set the "suggestion" text that is displayed when the text field is empty. + */ + public GuiTextField setSuggestion(Component suggestion) { + return setSuggestion(suggestion == null ? null : () -> suggestion); + } + + /** + * Set the "suggestion" text that is displayed when the text field is empty. + */ + public GuiTextField setSuggestion(@Nullable Supplier suggestion) { + this.suggestion = suggestion; + return this; + } + + /** + * Set the colour of the suggestion text. + */ + public void setSuggestionColour(Supplier suggestionColour) { + this.suggestionColour = suggestionColour; + } + + /** + * Allows you to apply a fielder to this text field. + * Whenever this field's value is updated, If the new value does not pass this filter + * It will not be applied. + */ + public GuiTextField setFilter(Predicate filter) { + this.filter = filter; + return this; + } + + /** + * Formats the current text value for display to the user. + * The integer is the current display start position within the current field value. + */ + public GuiTextField setFormatter(BiFunction formatter) { + this.formatter = formatter; + return this; + } + + /** + * If set to false, it will not be possible for the text field to lose focus via normal means. + * Focus can still be set via {@link #setFocus(boolean)} + */ + public GuiTextField setCanLoseFocus(boolean canLoseFocus) { + return setCanLoseFocus(() -> canLoseFocus); + } + + /** + * If set to false, it will not be possible for the text field to lose focus via normal means. + * Focus can still be set via {@link #setFocus(boolean)} + */ + public GuiTextField setCanLoseFocus(Supplier canLoseFocus) { + this.canLoseFocus = canLoseFocus; + return this; + } + + /** + * If false, It will not be possible to focus this element by clicking on it. + */ + public GuiTextField setFocusable(boolean focusable) { + return setFocusable(() -> focusable); + } + + /** + * If false, It will not be possible to focus this element by clicking on it. + */ + public GuiTextField setFocusable(Supplier focusable) { + this.isFocusable = focusable; + return this; + } + + /** + * If false, It will not be possible for the user to edit the value of this text field. + */ + public GuiTextField setEditable(boolean editable) { + return setEditable(() -> editable); + } + + /** + * If false, It will not be possible for the user to edit the value of this text field. + */ + public GuiTextField setEditable(Supplier editable) { + this.isEditable = editable; + return this; + } + + /** + * Sets the maximum allowed text length + */ + public GuiTextField setMaxLength(int newWidth) { + String value = getValue(); + maxLength = newWidth; + if (value.length() > newWidth) { + textState.setText(value.substring(0, newWidth)); + } + return this; + } + + private int getMaxLength() { + return maxLength; + } + + //=== Text field logic ===// + + /** + * Note, Initial value should be set after element is constrained, + * If element width is zero when set, nothing will render until the field is updated. + * TODO, I need to fix this. Element size should be able to change dynamically without things breaking. + */ + public GuiTextField setValue(String newValue) { + if (this.filter.test(newValue)) { + if (newValue.length() > maxLength) { + textState.setText(newValue.substring(0, maxLength)); + } else { + textState.setText(newValue); + } + moveCursorToEnd(); + setHighlightPos(cursorPos); + } + return this; + } + + public String getValue() { + return textState.getText(); + } + + public String getHighlighted() { + int i = Math.min(cursorPos, highlightPos); + int j = Math.max(cursorPos, highlightPos); + return getValue().substring(i, j); + } + + public void insertText(String text) { + String value = getValue(); + int selectStart = Math.min(cursorPos, highlightPos); + int selectEnd = Math.max(cursorPos, highlightPos); + int freeSpace = maxLength - value.length() - (selectStart - selectEnd); + String toInsert = SharedConstants.filterText(text); + int insertLen = toInsert.length(); + if (freeSpace < insertLen) { + toInsert = toInsert.substring(0, freeSpace); + insertLen = freeSpace; + } + + String newValue = (new StringBuilder(value)).replace(selectStart, selectEnd, toInsert).toString(); + if (filter.test(newValue)) { + textState.setText(newValue); + setCursorPosition(selectStart + insertLen); + setHighlightPos(cursorPos); + } + } + + private void deleteText(int i) { + if (Screen.hasControlDown()) { + deleteWords(i); + } else { + deleteChars(i); + } + } + + public void deleteWords(int i) { + if (!getValue().isEmpty()) { + if (highlightPos != cursorPos) { + insertText(""); + } else { + deleteChars(getWordPosition(i) - cursorPos); + } + } + } + + public void deleteChars(int i1) { + String value = getValue(); + if (!value.isEmpty()) { + if (highlightPos != cursorPos) { + insertText(""); + } else { + int i = getCursorPos(i1); + int j = Math.min(i, cursorPos); + int k = Math.max(i, cursorPos); + if (j != k) { + String s = (new StringBuilder(value)).delete(j, k).toString(); + if (filter.test(s)) { + textState.setText(s); + moveCursorTo(j); + } + } + } + } + } + + public int getWordPosition(int i) { + return getWordPosition(i, getCursorPosition()); + } + + private int getWordPosition(int i, int i1) { + return getWordPosition(i, i1, true); + } + + private int getWordPosition(int i1, int i2, boolean b) { + String value = getValue(); + int i = i2; + boolean flag = i1 < 0; + int j = Math.abs(i1); + + for (int k = 0; k < j; ++k) { + if (!flag) { + int l = value.length(); + i = value.indexOf(32, i); + if (i == -1) { + i = l; + } else { + while (b && i < l && value.charAt(i) == ' ') ++i; + } + } else { + while (b && i > 0 && value.charAt(i - 1) == ' ') --i; + while (i > 0 && value.charAt(i - 1) != ' ') --i; + } + } + + return i; + } + + public void moveCursor(int pos) { + moveCursorTo(getCursorPos(pos)); + } + + private int getCursorPos(int i) { + return Util.offsetByCodepoints(getValue(), cursorPos, i); + } + + public void moveCursorTo(int pos, boolean notify) { + setCursorPosition(pos); + if (!shiftPressed) { + setHighlightPos(cursorPos); + } + } + + public void moveCursorTo(int pos) { + moveCursorTo(pos, true); + } + + public void setCursorPosition(int pos) { + cursorPos = Mth.clamp(pos, 0, getValue().length()); + } + + public void moveCursorToStart() { + moveCursorTo(0); + } + + public void moveCursorToEnd(boolean notify) { + moveCursorTo(getValue().length(), notify); + } + + public void moveCursorToEnd() { + moveCursorToEnd(true); + } + + public boolean isEditable() { + return isEditable.get(); + } + + public void setFocus(boolean focused) { + if (this.focused && !focused && onEditComplete != null) { + onEditComplete.run(); + } + this.focused = focused; + } + + public boolean isFocused() { + return focused; + } + + public int getCursorPosition() { + return cursorPos; + } + + public void setHighlightPos(int newPos) { + String value = getValue(); + int length = value.length(); + highlightPos = Mth.clamp(newPos, 0, length); + + if (displayPos > length) { + displayPos = length; + } + + int width = (int) xSize(); + String visibleText = font().plainSubstrByWidth(value.substring(displayPos), width); + int endPos = displayPos + visibleText.length(); + if (highlightPos == displayPos) { + displayPos -= font().plainSubstrByWidth(value, width, true).length(); + } + + if (highlightPos > endPos) { + displayPos += highlightPos - endPos; + } else if (highlightPos <= displayPos) { + displayPos -= displayPos - highlightPos; + } + + displayPos = Mth.clamp(displayPos, 0, length); + } + + //=== Input Handling ===// + + @Override + public boolean keyPressed(int key, int scancode, int modifiers) { + if (!canConsumeInput()) { + return false; + } else { + shiftPressed = Screen.hasShiftDown(); + if (Screen.isSelectAll(key)) { + moveCursorToEnd(); + setHighlightPos(0); + return true; + } else if (Screen.isCopy(key)) { + Minecraft.getInstance().keyboardHandler.setClipboard(getHighlighted()); + return true; + } else if (Screen.isPaste(key)) { + if (isEditable()) { + insertText(Minecraft.getInstance().keyboardHandler.getClipboard()); + } + + return true; + } else if (Screen.isCut(key)) { + Minecraft.getInstance().keyboardHandler.setClipboard(getHighlighted()); + if (isEditable()) { + insertText(""); + } + + return true; + } else { + switch (key) { + case InputConstants.KEY_BACKSPACE: + if (isEditable()) { + shiftPressed = false; + deleteText(-1); + shiftPressed = Screen.hasShiftDown(); + } + + return true; + case InputConstants.KEY_NUMPADENTER: + case InputConstants.KEY_RETURN: { + if (onEditComplete != null) { + onEditComplete.run(); + } + if (onEnterPressed != null) { + onEnterPressed.run(); + } + } + case InputConstants.KEY_INSERT: + case InputConstants.KEY_DOWN: + case InputConstants.KEY_UP: + case InputConstants.KEY_PAGEUP: + case InputConstants.KEY_PAGEDOWN: + default: + //Consume key presses when we are typing so we dont do something dumb like close the screen when you type e + return key != GLFW.GLFW_KEY_ESCAPE; + case InputConstants.KEY_DELETE: + if (isEditable()) { + shiftPressed = false; + deleteText(1); + shiftPressed = Screen.hasShiftDown(); + } + return true; + case InputConstants.KEY_RIGHT: + if (Screen.hasControlDown()) { + moveCursorTo(getWordPosition(1)); + } else { + moveCursor(1); + } + return true; + case InputConstants.KEY_LEFT: + if (Screen.hasControlDown()) { + moveCursorTo(getWordPosition(-1)); + } else { + moveCursor(-1); + } + return true; + case InputConstants.KEY_HOME: + moveCursorToStart(); + return true; + case InputConstants.KEY_END: + moveCursorToEnd(); + return true; + } + } + } + } + + public boolean canConsumeInput() { + return isFocused() && isEditable() && isEnabled(); + } + + @Override + public boolean keyReleased(int key, int scancode, int modifiers, boolean consumed) { + this.shiftPressed = Screen.hasShiftDown(); + return super.keyReleased(key, scancode, modifiers, consumed); + } + + @Override + public boolean charTyped(char charTyped, int charCode) { + if (!canConsumeInput()) { + return false; + } else if (SharedConstants.isAllowedChatCharacter(charTyped)) { + if (isEditable()) { + insertText(Character.toString(charTyped)); + } + return true; + } else { + return false; + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button, boolean consumed) { + consumed = super.mouseClicked(mouseX, mouseY, button, consumed); + + boolean mouseOver = isMouseOver(); + if (isFocused() && !mouseOver) { + setFocus(isFocusable.get() && !canLoseFocus.get()); + } + + if (consumed) return true; + + if (canLoseFocus.get()) { + setFocus(mouseOver && isFocusable.get()); + } else { + setFocus(isFocusable.get()); + } + + if (isFocused() && mouseOver && button == 0) { + int i = (int) (Mth.floor(mouseX) - xMin()); + String s = font().plainSubstrByWidth(getValue().substring(displayPos), (int) xSize()); + moveCursorTo(font().plainSubstrByWidth(s, i).length() + displayPos); + return true; + } else { + return false; + } + } + + //=== Rendering ===// + + @Override + public double getBackgroundDepth() { + return 0.04; + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + String value = getValue(); + int colour = textColor.get(); + + int textStart = cursorPos - displayPos; + int highlightStart = highlightPos - displayPos; + String displayText = font().plainSubstrByWidth(value.substring(displayPos), (int) xSize()); + boolean flag = textStart >= 0 && textStart <= displayText.length(); + boolean cursorBlink = isFocused() && tick / 6 % 2 == 0 && flag; + double drawX = xMin(); + double drawY = yMin() + ((ySize() - 8) / 2); + int drawEnd = (int) drawX; + + if (highlightStart > displayText.length()) { + highlightStart = displayText.length(); + } + + if (!displayText.isEmpty()) { + String drawString = flag ? displayText.substring(0, textStart) : displayText; + drawEnd = render.drawString(formatter.apply(drawString, displayPos), drawX, drawY, colour, shadow.get()); + } + + boolean flag2 = cursorPos < value.length() || value.length() >= getMaxLength(); + int k1 = drawEnd; + + if (!flag) { + k1 = (int) (textStart > 0 ? drawX + xSize() : drawX); + } else if (flag2) { + k1 = drawEnd - 1; + --drawEnd; + } + + if (!displayText.isEmpty() && flag && textStart < displayText.length()) { + render.drawString(formatter.apply(displayText.substring(textStart), cursorPos), drawEnd, drawY, colour, shadow.get()); + } + + if (suggestion != null && value.isEmpty()) { + render.drawString(suggestion.get(), (float) (k1 - 1), (float) drawY, suggestionColour.get(), shadow.get()); + } + + if (cursorBlink) { + if (flag2) { + render.fill(k1, drawY - 1, k1 + 1, drawY + 1 + 9, -3092272); + } else { + render.drawString("_", (float) k1, (float) drawY, colour, shadow.get()); + } + } + + if (highlightStart != textStart) { + int l1 = (int) (drawX + font().width(displayText.substring(0, highlightStart))); + render.pose().translate(0, 0, 0.035); + render.fill(HIGHLIGHT_TYPE, k1, drawY - 1, l1 - 1, drawY + 1 + 9, 0xFF0000FF); + render.pose().translate(0, 0, -0.035); + } + } + + @Override + public void tick(double mouseX, double mouseY) { + super.tick(mouseX, mouseY); + tick++; + } + + public record TextField(GuiRectangle container, GuiTextField field) {} +} + diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiTextList.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiTextList.java new file mode 100644 index 00000000..865b935c --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiTextList.java @@ -0,0 +1,173 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.ForegroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.Align; +import codechicken.lib.gui.modular.lib.geometry.GeoParam; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.Align.MAX; +import static codechicken.lib.gui.modular.lib.geometry.Align.MIN; +import static codechicken.lib.gui.modular.lib.geometry.Constraint.dynamic; + +/** + * Created by brandon3055 on 10/10/2023 + */ +public class GuiTextList extends GuiElement implements ForegroundRender { + private Supplier> text; + private Supplier shadow = () -> true; + private Supplier textColour = () -> 0xFFFFFFFF; + private boolean scroll = true; + private Align horizontalAlign = Align.CENTER; + private Align verticalAlign = Align.TOP; + private int lineSpacing = 0; + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiTextList(@NotNull GuiParent parent) { + this(parent, () -> null); + } + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiTextList(@NotNull GuiParent parent, List text) { + this(parent, () -> text); + } + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiTextList(@NotNull GuiParent parent, @NotNull Supplier> text) { + super(parent); + this.text = text; + } + + /** + * Apply a dynamic height constraint that sets the height based on text height (accounting for wrapping) + */ + public GuiTextList autoHeight() { + constrain(GeoParam.HEIGHT, dynamic(() -> (double) ((font().lineHeight + lineSpacing) * getText().size()) - 1)); + return this; + } + + public GuiTextList setTextSupplier(@NotNull Supplier> textSupplier) { + this.text = textSupplier; + return this; + } + + public GuiTextList setText(List text) { + this.text = () -> text; + return this; + } + + public List getText() { + return text.get(); + } + + public GuiTextList setHorizontalAlign(Align horizontalAlign) { + this.horizontalAlign = horizontalAlign; + return this; + } + + public GuiTextList setVerticalAlign(Align verticalAlign) { + this.verticalAlign = verticalAlign; + return this; + } + + public Align getHorizontalAlign() { + return horizontalAlign; + } + + public Align getVerticalAlign() { + return verticalAlign; + } + + public GuiTextList setLineSpacing(int lineSpacing) { + this.lineSpacing = lineSpacing; + return this; + } + + public int getLineSpacing() { + return lineSpacing; + } + + /** + * Set to true the text scroll using the same logic as vanillas widgets if the text is too long to fit within the elements bounds + * Default enabled. + * Setting this to true will automatically disable trim and wrap ether are enabled. + */ + public GuiTextList setScroll(boolean scroll) { + this.scroll = scroll; + return this; + } + + public boolean getScroll() { + return scroll; + } + + public GuiTextList setShadow(@NotNull Supplier shadow) { + this.shadow = shadow; + return this; + } + + public GuiTextList setShadow(boolean shadow) { + this.shadow = () -> shadow; + return this; + } + + public boolean getShadow() { + return shadow.get(); + } + + public GuiTextList setTextColour(Supplier textColour) { + this.textColour = textColour; + return this; + } + + public GuiTextList setTextColour(int textColour) { + this.textColour = () -> textColour; + return this; + } + + public int getTextColour() { + return textColour.get(); + } + + @Override + public double getForegroundDepth() { + return 0.035; + } + + @Override + public void renderForeground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + List list = getText(); + if (list.isEmpty()) return; + Font font = render.font(); + + double height = (list.size() * (font.lineHeight + lineSpacing)) - lineSpacing; + double yPos = verticalAlign == MIN ? yMin() : verticalAlign == MAX ? yMax() - height : (yCenter() - (height / 2)) + 1; + for (Component line : list) { + int textWidth = font.width(line); + boolean tooLong = textWidth > xSize(); + + if (tooLong && scroll) { + render.pushScissorRect(getRectangle()); + render.drawScrollingString(line, xMin(), yPos, xMax(), getTextColour(), getShadow(), false); + render.popScissor(); + } else { + double xPos = horizontalAlign == MIN ? xMin() : horizontalAlign == MAX ? xMax() - textWidth : xMin() + xSize() / 2 - textWidth / 2D; + render.drawString(line, xPos, yPos, getTextColour(), getShadow()); + } + + yPos += font.lineHeight + lineSpacing; + } + } +} \ No newline at end of file diff --git a/src/main/java/codechicken/lib/gui/modular/elements/GuiTexture.java b/src/main/java/codechicken/lib/gui/modular/elements/GuiTexture.java new file mode 100644 index 00000000..da5c0577 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/elements/GuiTexture.java @@ -0,0 +1,110 @@ +package codechicken.lib.gui.modular.elements; + +import codechicken.lib.gui.modular.lib.BackgroundRender; +import codechicken.lib.gui.modular.lib.GuiRender; +import codechicken.lib.gui.modular.lib.geometry.Borders; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.gui.modular.sprite.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 28/08/2023 + */ +public class GuiTexture extends GuiElement implements BackgroundRender { + private Supplier getMaterial; + private Supplier colour = () -> 0xFFFFFFFF; + private Borders dynamicBorders = null; + + /** + * @param parent parent {@link GuiParent}. + */ + public GuiTexture(@NotNull GuiParent parent) { + super(parent); + } + + public GuiTexture(@NotNull GuiParent parent, Supplier supplier) { + super(parent); + setMaterial(supplier); + } + + public GuiTexture(@NotNull GuiParent parent, Material material) { + super(parent); + setMaterial(material); + } + + public GuiTexture setMaterial(Supplier supplier) { + this.getMaterial = supplier; + return this; + } + + public GuiTexture setMaterial(Material material) { + this.getMaterial = () -> material; + return this; + } + + @Nullable + public Material getMaterial() { + return getMaterial == null ? null : getMaterial.get(); + } + + /** + * Enables dynamic texture resizing though the use of cutting and tiling. + * Only works with textures that can be cut up and tiled without issues, e.g. background textures or button textures. + * This method uses the standard border with of 5 pixels on all sides. + */ + public GuiTexture dynamicTexture() { + return dynamicTexture(5); + } + + /** + * Enables dynamic texture resizing though the use of cutting and tiling. + * Only works with textures that can be cut up and tiled without issues, e.g. background textures or button textures. + * The border parameters indicate the width of border around the texture that must be maintained during the cutting and tiling process. + * For standardisation purposes the border width should be >= 5 + */ + public GuiTexture dynamicTexture(int textureBorders) { + return dynamicTexture(Borders.create(textureBorders)); + } + + /** + * Enables dynamic texture resizing though the use of cutting and tiling. + * Only works with textures that can be cut up and tiled without issues, e.g. background textures or button textures. + * The border parameters indicate the width of border around the texture that must be maintained during the cutting and tiling process. + * For standardisation purposes the border width should be >= 5 + */ + public GuiTexture dynamicTexture(Borders textureBorders) { + dynamicBorders = textureBorders; + return this; + } + + /** + * Allows you to set an argb colour. + * This colour will be applied when rendering the texture. + */ + public GuiTexture setColour(int colourARGB) { + return setColour(() -> colourARGB); + } + + /** + * Allows you to set an argb colour provider. + * This colour will be applied when rendering the texture. + */ + public GuiTexture setColour(Supplier colour) { + this.colour = colour; + return this; + } + + @Override + public void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks) { + Material material = getMaterial(); + if (material == null) return; + if (dynamicBorders != null) { + render.dynamicTex(material, getRectangle(), dynamicBorders, colour.get()); + } else { + render.texRect(material, getRectangle(), colour.get()); + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/BackgroundRender.java b/src/main/java/codechicken/lib/gui/modular/lib/BackgroundRender.java new file mode 100644 index 00000000..0291e8af --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/BackgroundRender.java @@ -0,0 +1,34 @@ +package codechicken.lib.gui.modular.lib; + +import com.mojang.blaze3d.vertex.PoseStack; + +/** + * Allows a Gui Elements to render content behind child elements. + * This is the default render mode for the majority of elements. + *

+ * Created by brandon3055 on 07/08/2023 + */ +public interface BackgroundRender { + + /** + * Specifies the z depth of the background content. + * After {@link #renderBackground(GuiRender, double, double, float)} is called, the PoseStack will be translated by this amount in the z direction + * before any assigned child elements are rendered. + * Recommended minimum depth is 0.01 or 0.035 if this element renders text. (text shadows are rendered with a 0.03 offset) + * + * @return the z height of the background content. + */ + default double getBackgroundDepth() { + return 0.01; + } + + /** + * Used to render content behind this elements child elements. + * When rendering element content, always use the {@link PoseStack} available via the provided {@link GuiRender} + * Where applicable, always use push/pop to ensure the stack is returned to its original state after your rendering is complete. + * + * @param render Contains gui context information as well as essential render methods/utils including the PoseStack. + */ + void renderBackground(GuiRender render, double mouseX, double mouseY, float partialTicks); + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/ColourState.java b/src/main/java/codechicken/lib/gui/modular/lib/ColourState.java new file mode 100644 index 00000000..850e0f14 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/ColourState.java @@ -0,0 +1,72 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.colour.Colour; +import codechicken.lib.colour.ColourARGB; + +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 19/11/2023 + */ +public interface ColourState { + + int get(); + + default ColourARGB getColour() { + return new ColourARGB(get()); + } + + void set(int colour); + + default void set(Colour colour) { + set(colour.argb()); + } + + default String getHexColour() { + return Integer.toHexString(get()).toUpperCase(Locale.ROOT); + } + + default void setHexColour(String hexColour) { + try { + set(Integer.parseUnsignedInt(hexColour, 16)); + } catch (Throwable e) { + set(0); + } + } + + static ColourState create() { + return create(null); + } + + static ColourState create(Consumer listener) { + return new ColourState() { + int colour = 0; + @Override + public int get() { + return colour; + } + + @Override + public void set(int colour) { + this.colour = colour; + if (listener != null) listener.accept(colour); + } + }; + } + + static ColourState create(Supplier getter, Consumer setter) { + return new ColourState() { + @Override + public int get() { + return getter.get(); + } + + @Override + public void set(int colour) { + setter.accept(colour); + } + }; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/Constraints.java b/src/main/java/codechicken/lib/gui/modular/lib/Constraints.java new file mode 100644 index 00000000..1cb4de5b --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/Constraints.java @@ -0,0 +1,209 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.gui.modular.lib.geometry.Borders; +import codechicken.lib.gui.modular.lib.geometry.ConstrainedGeometry; + +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.Constraint.*; +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * This class contains a bunch of static helper methods that can be used to quickly apply common constraints. + * The plan is to keep adding common constraints to this class as they pop up. + *

+ * Created by brandon3055 on 28/08/2023 + */ +public class Constraints { + + /** + * Bind an elements geometry to a reference element. + * + * @param element The element to be bound. + * @param reference The element to be bound to. + */ + public static void bind(ConstrainedGeometry element, ConstrainedGeometry reference) { + bind(element, reference, 0.0); + } + + /** + * Bind an elements geometry to a reference element with border offsets. (Border offsets may be positive or negative) + * + * @param element The element to be bound. + * @param reference The element to be bound to. + */ + public static void bind(ConstrainedGeometry element, ConstrainedGeometry reference, double borders) { + bind(element, reference, borders, borders, borders, borders); + } + + /** + * Bind an elements geometry to a reference element with border offsets. (Border offsets may be positive or negative) + * + * @param element The element to be bound. + * @param reference The element to be bound to. + */ + public static void bind(ConstrainedGeometry element, ConstrainedGeometry reference, double top, double left, double bottom, double right) { + element.constrain(TOP, relative(reference.get(TOP), top)); + element.constrain(LEFT, relative(reference.get(LEFT), left)); + element.constrain(BOTTOM, relative(reference.get(BOTTOM), -bottom)); + element.constrain(RIGHT, relative(reference.get(RIGHT), -right)); + } + + /** + * Bind an elements geometry to a reference element with border offsets. (Border offsets may be positive or negative) + * The border offsets are dynamic, meaning if the values stored in the {@link Borders} object are updated, this binding will reflect those changes automatically. + * + * @param element The element to be bound. + * @param reference The element to be bound to. + * @param borders Border offsets. + */ + public static void bind(ConstrainedGeometry element, ConstrainedGeometry reference, Borders borders) { + element.constrain(TOP, relative(reference.get(TOP), borders::top)); + element.constrain(LEFT, relative(reference.get(LEFT), borders::left)); + element.constrain(BOTTOM, relative(reference.get(BOTTOM), () -> -borders.bottom())); + element.constrain(RIGHT, relative(reference.get(RIGHT), () -> -borders.right())); + } + + + public static void size(ConstrainedGeometry element, double width, double height) { + element.constrain(WIDTH, literal(width)); + element.constrain(HEIGHT, literal(height)); + } + + public static void size(ConstrainedGeometry element, Supplier width, Supplier height) { + element.constrain(WIDTH, dynamic(width)); + element.constrain(HEIGHT, dynamic(height)); + } + + public static void pos(ConstrainedGeometry element, double left, double top) { + element.constrain(LEFT, literal(left)); + element.constrain(TOP, literal(top)); + } + + public static void pos(ConstrainedGeometry element, Supplier left, Supplier top) { + element.constrain(LEFT, dynamic(left)); + element.constrain(TOP, dynamic(top)); + } + + public static void center(ConstrainedGeometry element, ConstrainedGeometry centerOn) { + element.constrain(TOP, midPoint(centerOn.get(TOP), centerOn.get(BOTTOM), () -> element.ySize() / -2)); + element.constrain(LEFT, midPoint(centerOn.get(LEFT), centerOn.get(RIGHT), () -> element.xSize() / -2)); + } + + /** + * Constrain the specified element to a position inside the specified targetElement. + * See the following image for an example of what each LayoutPos does: + * https://ss.brandon3055.com/e89a6 + * + * @param target The element whose position we are setting. + * @param reference The reference element, the element that target will be placed inside. + * @param position The layout position. + */ + public static void placeInside(ConstrainedGeometry target, ConstrainedGeometry reference, LayoutPos position) { + placeInside(target, reference, position, 0, 0); + } + + /** + * Constrain the specified element to a position inside the specified targetElement. + * See the following image for an example of what each LayoutPos does: + * https://ss.brandon3055.com/e89a6 + * + * @param target The element whose position we are setting. + * @param reference The reference element, the element that target will be placed inside. + * @param position The layout position. + * @param xOffset Optional X offset to be applied to the final position. + * @param yOffset Optional Y offset to be applied to the final position. + */ + public static void placeInside(ConstrainedGeometry target, ConstrainedGeometry reference, LayoutPos position, double xOffset, double yOffset) { + switch (position) { + case TOP_LEFT -> target + .constrain(TOP, relative(reference.get(TOP), yOffset)) + .constrain(LEFT, relative(reference.get(LEFT), xOffset)); + case TOP_CENTER -> target + .constrain(TOP, relative(reference.get(TOP), yOffset)) + .constrain(LEFT, midPoint(reference.get(LEFT), reference.get(RIGHT), () -> (target.xSize() / -2) + xOffset)); + case TOP_RIGHT -> target + .constrain(TOP, relative(reference.get(TOP), yOffset)) + .constrain(RIGHT, relative(reference.get(RIGHT), xOffset)); + case MIDDLE_RIGHT -> target + .constrain(TOP, midPoint(reference.get(TOP), reference.get(BOTTOM), () -> (target.ySize() / -2) + yOffset)) + .constrain(RIGHT, relative(reference.get(RIGHT), xOffset)); + case BOTTOM_RIGHT -> target + .constrain(BOTTOM, relative(reference.get(BOTTOM), yOffset)) + .constrain(RIGHT, relative(reference.get(RIGHT), xOffset)); + case BOTTOM_CENTER -> target + .constrain(BOTTOM, relative(reference.get(BOTTOM), yOffset)) + .constrain(LEFT, midPoint(reference.get(LEFT), reference.get(RIGHT), () -> (target.xSize() / -2) + xOffset)); + case BOTTOM_LEFT -> target + .constrain(BOTTOM, relative(reference.get(BOTTOM), yOffset)) + .constrain(LEFT, relative(reference.get(LEFT), xOffset)); + case MIDDLE_LEFT -> target + .constrain(TOP, midPoint(reference.get(TOP), reference.get(BOTTOM), () -> (target.ySize() / -2) + yOffset)) + .constrain(LEFT, relative(reference.get(LEFT), xOffset)); + } + } + + /** + * Constrain the specified element to a position outside the specified targetElement. + * See the following image for an example of what each LayoutPos does: + * https://ss.brandon3055.com/baa7c + * + * @param target The element whose position we are setting. + * @param reference The reference element, the element that target will be placed outside of. + * @param position The layout position. + */ + public static void placeOutside(ConstrainedGeometry target, ConstrainedGeometry reference, LayoutPos position) { + placeOutside(target, reference, position, 0, 0); + } + + /** + * Constrain the specified element to a position outside the specified targetElement. + * See the following image for an example of what each LayoutPos does: + * https://ss.brandon3055.com/baa7c + * + * @param target The element whose position we are setting. + * @param reference The reference element, the element that target will be placed outside of. + * @param position The layout position. + * @param xOffset Optional X offset to be applied to the final position. + * @param yOffset Optional Y offset to be applied to the final position. + */ + public static void placeOutside(ConstrainedGeometry target, ConstrainedGeometry reference, LayoutPos position, double xOffset, double yOffset) { + switch (position) { + case TOP_LEFT -> target + .constrain(BOTTOM, relative(reference.get(TOP), yOffset)) + .constrain(RIGHT, relative(reference.get(LEFT), xOffset)); + case TOP_CENTER -> target + .constrain(BOTTOM, relative(reference.get(TOP), yOffset)) + .constrain(LEFT, midPoint(reference.get(LEFT), reference.get(RIGHT), () -> (target.xSize() / -2) + xOffset)); + case TOP_RIGHT -> target + .constrain(BOTTOM, relative(reference.get(TOP), yOffset)) + .constrain(LEFT, relative(reference.get(RIGHT), xOffset)); + case MIDDLE_RIGHT -> target + .constrain(TOP, midPoint(reference.get(TOP), reference.get(BOTTOM), () -> (target.ySize() / -2) + yOffset)) + .constrain(LEFT, relative(reference.get(RIGHT), xOffset)); + case BOTTOM_RIGHT -> target + .constrain(TOP, relative(reference.get(BOTTOM), yOffset)) + .constrain(LEFT, relative(reference.get(RIGHT), xOffset)); + case BOTTOM_CENTER -> target + .constrain(TOP, relative(reference.get(BOTTOM), yOffset)) + .constrain(LEFT, midPoint(reference.get(LEFT), reference.get(RIGHT), () -> (target.xSize() / -2) + xOffset)); + case BOTTOM_LEFT -> target + .constrain(TOP, relative(reference.get(BOTTOM), yOffset)) + .constrain(RIGHT, relative(reference.get(LEFT), xOffset)); + case MIDDLE_LEFT -> target + .constrain(TOP, midPoint(reference.get(TOP), reference.get(BOTTOM), () -> (target.ySize() / -2) + yOffset)) + .constrain(RIGHT, relative(reference.get(LEFT), xOffset)); + } + } + + public enum LayoutPos { + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + MIDDLE_RIGHT, + MIDDLE_LEFT, + BOTTOM_RIGHT, + BOTTOM_CENTER, + BOTTOM_LEFT + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/ContentElement.java b/src/main/java/codechicken/lib/gui/modular/lib/ContentElement.java new file mode 100644 index 00000000..6db976a5 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/ContentElement.java @@ -0,0 +1,14 @@ +package codechicken.lib.gui.modular.lib; + + +import codechicken.lib.gui.modular.elements.GuiElement; + +/** + * Implemented by elements that have a separate child element to which content should be added. + * e.g. scroll element, manipulable element etc... + * Created by brandon3055 on 13/11/2023 + */ +public interface ContentElement> { + + T getContentElement(); +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/CursorHelper.java b/src/main/java/codechicken/lib/gui/modular/lib/CursorHelper.java new file mode 100644 index 00000000..4ef4cd96 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/CursorHelper.java @@ -0,0 +1,100 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.CodeChickenLib; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.BufferUtils; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWImage; +import org.lwjgl.system.MemoryUtil; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Created by brandon3055 on 11/5/20. + */ +public class CursorHelper { + public static final Logger LOGGER = LogManager.getLogger(); + + public static final ResourceLocation DRAG = new ResourceLocation(CodeChickenLib.MOD_ID, "textures/gui/cursors/drag.png"); + public static final ResourceLocation RESIZE_H = new ResourceLocation(CodeChickenLib.MOD_ID, "textures/gui/cursors/resize_h.png"); + public static final ResourceLocation RESIZE_V = new ResourceLocation(CodeChickenLib.MOD_ID, "textures/gui/cursors/resize_v.png"); + public static final ResourceLocation RESIZE_TRBL = new ResourceLocation(CodeChickenLib.MOD_ID, "textures/gui/cursors/resize_diag_trbl.png"); + public static final ResourceLocation RESIZE_TLBR = new ResourceLocation(CodeChickenLib.MOD_ID, "textures/gui/cursors/resize_diag_tlbr.png"); + + private static final Map cursors = new HashMap<>(); + private static ResourceLocation active = null; + + private static long createCursor(ResourceLocation cursorTexture) { + try { + Resource resource = Minecraft.getInstance().getResourceManager().getResource(cursorTexture).orElse(null); + if (resource == null) return MemoryUtil.NULL; + BufferedImage bufferedimage = ImageIO.read(resource.open()); + GLFWImage glfwImage = imageToGLFWImage(bufferedimage); + return GLFW.glfwCreateCursor(glfwImage, 16, 16); + } catch (Exception e) { + LOGGER.warn("An error occurred while creating cursor", e); + } + return 0; + } + + private static GLFWImage imageToGLFWImage(BufferedImage image) { + if (image.getType() != BufferedImage.TYPE_INT_ARGB_PRE) { + final BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE); + final Graphics2D graphics = convertedImage.createGraphics(); + final int targetWidth = image.getWidth(); + final int targetHeight = image.getHeight(); + graphics.drawImage(image, 0, 0, targetWidth, targetHeight, null); + graphics.dispose(); + image = convertedImage; + } + final ByteBuffer buffer = BufferUtils.createByteBuffer(image.getWidth() * image.getHeight() * 4); + for (int i = 0; i < image.getHeight(); i++) { + for (int j = 0; j < image.getWidth(); j++) { + int colorSpace = image.getRGB(j, i); + buffer.put((byte) ((colorSpace << 8) >> 24)); + buffer.put((byte) ((colorSpace << 16) >> 24)); + buffer.put((byte) ((colorSpace << 24) >> 24)); + buffer.put((byte) (colorSpace >> 24)); + } + } + buffer.flip(); + final GLFWImage result = GLFWImage.create(); + result.set(image.getWidth(), image.getHeight(), buffer); + return result; + } + + public static void setCursor(@Nullable ResourceLocation cursor) { + if (cursor != active) { + active = cursor; + long window = Minecraft.getInstance().getWindow().getWindow(); + long newCursor = active == null ? 0 : cursors.computeIfAbsent(cursor, CursorHelper::createCursor); + GLFW.glfwSetCursor(window, newCursor); + } + } + + public static void resetCursor() { + if (active != null) { + setCursor(null); + } + } + + public static void onResourceReload() { + cursors.values().forEach(cursor -> { + if (cursor != MemoryUtil.NULL) { + GLFW.glfwDestroyCursor(cursor); + } + }); + cursors.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/codechicken/lib/gui/modular/lib/DynamicTextures.java b/src/main/java/codechicken/lib/gui/modular/lib/DynamicTextures.java new file mode 100644 index 00000000..aa15ddfa --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/DynamicTextures.java @@ -0,0 +1,30 @@ +package codechicken.lib.gui.modular.lib; + +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Function; + +/** + * Designed for use with DynamicTextureProvider + *

+ * Created by brandon3055 on 07/09/2023 + */ +public interface DynamicTextures { + + void makeTextures(Function textures); + + default String dynamicTexture(Function textures, ResourceLocation dynamicInput, ResourceLocation outputLocation, int width, int height, int border) { + return textures.apply(new DynamicTexture(dynamicInput, outputLocation, width, height, border, border, border, border)); + } + + default String dynamicTexture(Function textures, ResourceLocation dynamicInput, ResourceLocation outputLocation, int width, int height, int topBorder, int leftBorder, int bottomBorder, int rightBorder) { + return textures.apply(new DynamicTexture(dynamicInput, outputLocation, width, height, topBorder, leftBorder, bottomBorder, rightBorder)); + } + + record DynamicTexture(ResourceLocation dynamicInput, ResourceLocation outputLocation, int width, int height, int topBorder, int leftBorder, int bottomBorder, int rightBorder) { + public String guiTexturePath() { + return outputLocation.getPath().replace("textures/gui/", "").replace(".gui", ""); + } + } + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/ElementEvents.java b/src/main/java/codechicken/lib/gui/modular/lib/ElementEvents.java new file mode 100644 index 00000000..d4edcbe1 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/ElementEvents.java @@ -0,0 +1,272 @@ +package codechicken.lib.gui.modular.lib; + +import com.google.common.collect.Lists; +import codechicken.lib.gui.modular.elements.GuiElement; + +import java.util.List; + +/** + * This class defines the default implementation for all Screen events. + * Input events in Modular GUI v2 work similar to v2, events are passed to all elements recursively in a top-down order, + * and if any element 'consumes' the event, it will not be passed any further down the chain. + * However, this approach had issues in v2, because there are certain situations where an element needs to receive an event even if it has been consumed. + * To deal with that we now have two methods for each event, the main handler method that will always get called, and uses a 'consumed' flag to track whether the event has been consumed, + * as well as a simpler convenience method that will only get called if the event has not already been canceled, and does not require you to call super. + *

+ * Created by brandon3055 on 09/08/2023 + */ +public interface ElementEvents { + + /** + * @return An unmodifiable list of all assigned child elements assigned to this parent. The list should be sorted in the order they were added. + */ + List> getChildren(); + + //=== Mouse Events ==// + + /** + * Called whenever the cursor position changes. + * Vanillas mouseDragged is not passed through because it is redundant. + * All mouse drag functionality can be archived using available events. + * + * @param mouseX new mouse X position + * @param mouseY new mouse Y position + */ + default void mouseMoved(double mouseX, double mouseY) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + child.mouseMoved(mouseX, mouseY); + } + } + } + + /** + * Override this method to implement handling for the mouseClicked event. + * This event propagates through the entire gui element stack from top to bottom, If eny element consumes the event it will not propagate any further. + * For rare cases where you need to receive this even if it has been consumed, you can override {@link #mouseClicked(double, double, int, boolean)} + *

+ * Note: You do not need to call super when overriding this interface method. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param button Mouse Button + * @return true to consume event. + */ + default boolean mouseClicked(double mouseX, double mouseY, int button) { + return false; + } + + /** + * Root handler for mouseClick event. This method will always be called for all elements even if the event has already been consumed. + * There are a few uses for this method, but the fast majority of mouseClick handling should be implemented via {@link #mouseClicked(double, double, int)} + *

+ * Note: If overriding this method, do so with caution, You must either return true (if you wish to consume the event) or you must return the result of the super call. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param button Mouse Button + * @param consumed Will be true if this action has already been consumed. + * @return true if this event has been consumed. + */ + default boolean mouseClicked(double mouseX, double mouseY, int button, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.mouseClicked(mouseX, mouseY, button, consumed); + } + } + return consumed || mouseClicked(mouseX, mouseY, button) || blockMouseEvents(); + } + + /** + * Override this method to implement handling for the mouseReleased event. + * This event propagates through the entire gui element stack from top to bottom, If eny element consumes the event it will not propagate any further. + * For rare cases where you need to receive this even if it has been consumed, you can override {@link #mouseReleased(double, double, int, boolean)} + *

+ * Note: You do not need to call super when overriding this interface method. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param button Mouse Button + * @return true to consume event. + */ + default boolean mouseReleased(double mouseX, double mouseY, int button) { + return false; + } + + /** + * Root handler for mouseReleased event. This method will always be called for all elements even if the event has already been consumed. + * There are a few uses for this method, but the fast majority of mouseReleased handling should be implemented via {@link #mouseReleased(double, double, int)} + *

+ * Note: If overriding this method, do so with caution, You must either return true (if you wish to consume the event) or you must return the result of the super call. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param button Mouse Button + * @param consumed Will be true if this action has already been consumed. + * @return true if this event has been consumed. + */ + default boolean mouseReleased(double mouseX, double mouseY, int button, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.mouseReleased(mouseX, mouseY, button, consumed); + } + } + return consumed || mouseReleased(mouseX, mouseY, button) || blockMouseEvents(); + } + + /** + * Override this method to implement handling for the mouseScrolled event. + * This event propagates through the entire gui element stack from top to bottom, If eny element consumes the event it will not propagate any further. + * For rare cases where you need to receive this even if it has been consumed, you can override {@link #mouseScrolled(double, double, double, boolean)} + *

+ * Note: You do not need to call super when overriding this interface method. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param scroll Scroll direction and amount + * @return true to consume event. + */ + default boolean mouseScrolled(double mouseX, double mouseY, double scroll) { + return false; + } + + /** + * Root handler for mouseScrolled event. This method will always be called for all elements even if the event has already been consumed. + * There are a few uses for this method, but the fast majority of mouseScrolled handling should be implemented via {@link #mouseScrolled(double, double, double)} + *

+ * Note: If overriding this method, do so with caution, You must either return true (if you wish to consume the event) or you must return the result of the super call. + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param scroll Scroll direction and amount + * @param consumed Will be true if this action has already been consumed. + * @return true if this event has been consumed. + */ + default boolean mouseScrolled(double mouseX, double mouseY, double scroll, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.mouseScrolled(mouseX, mouseY, scroll, consumed); + } + } + return consumed || mouseScrolled(mouseX, mouseY, scroll) || blockMouseEvents(); + } + + /** + * @return True to prevent mouse events from being passed to elements bellow this element. + */ + default boolean blockMouseEvents() { + return false; + } + + //=== Keyboard Events ==// + + /** + * Override this method to implement handling for the keyPressed event. + * This event propagates through the entire gui element stack from top to bottom, If eny element consumes the event it will not propagate any further. + * For rare cases where you need to receive this even if it has been consumed, you can override {@link #keyPressed(int, int, int, boolean)} + *

+ * Note: You do not need to call super when overriding this interface method. + * + * @param key the keyboard key that was pressed. + * @param scancode the system-specific scancode of the key + * @param modifiers bitfield describing which modifier keys were held down. + * @return true to consume event. + */ + default boolean keyPressed(int key, int scancode, int modifiers) { + return false; + } + + /** + * Root handler for keyPressed event. This method will always be called for all elements even if the event has already been consumed. + * There are a few uses for this method, but the fast majority of keyPressed handling should be implemented via {@link #keyPressed(int, int, int)} + *

+ * Note: If overriding this method, do so with caution, You must either return true (if you wish to consume the event) or you must return the result of the super call. + * + * @param key the keyboard key that was pressed. + * @param scancode the system-specific scancode of the key + * @param modifiers bitfield describing which modifier keys were held down. + * @param consumed Will be true if this action has already been consumed. + * @return true if this event has been consumed. + */ + default boolean keyPressed(int key, int scancode, int modifiers, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.keyPressed(key, scancode, modifiers, consumed); + } + } + return consumed || keyPressed(key, scancode, modifiers); + } + + /** + * Override this method to implement handling for the keyReleased event. + * This event propagates through the entire gui element stack from top to bottom, If eny element consumes the event it will not propagate any further. + * For rare cases where you need to receive this even if it has been consumed, you can override {@link #keyReleased(int, int, int, boolean)} + *

+ * Note: You do not need to call super when overriding this interface method. + * + * @param key the keyboard key that was released. + * @param scancode the system-specific scancode of the key + * @param modifiers bitfield describing which modifier keys were held down. + * @return true to consume event. + */ + default boolean keyReleased(int key, int scancode, int modifiers) { + return false; + } + + /** + * Root handler for keyReleased event. This method will always be called for all elements even if the event has already been consumed. + * There are a few uses for this method, but the fast majority of keyReleased handling should be implemented via {@link #keyReleased(int, int, int)} + *

+ * Note: If overriding this method, do so with caution, You must either return true (if you wish to consume the event) or you must return the result of the super call. + * + * @param key the keyboard key that was released. + * @param scancode the system-specific scancode of the key + * @param modifiers bitfield describing which modifier keys were held down. + * @param consumed Will be true if this action has already been consumed. + * @return true if this event has been consumed. + */ + default boolean keyReleased(int key, int scancode, int modifiers, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.keyReleased(key, scancode, modifiers, consumed); + } + } + return consumed || keyReleased(key, scancode, modifiers); + } + + /** + * Override this method to implement handling for the charTyped event. + * This event propagates through the entire gui element stack from top to bottom, If eny element consumes the event it will not propagate any further. + * For rare cases where you need to receive this even if it has been consumed, you can override {@link #charTyped(char, int, boolean)} + *

+ * Note: You do not need to call super when overriding this interface method. + * + * @param character The character typed. + * @param modifiers bitfield describing which modifier keys were held down. + * @return true to consume event. + */ + default boolean charTyped(char character, int modifiers) { + return false; + } + + /** + * Root handler for charTyped event. This method will always be called for all elements even if the event has already been consumed. + * There are a few uses for this method, but the fast majority of charTyped handling should be implemented via {@link #charTyped(char, int)} + *

+ * Note: If overriding this method, do so with caution, You must either return true (if you wish to consume the event) or you must return the result of the super call. + * + * @param character The character typed. + * @param modifiers bitfield describing which modifier keys were held down. + * @param consumed Will be true if this action has already been consumed. + * @return true if this event has been consumed. + */ + default boolean charTyped(char character, int modifiers, boolean consumed) { + for (GuiElement child : Lists.reverse(getChildren())) { + if (child.isEnabled()) { + consumed |= child.charTyped(character, modifiers, consumed); + } + } + return consumed || charTyped(character, modifiers); + } + +} \ No newline at end of file diff --git a/src/main/java/codechicken/lib/gui/modular/lib/ForegroundRender.java b/src/main/java/codechicken/lib/gui/modular/lib/ForegroundRender.java new file mode 100644 index 00000000..76dfcfb3 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/ForegroundRender.java @@ -0,0 +1,32 @@ +package codechicken.lib.gui.modular.lib; + +import com.mojang.blaze3d.vertex.PoseStack; + +/** + * Allows a Gui Elements to render content in front of child elements. + * Note: Most elements should use {@link BackgroundRender} to render their content. + *

+ * Created by brandon3055 on 07/08/2023 + */ +public interface ForegroundRender { + + /** + * Specifies the z depth of the foreground content. + * Used when calculating the total depth of this gui element. + * Recommended minimum depth is 0.01 or 0.035 if this element renders text. (text shadows are rendered with a 0.03 offset) + * + * @return the z height of the background content. + */ + default double getForegroundDepth() { + return 0.01; + } + + /** + * Used to render content in front of this elements child elements. + * When rendering element content, always use the {@link PoseStack} available via the provided {@link GuiRender} + * Where applicable, always use push/pop to ensure the stack is returned to its original state after your rendering is complete. + * + * @param render Contains gui context information as well as essential render methods/utils including the PoseStack. + */ + void renderForeground(GuiRender render, double mouseX, double mouseY, float partialTicks); +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/GuiProvider.java b/src/main/java/codechicken/lib/gui/modular/lib/GuiProvider.java new file mode 100644 index 00000000..32dbcbb5 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/GuiProvider.java @@ -0,0 +1,42 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.gui.modular.ModularGui; +import codechicken.lib.gui.modular.elements.GuiElement; +import codechicken.lib.gui.modular.lib.container.ContainerGuiProvider; + +/** + * This interface is used to build modular gui Screens. + * For modular gui container screens use {@link ContainerGuiProvider} + * + * Created by brandon3055 on 19/08/2023 + */ +public interface GuiProvider { + + /** + * Override this to defile a custom root gui element. + * Useful if you want to use something like a background texture or a manipulable element as the root element. + * + * @param gui The modular GUI. + * @return the root gui element. + */ + default GuiElement createRootElement(ModularGui gui) { + return new GuiElement<>(gui); + } + + /** + * Use this method to build the modular gui. + *

+ * Initialize the gui root element with {@link ModularGui#initStandardGui(int, int)}, {@link ModularGui#initFullscreenGui()} + * This applies bindings to fix the size and position of the root element, you can also do this manually for custom configurations. + *

+ * Build your gui by adding and configuring your desired gui elements. + * Elements must be added to the root gui element which is obtainable via gui.getRoot() + *

+ * Note: gui elements are added on construction, meaning you do not need to use element.addChild. + * Instead, just construct the elements, and pass in the root element (or any other initialized element) as the parent. + * + * @param gui The modular gui instance. + */ + void buildGui(ModularGui gui); + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/GuiRender.java b/src/main/java/codechicken/lib/gui/modular/lib/GuiRender.java new file mode 100644 index 00000000..a91b7bd8 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/GuiRender.java @@ -0,0 +1,1728 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.gui.modular.lib.geometry.Borders; +import codechicken.lib.gui.modular.lib.geometry.Rectangle; +import codechicken.lib.gui.modular.sprite.Material; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.Util; +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.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraftforge.client.ForgeHooksClient; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.common.MinecraftForge; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector2ic; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * This class primarily based on GuiHelper from BrandonsCore + * But its implementation is heavily inspired by the new GuiGraphics system in 1.20+ + *

+ * The purpose of this class is to provide most of the basic rendering functions required to render various GUI geometry. + * This includes things like simple rectangles, textures, strings, etc. + *

+ * Created by brandon3055 on 29/06/2023 + */ +public class GuiRender { + public static final RenderType SOLID = RenderType.gui(); + + //Used for things like events that require the vanilla GuiGraphics + private final RenderWrapper renderWrapper; + + private final Minecraft mc; + private final PoseStack pose; + private final ScissorHandler scissorHandler = new ScissorHandler(); + private final MultiBufferSource.BufferSource buffers; + private boolean batchDraw; + private Font fontOverride; + + public GuiRender(Minecraft mc, PoseStack poseStack, MultiBufferSource.BufferSource buffers) { + this.mc = mc; + this.pose = poseStack; + this.buffers = buffers; + this.renderWrapper = new RenderWrapper(this); + } + + public GuiRender(Minecraft mc, MultiBufferSource.BufferSource buffers) { + this(mc, new PoseStack(), buffers); + } + + public static GuiRender convert(GuiGraphics graphics) { + return new GuiRender(Minecraft.getInstance(), graphics.pose(), graphics.bufferSource()); + } + + public PoseStack pose() { + return pose; + } + + public MultiBufferSource.BufferSource buffers() { + return buffers; + } + + public Minecraft mc() { + return mc; + } + + public Font font() { + return fontOverride == null ? mc().font : fontOverride; + } + + public int guiWidth() { + return mc().getWindow().getGuiScaledWidth(); + } + + public int guiHeight() { + return mc().getWindow().getGuiScaledHeight(); + } + + /** + * Allows you to override the font renderer used for all text rendering. + * Be sure to set the override back to null when you are finished using your custom font! + * + * @param font The font to use, or null to disable override. + */ + public void overrideFont(@Nullable Font font) { + this.fontOverride = font; + } + + /** + * Allow similar render calls to be batched together into a single draw for better render efficiency. + * All render calls in batch must use the same render type. + * + * @param batch callback in which the rendering should be implemented. + */ + public void batchDraw(Runnable batch) { + flush(); + batchDraw = true; + batch.run(); + batchDraw = false; + flush(); + } + + private void flushIfUnBatched() { + if (!batchDraw) flush(); + } + + private void flushIfBatched() { + if (batchDraw) flush(); + } + + public void flush() { + RenderSystem.disableDepthTest(); + buffers.endBatch(); + RenderSystem.enableDepthTest(); + } + + /** + * Only use this as a last resort! It may explode... Have fun! + * + * @return A Vanilla GuiGraphics instance that wraps this {@link GuiRender} + */ + @Deprecated + public RenderWrapper guiGraphicsWrapper() { + return renderWrapper; + } + + //=== Un-Textured geometry ===// + + /** + * Fill rectangle with solid colour + */ + public void rect(Rectangle rectangle, int colour) { + this.rect(SOLID, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), colour); + } + + /** + * Fill rectangle with solid colour + */ + public void rect(RenderType type, Rectangle rectangle, int colour) { + this.rect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), colour); + } + + /** + * Fill rectangle with solid colour + */ + public void rect(double x, double y, double width, double height, int colour) { + this.fill(SOLID, x, y, x + width, y + height, colour); + } + + /** + * Fill rectangle with solid colour + */ + public void rect(RenderType type, double x, double y, double width, double height, int colour) { + this.fill(type, x, y, x + width, y + height, colour); + } + + /** + * Fill area with solid colour + */ + public void fill(double xMin, double yMin, double xMax, double yMax, int colour) { + this.fill(SOLID, xMin, yMin, xMax, yMax, colour); + } + + /** + * Fill area with solid colour + */ + public void fill(RenderType type, double xMin, double yMin, double xMax, double yMax, int colour) { + if (xMax < xMin) { + double min = xMax; + xMax = xMin; + xMin = min; + } + if (yMax < yMin) { + double min = yMax; + yMax = yMin; + yMin = min; + } + + Matrix4f mat = pose.last().pose(); + VertexConsumer buffer = buffers.getBuffer(type); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(colour).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(colour).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(colour).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(colour).endVertex(); //L-B + flushIfUnBatched(); + } + + /** + * Fill area with colour gradient from top to bottom + */ + public void gradientFillV(double xMin, double yMin, double xMax, double yMax, int topColour, int bottomColour) { + this.gradientFillV(SOLID, xMin, yMin, xMax, yMax, topColour, bottomColour); + } + + /** + * Fill area with colour gradient from top to bottom + */ + public void gradientFillV(RenderType type, double xMin, double yMin, double xMax, double yMax, int topColour, int bottomColour) { + VertexConsumer buffer = buffers().getBuffer(type); + float sA = a(topColour) / 255.0F; + float sR = r(topColour) / 255.0F; + float sG = g(topColour) / 255.0F; + float sB = b(topColour) / 255.0F; + float eA = a(bottomColour) / 255.0F; + float eR = r(bottomColour) / 255.0F; + float eG = g(bottomColour) / 255.0F; + float eB = b(bottomColour) / 255.0F; + Matrix4f mat = pose.last().pose(); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(eR, eG, eB, eA).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(sR, sG, sB, sA).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(sR, sG, sB, sA).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(eR, eG, eB, eA).endVertex(); //L-B + this.flushIfUnBatched(); + } + + /** + * Fill area with colour gradient from left to right + */ + public void gradientFillH(double xMin, double yMin, double xMax, double yMax, int leftColour, int rightColour) { + this.gradientFillH(SOLID, xMin, yMin, xMax, yMax, leftColour, rightColour); + } + + /** + * Fill area with colour gradient from left to right + */ + public void gradientFillH(RenderType type, double xMin, double yMin, double xMax, double yMax, int leftColour, int rightColour) { + VertexConsumer buffer = buffers().getBuffer(type); + float sA = a(leftColour) / 255.0F; + float sR = r(leftColour) / 255.0F; + float sG = g(leftColour) / 255.0F; + float sB = b(leftColour) / 255.0F; + float eA = a(rightColour) / 255.0F; + float eR = r(rightColour) / 255.0F; + float eG = g(rightColour) / 255.0F; + float eB = b(rightColour) / 255.0F; + Matrix4f mat = pose.last().pose(); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(eR, eG, eB, eA).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(eR, eG, eB, eA).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(sR, sG, sB, sA).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(sR, sG, sB, sA).endVertex(); //L-B + this.flushIfUnBatched(); + } + + /** + * Draw a bordered rectangle of specified with specified border width, border colour and fill colour. + */ + public void borderRect(Rectangle rectangle, double borderWidth, int fillColour, int borderColour) { + borderFill(rectangle.x(), rectangle.y(), rectangle.xMax(), rectangle.yMax(), borderWidth, fillColour, borderColour); + } + + /** + * Draw a bordered rectangle of specified with specified border width, border colour and fill colour. + */ + public void borderRect(double x, double y, double width, double height, double borderWidth, int fillColour, int borderColour) { + borderFill(x, y, x + width, y + height, borderWidth, fillColour, borderColour); + } + + /** + * Draw a bordered rectangle of specified with specified border width, border colour and fill colour. + */ + public void borderRect(RenderType type, Rectangle rectangle, double borderWidth, int fillColour, int borderColour) { + borderFill(type, rectangle.x(), rectangle.y(), rectangle.xMax(), rectangle.yMax(), borderWidth, fillColour, borderColour); + } + + /** + * Draw a bordered rectangle of specified with specified border width, border colour and fill colour. + */ + public void borderRect(RenderType type, double x, double y, double width, double height, double borderWidth, int fillColour, int borderColour) { + borderFill(type, x, y, x + width, y + height, borderWidth, fillColour, borderColour); + } + + /** + * Draw a border of specified with, fill internal area with solid colour. + */ + public void borderFill(double xMin, double yMin, double xMax, double yMax, double borderWidth, int fillColour, int borderColour) { + borderFill(SOLID, xMin, yMin, xMax, yMax, borderWidth, fillColour, borderColour); + } + + /** + * Draw a border of specified with, fill internal area with solid colour. + */ + public void borderFill(RenderType type, double xMin, double yMin, double xMax, double yMax, double borderWidth, int fillColour, int borderColour) { + if (batchDraw) { //Draw batched for efficiency, unless already doing a batch draw. + borderFillInternal(type, xMin, yMin, xMax, yMax, borderWidth, fillColour, borderColour); + } else { + batchDraw(() -> borderFillInternal(type, xMin, yMin, xMax, yMax, borderWidth, fillColour, borderColour)); + } + } + + private void borderFillInternal(RenderType type, double xMin, double yMin, double xMax, double yMax, double borderWidth, int fillColour, int borderColour) { + fill(type, xMin, yMin, xMax, yMin + borderWidth, borderColour); //Top + fill(type, xMin, yMin + borderWidth, xMin + borderWidth, yMax - borderWidth, borderColour); //Left + fill(type, xMin, yMax - borderWidth, xMax, yMax, borderColour); //Bottom + fill(type, xMax - borderWidth, yMin + borderWidth, xMax, yMax - borderWidth, borderColour); //Right + if (fillColour != 0) //No point rendering fill if there is no fill colour + fill(type, xMin + borderWidth, yMin + borderWidth, xMax - borderWidth, yMax - borderWidth, fillColour); //Fill + } + + /** + * Can be used to create the illusion of an inset / outset rectangle. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedRect(Rectangle rectangle, double borderWidth, int topLeftColour, int bottomRightColour, int fillColour) { + shadedFill(SOLID, rectangle.x(), rectangle.y(), rectangle.xMax(), rectangle.yMax(), borderWidth, topLeftColour, bottomRightColour, midColour(topLeftColour, bottomRightColour), fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset rectangle. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedRect(double x, double y, double width, double height, double borderWidth, int topLeftColour, int bottomRightColour, int fillColour) { + shadedFill(SOLID, x, y, x + width, y + height, borderWidth, topLeftColour, bottomRightColour, midColour(topLeftColour, bottomRightColour), fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset rectangle. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedRect(Rectangle rectangle, double borderWidth, int topLeftColour, int bottomRightColour, int cornerMixColour, int fillColour) { + shadedFill(SOLID, rectangle.x(), rectangle.y(), rectangle.xMax(), rectangle.yMax(), borderWidth, topLeftColour, bottomRightColour, cornerMixColour, fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset rectangle. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedRect(double x, double y, double width, double height, double borderWidth, int topLeftColour, int bottomRightColour, int cornerMixColour, int fillColour) { + shadedFill(SOLID, x, y, x + width, y + height, borderWidth, topLeftColour, bottomRightColour, cornerMixColour, fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset rectangle. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedRect(RenderType type, double x, double y, double width, double height, double borderWidth, int topLeftColour, int bottomRightColour, int cornerMixColour, int fillColour) { + shadedFill(type, x, y, x + width, y + height, borderWidth, topLeftColour, bottomRightColour, cornerMixColour, fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset area. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedFill(double xMin, double yMin, double xMax, double yMax, double borderWidth, int topLeftColour, int bottomRightColour, int fillColour) { + shadedFill(SOLID, xMin, yMin, xMax, yMax, borderWidth, topLeftColour, bottomRightColour, midColour(topLeftColour, bottomRightColour), fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset area. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedFill(double xMin, double yMin, double xMax, double yMax, double borderWidth, int topLeftColour, int bottomRightColour, int cornerMixColour, int fillColour) { + shadedFill(SOLID, xMin, yMin, xMax, yMax, borderWidth, topLeftColour, bottomRightColour, cornerMixColour, fillColour); + } + + /** + * Can be used to create the illusion of an inset / outset area. This is identical to the way inventory slots are rendered except in code rather than via a texture. + * Example Usage: render.shadedFill(0, 0, 18, 18, 1, 0xFF373737, 0xFFffffff, 0xFF8b8b8b, 0xFF8b8b8b); //Renders a vanilla style inventory slot + * This can also be used to render things like buttons that appear to actually "push in" when you press them. + */ + public void shadedFill(RenderType type, double xMin, double yMin, double xMax, double yMax, double borderWidth, int topLeftColour, int bottomRightColour, int cornerMixColour, int fillColour) { + if (batchDraw) { //Draw batched for efficiency, unless already doing a batch draw. + shadedFillInternal(type, xMin, yMin, xMax, yMax, borderWidth, topLeftColour, bottomRightColour, cornerMixColour, fillColour); + } else { + batchDraw(() -> shadedFillInternal(type, xMin, yMin, xMax, yMax, borderWidth, topLeftColour, bottomRightColour, cornerMixColour, fillColour)); + } + } + + public void shadedFillInternal(RenderType type, double xMin, double yMin, double xMax, double yMax, double borderWidth, int topLeftColour, int bottomRightColour, int cornerMixColour, int fillColour) { + fill(type, xMin, yMin, xMax - borderWidth, yMin + borderWidth, topLeftColour); //Top + fill(type, xMin, yMin + borderWidth, xMin + borderWidth, yMax - borderWidth, topLeftColour); //Left + fill(type, xMin + borderWidth, yMax - borderWidth, xMax, yMax, bottomRightColour); //Bottom + fill(type, xMax - borderWidth, yMin + borderWidth, xMax, yMax - borderWidth, bottomRightColour); //Right + fill(type, xMax - borderWidth, yMin, xMax, yMin + borderWidth, cornerMixColour); //Top Right Corner + fill(type, xMin, yMax - borderWidth, xMin + borderWidth, yMax, cornerMixColour); //Bottom Left Corner + + if (fillColour != 0) //No point rendering fill if there is no fill colour + fill(type, xMin + borderWidth, yMin + borderWidth, xMax - borderWidth, yMax - borderWidth, fillColour); //Fill + } + + //=== Generic Backgrounds ===// + + /** + * Draws a rectangle / background with a style matching vanilla tool tips. + */ + public void toolTipBackground(double x, double y, double width, double height) { + toolTipBackground(x, y, width, height, 0xF0100010, 0x505000FF, 0x5028007f); + } + + /** + * Draws a rectangle / background with a style matching vanilla tool tips. + * Vanilla Default Colours: 0xF0100010, 0x505000FF, 0x5028007f + */ + public void toolTipBackground(double x, double y, double width, double height, int backgroundColour, int borderColourTop, int borderColourBottom) { + toolTipBackground(x, y, width, height, backgroundColour, backgroundColour, borderColourTop, borderColourBottom, false); + } + + /** + * Draws a rectangle / background with a style matching vanilla tool tips. + * Vanilla Default Colours: 0xF0100010, 0xF0100010, 0x505000FF, 0x5028007f + */ + public void toolTipBackground(double x, double y, double width, double height, int backgroundColourTop, int backgroundColourBottom, int borderColourTop, int borderColourBottom, boolean empty) { + if (batchDraw) { //Draw batched for efficiency, unless already doing a batch draw. + toolTipBackgroundInternal(x, y, x + width, y + height, backgroundColourTop, backgroundColourBottom, borderColourTop, borderColourBottom, false); + } else { + batchDraw(() -> toolTipBackgroundInternal(x, y, x + width, y + height, backgroundColourTop, backgroundColourBottom, borderColourTop, borderColourBottom, false)); + } + } + + private void toolTipBackgroundInternal(double xMin, double yMin, double xMax, double yMax, int backgroundColourTop, int backgroundColourBottom, int borderColourTop, int borderColourBottom, boolean empty) { + fill(xMin + 1, yMin, xMax - 1, yMin + 1, backgroundColourTop); // Top + fill(xMin + 1, yMax - 1, xMax - 1, yMax, backgroundColourBottom); // Bottom + gradientFillV(xMin, yMin + 1, xMin + 1, yMax - 1, backgroundColourTop, backgroundColourBottom); // Left + gradientFillV(xMax - 1, yMin + 1, xMax, yMax - 1, backgroundColourTop, backgroundColourBottom); // Right + if (!empty) { + gradientFillV(xMin + 1, yMin + 1, xMax - 1, yMax - 1, backgroundColourTop, backgroundColourBottom); // Fill + } + gradientFillV(xMin + 1, yMin + 1, xMin + 2, yMax - 1, borderColourTop, borderColourBottom); // Left Accent + gradientFillV(xMax - 2, yMin + 1, xMax - 1, yMax - 1, borderColourTop, borderColourBottom); // Right Accent + fill(xMin + 2, yMin + 1, xMax - 2, yMin + 2, borderColourTop); // Top Accent + fill(xMin + 2, yMax - 2, xMax - 2, yMax - 1, borderColourBottom); // Bottom Accent + } + + //=== Textured geometry ===// + + //Sprite plus RenderType + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void spriteRect(RenderType type, Rectangle rectangle, TextureAtlasSprite sprite) { + spriteRect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), sprite, 1F, 1F, 1F, 1F); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void spriteRect(RenderType type, Rectangle rectangle, TextureAtlasSprite sprite, int argb) { + spriteRect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), sprite, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void spriteRect(RenderType type, Rectangle rectangle, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + spriteRect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), sprite, red, green, blue, alpha); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void spriteRect(RenderType type, double x, double y, double width, double height, TextureAtlasSprite sprite) { + spriteRect(type, x, y, width, height, sprite, 1F, 1F, 1F, 1F); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void spriteRect(RenderType type, double x, double y, double width, double height, TextureAtlasSprite sprite, int argb) { + spriteRect(type, x, y, width, height, sprite, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void spriteRect(RenderType type, double x, double y, double width, double height, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + sprite(type, x, y, x + width, y + height, sprite, red, green, blue, alpha); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void sprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite) { + sprite(type, xMin, yMin, xMax, yMax, sprite, 1F, 1F, 1F, 1F); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void sprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, int argb) { + sprite(type, xMin, yMin, xMax, yMax, sprite, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void sprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + VertexConsumer buffer = buffers().getBuffer(type); + Matrix4f mat = pose.last().pose(); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(red, green, blue, alpha).uv(sprite.getU1(), sprite.getV1()).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(red, green, blue, alpha).uv(sprite.getU1(), sprite.getV0()).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(red, green, blue, alpha).uv(sprite.getU0(), sprite.getV0()).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(red, green, blue, alpha).uv(sprite.getU0(), sprite.getV1()).endVertex(); //L-B + flushIfUnBatched(); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void spriteRect(RenderType type, Rectangle rectangle, int rotation, TextureAtlasSprite sprite) { + spriteRect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), rotation, sprite, 1F, 1F, 1F, 1F); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void spriteRect(RenderType type, Rectangle rectangle, int rotation, TextureAtlasSprite sprite, int argb) { + spriteRect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), rotation, sprite, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void spriteRect(RenderType type, Rectangle rectangle, int rotation, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + spriteRect(type, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), rotation, sprite, red, green, blue, alpha); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void spriteRect(RenderType type, double x, double y, double width, double height, int rotation, TextureAtlasSprite sprite) { + spriteRect(type, x, y, width, height, rotation, sprite, 1F, 1F, 1F, 1F); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void spriteRect(RenderType type, double x, double y, double width, double height, int rotation, TextureAtlasSprite sprite, int argb) { + spriteRect(type, x, y, width, height, rotation, sprite, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void spriteRect(RenderType type, double x, double y, double width, double height, int rotation, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + sprite(type, x, y, x + width, y + height, rotation, sprite, red, green, blue, alpha); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void sprite(RenderType type, double xMin, double yMin, double xMax, double yMax, int rotation, TextureAtlasSprite sprite) { + sprite(type, xMin, yMin, xMax, yMax, rotation, sprite, 1F, 1F, 1F, 1F); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void sprite(RenderType type, double xMin, double yMin, double xMax, double yMax, int rotation, TextureAtlasSprite sprite, int argb) { + sprite(type, xMin, yMin, xMax, yMax, rotation, sprite, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void sprite(RenderType type, double xMin, double yMin, double xMax, double yMax, int rotation, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + float[] u = {sprite.getU0(), sprite.getU1(), sprite.getU1(), sprite.getU0()}; + float[] v = {sprite.getV1(), sprite.getV1(), sprite.getV0(), sprite.getV0()}; + VertexConsumer buffer = buffers().getBuffer(type); + Matrix4f mat = pose.last().pose(); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(red, green, blue, alpha).uv(u[(1 + rotation) % 4], v[(1 + rotation) % 4]).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(red, green, blue, alpha).uv(u[(2 + rotation) % 4], v[(2 + rotation) % 4]).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(red, green, blue, alpha).uv(u[(3 + rotation) % 4], v[(3 + rotation) % 4]).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(red, green, blue, alpha).uv(u[(0 + rotation) % 4], v[(0 + rotation) % 4]).endVertex(); //L-B + flushIfUnBatched(); + } + + //Partial Sprite + + //TODO figure out if there is a way to make this work. +// /** +// * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX +// * Texture will be resized / reshaped as appropriate to fit the defined area. +// *

+// * This is similar to {@link #partialSprite(RenderType, double, double, double, double, TextureAtlasSprite, float, float, float, float, int)} +// * Except the input uv values are in texture coordinates. So to draw a full 16x16 sprite with this you would supply 0, 0, 16, 16 +// * +// * @param rotation Rotates sprite clockwise in 90 degree steps. +// */ +// public void partialSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, int rotation, TextureAtlasSprite sprite, int texXMin, int texYMin, int texXMax, int texYMax, int argb) { +// float width = sprite.contents().width(); +// float height = sprite.contents().height(); +// partialSprite(type, xMin, yMin, xMax, yMax, rotation, sprite, texXMin / width, texYMin / height, texXMax / width, texYMax / height, argb); +// } +// + +// /** +// * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX +// * Texture will be resized / reshaped as appropriate to fit the defined area. +// * Valid input u/v value range is 0 to 1 [0, 0, 1, 1 would render the full sprite] +// * +// * @param rotation Rotates sprite clockwise in 90 degree steps. +// */ +// public void partialSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, int rotation, TextureAtlasSprite sprite, float uMin, float vMin, float uMax, float vMax, int argb) { +// partialSprite(type, xMin, yMin, xMax, yMax, rotation, sprite, uMin, vMin, uMax, vMax, r(argb), g(argb), b(argb), a(argb)); +// } +// +// /** +// * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX +// * Texture will be resized / reshaped as appropriate to fit the defined area. +// * Valid input u/v value range is 0 to 1 [0, 0, 1, 1 would render the full sprite] +// * +// * @param rotation Rotates sprite clockwise in 90 degree steps. +// */ +// public void partialSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, int rotation, TextureAtlasSprite sprite, float left, float top, float right, float bottom, float red, float green, float blue, float alpha) { +// VertexConsumer buffer = buffers().getBuffer(type); +// Matrix4f mat = pose.last().pose(); +// rotation = Math.floorMod(rotation, 4); +// +// float[] sub = {left, top, right, bottom}; +// left = sub[rotation % 4]; +// top = sub[(rotation + 1) % 4]; +// right = sub[(rotation + 2) % 4]; +// bottom = sub[(rotation + 3) % 4]; +// +// float ul = sprite.getU1() - sprite.getU0(); +// float vl = sprite.getV1() - sprite.getV0(); +// float u0 = sprite.getU0() + (left * ul); +// float v0 = sprite.getV0() + (top * vl); +// float u1 = sprite.getU0() + (right * ul); +// float v1 = sprite.getV0() + (bottom * vl); +// float[] u = {u0, u1, u1, u0}; +// float[] v = {v1, v1, v0, v0}; +// buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(red, green, blue, alpha).uv(u[(1 + rotation) % 4], v[(1 + rotation) % 4]).endVertex(); //R-B +// buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(red, green, blue, alpha).uv(u[(2 + rotation) % 4], v[(2 + rotation) % 4]).endVertex(); //R-T +// buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(red, green, blue, alpha).uv(u[(3 + rotation) % 4], v[(3 + rotation) % 4]).endVertex(); //L-T +// buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(red, green, blue, alpha).uv(u[(0 + rotation) % 4], v[(0 + rotation) % 4]).endVertex(); //L-B +// flushIfUnBatched(); +// } + + /** + * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + *

+ * This is similar to {@link #partialSprite(RenderType, double, double, double, double, TextureAtlasSprite, float, float, float, float, int)} + * Except the input uv values are in texture coordinates. So to draw a full 16x16 sprite with this you would supply 0, 0, 16, 16 + */ + public void partialSpriteTex(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, double texXMin, double texYMin, double texXMax, double texYMax, int argb) { + partialSpriteTex(type, xMin, yMin, xMax, yMax, sprite, texXMin, texYMin, texXMax, texYMax, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + *

+ * This is similar to {@link #partialSprite(RenderType, double, double, double, double, TextureAtlasSprite, float, float, float, float, int)} + * Except the input uv values are in texture coordinates. So to draw a full 16x16 sprite with this you would supply 0, 0, 16, 16 + */ + public void partialSpriteTex(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, double texXMin, double texYMin, double texXMax, double texYMax, float red, float green, float blue, float alpha) { + int width = sprite.contents().width(); + int height = sprite.contents().height(); + partialSprite(type, xMin, yMin, xMax, yMax, sprite, (float) texXMin / width, (float) texYMin / height, (float) texXMax / width, (float) texYMax / height, red, green, blue, alpha); + } + + /** + * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * Valid input u/v value range is 0 to 1 [0, 0, 1, 1 would render the full sprite] + */ + public void partialSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, float uMin, float vMin, float uMax, float vMax, int argb) { + partialSprite(type, xMin, yMin, xMax, yMax, sprite, uMin, vMin, uMax, vMax, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a subsection of a TextureAtlasSprite using the given render type, Vertex format should be POSITION_COLOR_TEX + * Texture will be resized / reshaped as appropriate to fit the defined area. + * Valid input u/v value range is 0 to 1 [0, 0, 1, 1 would render the full sprite] + */ + public void partialSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, float uMin, float vMin, float uMax, float vMax, float red, float green, float blue, float alpha) { + VertexConsumer buffer = buffers().getBuffer(type); + Matrix4f mat = pose.last().pose(); + float u0 = sprite.getU0(); + float v0 = sprite.getV0(); + float u1 = sprite.getU1(); + float v1 = sprite.getV1(); + float ul = u1 - u0; + float vl = v1 - v0; + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(red, green, blue, alpha).uv(u0 + (uMax * ul), v0 + (vMax * vl)).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(red, green, blue, alpha).uv(u0 + (uMax * ul), v0 + (vMin * vl)).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(red, green, blue, alpha).uv(u0 + (uMin * ul), v0 + (vMin * vl)).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(red, green, blue, alpha).uv(u0 + (uMin * ul), v0 + (vMax * vl)).endVertex(); //L-B + flushIfUnBatched(); + } + + /** + * Draw a sprite tiled to fit the specified area. + * Sprite is drawn from the top-left so sprite will be tiled right and down. + */ + public void tileSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, int argb) { + tileSprite(type, xMin, yMin, xMax, yMax, sprite, sprite.contents().width(), sprite.contents().height(), argb); + } + + /** + * Draw a sprite tiled to fit the specified area. + * Sprite is drawn from the top-left so sprite will be tiled right and down. + * + * @param textureWidth Set base width of the sprite texture in pixels + * @param textureHeight Set base height of the sprite texture in pixels + */ + public void tileSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, int textureWidth, int textureHeight, int argb) { + tileSprite(type, xMin, yMin, xMax, yMax, sprite, textureWidth, textureHeight, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draw a sprite tiled to fit the specified area. + * Sprite is drawn from the top-left so sprite will be tiled right and down. + */ + public void tileSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, float red, float green, float blue, float alpha) { + tileSprite(type, xMin, yMin, xMax, yMax, sprite, sprite.contents().width(), sprite.contents().height(), red, green, blue, alpha); + } + + /** + * Draw a sprite tiled to fit the specified area. + * Sprite is drawn from the top-left so sprite will be tiled right and down. + * + * @param textureWidth Set base width of the sprite texture in pixels + * @param textureHeight Set base height of the sprite texture in pixels + */ + public void tileSprite(RenderType type, double xMin, double yMin, double xMax, double yMax, TextureAtlasSprite sprite, int textureWidth, int textureHeight, float red, float green, float blue, float alpha) { + double width = xMax - xMin; + double height = yMax - yMin; + if (width <= textureWidth && height <= textureHeight) { + partialSprite(type, xMin, yMin, xMax, yMax, sprite, 0F, 0F, (float) width / textureWidth, (float) height / textureHeight, red, green, blue, alpha); + } else { + Runnable draw = () -> { + double xPos = xMin; + do { + double sectionWidth = Math.min(textureWidth, xMax - xPos); + double uWidth = sectionWidth / textureWidth; + double yPos = yMin; + do { + double sectionHeight = Math.min(textureHeight, yMax - yPos); + double vWidth = sectionHeight / textureHeight; + partialSprite(type, xPos, yPos, xPos + sectionWidth, yPos + sectionHeight, sprite, 0, 0, (float) uWidth, (float) vWidth, red, green, blue, alpha); + yPos += textureHeight; + } while (yPos < yMax); + xPos += textureWidth; + } while (xPos < xMax); + }; + if (batchDraw) { + draw.run(); + } else { + batchDraw(draw); + } + } + } + + //Material + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void texRect(Material material, Rectangle rectangle) { + texRect(material, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), 1F, 1F, 1F, 1F); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void texRect(Material material, Rectangle rectangle, int argb) { + texRect(material, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void texRect(Material material, Rectangle rectangle, float red, float green, float blue, float alpha) { + texRect(material, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), red, green, blue, alpha); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void texRect(Material material, double x, double y, double width, double height) { + texRect(material, x, y, width, height, 1F, 1F, 1F, 1F); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void texRect(Material material, double x, double y, double width, double height, int argb) { + texRect(material, x, y, width, height, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void texRect(Material material, double x, double y, double width, double height, float red, float green, float blue, float alpha) { + tex(material, x, y, x + width, y + height, red, green, blue, alpha); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void tex(Material material, double xMin, double yMin, double xMax, double yMax) { + tex(material, xMin, yMin, xMax, yMax, 1F, 1F, 1F, 1F); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void tex(Material material, double xMin, double yMin, double xMax, double yMax, int argb) { + tex(material, xMin, yMin, xMax, yMax, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + */ + public void tex(Material material, double xMin, double yMin, double xMax, double yMax, float red, float green, float blue, float alpha) { + TextureAtlasSprite sprite = material.sprite(); + VertexConsumer buffer = material.buffer(buffers, GuiRender::texColType); + Matrix4f mat = pose.last().pose(); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(red, green, blue, alpha).uv(sprite.getU1(), sprite.getV1()).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(red, green, blue, alpha).uv(sprite.getU1(), sprite.getV0()).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(red, green, blue, alpha).uv(sprite.getU0(), sprite.getV0()).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(red, green, blue, alpha).uv(sprite.getU0(), sprite.getV1()).endVertex(); //L-B + flushIfUnBatched(); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void texRect(Material material, int rotation, Rectangle rectangle) { + texRect(material, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), rotation, 1F, 1F, 1F, 1F); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void texRect(Material material, int rotation, Rectangle rectangle, int argb) { + texRect(material, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), rotation, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void texRect(Material material, int rotation, Rectangle rectangle, float red, float green, float blue, float alpha) { + texRect(material, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), rotation, red, green, blue, alpha); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void texRect(Material material, int rotation, double x, double y, double width, double height) { + texRect(material, x, y, width, height, rotation, 1F, 1F, 1F, 1F); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void texRect(Material material, int rotation, double x, double y, double width, double height, int argb) { + texRect(material, x, y, width, height, rotation, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void texRect(Material material, double x, double y, double width, double height, int rotation, float red, float green, float blue, float alpha) { + tex(material, x, y, x + width, y + height, rotation, red, green, blue, alpha); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void tex(Material material, int rotation, double xMin, double yMin, double xMax, double yMax) { + tex(material, xMin, yMin, xMax, yMax, rotation, 1F, 1F, 1F, 1F); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void tex(Material material, double xMin, double yMin, double xMax, double yMax, int rotation, int argb) { + tex(material, xMin, yMin, xMax, yMax, rotation, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * Draws a texture sprite derived from the provided material. + * Texture will be resized / reshaped as appropriate to fit the defined area. + * + * @param rotation Rotates sprite clockwise in 90 degree steps. + */ + public void tex(Material material, double xMin, double yMin, double xMax, double yMax, int rotation, float red, float green, float blue, float alpha) { + TextureAtlasSprite sprite = material.sprite(); + VertexConsumer buffer = material.buffer(buffers, GuiRender::texColType); + float[] u = {sprite.getU0(), sprite.getU1(), sprite.getU1(), sprite.getU0()}; + float[] v = {sprite.getV1(), sprite.getV1(), sprite.getV0(), sprite.getV0()}; + Matrix4f mat = pose.last().pose(); + buffer.vertex(mat, (float) xMax, (float) yMax, 0).color(red, green, blue, alpha).uv(u[(1 + rotation) % 4], v[(1 + rotation) % 4]).endVertex(); //R-B + buffer.vertex(mat, (float) xMax, (float) yMin, 0).color(red, green, blue, alpha).uv(u[(2 + rotation) % 4], v[(2 + rotation) % 4]).endVertex(); //R-T + buffer.vertex(mat, (float) xMin, (float) yMin, 0).color(red, green, blue, alpha).uv(u[(3 + rotation) % 4], v[(3 + rotation) % 4]).endVertex(); //L-T + buffer.vertex(mat, (float) xMin, (float) yMax, 0).color(red, green, blue, alpha).uv(u[(0 + rotation) % 4], v[(0 + rotation) % 4]).endVertex(); //L-B + flushIfUnBatched(); + } + + //Slice and stitch + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, Rectangle rectangle, Borders borders, int argb) { + dynamicTex(material, (int) rectangle.x(), (int) rectangle.y(), (int) rectangle.width(), (int) rectangle.height(), (int) borders.top(), (int) borders.left(), (int) borders.bottom(), (int) borders.right(), argb); + } + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, Rectangle rectangle, int topBorder, int leftBorder, int bottomBorder, int rightBorder, int argb) { + dynamicTex(material, (int) rectangle.x(), (int) rectangle.y(), (int) rectangle.width(), (int) rectangle.height(), topBorder, leftBorder, bottomBorder, rightBorder, argb); + } + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, int x, int y, int width, int height, int topBorder, int leftBorder, int bottomBorder, int rightBorder, int argb) { + dynamicTex(material, x, y, width, height, topBorder, leftBorder, bottomBorder, rightBorder, r(argb), g(argb), b(argb), a(argb)); + } + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, Rectangle rectangle, Borders borders) { + dynamicTex(material, (int) rectangle.x(), (int) rectangle.y(), (int) rectangle.width(), (int) rectangle.height(), (int) borders.top(), (int) borders.left(), (int) borders.bottom(), (int) borders.right()); + } + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, Rectangle rectangle, int topBorder, int leftBorder, int bottomBorder, int rightBorder) { + dynamicTex(material, (int) rectangle.x(), (int) rectangle.y(), (int) rectangle.width(), (int) rectangle.height(), topBorder, leftBorder, bottomBorder, rightBorder); + } + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, int x, int y, int width, int height, int topBorder, int leftBorder, int bottomBorder, int rightBorder) { + dynamicTex(material, x, y, width, height, topBorder, leftBorder, bottomBorder, rightBorder, 1F, 1F, 1F, 1F); + } + + /** + * This can be used to take something like a generic bordered background texture and dynamically resize it to draw at any size and shape you want. + * This is done by cutting up the texture and stitching it back to together using, cutting and tiling as required. + * The border parameters indicate the width of the borders around the texture, e.g. a vanilla gui texture has 4 pixel borders. + */ + public void dynamicTex(Material material, int x, int y, int width, int height, int topBorder, int leftBorder, int bottomBorder, int rightBorder, float red, float green, float blue, float alpha) { + if (batchDraw) {//Draw batched for efficiency, unless already doing a batch draw. + dynamicTexInternal(material, x, y, width, height, topBorder, leftBorder, bottomBorder, rightBorder, red, green, blue, alpha); + } else { + batchDraw(() -> dynamicTexInternal(material, x, y, width, height, topBorder, leftBorder, bottomBorder, rightBorder, red, green, blue, alpha)); + } + } + + //Todo, This method can probably be made a lot more efficient. + private void dynamicTexInternal(Material material, int xPos, int yPos, int xSize, int ySize, int topBorder, int leftBorder, int bottomBorder, int rightBorder, float red, float green, float blue, float alpha) { + TextureAtlasSprite sprite = material.sprite(); + VertexConsumer buffer = material.buffer(buffers, GuiRender::texColType); + Matrix4f mat = pose.last().pose(); + SpriteContents contents = sprite.contents(); + int texWidth = contents.width(); + int texHeight = contents.height(); + int trimWidth = texWidth - leftBorder - rightBorder; + int trimHeight = texHeight - topBorder - bottomBorder; + if (xSize <= texWidth) trimWidth = Math.min(trimWidth, xSize - rightBorder); + if (xSize <= 0 || ySize <= 0 || trimWidth <= 0 || trimHeight <= 0) return; + + for (int x = 0; x < xSize; ) { + int rWidth = Math.min(xSize - x, trimWidth); + int trimU = 0; + if (x != 0) { + if (x + leftBorder + trimWidth <= xSize) { + trimU = leftBorder; + } else { + trimU = (texWidth - (xSize - x)); + } + } + + //Top & Bottom trim + bufferDynamic(buffer, mat, sprite, xPos + x, yPos, trimU, 0, rWidth, topBorder, red, green, blue, alpha); + bufferDynamic(buffer, mat, sprite, xPos + x, yPos + ySize - bottomBorder, trimU, texHeight - bottomBorder, rWidth, bottomBorder, red, green, blue, alpha); + + rWidth = Math.min(xSize - x - leftBorder - rightBorder, trimWidth); + for (int y = 0; y < ySize; ) { + int rHeight = Math.min(ySize - y - topBorder - bottomBorder, trimHeight); + int trimV; + if (y + (texHeight - topBorder - bottomBorder) <= ySize) { + trimV = topBorder; + } else { + trimV = texHeight - (ySize - y); + } + + //Left & Right trim + if (x == 0 && y + topBorder < ySize - bottomBorder) { + bufferDynamic(buffer, mat, sprite, xPos, yPos + y + topBorder, 0, trimV, leftBorder, rHeight, red, green, blue, alpha); + bufferDynamic(buffer, mat, sprite, xPos + xSize - rightBorder, yPos + y + topBorder, trimU + texWidth - rightBorder, trimV, rightBorder, rHeight, red, green, blue, alpha); + } + + //Core + if (y + topBorder < ySize - bottomBorder && x + leftBorder < xSize - rightBorder) { + bufferDynamic(buffer, mat, sprite, xPos + x + leftBorder, yPos + y + topBorder, leftBorder, topBorder, rWidth, rHeight, red, green, blue, alpha); + } + y += trimHeight; + } + x += trimWidth; + } + } + + private void bufferDynamic(VertexConsumer builder, Matrix4f mat, TextureAtlasSprite tex, int x, int y, double textureX, double textureY, int width, int height, float red, float green, float blue, float alpha) { + int w = tex.contents().width(); + int h = tex.contents().height(); + //@formatter:off + builder.vertex(mat, x, y + height, 0).color(red, green, blue, alpha).uv(tex.getU((textureX / w) * 16D), tex.getV(((textureY + height) / h) * 16)).endVertex(); + builder.vertex(mat, x + width, y + height, 0).color(red, green, blue, alpha).uv(tex.getU(((textureX + width) / w) * 16), tex.getV(((textureY + height) / h) * 16)).endVertex(); + builder.vertex(mat, x + width, y, 0).color(red, green, blue, alpha).uv(tex.getU(((textureX + width) / w) * 16), tex.getV(((textureY) / h) * 16)).endVertex(); + builder.vertex(mat, x, y, 0).color(red, green, blue, alpha).uv(tex.getU((textureX / w) * 16), tex.getV(((textureY) / h) * 16)).endVertex(); + //@formatter:on + } + + //=== Strings ===// + + /** + * Draw string with shadow. + */ + public int drawString(@Nullable String message, double x, double y, int colour) { + return drawString(message, x, y, colour, true); + } + + public int drawString(@Nullable String message, double x, double y, int colour, boolean shadow) { + if (message == null) return 0; + int i = font().drawInBatch(message, (float) x, (float) y, colour, shadow, pose.last().pose(), buffers, Font.DisplayMode.NORMAL, 0, 15728880, font().isBidirectional()); + this.flushIfUnBatched(); + return i; + } + + /** + * Draw string with shadow. + */ + public int drawString(FormattedCharSequence message, double x, double y, int colour) { + return drawString(message, x, y, colour, true); + } + + public int drawString(FormattedCharSequence message, double x, double y, int colour, boolean shadow) { + int i = font().drawInBatch(message, (float) x, (float) y, colour, shadow, pose.last().pose(), buffers, Font.DisplayMode.NORMAL, 0, 15728880); + this.flushIfUnBatched(); + return i; + } + + /** + * Draw string with shadow. + */ + public int drawString(Component message, double x, double y, int colour) { + return drawString(message, x, y, colour, true); + } + + public int drawString(Component message, double x, double y, int colour, boolean shadow) { + return drawString(message.getVisualOrderText(), x, y, colour, shadow); + } + + /** + * Draw wrapped string with shadow. + */ + public void drawWordWrap(FormattedText message, double x, double y, int width, int colour) { + drawWordWrap(message, x, y, width, colour, false); + } + + public void drawWordWrap(FormattedText message, double x, double y, int width, int colour, boolean shadow) { + drawWordWrap(message, x, y, width, colour, shadow, font().lineHeight); + } + + public void drawWordWrap(FormattedText message, double x, double y, int width, int colour, boolean shadow, double spacing) { + for (FormattedCharSequence formattedcharsequence : font().split(message, width)) { + drawString(formattedcharsequence, x, y, colour, shadow); + y += spacing; + } + } + + /** + * Draw centered string with shadow. (centered on x position) + */ + public void drawCenteredString(String message, double x, double y, int colour) { + drawCenteredString(message, x, y, colour, true); + } + + public void drawCenteredString(String message, double x, double y, int colour, boolean shadow) { + drawString(message, x - font().width(message) / 2D, y, colour, shadow); + } + + /** + * Draw centered string with shadow. (centered on x position) + */ + public void drawCenteredString(Component message, double x, double y, int colour) { + drawCenteredString(message, x, y, colour, true); + } + + public void drawCenteredString(Component message, double x, double y, int colour, boolean shadow) { + FormattedCharSequence formattedcharsequence = message.getVisualOrderText(); + drawString(formattedcharsequence, x - font().width(formattedcharsequence) / 2D, y, colour, shadow); + } + + /** + * Draw centered string with shadow. (centered on x position) + */ + public void drawCenteredString(FormattedCharSequence message, double x, double y, int colour) { + drawCenteredString(message, x, y, colour, true); + } + + public void drawCenteredString(FormattedCharSequence message, double x, double y, int colour, boolean shadow) { + drawString(message, x - font().width(message) / 2D, y, colour, shadow); + } + + /** + * If text is to long ti fit between x and xMaz, the text will scroll from left to right. + * Otherwise, will render centered. + * This is mostly copied from {@link AbstractWidget#renderScrollingString(GuiGraphics, Font, Component, int, int, int, int, int)} + */ + @SuppressWarnings ("JavadocReference") + public void drawScrollingString(Component component, double x, double y, double xMax, int colour, boolean shadow) { + drawScrollingString(component, x, y, xMax, colour, shadow, true); + } + + /** + * If text is to long ti fit between x and xMaz, the text will scroll from left to right. + * Otherwise, will render centered. + * This is mostly copied from {@link net.minecraft.client.gui.components.AbstractWidget#renderScrollingString(GuiGraphics, Font, Component, int, int, int, int, int)} + */ + @SuppressWarnings ("JavadocReference") + public void drawScrollingString(Component component, double x, double y, double xMax, int colour, boolean shadow, boolean doScissor) { + int textWidth = font().width(component); + double width = xMax - x; + if (textWidth > width) { + double outside = textWidth - width; + double anim = (double) Util.getMillis() / 1000.0; + double e = Math.max(outside * 0.5, 3.0); + double f = Math.sin(1.5707963267948966 * Math.cos(6.283185307179586 * anim / e)) / 2.0 + 0.5; + double offset = Mth.lerp(f, 0.0, outside); + if (doScissor) pushScissor(x, y - 1, xMax, y + font().lineHeight + 1); + drawString(component, x - offset, y, colour, shadow); + if (doScissor) popScissor(); + } else { + drawCenteredString(component, (x + xMax) / 2, y, colour, shadow); + } + } + + //=== Tool Tips ===// + + private ItemStack tooltipStack = ItemStack.EMPTY; + + public void renderTooltip(ItemStack stack, double mouseX, double mouseY) { + renderTooltip(stack, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f); + } + + public void renderTooltip(ItemStack stack, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + this.tooltipStack = stack; + this.toolTipWithImage(Screen.getTooltipFromItem(this.mc(), stack), stack.getTooltipImage(), mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom); + this.tooltipStack = ItemStack.EMPTY; + } + + public void toolTipWithImage(List tooltips, Optional tooltipImage, ItemStack stack, double mouseX, double mouseY) { + toolTipWithImage(tooltips, tooltipImage, stack, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f); + } + + public void toolTipWithImage(List tooltips, Optional tooltipImage, ItemStack stack, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + this.tooltipStack = stack; + this.toolTipWithImage(tooltips, tooltipImage, mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom); + this.tooltipStack = ItemStack.EMPTY; + } + + public void toolTipWithImage(List tooltip, Optional tooltipImage, double mouseX, double mouseY) { + toolTipWithImage(tooltip, tooltipImage, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f); + } + + public void toolTipWithImage(List tooltip, Optional tooltipImage, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + List list = ForgeHooksClient.gatherTooltipComponents(tooltipStack, tooltip, tooltipImage, (int) mouseX, guiWidth(), guiHeight(), font()); + this.renderTooltipInternal(list, mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom, DefaultTooltipPositioner.INSTANCE); + } + + public void renderTooltip(Component message, double mouseX, double mouseY) { + renderTooltip(message, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f); + } + + public void renderTooltip(Component message, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + this.renderTooltip(List.of(message.getVisualOrderText()), mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom); + } + + public void componentTooltip(List tooltips, double mouseX, double mouseY) { + componentTooltip(tooltips, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f); + } + + public void componentTooltip(List tooltips, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + List list = ForgeHooksClient.gatherTooltipComponents(tooltipStack, tooltips, Optional.empty(), (int) mouseX, guiWidth(), guiHeight(), font()); + this.renderTooltipInternal(list, mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom, DefaultTooltipPositioner.INSTANCE); + } + + public void componentTooltip(List tooltips, double mouseX, double mouseY, ItemStack stack) { + componentTooltip(tooltips, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f, stack); + } + + public void componentTooltip(List tooltips, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom, ItemStack stack) { + this.tooltipStack = stack; + List list = ForgeHooksClient.gatherTooltipComponents(tooltipStack, tooltips, Optional.empty(), (int) mouseX, guiWidth(), guiHeight(), font()); + this.renderTooltipInternal(list, mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom, DefaultTooltipPositioner.INSTANCE); + this.tooltipStack = ItemStack.EMPTY; + } + + public void renderTooltip(List tooltips, double mouseX, double mouseY) { + renderTooltip(tooltips, mouseX, mouseY, 0xf0100010, 0xf0100010, 0x505000ff, 0x5028007f); + } + + public void renderTooltip(List tooltips, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + this.renderTooltipInternal(tooltips.stream().map(ClientTooltipComponent::create).collect(Collectors.toList()), mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom, DefaultTooltipPositioner.INSTANCE); + } + + public void renderTooltip(List tooltips, ClientTooltipPositioner positioner, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom) { + this.renderTooltipInternal(tooltips.stream().map(ClientTooltipComponent::create).collect(Collectors.toList()), mouseX, mouseY, backgroundTop, backgroundBottom, borderTop, borderBottom, positioner); + } + + private void renderTooltipInternal(List tooltips, double mouseX, double mouseY, int backgroundTop, int backgroundBottom, int borderTop, int borderBottom, ClientTooltipPositioner positioner) { + if (!tooltips.isEmpty()) { + RenderTooltipEvent.Pre event = ForgeHooksClient.onRenderTooltipPre(tooltipStack, guiGraphicsWrapper(), (int) mouseX, (int) mouseY, guiWidth(), guiHeight(), tooltips, font(), positioner); + if (event.isCanceled()) return; + + + int width = 0; + int height = tooltips.size() == 1 ? -2 : 0; + for (ClientTooltipComponent line : tooltips) { + width = Math.max(width, line.getWidth(event.getFont())); + height += line.getHeight(); + } + + Vector2ic position = positioner.positionTooltip(guiWidth(), guiHeight(), event.getX(), event.getY(), width, height); + int xPos = position.x(); + int yPos = Math.max(position.y(), 3); //Default positioner allows negative y-pos for some reason... + + RenderTooltipEvent.Color colorEvent = new RenderTooltipEvent.Color(tooltipStack, guiGraphicsWrapper(), (int) mouseX, (int) mouseY, font(), backgroundTop, borderTop, borderBottom, tooltips); + colorEvent.setBackgroundEnd(backgroundBottom); + MinecraftForge.EVENT_BUS.post(colorEvent); + + toolTipBackground(xPos - 3, yPos - 3, width + 6, height + 6, colorEvent.getBackgroundStart(), colorEvent.getBackgroundEnd(), colorEvent.getBorderStart(), colorEvent.getBorderEnd(), false); + int linePos = yPos; + + for (int i = 0; i < tooltips.size(); ++i) { + ClientTooltipComponent component = tooltips.get(i); + component.renderText(event.getFont(), xPos, linePos, pose.last().pose(), buffers); + linePos += component.getHeight() + (i == 0 ? 2 : 0); + } + + linePos = yPos; + + for (int i = 0; i < tooltips.size(); ++i) { + ClientTooltipComponent component = tooltips.get(i); + component.renderImage(event.getFont(), xPos, linePos, renderWrapper); + linePos += component.getHeight() + (i == 0 ? 2 : 0); + } + } + } + + public void renderComponentHoverEffect(@Nullable Style style, int mouseX, int mouseY) { + if (style != null && style.getHoverEvent() != null) { + HoverEvent event = style.getHoverEvent(); + HoverEvent.ItemStackInfo stackInfo = event.getValue(HoverEvent.Action.SHOW_ITEM); + if (stackInfo != null) { + renderTooltip(stackInfo.getItemStack(), mouseX, mouseY); + } else { + HoverEvent.EntityTooltipInfo tooltipInfo = event.getValue(HoverEvent.Action.SHOW_ENTITY); + if (tooltipInfo != null) { + if (mc().options.advancedItemTooltips) { + componentTooltip(tooltipInfo.getTooltipLines(), mouseX, mouseY); + } + } else { + Component component = event.getValue(HoverEvent.Action.SHOW_TEXT); + if (component != null) { + renderTooltip(font().split(component, Math.max(this.guiWidth() / 2, 200)), mouseX, mouseY); + } + } + } + } + } + + //=== ItemStacks ===// + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is 32, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderItem(ItemStack stack, double x, double y) { + renderItem(stack, x, y, 16); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is size*2, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderItem(ItemStack stack, double x, double y, double size) { + this.renderItem(mc().player, mc().level, stack, x, y, size, 0); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is size*2, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderItem(ItemStack stack, double x, double y, double size, int modelRand) { + this.renderItem(mc().player, mc().level, stack, x, y, size, modelRand); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is 32, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderFakeItem(ItemStack stack, double x, double y) { + renderFakeItem(stack, x, y, 16); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is size*2, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderFakeItem(ItemStack stack, double x, double y, double size) { + this.renderItem(null, mc().level, stack, x, y, size, 0); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is 32, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderItem(LivingEntity entity, ItemStack stack, double x, double y, int modelRand) { + renderItem(entity, stack, x, y, 16, modelRand); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is size*2, This must be accounted for when setting the z depth in an element using this render method. + */ + public void renderItem(LivingEntity entity, ItemStack stack, double x, double y, double size, int modelRand) { + this.renderItem(entity, entity.level(), stack, x, y, size, modelRand); + } + + /** + * Renders an item stack on the screen. + * Important Note: Required z clearance is size*2, This must be accounted for when setting the z depth in an element using this render method. + * + * @param size Width and height of the stack in pixels (Standard default is 16) + * @param modelRand A somewhat random value used in model gathering, Not very important, Can just use 0 or x/y position. + */ + public void renderItem(@Nullable LivingEntity entity, @Nullable Level level, ItemStack stack, double x, double y, double size, int modelRand) { + if (!stack.isEmpty()) { + BakedModel bakedmodel = mc().getItemRenderer().getModel(stack, level, entity, modelRand); + pose.pushPose(); + pose.translate(x + (size / 2D), y + (size / 2D), size); + try { + pose.mulPoseMatrix((new Matrix4f()).scaling(1.0F, -1.0F, 1.0F)); + pose.scale((float) size, (float) size, (float) size); + boolean flag = !bakedmodel.usesBlockLight(); + if (flag) Lighting.setupForFlatItems(); + mc().getItemRenderer().render(stack, ItemDisplayContext.GUI, false, pose, buffers, 0xf000f0, OverlayTexture.NO_OVERLAY, bakedmodel); + this.flush(); + if (flag) Lighting.setupFor3DItems(); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Rendering item"); + CrashReportCategory crashreportcategory = crashreport.addCategory("Item being rendered"); + crashreportcategory.setDetail("Item Type", () -> String.valueOf(stack.getItem())); + crashreportcategory.setDetail("Item Stack", () -> String.valueOf(stack.getItem())); + crashreportcategory.setDetail("Item Damage", () -> String.valueOf(stack.getDamageValue())); + crashreportcategory.setDetail("Item NBT", () -> String.valueOf(stack.getTag())); + crashreportcategory.setDetail("Item Foil", () -> String.valueOf(stack.hasFoil())); + throw new ReportedException(crashreport); + } + pose.popPose(); + } + } + + /** + * Draw item decorations (Count, Damage, Cool-down) + * This should be rendered at the same position and size as the item. + * There is no need to fiddle with z offsets or anything, just call renderItemDecorations after renderItem and it will work. + * Z depth requirements are the same as the renderItem method. + */ + public void renderItemDecorations(ItemStack stack, double x, double y) { + renderItemDecorations(stack, x, y, 16); + } + + /** + * Draw item decorations (Count, Damage, Cool-down) + * This should be rendered at the same position and size as the item. + * There is no need to fiddle with z offsets or anything, just call renderItemDecorations after renderItem and it will work. + * Z depth requirements are the same as the renderItem method. + */ + public void renderItemDecorations(ItemStack stack, double x, double y, double size) { + this.renderItemDecorations(stack, x, y, size, null); + } + + /** + * Draw item decorations (Count, Damage, Cool-down) + * This should be rendered at the same position and size as the item. + * There is no need to fiddle with z offsets or anything, just call renderItemDecorations after renderItem and it will work. + * Z depth requirements are the same as the renderItem method. + */ + public void renderItemDecorations(ItemStack stack, double x, double y, @Nullable String text) { + renderItemDecorations(stack, x, y, 16, text); + } + + /** + * Draw item decorations (Count, Damage, Cool-down) + * This should be rendered at the same position and size as the item. + * There is no need to fiddle with z offsets or anything, just call renderItemDecorations after renderItem and it will work. + * Z depth requirements are the same as the renderItem method. + */ + public void renderItemDecorations(ItemStack stack, double x, double y, double size, @Nullable String text) { + if (!stack.isEmpty()) { + pose.pushPose(); + float scale = (float) size / 16F; + pose.translate(x, y, (size * 2) - 0.1); + pose.scale(scale, scale, 1F); + pose.translate(-x, -y, 0); + + if (stack.getCount() != 1 || text != null) { + String s = text == null ? String.valueOf(stack.getCount()) : text; + drawString(s, x + 19 - 2 - font().width(s), y + 6 + 3, 0xffffff, true); + } + + if (stack.isBarVisible()) { + int l = stack.getBarWidth(); + int i = stack.getBarColor(); + double j = x + 2; + double k = y + 13; + pose.translate(0.0F, 0.0F, 0.04); + fill(j, k, j + 13, k + 2, 0xff000000); + pose.translate(0.0F, 0.0F, 0.02); + fill(j, k, j + l, k + 1, i | 0xff000000); + } + + LocalPlayer localplayer = mc().player; + float f = localplayer == null ? 0.0F : localplayer.getCooldowns().getCooldownPercent(stack.getItem(), mc().getFrameTime()); + if (f > 0.0F) { + double i1 = y + Mth.floor(16.0F * (1.0F - f)); + double j1 = i1 + Mth.ceil(16.0F * f); + pose.translate(0.0F, 0.0F, 0.02); + fill(x, i1, x + 16, j1, Integer.MAX_VALUE); + } + + pose.popPose(); + if (size == 16) { + net.minecraftforge.client.ItemDecoratorHandler.of(stack).render(guiGraphicsWrapper(), font(), stack, (int) x, (int) y); + } + } + } + + + //=== Entity? ===// + + + //=== Render Utils ===// + + public void pushScissorRect(Rectangle rectangle) { + pushScissorRect(rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height()); + } + + public void pushScissorRect(double x, double y, double width, double height) { + flushIfBatched(); + scissorHandler.pushGuiScissor(x, y, width, height); + } + + public void pushScissor(double xMin, double yMin, double xMax, double yMax) { + flushIfBatched(); + scissorHandler.pushGuiScissor(xMin, yMin, xMax - xMin, yMax - yMin); + } + + public void popScissor() { + scissorHandler.popScissor(); + } + + /** + * Sets the render system shader colour, Effect will vary depending on what is being rendered. + * Ideally this should be avoided in favor of render calls that accept colour. + */ + public void setColor(float red, float green, float blue, float alpha) { + this.flushIfBatched(); + RenderSystem.setShaderColor(red, green, blue, alpha); + } + + //=== Static Utils ===// + + public static boolean isInRect(double minX, double minY, double width, double height, double testX, double testY) { + return ((testX >= minX && testX < minX + width) && (testY >= minY && testY < minY + height)); + } + + public static boolean isInRect(int minX, int minY, int width, int height, double testX, double testY) { + return ((testX >= minX && testX < minX + width) && (testY >= minY && testY < minY + height)); + } + + /** + * Mixes the two input colours by adding up the R, G, B and A values of each input. + */ + public static int mixColours(int colour1, int colour2) { + return mixColours(colour1, colour2, false); + } + + /** + * Mixes the two input colours by adding up the R, G, B and A values of each input. + * + * @param subtract If true, subtract colour2 from colour1, otherwise add colour2 to colour1. + */ + public static int mixColours(int colour1, int colour2, boolean subtract) { + int alpha1 = colour1 >> 24 & 255; + int alpha2 = colour2 >> 24 & 255; + int red1 = colour1 >> 16 & 255; + int red2 = colour2 >> 16 & 255; + int green1 = colour1 >> 8 & 255; + int green2 = colour2 >> 8 & 255; + int blue1 = colour1 & 255; + int blue2 = colour2 & 255; + + int alpha = Mth.clamp(alpha1 + (subtract ? -alpha2 : alpha2), 0, 255); + int red = Mth.clamp(red1 + (subtract ? -red2 : red2), 0, 255); + int green = Mth.clamp(green1 + (subtract ? -green2 : green2), 0, 255); + int blue = Mth.clamp(blue1 + (subtract ? -blue2 : blue2), 0, 255); + + return (alpha & 0xFF) << 24 | (red & 0xFF) << 16 | (green & 0xFF) << 8 | blue & 0xFF; + } + + /** + * Returns a colour half-way between the two input colours. + * The R, G, B and A channels are extracted from each input, + * Then for each chanel, a midpoint is determined, + * And a new colour is constructed based on the midpoint of each channel. + */ + public static int midColour(int colour1, int colour2) { + int alpha1 = colour1 >> 24 & 255; + int alpha2 = colour2 >> 24 & 255; + int red1 = colour1 >> 16 & 255; + int red2 = colour2 >> 16 & 255; + int green1 = colour1 >> 8 & 255; + int green2 = colour2 >> 8 & 255; + int blue1 = colour1 & 255; + int blue2 = colour2 & 255; + return (alpha2 + (alpha1 - alpha2) / 2 & 0xFF) << 24 | (red2 + (red1 - red2) / 2 & 0xFF) << 16 | (green2 + (green1 - green2) / 2 & 0xFF) << 8 | blue2 + (blue1 - blue2) / 2 & 0xFF; + } + + private static float r(int argb) { + return (argb >> 16 & 255) / 255F; + } + + private static float g(int argb) { + return (argb >> 8 & 255) / 255F; + } + + private static float b(int argb) { + return (argb & 255) / 255F; + } + + private static float a(int argb) { + return (argb >>> 24) / 255F; + } + + //Render Type Builders + + public static RenderType texType(ResourceLocation location) { + return RenderType.create("tex_type", DefaultVertexFormat.POSITION_TEX, VertexFormat.Mode.QUADS, 256, RenderType.CompositeState.builder() + .setShaderState(new RenderStateShard.ShaderStateShard(GameRenderer::getPositionTexShader)) + .setTextureState(new RenderStateShard.TextureStateShard(location, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.NO_CULL) + .createCompositeState(false)); + } + + public static RenderType texColType(ResourceLocation location) { + return RenderType.create("tex_col_type", DefaultVertexFormat.POSITION_COLOR_TEX, VertexFormat.Mode.QUADS, 256, RenderType.CompositeState.builder() + .setShaderState(new RenderStateShard.ShaderStateShard(GameRenderer::getPositionColorTexShader)) + .setTextureState(new RenderStateShard.TextureStateShard(location, false, false)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setCullState(RenderStateShard.NO_CULL) + .createCompositeState(false)); + } + + /** + * This exists to allow thing like the Tooltip events to still function correctly, hopefully without exploding... + */ + public static class RenderWrapper extends GuiGraphics { + private final GuiRender wrapped; + + private RenderWrapper(GuiRender wrapped) { + super(wrapped.mc(), wrapped.pose(), wrapped.buffers()); + this.wrapped = wrapped; + } + + @Override + public void drawManaged(Runnable runnable) { + wrapped.batchDraw(runnable); + } + + @Override + public void flush() { + wrapped.flush(); + } + + @Override + public void flushIfManaged() { + wrapped.flushIfBatched(); + } + + @Override + public void flushIfUnmanaged() { + wrapped.flushIfUnBatched(); + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/ScissorHandler.java b/src/main/java/codechicken/lib/gui/modular/lib/ScissorHandler.java new file mode 100644 index 00000000..06952dec --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/ScissorHandler.java @@ -0,0 +1,75 @@ +package codechicken.lib.gui.modular.lib; + +import com.google.common.collect.Queues; +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Deque; + +/** + * Created by brandon3055 on 29/06/2023 + */ +public class ScissorHandler { + public static final Logger LOGGER = LogManager.getLogger(); + private final Deque stack = Queues.newArrayDeque(); + + public void pushGuiScissor(double x, double y, double width, double height) { + Window window = Minecraft.getInstance().getWindow(); + int windowHeight = window.getHeight(); + double scale = window.getGuiScale(); + double scX = x * scale; + double scY = (double) windowHeight - (y + height) * scale; + double scW = Math.max(width * scale, 0); + double scH = Math.max(height * scale, 0); + pushScissor((int) scX, (int) scY, (int) scW, (int) scH); + } + + public void pushScissor(int x, int y, int width, int height) { + int xMax = x + width; + int yMax = y + height; + stack.addLast(ScissorState.createState(x, y, xMax, yMax, stack.peekLast()).apply()); + } + + public void popScissor() { + if (stack.isEmpty()) { + LOGGER.error("Scissor stack underflow"); + } + stack.removeLast(); + ScissorState active = stack.peekLast(); + if (active != null) { + active.apply(); + } else { + RenderSystem.disableScissor(); + } + } + + private record ScissorState(int x, int y, int xMax, int yMax) { + + private ScissorState apply() { + RenderSystem.enableScissor(x, y, xMax - x, yMax - y); + return this; + } + + private static ScissorState createState(int newX, int newY, int newXMax, int newYMax, ScissorState prevState) { + if (prevState != null) { + int x = Math.max(prevState.x, newX); + int y = Math.max(prevState.y, newY); + int xMax = Math.min(prevState.xMax, newXMax); + int yMax = Math.min(prevState.yMax, newYMax); + Minecraft mc = Minecraft.getInstance(); + if (x < 0) x = 0; + if (y < 0) y = 0; + if (xMax > mc.getWindow().getScreenWidth()) xMax = mc.getWindow().getScreenWidth(); + if (yMax > mc.getWindow().getScreenHeight()) yMax = mc.getWindow().getScreenHeight(); + if (xMax < x) xMax = x; + if (yMax < y) yMax = y; + return new ScissorState(x, y, xMax, yMax); + } else { + return new ScissorState(newX, newY, newXMax, newYMax); + } + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/SliderState.java b/src/main/java/codechicken/lib/gui/modular/lib/SliderState.java new file mode 100644 index 00000000..a9724798 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/SliderState.java @@ -0,0 +1,139 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.gui.modular.lib.geometry.Axis; +import net.minecraft.client.gui.screens.Screen; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * The primary interface for managing getting and setting the position of slider elements. + *

+ * Created by brandon3055 on 01/09/2023 + */ +public interface SliderState { + + /** + * @return the current position. (Between 0 and 1) + */ + double getPos(); + + /** + * Set the current slide position, + * When using this method, make sure the provided value can not go outside the valid range of 0 to 1. + * + * @param pos Set the current position (Between 0 and 1) + */ + void setPos(double pos); + + /** + * The slider ratio is computed by dividing the length of the slider element by the total length of the slider track. + * This is primarily for things like setting the size of scroll bar sliders, and calibrating scrolling via middle-click + drag. + * + * @return scroll ratio, viewable content size / total content size (Range 0 to 1) + */ + default double sliderRatio() { + return 0.1; + } + + /** + * For controlling a slider via mouse scroll wheel. + * You can return a negative value to invert the scroll direction. + * + * @return The amount added to the position per scroll increment. + */ + default double scrollSpeed() { + double ratio = sliderRatio(); + return ratio < 0.1 ? ratio * 0.1 : ratio * ratio; + } + + /** + * @param scrollAxis The moving axis of the slider element. + * @return true if the current scroll wheel event should affect this slider. + */ + default boolean canScroll(Axis scrollAxis) { + return true; + } + + /** + * Creates a basic slide state which stores its position internally. + * Useful for things like simple slide control elements. + */ + static SliderState create(double speed) { + return create(speed, null); + } + + /** + * Creates a basic slide state which stores its position internally. + * And allows you to attach a change listener. + * Useful for things like simple slide control elements. + */ + static SliderState create(double speed, @Nullable Consumer changeListener) { + return new SliderState() { + double pos = 0; + + @Override + public double getPos() { + return pos; + } + + @Override + public void setPos(double pos) { + this.pos = pos; + if (changeListener != null) { + changeListener.accept(pos); + } + } + + @Override + public double scrollSpeed() { + return speed; + } + }; + } + + static SliderState forScrollBar(Supplier getPos, Consumer setPos, Supplier getRatio) { + return new SliderState() { + @Override + public double getPos() { + return getPos.get(); + } + + @Override + public void setPos(double pos) { + setPos.accept(pos); + } + + @Override + public double sliderRatio() { + return getRatio.get(); + } + + @Override + public boolean canScroll(Axis scrollAxis) { + //Controls scrolling left and right when shift key is down. + return (scrollAxis == Axis.Y) != Screen.hasShiftDown(); + } + }; + } + + static SliderState forSlider(Supplier getPos, Consumer setPos, Supplier getSpeed) { + return new SliderState() { + @Override + public double getPos() { + return getPos.get(); + } + + @Override + public void setPos(double pos) { + setPos.accept(pos); + } + + @Override + public double scrollSpeed() { + return getSpeed.get(); + } + }; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/TextState.java b/src/main/java/codechicken/lib/gui/modular/lib/TextState.java new file mode 100644 index 00000000..f92af296 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/TextState.java @@ -0,0 +1,56 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.gui.modular.elements.GuiTextField; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * The primary interface for managing getting and setting the text in a {@link GuiTextField} + *

+ * Created by brandon3055 on 03/09/2023 + */ +public interface TextState { + + String getText(); + + void setText(String text); + + static TextState simpleState(String defaultValue) { + return simpleState(defaultValue, null); + } + + static TextState simpleState(String defaultValue, @Nullable Consumer changeListener) { + return new TextState() { + private String value = defaultValue; + + @Override + public String getText() { + return value; + } + + @Override + public void setText(String text) { + this.value = text; + if (changeListener != null) { + changeListener.accept(value); + } + } + }; + } + + static TextState create(Supplier getValue, Consumer setValue) { + return new TextState() { + @Override + public String getText() { + return getValue.get(); + } + + @Override + public void setText(String text) { + setValue.accept(text); + } + }; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/TooltipHandler.java b/src/main/java/codechicken/lib/gui/modular/lib/TooltipHandler.java new file mode 100644 index 00000000..7bc1bb20 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/TooltipHandler.java @@ -0,0 +1,105 @@ +package codechicken.lib.gui.modular.lib; + +import codechicken.lib.gui.modular.elements.GuiElement; +import net.covers1624.quack.util.SneakyUtils; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 01/09/2023 + */ +public interface TooltipHandler> { + + Supplier> getTooltip(); + + /** + * Set a delay before element tooltip is displayed. + * Default delay is 10 ticks. + */ + T setTooltipDelay(int tooltipDelay); + + int getTooltipDelay(); + + /** + * Add hover text that is to be displayed when the user hovers their cursor over this element. (with a delay of 10 ticks) + * If you have multiple stacked elements with tooltips, only the top most element under the cursor will display its hover text. + * + * @param Tooltip A single tooltip text component supplier. + * @see #setTooltipDelay(int) + */ + default T setTooltipSingle(@Nullable Component Tooltip) { + return setTooltip(Tooltip == null ? null : () -> Collections.singletonList(Tooltip)); + } + + /** + * Add hover text that is to be displayed when the user hovers their cursor over this element. (with a delay of 10 ticks) + * If you have multiple stacked elements with tooltips, only the top most element under the cursor will display its hover text. + * + * @param Tooltip A single tooltip text component supplier. + * @see #setTooltipDelay(int) + */ + default T setTooltip(Component... Tooltip) { + return setTooltip(Tooltip == null ? null : () -> List.of(Tooltip)); + } + + /** + * Add hover text that is to be displayed when the user hovers their cursor over this element. (with a delay of 10 ticks) + * If you have multiple stacked elements with tooltips, only the top most element under the cursor will display its hover text. + * + * @param tooltip A single line tooltip component supplier. + * @see #setTooltipDelay(int) + */ + default T setTooltipSingle(@Nullable Supplier tooltip) { + return setTooltip(tooltip == null ? null : () -> Collections.singletonList(tooltip.get())); + } + + /** + * Add hover text that is to be displayed when the user hovers their cursor over this element. (with a delay of 10 ticks) + * If you have multiple stacked elements with tooltips, only the top most element under the cursor will display its hover text. + * + * @param tooltip The tooltip lines. If null or empty, hover text will be disabled + * @see #setTooltipDelay(int) + */ + default T setTooltip(@Nullable List tooltip) { + return setTooltip(tooltip == null ? null : () -> tooltip); + } + + /** + * Add hover text that is to be displayed when the user hovers their cursor over this element. (with a delay of 10 ticks) + * If you have multiple stacked elements with tooltips, only the top most element under the cursor will display its hover text. + * + * @param tooltip The tooltip lines. If null or the returned list is empty, hover text will be disabled + * @see #setTooltipDelay(int) + */ + T setTooltip(@Nullable Supplier> tooltip); + + /** + * Add hover text that is to be displayed when the user hovers their cursor over this element. + * If you have multiple stacked elements with tooltips, only the top most element under the cursor will display its hover text. + * + * @param tooltip The tooltip lines. If null or the returned list is empty, hover text will be disabled + * @param tooltipDelay Delay before hover text is shown. + */ + default T setTooltip(@Nullable Supplier> tooltip, int tooltipDelay) { + setTooltip(tooltip); + setTooltipDelay(tooltipDelay); + return SneakyUtils.unsafeCast(this); + } + + /** + * The method responsible for rendering element tool tips. + * Called from {@link GuiElement#renderOverlay(GuiRender, double, double, float, boolean)} + */ + default boolean renderTooltip(GuiRender render, double mouseX, double mouseY) { + Supplier> supplier = getTooltip(); + if (supplier == null) return false; + List list = supplier.get(); + if (list.isEmpty()) return false; + render.componentTooltip(list, mouseX, mouseY); + return true; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/container/ContainerGuiProvider.java b/src/main/java/codechicken/lib/gui/modular/lib/container/ContainerGuiProvider.java new file mode 100644 index 00000000..92282ee8 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/container/ContainerGuiProvider.java @@ -0,0 +1,33 @@ +package codechicken.lib.gui.modular.lib.container; + +import codechicken.lib.gui.modular.ModularGui; +import codechicken.lib.gui.modular.ModularGuiContainer; +import codechicken.lib.gui.modular.lib.GuiProvider; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.world.inventory.AbstractContainerMenu; + +/** + * Created by brandon3055 on 08/09/2023 + */ +public abstract class ContainerGuiProvider implements GuiProvider { + + private ContainerScreenAccess screenAccess; + + public void setMenuAccess(ContainerScreenAccess screenAccess) { + this.screenAccess = screenAccess; + } + + @Override + public final void buildGui(ModularGui gui) { + buildGui(gui, screenAccess); + } + + /** + * This is the same as {@link GuiProvider#buildGui(ModularGui)} except you have access to the {@link MenuAccess} + * The given menu accessor should always be the parent screen unless your using some custom modular gui implementation. + * + * @param gui The modular gui instance. + * @param screenAccess The screen access (This will be a gui class that extends {@link ModularGuiContainer} + */ + public abstract void buildGui(ModularGui gui, ContainerScreenAccess screenAccess); +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/container/ContainerScreenAccess.java b/src/main/java/codechicken/lib/gui/modular/lib/container/ContainerScreenAccess.java new file mode 100644 index 00000000..d8998dd2 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/container/ContainerScreenAccess.java @@ -0,0 +1,22 @@ +package codechicken.lib.gui.modular.lib.container; + +import codechicken.lib.gui.modular.elements.GuiSlots; +import codechicken.lib.gui.modular.lib.GuiRender; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; + +/** + * Used by {@link ContainerGuiProvider} + * Provides access to the ContainerMenu as well as some essential functions. + *

+ * Created by brandon3055 on 08/09/2023 + */ +public interface ContainerScreenAccess extends MenuAccess { + + /** + * This is the modular gui friendly method used by elements such as {@link GuiSlots} to render inventory item stacks. + */ + void renderSlot(GuiRender render, Slot slot); + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/container/DataSync.java b/src/main/java/codechicken/lib/gui/modular/lib/container/DataSync.java new file mode 100644 index 00000000..4b0cb9ec --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/container/DataSync.java @@ -0,0 +1,48 @@ +package codechicken.lib.gui.modular.lib.container; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.inventory.container.data.AbstractDataStore; +import codechicken.lib.inventory.container.modular.ModularGuiContainerMenu; + +import java.util.function.Supplier; + +/** + * This class provides a convenient way to synchronize server side data with a client side screen via the ContainerMenu + *

+ * Created by brandon3055 on 09/09/2023 + */ +public class DataSync { + public static final int PKT_SEND_CHANGES = 255; + private final ModularGuiContainerMenu containerMenu; + private final AbstractDataStore dataStore; + private final Supplier valueGetter; + + public DataSync(ModularGuiContainerMenu containerMenu, AbstractDataStore dataStore, Supplier valueGetter) { + this.containerMenu = containerMenu; + this.dataStore = dataStore; + this.valueGetter = valueGetter; + containerMenu.dataSyncs.add(this); + } + + public T get() { + return dataStore.get(); + } + + /** + * This should only ever be called server side! + */ + public void detectAndSend() { + if (dataStore.isSameValue(valueGetter.get())) { + return; + } + dataStore.set(valueGetter.get()); + containerMenu.sendPacketToClient(PKT_SEND_CHANGES, buf -> { + buf.writeByte((byte) containerMenu.dataSyncs.indexOf(this)); + dataStore.toBytes(buf); + }); + } + + public void handleSyncPacket(MCDataInput buf) { + dataStore.fromBytes(buf); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/container/SlotGroup.java b/src/main/java/codechicken/lib/gui/modular/lib/container/SlotGroup.java new file mode 100644 index 00000000..1b6ff101 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/container/SlotGroup.java @@ -0,0 +1,119 @@ +package codechicken.lib.gui.modular.lib.container; + +import codechicken.lib.gui.modular.elements.GuiSlots; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.inventory.container.modular.ModularGuiContainerMenu; +import codechicken.lib.inventory.container.modular.ModularSlot; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.EnchantmentHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Used to configure slots a set of slots in a ContainerMenu. + * The SlotGroup can then be passed to ModularGui elements such as {@link GuiSlots} in order to give the gui control over slot positioning and rendering. + *

+ * Ideally you should use a separate SlotGroup for each 'group' of slots, Meaning the players main inventory and hot bar should be separate ranges. + * That said, if you have multiple slots spread out across something like a machine gui, You can add them all to one SlotGroup then + * pass each individual slot from that range to a {@link GuiSlots#singleSlot(GuiParent, ContainerScreenAccess, SlotGroup, int)} + *

+ * Created by brandon3055 on 08/09/2023 + */ +public class SlotGroup { + + public final int zone; + public final List quickMoveTo; + + private final ModularGuiContainerMenu containerMenu; + private final List slots = new ArrayList<>(); + + public SlotGroup(ModularGuiContainerMenu containerMenu, int zone, int... quickMoveTo) { + this.zone = zone; + this.containerMenu = containerMenu; + this.quickMoveTo = Arrays.stream(quickMoveTo).boxed().toList(); + } + + public ModularSlot addSlot(ModularSlot slot) { + slots.add(slot); + containerMenu.addSlot(slot); + containerMenu.mapSlot(slot, this); + return slot; + } + + /** + * Convenient method for adding multiple slots. + * + * @param slotCount The number of slots to be added. + * @param startIndex Slot starting index. + * @param makeSlot Builder used to create the slots, Input integer will start at startIndex and increment by one for each slot. + */ + public void addSlots(int slotCount, int startIndex, Function makeSlot) { + for (int index = startIndex; index < startIndex + slotCount; index++) { + addSlot(makeSlot.apply(index)); + } + } + + public void addAllSlots(Container container) { + addAllSlots(container, ModularSlot::new); + } + + public void addAllSlots(Container container, BiFunction makeSlot) { + for (int index = 0; index < container.getContainerSize(); index++) { + addSlot(makeSlot.apply(container, index)); + } + } + + public void addPlayerMain(Inventory inventory) { + addSlots(27, 9, index -> new ModularSlot(inventory, index)); + } + + public void addPlayerBar(Inventory inventory) { + addSlots(9, 0, index -> new ModularSlot(inventory, index)); + } + + public void addPlayerArmor(Inventory inventory) { + for (int i = 0; i < 4; ++i) { + EquipmentSlot slot = EquipmentSlot.byTypeAndIndex(EquipmentSlot.Type.ARMOR, 3 - i); + addSlot(new ModularSlot(inventory, 39 - i) + .onSet((oldStack, newStack) -> onEquipItem(inventory, slot, newStack, oldStack)) + .setStackLimit(stack -> 1) + .setValidator(stack -> slot == Mob.getEquipmentSlotForItem(stack)) + .setCanRemove((player, stack) -> stack.isEmpty() || player.isCreative() || !EnchantmentHelper.hasBindingCurse(stack)) + ); + } + } + + public void addPlayerOffhand(Inventory inventory) { + addSlot(new ModularSlot(inventory, 40).onSet((oldStack, newStack) -> onEquipItem(inventory, EquipmentSlot.OFFHAND, newStack, oldStack))); + } + + static void onEquipItem(Inventory inventory, EquipmentSlot slot, ItemStack newStack, ItemStack oldStack) { + inventory.player.onEquipItem(slot, oldStack, newStack); + } + + public int size() { + return slots.size(); + } + + public ModularSlot getSlot(int index) { + return slots.get(index); + } + + public int indexOf(Slot slot) { + return slots.indexOf(slot); + } + + public List slots() { + return Collections.unmodifiableList(slots); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Align.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Align.java new file mode 100644 index 00000000..c38b84b3 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Align.java @@ -0,0 +1,14 @@ +package codechicken.lib.gui.modular.lib.geometry; + +/** + * Created by brandon3055 on 31/08/2023 + */ +public enum Align { + MIN, + CENTER, + MAX; + public static Align LEFT = MIN; + public static Align RIGHT = MAX; + public static Align TOP = MIN; + public static Align BOTTOM = MAX; +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Axis.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Axis.java new file mode 100644 index 00000000..7f4ac8f2 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Axis.java @@ -0,0 +1,13 @@ +package codechicken.lib.gui.modular.lib.geometry; + +/** + * Created by brandon3055 on 02/09/2023 + */ +public enum Axis { + X, + Y; + + public Axis opposite() { + return this == X ? Y : X; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/AxisConfig.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/AxisConfig.java new file mode 100644 index 00000000..5fbd204b --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/AxisConfig.java @@ -0,0 +1,96 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import org.apache.commons.lang3.function.TriFunction; +import org.jetbrains.annotations.Nullable; + +/** + * Denies how each of the three axis parameters are computed based on the available constraints. + * Note: If both min and max are defined, size is ignored. + */ +public enum AxisConfig { + //@formatter:off + // | Compute Axis Min | Compute Axis Max | Compute Axis Size | + /** + * If nothing is constrained. + * min=0, max=0, size=0 + * */ + NONE (0, (min, max, size) -> 0D, (min, max, size) -> 0D, (min, max, size) -> 0D), + /** + * If only Min is constrained. + * min=min, max=min, size=0 + * */ + MIN_ONLY (1, (min, max, size) -> min.get(), (min, max, size) -> min.get(), (min, max, size) -> 0D), + /** + * If only Max is constrained. + * min=max, max=max, size=0 + * */ + MAX_ONLY (1, (min, max, size) -> max.get(), (min, max, size) -> max.get(), (min, max, size) -> 0D), + /** + * If only Size is constrained. + * min=0, max=size, size=size + * */ + SIZE_ONLY (1, (min, max, size) -> 0D, (min, max, size) -> size.get(), (min, max, size) -> size.get()), + /** + * If Min and Size are constrained . + * min=min, max=min+size, size=size + * */ + MIN_SIZE (2, (min, max, size) -> min.get(), (min, max, size) -> min.get() + size.get(), (min, max, size) -> size.get()), + /** + * If Max and Size are constrained. + * min=max-size, max=max, size=size + * */ + MAX_SIZE (2, (min, max, size) -> max.get() - size.get(), (min, max, size) -> max.get(), (min, max, size) -> size.get()), + /** + * If Min and Max are constrained. + * min=min, max=max, size=max-min + * */ + MIN_MAX (2, (min, max, size) -> min.get(), (min, max, size) -> max.get(), (min, max, size) -> max.get() - min.get()), + /** + * If Min, Max and Size are constrained the Size is ignored. + * min=min, max=max, size=max-min + * */ + MIN_MAX_SIZE(3, (min, max, size) -> min.get(), (min, max, size) -> max.get(), (min, max, size) -> max.get() - min.get()); + //@formatter:on + + public final int constraints; + public final TriFunction min; + public final TriFunction max; + public final TriFunction size; + //[min][max][size] TODO, I Clean this up once i confirm everything works correctly. + private static final AxisConfig[][][] LOOKUP = new AxisConfig[][][]{ + { //Min = 0 + { //Max = 0 + NONE, //Size = 0 + SIZE_ONLY //Size = 1 + }, + { //Max = 1 + MAX_ONLY, //Size = 0 + MAX_SIZE //Size = 1 + } + }, + { //Min = 1 + { //Max = 0 + MIN_ONLY, //Size = 0 + MIN_SIZE //Size = 1 + }, + { //Max = 1 + MIN_MAX, //Size = 0 + MIN_MAX_SIZE//Size = 1 + } + } + }; + + AxisConfig(int constraints, + TriFunction min, + TriFunction max, + TriFunction size) { + this.constraints = constraints; + this.min = min; + this.max = max; + this.size = size; + } + + public static AxisConfig getConfigFor(@Nullable Constraint min, @Nullable Constraint max, @Nullable Constraint size) { + return LOOKUP[min != null ? 1 : 0][max != null ? 1 : 0][size != null ? 1 : 0]; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Borders.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Borders.java new file mode 100644 index 00000000..d9ef9ce7 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Borders.java @@ -0,0 +1,118 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import java.util.Objects; + +/** + * Created by brandon3055 on 28/08/2023 + */ +public final class Borders { + public double top; + public double left; + public double bottom; + public double right; + + public Borders(double top, double left, double bottom, double right) { + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + } + + public static Borders create(double borders) { + return create(borders, borders); + } + + public static Borders create(double leftRight, double topBottom) { + return create(topBottom, leftRight, topBottom, leftRight); + } + + public static Borders create(double top, double left, double bottom, double right) { + return new Borders(top, left, bottom, right); + } + + public double top() { + return top; + } + + public double left() { + return left; + } + + public double bottom() { + return bottom; + } + + public double right() { + return right; + } + + public Borders top(double top) { + this.top = top; + return this; + } + + public Borders left(double left) { + this.left = left; + return this; + } + + public Borders bottom(double bottom) { + this.bottom = bottom; + return this; + } + + public Borders right(double right) { + this.right = right; + return this; + } + + public Borders setTopBottom(double topBottom) { + return top(topBottom).bottom(topBottom); + } + + public Borders setLeftRight(double leftRight) { + return left(leftRight).setLeftRight(leftRight); + } + + public Borders setBorders(double borders) { + return setBorders(borders, borders); + } + + public Borders setBorders(double topBottom, double leftRight) { + return setBorders(topBottom, leftRight, topBottom, leftRight); + } + + public Borders setBorders(double top, double left, double bottom, double right) { + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + return this; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Borders) obj; + return Double.doubleToLongBits(this.top) == Double.doubleToLongBits(that.top) && + Double.doubleToLongBits(this.left) == Double.doubleToLongBits(that.left) && + Double.doubleToLongBits(this.bottom) == Double.doubleToLongBits(that.bottom) && + Double.doubleToLongBits(this.right) == Double.doubleToLongBits(that.right); + } + + @Override + public int hashCode() { + return Objects.hash(top, left, bottom, right); + } + + @Override + public String toString() { + return "Borders[" + + "top=" + top + ", " + + "left=" + left + ", " + + "bottom=" + bottom + ", " + + "right=" + right + ']'; + } + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/ConstrainedGeometry.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/ConstrainedGeometry.java new file mode 100644 index 00000000..b0c62353 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/ConstrainedGeometry.java @@ -0,0 +1,376 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import codechicken.lib.gui.modular.elements.GuiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +import static codechicken.lib.gui.modular.lib.geometry.GeoParam.*; + +/** + * This is the base class used to define the size and position of a GuiElement. + *

+ * The geometry system is designed to be very user-friendly, yet very powerful, with the ability to do all sorts of fun and interesting things. + * But if all you want is the ability to set simple size and position values, then there is no need to read any further. + * Just use the basic setters setXPos, setYPos, setWidth, setHeight and everything should just work. + * If you want to dive deeper, read on. + *

+ * The geometry system is based around 6 core parameters: xMin, xMax, xSize, yMin, yMax and ySize. See: {@link GeoParam}
+ * These parameters are defined using 'Constraints' See: {@link Constraint}
+ * When properly constrained, it will be possible to request values for any of these 6 parameters.
+ * For each axis (x and y) there are 3 constraints: min, max and size.
+ * In order for an axis to be properly defined 2 of these constraints need to be set.
+ * e.g. (xMin and xSize) or (xMin and xMax) or (xMax and xSize)
+ * Using any of these combinations the position and size of an axis can be computed. + *

+ * Look at {@link AxisConfig} for details on the values that get returned when too few constraints are set.
+ * TLDR: We will always try to return "reasonable" default values. e.g. if only size is constrained, xMin will default + * to 0 and xMax will default to 0 + xSize.
+ * If all three constraints are set then the size constraint will be ignored.
+ * You can use {@link #strictMode(boolean)} to change this behavior. + *

+ * There are a number of different types of constraints that can be used, including completely custom constraints.
+ * Take a look at the {@link Constraint} class for a detailed list with explanations. + *

+ * Note: + * All position and size values in ModularGui use doubles. This is because floating point position and size values + * can be very useful at times. But they can also cause a lot of ugly visual artifacts when not used properly. + * So by default, all the builtin constraints will cast their outputs to an integer value. + * If you need floating point precision, you can enable it by calling .precise() on any of the builtin constraints. + *

+ * Created by brandon3055 on 29/06/2023 + */ +public abstract class ConstrainedGeometry> implements GuiParent { + + private Constraint xMin = null; + private Constraint xMax = null; + private Constraint xSize = null; + private AxisConfig xAxis = AxisConfig.NONE; + + private Constraint yMin = null; + private Constraint yMax = null; + private Constraint ySize = null; + private AxisConfig yAxis = AxisConfig.NONE; + + //Permanently bound immutable position and rectangle elements. + private final Position position = Position.create(this); + private final Rectangle rectangle = Rectangle.create(this); + private final Rectangle.Mutable childBounds = getRectangle().mutable(); + + private boolean strictMode = false; + + @NotNull + public abstract GuiParent getParent(); + + public GeoRef getParent(GeoParam param) { + return getParent().get(param); + } + + //=== Simple Setters ===// + + /** + * Simple method for setting the x position of this element. + *

+ * Constrains the left side of this element, to the left side of the parent element, + * with an offset that is calculated by subtracting parent's current x pos from the given x pos. + *

+ * In other words, if the parent element's position changes, this element will move with it. + * + * @param x The X position in screen space. + * @return This Element. + */ + public T setXPos(double x) { + GeoRef parentLeft = getParent(LEFT); + return constrain(LEFT, Constraint.relative(parentLeft, x - parentLeft.get())); + } + + /** + * Simple method for setting the y position of this element. + *

+ * Constrains the top side of this element, to the top side of the parent element, + * with an offset that is calculated by subtracting parent's current y pos from the given y pos. + *

+ * In other words, if the parent element's position changes, this element will move with it. + * + * @param y The Y position in screen space. + * @return This Element. + */ + public T setYPos(double y) { + GeoRef parentTop = getParent(LEFT); + return constrain(LEFT, Constraint.relative(parentTop, y - parentTop.get())); + } + + /** + * Convenience method for setting both x and y positions. + * + * @param x The X position in screen space. + * @param y The Y position in screen space. + * @return This Element. + * @see #setXPos(double) + * @see #setYPos(double) + */ + public T setPos(double x, double y) { + return setXPos(x).setYPos(y); + } + + /** + * Simple method for setting the width of this element. + * + * @param width The width to apply. + * @return This Element. + */ + public T setWidth(double width) { + return constrain(WIDTH, Constraint.literal(width)); + } + + /** + * Simple method for setting the height of this element. + * + * @param height The height to apply. + * @return This Element. + */ + public T setHeight(double height) { + return constrain(HEIGHT, Constraint.literal(height)); + } + + /** + * Convenience method for setting both width and height. + * + * @param width The width to apply. + * @param height The height to apply. + * @return This Element. + * @see #setWidth(double) + * @see #setHeight(double) + */ + public T setSize(double width, double height) { + return setWidth(width).setHeight(height); + } + + //=== Everything related to constraint based geometry ===// + + /** + * @return The position of the Left edge of this element. + */ + @Override + public double xMin() { + return xAxis.min.apply(xMin, xMax, xSize); + } + + /** + * @return The position of the Right edge of this element. + */ + @Override + public double xMax() { + return xAxis.max.apply(xMin, xMax, xSize); + } + + /** + * @return The Width of this element. + */ + @Override + public double xSize() { + return xAxis.size.apply(xMin, xMax, xSize); + } + + /** + * @return The position of the Top edge of this element. + */ + @Override + public double yMin() { + return yAxis.min.apply(yMin, yMax, ySize); + } + + /** + * @return The position of the Bottom edge of this element. + */ + @Override + public double yMax() { + return yAxis.max.apply(yMin, yMax, ySize); + } + + /** + * @return The Height of this element. + */ + @Override + public double ySize() { + return yAxis.size.apply(yMin, yMax, ySize); + } + + /** + * Returns a reference to the specified geometry parameter. + * This is primarily used when defining geometry constraints. + * But it can also be used as a simple {@link Supplier} + * that will return the current parameter value when requested. + *

+ * Note: The returned geometry reference will always be valid + * + * @param param The geometry parameter. + * @return A Geometry Reference + */ + @Override + public GeoRef get(GeoParam param) { + return new GeoRef(this, param); + } + + /** + * @param param The geometry parameter to be constrained. + * @param constraint The constraint to apply + * @return This Element. + */ + @SuppressWarnings ("unchecked") + public T constrain(GeoParam param, @Nullable Constraint constraint) { + if (constraint != null && constraint.axis() != null && constraint.axis() != param.axis) { + throw new IllegalStateException("Attempted to apply constraint for axis: " + constraint.axis() + ", to Parameter: " + param); + } + if (param.axis == Axis.X) { + constrainX(param, constraint); + } else if (param.axis == Axis.Y) { + constrainY(param, constraint); + } + return (T) this; + } + + /** + * Clear any configured constraints and reset this element to default unconstrained state. + * Convenient when reconfiguring an elements constraints or applying constraints to an element + * with an existing, unknown constraint configuration. + */ + @SuppressWarnings ("unchecked") + public T clearConstraints() { + xMin = xMax = xSize = yMin = yMax = ySize = null; + xAxis = yAxis = AxisConfig.NONE; + return (T) this; + } + + private void constrainX(GeoParam param, @Nullable Constraint constraint) { + if (param == GeoParam.LEFT) { + xMin = constraint; + } else if (param == GeoParam.RIGHT) { + xMax = constraint; + } else if (param == WIDTH) { + xSize = constraint; + } + xAxis = AxisConfig.getConfigFor(xMin, xMax, xSize); + validate(); + } + + private void constrainY(GeoParam param, @Nullable Constraint constraint) { + if (param == GeoParam.TOP) { + yMin = constraint; + } else if (param == GeoParam.BOTTOM) { + yMax = constraint; + } else if (param == GeoParam.HEIGHT) { + ySize = constraint; + } + yAxis = AxisConfig.getConfigFor(yMin, yMax, ySize); + validate(); + } + + /** + * Strict mode is intended to help catch potential mistakes when writing modular GUIs + *

+ * Enforces a strict requirement for each exist to have two and only two constraints. + * Any attempt to over-constrain an axis will throw an immediate fatal exception. + * If an axis is under-constrained then a fatal exception will be thrown when a value from the axis is queried. + *

+ * Strict mode applies to this element, and recursively to all children of this element. + * + * @param strictMode Enable strict mode. + * @return the geometry object. + */ + @SuppressWarnings ("unchecked") + public T strictMode(boolean strictMode) { + this.strictMode = strictMode; + //TODO Propagate to children (Will be handled in the base GuiElement) + return (T) this; + } + + //TODO This needs to be called from the parent element somewhere. Possibly on tick or render + //Ideally i would like to find a way to only call it once. we need to account for constraints being modified after initial element construction. + public void validate() { + if (strictMode) { + if (xAxis.constraints != 2) { + throw new IllegalStateException(String.format("X axis of element: %s is %s constrained!", getParent(), xAxis.constraints < 2 ? "under" : "over")); + } else if (yAxis.constraints != 2) { + throw new IllegalStateException(String.format("Y axis of element: %s is %s constrained!", getParent(), yAxis.constraints < 2 ? "under" : "over")); + } + } + } + + public void clearGeometryCache() { + if (xMin != null) xMin.markDirty(); + if (xMax != null) xMax.markDirty(); + if (xSize != null) xSize.markDirty(); + if (yMin != null) yMin.markDirty(); + if (yMax != null) yMax.markDirty(); + if (ySize != null) ySize.markDirty(); + getChildren().forEach(ConstrainedGeometry::clearGeometryCache); + } + + //=== Geometry Utilities ===// + + /** + * Returns a {@link Position} that is permanently bound to this element. + */ + public Position getPosition() { + return position; + } + + /** + * Returns a {@link Rectangle} that is permanently bound to this element. + */ + public Rectangle getRectangle() { + return rectangle; + } + + public double xCenter() { + return xMin() + (xSize() / 2); + } + + public double yCenter() { + return yMin() + (ySize() / 2); + } + + /** + * Returns a new {@link Rectangle} the bounds of which will enclose this element and all of its child + * elements recursively. + */ + public Rectangle.Mutable getEnclosingRect() { + return addBoundsToRect(getRectangle().mutable()); + } + + /** + * Expands the bounds of the given rectangle (if needed) so that they enclose this element. + * And all of its child elements recursively. + */ + public Rectangle.Mutable addBoundsToRect(Rectangle.Mutable enclosingRect) { + enclosingRect.combine(getRectangle()); + for (GuiElement element : getChildren()) { + if (element.isEnabled()) { + element.addBoundsToRect(enclosingRect); + } + } + return enclosingRect; + } + + /** + * @return a rectangle, the bounds of which enclose all enabled child elements. + * If there are no enabled child elements the returned rect will have the position of this element, with zero size. + */ + public Rectangle.Mutable getChildBounds() { + boolean set = false; + for (GuiElement element : getChildren()) { + if (element.isEnabled()) { + if (!set) { + childBounds.set(element.getRectangle()); + set = true; + } else { + element.addBoundsToRect(childBounds); + } + } + } + if (!set) childBounds.setPos(xMin(), yMin()).setSize(0, 0); + return childBounds; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Constraint.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Constraint.java new file mode 100644 index 00000000..63d80344 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Constraint.java @@ -0,0 +1,154 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Constraints are used to define an elements position and shape by constraining the elements geometry parameters. {@link GeoParam} + * Constraints can be as simple as literal values representing an elements exact position and / or size in screen space, + * They can be relative coordinates, (Relative to another element's Geometry) + * Or they can be used to supply completely custom dynamic values. + *

+ * All the built-in constraints are implemented and documented in this class. + *

+ * Created by brandon3055 on 30/06/2023 + * + * @see ConstrainedGeometry + */ +public sealed interface Constraint permits ConstraintImpl { + + /** + * This method will return the current value of this constraint. + * This could be a fixed stored value, or a dynamically computed value + * depending on the type of constraint. + *

+ * All position and size values in ModularGui are doubles. + * However, by default, all the builtin constraints will cast their outputs to integer values. + * This avoids a lot of random visual artifacts that can occur when using floating point values in MC Screens. + *

+ * If you need floating-point precision, it can be enabled calling .precise() on any of the builtin constraints. + * + * @return The computed or stored value of this constraint. + */ + double get(); + + /** + * @return the axis this constraint applies to, Ether X, Y or null for undefined. + */ + @Nullable + Axis axis(); + + /** + * This is part of a late addition to improve performance. + * Rather than computing a constraint value every single time it is queried, which can be many, many times per render frame, + * We now cache the constraint value, and that cache is cleared at the start of each render frame. + */ + void markDirty(); + + /** + * This is the most basic constraint. It constrains a parameter to a single fixed value. + * + * @param value The fixed value that will be returned by this constraint. + */ + static ConstraintImpl.Literal literal(double value) { + return new ConstraintImpl.Literal(value); + } + + /** + * Constrains a parameter to the value provided by the given supplier. + * + * @param valueSupplier The dynamic value supplier. + */ + static ConstraintImpl.Dynamic dynamic(Supplier valueSupplier) { + return new ConstraintImpl.Dynamic(valueSupplier); + } + + /** + * Contains a parameter to the exact value of the given reference. + * This is effectively a relative constraint with no offset. + * + * @param relativeTo The relative geometry. + */ + static ConstraintImpl.Relative match(GeoRef relativeTo) { + return new ConstraintImpl.Relative(relativeTo, 0); + } + + /** + * Contains a parameter to the given reference plus the provided fixed offset. + * + * @param relativeTo The relative geometry. + * @param offset The offset to apply. + */ + static ConstraintImpl.Relative relative(GeoRef relativeTo, double offset) { + return new ConstraintImpl.Relative(relativeTo, offset); + } + + /** + * Contains a parameter to the given reference plus the provided dynamic offset. + * + * @param relativeTo The relative geometry. + * @param offset The dynamic offset to apply. + */ + static ConstraintImpl.RelativeDynamic relative(GeoRef relativeTo, Supplier offset) { + return new ConstraintImpl.RelativeDynamic(relativeTo, offset); + } + + /** + * Contains a parameter to a fixed position between the two provided references. + * Note: it is possible to go outside the given range if the given position is greater than 1 or less than 0. + * To prevent this call .clamp() on the returned constraint. + * + * @param start The Start position. + * @param end The End position. + * @param position The position between start and end. (0=start to 1=end) + */ + static ConstraintImpl.Between between(GeoRef start, GeoRef end, double position) { + return new ConstraintImpl.Between(start, end, position); + } + + /** + * Contains a parameter to a dynamic position between the two provided references. + * Note: it is possible to go outside the given range if the given position is greater than 1 or less than 0. + * To prevent this call .clamp() on the returned constraint. + * + * @param start The Start position. + * @param end The End position. + * @param position The dynamic position between start and end. (0=start to 1=end) + */ + static ConstraintImpl.BetweenDynamic between(GeoRef start, GeoRef end, Supplier position) { + return new ConstraintImpl.BetweenDynamic(start, end, position); + } + + /** + * Contains a parameter to the mid-point between the two provided references. + * + * @param start The Start position. + * @param end The End position. + */ + static ConstraintImpl.MidPoint midPoint(GeoRef start, GeoRef end) { + return new ConstraintImpl.MidPoint(start, end, 0); + } + + /** + * Contains a parameter to the mid-point between the two provided references with a fixed offset. + * + * @param start The Start position. + * @param end The End position. + * @param offset offset distance. + */ + static ConstraintImpl.MidPoint midPoint(GeoRef start, GeoRef end, double offset) { + return new ConstraintImpl.MidPoint(start, end, offset); + } + + /** + * Contains a parameter to the mid-point between the two provided references with a dynamic offset. + * + * @param start The Start position. + * @param end The End position. + * @param offset offset distance suppler. + */ + static ConstraintImpl.MidPointDynamic midPoint(GeoRef start, GeoRef end, Supplier offset) { + return new ConstraintImpl.MidPointDynamic(start, end, offset); + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/ConstraintImpl.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/ConstraintImpl.java new file mode 100644 index 00000000..646abe93 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/ConstraintImpl.java @@ -0,0 +1,294 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 04/07/2023 + */ +public abstract non-sealed class ConstraintImpl> implements Constraint { + + protected boolean precise = false; + protected Axis axis = null; + private double value; + private boolean isDirty = true; + + /** + * @return True if precise mode is enabled. + * @see #precise() + */ + public boolean isPrecise() { + return precise; + } + + /** + * By default, all constraint values are cast to (int). + * This helps avoid a lot of visual artifacts that can occur when using floating point positions in MC Screens. + *

+ * Calling this enables precise mode, which allows floating point precision to be used. + * + * @return The Constraint. + */ + @SuppressWarnings("unchecked") + public T precise() { + this.precise = true; + return (T) this; + } + + @Override + public double get() { + if (isDirty) { + value = isPrecise() ? getImpl() : (int) getImpl(); + isDirty = false; + } + return value; + } + + @Override + public @Nullable Axis axis() { + return axis; + } + + @SuppressWarnings("unchecked") + public T setAxis(@Nullable Axis axis) { + this.axis = axis; + return (T) this; + } + + @Override + public void markDirty() { + isDirty = true; + } + + protected abstract double getImpl(); + + public static class Literal extends ConstraintImpl { + protected final double value; + + /** + * Returns the literal value supplied. + */ + public Literal(double value) { + this.value = value; + } + + @Override + protected double getImpl() { + return value; + } + } + + public static class Dynamic extends ConstraintImpl { + protected final Supplier value; + + /** + * Returns a dynamic value that will be retrieved from the provided supplier. + */ + public Dynamic(Supplier value) { + this.value = value; + } + + @Override + protected double getImpl() { + return value.get(); + } + } + + public static class Relative extends ConstraintImpl { + protected final GeoRef relTo; + protected final double offset; + + /** + * Returns the value of the provided reference plus the given offset. + */ + public Relative(GeoRef relTo, double offset) { + this.setAxis(relTo.parameter.axis); + this.relTo = relTo; + this.offset = offset; + } + + @Override + protected double getImpl() { + return relTo.get() + getOffset(); + } + + public double getOffset() { + return offset; + } + } + + public static class RelativeDynamic extends ConstraintImpl { + protected final GeoRef relTo; + protected final Supplier offset; + + /** + * Returns the value of the provided reference plus the given dynamic offset. + */ + public RelativeDynamic(GeoRef relTo, Supplier offset) { + this.setAxis(relTo.parameter.axis); + this.relTo = relTo; + this.offset = offset; + } + + @Override + protected double getImpl() { + return relTo.get() + getOffset(); + } + + public double getOffset() { + return offset.get(); + } + } + + public static class Between extends ConstraintImpl { + protected final GeoRef start; + protected final GeoRef end; + protected final double pos; + protected boolean clamp = false; + + public Between(GeoRef start, GeoRef end, double pos) { + this.setAxis(start.parameter.axis); + if (start.parameter.axis != end.parameter.axis) { + throw new IllegalStateException("Attempted to define a 'Between' Constraint with parameters on different axes."); + } + this.start = start; + this.end = end; + this.pos = pos; + } + + @Override + protected double getImpl() { + return start.get() + (end.get() - start.get()) * getPos(); + } + + public double getPos() { + return clamp ? Mth.clamp(pos, 0, 1) : pos; + } + + public double getStart() { + return start.get(); + } + + public double getEnd() { + return end.get(); + } + + /** + * Ensure the output can not go bellow the min reference or above the max reference. + */ + public Between clamp() { + this.clamp = true; + return this; + } + } + + public static class BetweenDynamic extends ConstraintImpl { + protected final GeoRef start; + protected final GeoRef end; + protected final Supplier pos; + protected boolean clamp = false; + + public BetweenDynamic(GeoRef start, GeoRef end, Supplier pos) { + this.setAxis(start.parameter.axis); + if (start.parameter.axis != end.parameter.axis) { + throw new IllegalStateException("Attempted to define a 'Between' Constraint with parameters on different axes."); + } + this.start = start; + this.end = end; + this.pos = pos; + } + + @Override + protected double getImpl() { + return start.get() + (end.get() - start.get()) * getPos(); + } + + public double getPos() { + return clamp ? Mth.clamp(pos.get(), 0, 1) : pos.get(); + } + + public double getStart() { + return start.get(); + } + + public double getEnd() { + return end.get(); + } + + /** + * Ensure the output can not go bellow the min reference or above the max reference. + */ + public BetweenDynamic clamp() { + this.clamp = true; + return this; + } + } + + public static class MidPoint extends ConstraintImpl { + protected final GeoRef start; + protected final GeoRef end; + protected final double offset; + + public MidPoint(GeoRef start, GeoRef end, double offset) { + this.setAxis(start.parameter.axis); + if (start.parameter.axis != end.parameter.axis) { + throw new IllegalStateException("Attempted to define a 'MidPoint' Constraint with parameters on different axes."); + } + this.start = start; + this.end = end; + this.offset = offset; + } + + @Override + protected double getImpl() { + return start.get() + ((end.get() - start.get()) / 2) + getOffset(); + } + + public double getOffset() { + return offset; + } + + public double getStart() { + return start.get(); + } + + public double getEnd() { + return end.get(); + } + } + + public static class MidPointDynamic extends ConstraintImpl { + protected final GeoRef start; + protected final GeoRef end; + protected final Supplier offset; + + public MidPointDynamic(GeoRef start, GeoRef end, Supplier offset) { + this.setAxis(start.parameter.axis); + if (start.parameter.axis != end.parameter.axis) { + throw new IllegalStateException("Attempted to define a 'MidPoint' Constraint with parameters on different axes."); + } + this.start = start; + this.end = end; + this.offset = offset; + } + + @Override + protected double getImpl() { + return start.get() + ((end.get() - start.get()) / 2) + getOffset(); + } + + public double getOffset() { + return offset.get(); + } + + public double getStart() { + return start.get(); + } + + public double getEnd() { + return end.get(); + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Direction.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Direction.java new file mode 100644 index 00000000..9e2513e8 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Direction.java @@ -0,0 +1,40 @@ +package codechicken.lib.gui.modular.lib.geometry; + +/** + * Created by brandon3055 on 04/09/2023 + */ +public enum Direction { + UP(Axis.Y), + LEFT(Axis.X), + DOWN(Axis.Y), + RIGHT(Axis.X); + + private static Direction[] VALUES = values(); + + private final Axis axis; + + Direction(Axis axis) { + this.axis = axis; + } + + public Axis getAxis() { + return axis; + } + + public Direction opposite() { + if (axis == Axis.X) return this == LEFT ? RIGHT : LEFT; + else return this == UP ? DOWN : UP; + } + + public Direction rotateCW() { + return values()[(ordinal() + VALUES.length - 1) % VALUES.length]; + } + + public Direction rotateCCW() { + return values()[(ordinal() + 1) % VALUES.length]; + } + + public double rotationTo(Direction other) { + return (this.ordinal() - other.ordinal()) * 90; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/GeoParam.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/GeoParam.java new file mode 100644 index 00000000..0f37720f --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/GeoParam.java @@ -0,0 +1,32 @@ +package codechicken.lib.gui.modular.lib.geometry; + +/** + * Used to define the 6 core parameters that make up an elements geometry + * These are named Left, Right, Width, Top, Bottom, Height. + * These names were chosen for ease of use, and to make it clear what they represent. + * Internally they are known as xMin, xMax, xSize, yMin, yMax, ySize. + *

+ * Created by brandon3055 on 30/06/2023 + */ +public enum GeoParam { + /** X_MIN */ + LEFT(Axis.X), + /** X_MAX */ + RIGHT(Axis.X), + /** X_SIZE */ + WIDTH(Axis.X), + + /** Y_MIN */ + TOP(Axis.Y), + /** Y_MAX */ + BOTTOM(Axis.Y), + /** Y_SIZE */ + HEIGHT(Axis.Y); + + public final Axis axis; + + GeoParam(Axis axis) { + this.axis = axis; + } + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/GeoRef.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/GeoRef.java new file mode 100644 index 00000000..93ebf68e --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/GeoRef.java @@ -0,0 +1,36 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import java.util.function.Supplier; + +/** + * Used to access one of the 6 core parameters that make up an element's geometry. + *

+ * The primary purpose of this class is to provide a convenient way to reference a geometry + * parameter when defining constraints. + * It also helps make the code more debuggable. + * I could just make literally everything a lambda, but that makes debugging kinda painful when things break. + *

+ * Created by brandon3055 on 30/06/2023 + */ +public class GeoRef implements Supplier { + public final GuiParent geometry; + public final GeoParam parameter; + + public GeoRef(GuiParent geometry, GeoParam parameter) { + this.geometry = geometry; + this.parameter = parameter; + } + + @Override + public Double get() { + return geometry.getValue(parameter); + } + + @Override + public String toString() { + return "GeoReference{" + + "geometry=" + geometry + + ", parameter=" + parameter + + '}'; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/GuiParent.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/GuiParent.java new file mode 100644 index 00000000..9a2b8d77 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/GuiParent.java @@ -0,0 +1,190 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import codechicken.lib.gui.modular.ModularGui; +import codechicken.lib.gui.modular.elements.GuiElement; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This is the base interface that allows an element or screen to define its basic geometry. + * As well as defining the primary methods for handling child elements. + *

+ * It also provides a way to access some common minecraft fields. + *

+ * Created by brandon3055 on 29/06/2023 + */ +public interface GuiParent> { + + /** + * @return The position of the Left edge of this element. + */ + double xMin(); + + /** + * @return The position of the Right edge of this element. + */ + double xMax(); + + /** + * @return The Width of this element. + */ + double xSize(); + + /** + * @return The position of the Top edge of this element. + */ + double yMin(); + + /** + * @return The position of the Bottom edge of this element. + */ + double yMax(); + + /** + * @return The Height of this element. + */ + double ySize(); + + /** + * Returns a reference to the specified geometry parameter. + * This is primarily used when defining geometry constraints. + * But it can also be used as a simple {@link Supplier} + * that will return the current parameter value when requested. + *

+ * Note: The returned geometry reference will always be valid + * + * @param param The geometry parameter. + * @return A Geometry Reference + */ + default GeoRef get(GeoParam param) { + return new GeoRef(this, param); + } + + /** + * @param param The geometry parameter. + * @return The current value of the specified parameter. + */ + default double getValue(GeoParam param) { + return switch (param) { + case LEFT -> xMin(); + case RIGHT -> xMax(); + case WIDTH -> xSize(); + case TOP -> yMin(); + case BOTTOM -> yMax(); + case HEIGHT -> ySize(); + }; + } + + /** + * @return An unmodifiable list of all assigned child elements assigned to this parent. The list should be sorted in the order they were added. + */ + List> getChildren(); + + /** + * Adds a new child element to this parent. + * You should almost never need to use this because this is handled automatically when an element is created. + *

+ * Note: Due to the way relative coordinates work with the new geometry system, + * Transferring an element to a different parent can have unpredictable results. + * Therefor, to help avoid confusion it is not possible to transfer a child to a new parent using this method. + * + * @param child The child element to be added. + * @throws UnsupportedOperationException - If child has previously been assigned to a different parent. + * @see #adoptChild(GuiElement) + */ + void addChild(GuiElement child); + + /** + * This meant to be a convenience method that allows builder style addition of a child element. + * I'm not sure how useful it will be yet, so it may or may not stay. + * + * @param createChild A consumer that is given this element to be used in the construction of the child element. + * @return The parent element + */ + @ApiStatus.Experimental + @SuppressWarnings ("unchecked") + default T addChild(Consumer createChild) { + createChild.accept((T) this); + return (T) this; + } + + /** + * This method can be used to transfer an already initialized child to this parent element. + * This automatically handles removing the element from its previous parent, adds it to this element. + * Note: This will most likely break any relative constraints on the child's geometry. + * To fix this you will need to re-apply geometry constraints after the transfer. + * + * @param child The child element to be adopted. + */ + void adoptChild(GuiElement child); + + /** + * Allows the removal of a child element. + * Child removal is not instantaneous, Instead all removals occur at the end of the current screen thick. + * This is to avoid any possible concurrency issues. + * + * @param child The child element to be removed. + */ + void removeChild(GuiElement child); + + /** + * Checks if this element is a descendant of the specified. + * @return true if the specified element is a parent or grandparent etc... of this element. + */ + default boolean isDescendantOf(GuiElement ancestor) { + return false; + } + + /** + * @return The minecraft instance. + */ + Minecraft mc(); + + /** + * @return The active font instance. + */ + Font font(); + + /** + * @return The current gui screen width, As returned by mc.getWindow().getGuiScaledWidth() + */ + int scaledScreenWidth(); + + /** + * @return The current gui screen height, As returned by mc.getWindow().getGuiScaledHeight() + */ + int scaledScreenHeight(); + + /** + * @return the parent ModularGui instance. + */ + ModularGui getModularGui(); + + /** + * Called when the minecraft Screen is initialised or resized. + * + * @param mc The Minecraft instance. + * @param font The active font. + * @param screenWidth The current guiScaledWidth. + * @param screenHeight The current guiScaledHeight. + */ + default void onScreenInit(Minecraft mc, Font font, int screenWidth, int screenHeight) { + getChildren().forEach(e -> e.onScreenInit(mc, font, screenWidth, screenHeight)); + } + + /** + * Allows an element to override the {@link GuiElement#isMouseOver()} method of its children. + * This is primarily used for things like scroll elements where mouseover interactions need to be blocked outside the view area. + * + * @param element The element on which isMouseOver is getting called. + * @return true if mouse-over interaction should be blocked for this child element. + */ + default boolean blockMouseOver(GuiElement element, double mouseX, double mouseY) { + return false; + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Position.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Position.java new file mode 100644 index 00000000..df430507 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Position.java @@ -0,0 +1,103 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 24/08/2023 + */ +public interface Position { + + double x(); + + double y(); + + default Position offset(double x, double y) { + return create(x() + x, y() + y); + } + + /** + * Returns the position value for the given axis. + */ + default double get(Axis axis) { + return axis == Axis.X ? x() : y(); + } + + static Position create(double x, double y) { + return new Immutable(x, y); + } + + static Position create(Supplier getX, Supplier getY) { + return new Dynamic(getX, getY); + } + + /** + * Creates a new position, bound to the specified parent's position. + * */ + static Position create(GuiParent parent) { + return new Dynamic(parent::xMin, parent::yMin); + } + + record Immutable(@Override double x, @Override double y) implements Position { } + + record Dynamic(Supplier getX, Supplier getY) implements Position { + @Override + public double x() { + return getX.get(); + } + + @Override + public double y() { + return getY.get(); + } + + @Override + public String toString() { + return "Dynamic{" + + "x=" + x() + + ", y=" + y() + + '}'; + } + } + + static class Mutable implements Position { + private double x; + private double y; + + public Mutable(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public double x() { + return x; + } + + @Override + public double y() { + return y; + } + + @Override + public Position offset(double x, double y) { + this.x += x; + this.y += y; + return this; + } + + public Position set(double x, double y) { + this.x = x; + this.y = y; + return this; + } + + @Override + public String toString() { + return "Mutable{" + + "x=" + x() + + ", y=" + y() + + '}'; + } + } + +} diff --git a/src/main/java/codechicken/lib/gui/modular/lib/geometry/Rectangle.java b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Rectangle.java new file mode 100644 index 00000000..4ad613f7 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/lib/geometry/Rectangle.java @@ -0,0 +1,308 @@ +package codechicken.lib.gui.modular.lib.geometry; + +import net.minecraft.client.renderer.Rect2i; + +import java.util.function.Supplier; + +/** + * Created by brandon3055 on 14/08/2023 + */ +public interface Rectangle { + + Position pos(); + + default double x() { + return pos().x(); + } + + default double y() { + return pos().y(); + } + + double width(); + + double height(); + + default double xMax() { + return x() + width(); + } + + default double yMax() { + return y() + height(); + } + + /** + * Returns a new rectangle with this operation applied + */ + default Rectangle offsetPos(double xAmount, double yAmount) { + return create(x() + xAmount, y() + yAmount, width(), height()); + } + + /** + * Returns a new rectangle with this operation applied + */ + default Rectangle setPos(double newX, double newY) { + return create(newX, newY, width(), height()); + } + + /** + * Returns a new rectangle with this operation applied + */ + default Rectangle setSize(double width, double height) { + return create(x(), y(), width, height); + } + + /** + * Returns a new rectangle with this operation applied + */ + default Rectangle offsetSize(double xAmount, double yAmount) { + return create(x(), y(), width() + xAmount, height() + yAmount); + } + + default Rect2i toRect2i() { + return new Rect2i((int) x(), (int) y(), (int) width(), (int) height()); + } + + default boolean intersects(double x, double y, double w, double h) { + double x0 = x(); + double y0 = y(); + return (x + w > x0 && y + h > y0 && x < x0 + width() && y < y0 + height()); + } + + default boolean intersects(Rectangle other) { + double x0 = x(); + double y0 = y(); + return (other.x() + other.width() > x0 && other.y() + other.height() > y0 && other.x() < x0 + width() && other.y() < y0 + height()); + } + + /** + * Returns a new rectangle that represents the intersection area between the two inputs + */ + default Rectangle intersect(Rectangle other) { + double x = Math.max(x(), other.x()); + double y = Math.max(y(), other.y()); + double width = Math.max(0, Math.min(xMax(), other.xMax()) - x()); + double height = Math.max(0, Math.min(yMax(), other.yMax()) - y()); + return create(x, y, width, height); + } + + /** + * Returns a new rectangle, the bounds of which enclose all the input rectangles. + * + * @param combineWith Rectangles to combine with the start rectangle + */ + default Rectangle combine(Rectangle... combineWith) { + double x = x(); + double y = y(); + double maxX = xMax(); + double maxY = yMax(); + for (Rectangle other : combineWith) { + x = Math.min(x, other.x()); + y = Math.min(y, other.y()); + maxX = Math.max(maxX, other.xMax()); + maxY = Math.max(maxY, other.yMax()); + } + return create(x, y, maxX - x, maxY - y); + } + + default boolean contains(double x, double y) { + return x >= x() && x <= x() + width() && y >= y() && y <= y() + height(); + } + + /** + * @return the size of this rectangle on the given axis. + */ + default double size(Axis axis) { + return axis == Axis.X ? width() : height(); + } + + /** + * @return the min value the specified axis (meaning x() or y()) + */ + default double min(Axis axis) { + return axis == Axis.X ? x() : y(); + } + + /** + * @return the max value the specified axis (meaning x() + width() or y() + height()) + */ + default double max(Axis axis) { + return axis == Axis.X ? xMax() : yMax(); + } + + /** + * Return the distance the given position is from this rectangle on the specified axis. + * Will return 0 if position is inside this rectangle on the given axis. + */ + default double distance(Axis axis, Position position) { + double pos = position.get(axis); + double min = min(axis); + double max = max(axis); + return pos < min ? min - pos : pos > max ? pos - max : 0; + } + + static Rectangle create(Position position, double width, double height) { + return new Immutable(position, width, height); + } + + static Rectangle create(double x, double y, double width, double height) { + return new Immutable(Position.create(x, y), width, height); + } + + /** + * Returns a new rectangle bound to the specified parent's geometry. + */ + static Rectangle create(GuiParent parent) { + return new Dynamic(Position.create(parent), parent::xSize, parent::ySize); + } + + static Rectangle create(Position position, Supplier getWidth, Supplier getHeight) { + return new Dynamic(position, getWidth, getHeight); + } + + default Mutable mutable() { + return new Mutable(new Position.Mutable(x(), y()), width(), height()); + } + + /** + * Should not be created directly + */ + record Immutable(Position position, double xSize, double ySize) implements Rectangle { + @Override + public Position pos() { + return position; + } + + @Override + public double width() { + return xSize; + } + + @Override + public double height() { + return ySize; + } + + @Override + public String toString() { + return "Immutable{" + + "pos=" + pos() + + ", width=" + width() + + ", height=" + height() + + '}'; + } + } + + record Dynamic(Position position, Supplier getWidth, Supplier getHeight) implements Rectangle { + @Override + public Position pos() { + return position; + } + + @Override + public double width() { + return getWidth.get(); + } + + @Override + public double height() { + return getHeight.get(); + } + + @Override + public String toString() { + return "Dynamic{" + + "pos=" + pos() + + ", width=" + width() + + ", height=" + height() + + '}'; + } + } + + class Mutable implements Rectangle { + private Position.Mutable pos; + private double width; + private double height; + + public Mutable(Position.Mutable pos, double width, double height) { + this.pos = pos; + this.width = width; + this.height = height; + } + + @Override + public Position pos() { + return pos; + } + + @Override + public double width() { + return width; + } + + @Override + public double height() { + return height; + } + + @Override + public Rectangle offsetPos(double xAmount, double yAmount) { + pos.offset(xAmount, yAmount); + return this; + } + + @Override + public Rectangle setPos(double newX, double newY) { + pos.set(newX, newY); + return this; + } + + @Override + public Rectangle setSize(double width, double height) { + this.width = width; + this.height = height; + return this; + } + + @Override + public Rectangle offsetSize(double xAmount, double yAmount) { + this.width += xAmount; + this.height += yAmount; + return this; + } + + @Override + public Rectangle intersect(Rectangle other) { + double x = Math.max(x(), other.x()); + double y = Math.max(y(), other.y()); + width = Math.max(0, Math.min(xMax(), other.xMax()) - x()); + height = Math.max(0, Math.min(yMax(), other.yMax()) - y()); + return setPos(x, y); + } + + public void set(Rectangle rectangle) { + this.pos.set(rectangle.x(), rectangle.y()); + this.width = rectangle.width(); + this.height = rectangle.height(); + } + + @Override + public Rectangle combine(Rectangle... combineWith) { + double x = x(); + double y = y(); + double maxX = xMax(); + double maxY = yMax(); + for (Rectangle other : combineWith) { + x = Math.min(x, other.x()); + y = Math.min(y, other.y()); + maxX = Math.max(maxX, other.xMax()); + maxY = Math.max(maxY, other.yMax()); + } + return setPos(x, y).setSize(maxX - x, maxY - y); + } + + public Immutable immutable() { + return new Immutable(Position.create(x(), y()), width(), height()); + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/sprite/CCGuiTextures.java b/src/main/java/codechicken/lib/gui/modular/sprite/CCGuiTextures.java new file mode 100644 index 00000000..a3aaf28d --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/sprite/CCGuiTextures.java @@ -0,0 +1,59 @@ +package codechicken.lib.gui.modular.sprite; + +import codechicken.lib.CodeChickenLib; +import net.minecraft.resources.ResourceLocation; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Gui texture handler implementation. + * This sets up a custom atlas that will be populated with all textures in "modid:textures/gui/" + * To use your own textures you can just create a carbon copy of this class with that uses your own modid. + *

+ * Created by brandon3055 on 21/10/2023 + */ +public class CCGuiTextures { + private static final ModAtlasHolder ATLAS_HOLDER = new ModAtlasHolder(CodeChickenLib.MOD_ID, "textures/atlas/gui.png", "gui"); + private static final Map MATERIAL_CACHE = new HashMap<>(); + + /** + * The returned AtlasLoader needs to be registered as a resource reload listener using the appropriate NeoForge / Fabric event. + */ + public static ModAtlasHolder getAtlasHolder() { + return ATLAS_HOLDER; + } + + /** + * Returns a cached Material for the specified gui texture. + * Warning: Do not use this if you intend to use the material with multiple render types. + * The material will cache the first render type it is used with. + * Instead use {@link #getUncached(String)} + * + * @param texture The texture path relative to "modid:gui/" + */ + public static Material get(String texture) { + return MATERIAL_CACHE.computeIfAbsent(CodeChickenLib.MOD_ID + ":" + texture, e -> getUncached(texture)); + } + + public static Material get(Supplier texture) { + return get(texture.get()); + } + + public static Supplier getter(Supplier texture) { + return () -> get(texture.get()); + } + + /** + * Use this to retrieve a new uncached material for the specified gui texture. + * Feel free to hold onto the returned material. + * Storing it somewhere is more efficient than recreating it every render frame. + * + * @param texture The texture path relative to "modid:gui/" + * @return A new Material for the specified gui texture. + */ + public static Material getUncached(String texture) { + return new Material(ATLAS_HOLDER.atlasLocation(), new ResourceLocation(CodeChickenLib.MOD_ID, "gui/" + texture), ATLAS_HOLDER::getSprite); + } +} \ No newline at end of file diff --git a/src/main/java/codechicken/lib/gui/modular/sprite/Material.java b/src/main/java/codechicken/lib/gui/modular/sprite/Material.java new file mode 100644 index 00000000..888b45b3 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/sprite/Material.java @@ -0,0 +1,126 @@ +package codechicken.lib.gui.modular.sprite; + +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.animation.AnimationMetadataSection; +import net.minecraft.client.resources.metadata.animation.FrameSize; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; + +/** + * This is similar to Minecraft's {@link net.minecraft.client.resources.model.Material} + * This contains the essential data required to render an atlas sprite. + *

+ * The primary purpose of this class is to make porting between MC versions easier. + * It also allows for loading sprites from a custom texture atlas. Minecraft's material class can only load from vanilla atlases. + *

+ * Created by brandon3055 on 20/08/2023 + */ +public class Material { + private final ResourceLocation atlasLocation; + private final ResourceLocation texture; + private final Function spriteFunction; + + @Nullable + private RenderType renderType; + @Nullable + private net.minecraft.client.resources.model.Material vanillaMat; + + public Material(ResourceLocation atlasLocation, ResourceLocation texture, Function spriteFunction) { + this.atlasLocation = atlasLocation; + this.texture = texture; + this.spriteFunction = spriteFunction; + } + + public ResourceLocation atlasLocation() { + return atlasLocation; + } + + public ResourceLocation texture() { + return texture; + } + + public TextureAtlasSprite sprite() { + return spriteFunction.apply(texture()); + } + + /** + * Returns the cached render type for this material. + * The supplied function will be used to create the render type the first time this method is called. + * + * @param typeBuilder a function that will be used to create the render type if it does not already exist. + * @return The render type for this material. + */ + public RenderType renderType(Function typeBuilder) { + if (this.renderType == null) { + this.renderType = typeBuilder.apply(atlasLocation()); + } + return this.renderType; + } + + /** + * Convenience method to create a vertex consumer using this materials render type. + * + * @param buffers bugger source. + * @param typeBuilder a function that will be used to create the render type if it does not already exist. + */ + public VertexConsumer buffer(MultiBufferSource buffers, Function typeBuilder) { + return buffers.getBuffer(renderType(typeBuilder)); + } + + public net.minecraft.client.resources.model.Material getVanillaMat() { + if (vanillaMat == null) { + vanillaMat = new net.minecraft.client.resources.model.Material(atlasLocation, texture); + } + return vanillaMat; + } + + /** + * Convenient method for getting a material from a vanilla texture atlas. + * + * @return an un-cached material from a vanilla atlas. + */ + public static Material fromAtlas(ResourceLocation atlasLocation, String texture) { + return new Material(atlasLocation, new ResourceLocation(atlasLocation.getNamespace(), texture), e -> Minecraft.getInstance().getTextureAtlas(atlasLocation).apply(e)); + } + + /** + * Create a material from an existing sprite. + * Note: This will only work with sprites from a vanilla atlas. + */ + @Nullable + public static Material fromSprite(@Nullable TextureAtlasSprite sprite) { + if (sprite == null) return null; + return new Material(sprite.atlasLocation(), sprite.contents().name(), e -> Minecraft.getInstance().getTextureAtlas(sprite.atlasLocation()).apply(e)); + } + + public static Material fromRawTexture(ResourceLocation texture) { + return new Material(texture, texture, FullSprite::new); + } + + private static class FullSprite extends TextureAtlasSprite { + private FullSprite(ResourceLocation location) { + super(location, new SpriteContents(location, new FrameSize(1, 1), new NativeImage(1, 1, false), AnimationMetadataSection.EMPTY), 1, 1, 0, 0); + } + + @Override + public float getU(double u) + { + return (float) u / 16; + } + + @Override + public float getV(double v) + { + return (float) v / 16; + } + } +} diff --git a/src/main/java/codechicken/lib/gui/modular/sprite/ModAtlasHolder.java b/src/main/java/codechicken/lib/gui/modular/sprite/ModAtlasHolder.java new file mode 100644 index 00000000..9d361894 --- /dev/null +++ b/src/main/java/codechicken/lib/gui/modular/sprite/ModAtlasHolder.java @@ -0,0 +1,101 @@ +package codechicken.lib.gui.modular.sprite; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.SpriteLoader; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * Created by brandon3055 on 20/08/2023 + */ +public class ModAtlasHolder implements PreparableReloadListener, AutoCloseable { + private final TextureAtlas textureAtlas; + private final ResourceLocation atlasLocation; + private final ResourceLocation atlasInfoLocation; + private final String modid; + + /** + * Defines a mod texture atlas. + * Must be registered as a resource reload listener via RegisterClientReloadListenersEvent + * This is all that is needed to create a custom texture atlas. + * + * @param modid The mod id of the mod registering this atlas. + * @param atlasLocation The texture atlas location. e.g. "textures/atlas/gui.png" (Will have the modid: prefix added automatically) + * @param atlasInfoLocation The path to the atlas json file relative to modid:atlases/ + * e.g. "gui" will point to modid:atlases/gui.json + */ + public ModAtlasHolder(String modid, String atlasLocation, String atlasInfoLocation) { + this.atlasInfoLocation = new ResourceLocation(modid, atlasInfoLocation); + this.atlasLocation = new ResourceLocation(modid, atlasLocation); + this.textureAtlas = new TextureAtlas(this.atlasLocation); + this.modid = modid; + Minecraft.getInstance().getTextureManager().register(this.textureAtlas.location(), this.textureAtlas); + } + + public ResourceLocation atlasLocation() { + return atlasLocation; + } + + public TextureAtlasSprite getSprite(ResourceLocation resourceLocation) { + return this.textureAtlas.getSprite(resourceLocation); + } + + @Override + public final @NotNull CompletableFuture reload(PreparationBarrier prepBarrier, ResourceManager resourceManager, ProfilerFiller profiler, ProfilerFiller profiler2, Executor executor, Executor executor2) { + Objects.requireNonNull(prepBarrier); + SpriteLoader spriteLoader = SpriteLoader.create(this.textureAtlas); + return spriteLoader.loadAndStitch(new ModResourceManager(resourceManager, modid), this.atlasInfoLocation, 0, executor) + .thenCompose(SpriteLoader.Preparations::waitForUpload) + .thenCompose(prepBarrier::wait) + .thenAcceptAsync((preparations) -> this.apply(preparations, profiler2), executor2); + } + + private void apply(SpriteLoader.Preparations preparations, ProfilerFiller profilerFiller) { + profilerFiller.startTick(); + profilerFiller.push("upload"); + this.textureAtlas.upload(preparations); + profilerFiller.pop(); + profilerFiller.endTick(); + } + + @Override + public void close() { + this.textureAtlas.clearTextureData(); + } + + public static class ModResourceManager implements ResourceManager { + private final ResourceManager wrapped; + private final String modid; + + public ModResourceManager(ResourceManager wrapped, String modid) { + this.wrapped = wrapped; + this.modid = modid; + } + + @Override + public Map listResources(String pPath, Predicate pFilter) { + return wrapped.listResources(pPath, pFilter.and(e -> e.getNamespace().equals(modid))); + } + + //@formatter:off + @Override public Set getNamespaces() { return wrapped.getNamespaces(); } + @Override public List getResourceStack(ResourceLocation pLocation) { return wrapped.getResourceStack(pLocation); } + @Override public Map> listResourceStacks(String pPath, Predicate pFilter) { return wrapped.listResourceStacks(pPath, pFilter); } + @Override public Stream listPacks() { return wrapped.listPacks(); } + @Override public Optional getResource(ResourceLocation pLocation) { return wrapped.getResource(pLocation); } + //@formatter:on + } +} diff --git a/src/main/java/codechicken/lib/internal/ClientInit.java b/src/main/java/codechicken/lib/internal/ClientInit.java index a0186ba8..1e1eb056 100644 --- a/src/main/java/codechicken/lib/internal/ClientInit.java +++ b/src/main/java/codechicken/lib/internal/ClientInit.java @@ -3,13 +3,19 @@ import codechicken.lib.CodeChickenLib; import codechicken.lib.config.ConfigCategory; import codechicken.lib.config.ConfigSyncManager; +import codechicken.lib.gui.modular.lib.CursorHelper; +import codechicken.lib.gui.modular.sprite.CCGuiTextures; import codechicken.lib.model.CompositeItemModel; import codechicken.lib.model.ClassModelLoader; import codechicken.lib.render.CCRenderEventHandler; import codechicken.lib.render.block.BlockRenderingRegistry; import net.covers1624.quack.util.CrashLock; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.minecraftforge.client.event.ClientPlayerNetworkEvent; import net.minecraftforge.client.event.ModelEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; @@ -36,6 +42,7 @@ public static void init() { bus.addListener(ClientInit::onClientSetup); bus.addListener(ClientInit::onRegisterGeometryLoaders); + bus.addListener(ClientInit::onResourceReload); } private static void onClientSetup(FMLClientSetupEvent event) { @@ -74,4 +81,9 @@ private static void onRegisterGeometryLoaders(ModelEvent.RegisterGeometryLoaders event.register("item_composite", new CompositeItemModel()); event.register("class", new ClassModelLoader()); } + + public static void onResourceReload(RegisterClientReloadListenersEvent event) { + event.registerReloadListener(CCGuiTextures.getAtlasHolder()); + event.registerReloadListener((ResourceManagerReloadListener) e -> CursorHelper.onResourceReload()); + } } diff --git a/src/main/java/codechicken/lib/internal/network/CCLNetwork.java b/src/main/java/codechicken/lib/internal/network/CCLNetwork.java index 99b73ea5..5655ef14 100644 --- a/src/main/java/codechicken/lib/internal/network/CCLNetwork.java +++ b/src/main/java/codechicken/lib/internal/network/CCLNetwork.java @@ -15,6 +15,10 @@ public class CCLNetwork { //Client handled. public static final int C_ADD_LANDING_EFFECTS = 1; public static final int C_OPEN_CONTAINER = 10; + public static final int C_GUI_SYNC = 20; + + //Server handled. + public static final int S_GUI_SYNC = 20; //Login handled. public static final int L_CONFIG_SYNC = 1; @@ -22,8 +26,8 @@ public class CCLNetwork { public static void init() { netChannel = PacketCustomChannelBuilder.named(NET_CHANNEL)// .assignClientHandler(() -> ClientPacketHandler::new)// + .assignServerHandler(() -> ServerPacketHandler::new)// .assignLoginHandler(() -> LoginPacketHandler::new)// .build(); } - } diff --git a/src/main/java/codechicken/lib/internal/network/ClientPacketHandler.java b/src/main/java/codechicken/lib/internal/network/ClientPacketHandler.java index 8f7dd554..32bccf89 100644 --- a/src/main/java/codechicken/lib/internal/network/ClientPacketHandler.java +++ b/src/main/java/codechicken/lib/internal/network/ClientPacketHandler.java @@ -1,6 +1,7 @@ package codechicken.lib.internal.network; import codechicken.lib.inventory.container.ICCLContainerType; +import codechicken.lib.inventory.container.modular.ModularGuiContainerMenu; import codechicken.lib.packet.ICustomPacketHandler.IClientPacketHandler; import codechicken.lib.packet.PacketCustom; import codechicken.lib.render.particle.CustomParticleHandler; @@ -17,8 +18,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraftforge.registries.ForgeRegistries; -import static codechicken.lib.internal.network.CCLNetwork.C_ADD_LANDING_EFFECTS; -import static codechicken.lib.internal.network.CCLNetwork.C_OPEN_CONTAINER; +import static codechicken.lib.internal.network.CCLNetwork.*; /** * Created by covers1624 on 14/07/2017. @@ -36,6 +36,7 @@ public void handlePacket(PacketCustom packet, Minecraft mc, ClientPacketListener CustomParticleHandler.addLandingEffects(mc.level, pos, state, vec, numParticles); } case C_OPEN_CONTAINER -> handleOpenContainer(packet, mc); + case C_GUI_SYNC -> ModularGuiContainerMenu.handlePacketFromServer(mc.player, packet); } } diff --git a/src/main/java/codechicken/lib/internal/network/ServerPacketHandler.java b/src/main/java/codechicken/lib/internal/network/ServerPacketHandler.java new file mode 100644 index 00000000..4edab263 --- /dev/null +++ b/src/main/java/codechicken/lib/internal/network/ServerPacketHandler.java @@ -0,0 +1,22 @@ +package codechicken.lib.internal.network; + +import codechicken.lib.inventory.container.modular.ModularGuiContainerMenu; +import codechicken.lib.packet.ICustomPacketHandler.IServerPacketHandler; +import codechicken.lib.packet.PacketCustom; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; + +import static codechicken.lib.internal.network.CCLNetwork.S_GUI_SYNC; + +/** + * Created by covers1624 on 14/07/2017. + */ +public class ServerPacketHandler implements IServerPacketHandler { + + @Override + public void handlePacket(PacketCustom packet, ServerPlayer sender, ServerGamePacketListenerImpl handler) { + switch (packet.getType()) { + case S_GUI_SYNC -> ModularGuiContainerMenu.handlePacketFromClient(sender, packet); + } + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/AbstractDataStore.java b/src/main/java/codechicken/lib/inventory/container/data/AbstractDataStore.java new file mode 100644 index 00000000..759b2db8 --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/AbstractDataStore.java @@ -0,0 +1,45 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.Tag; + +import java.util.Objects; + +/** + * The base class of a simple general purpose serializable data system. + * + * Created by brandon3055 on 08/09/2023 + */ +public abstract class AbstractDataStore { + + protected T value; + + public AbstractDataStore(T defaultValue) { + this.value = defaultValue; + } + + public T get() { + return value; + } + + public void set(T value) { + this.value = value; + markDirty(); + } + + public void markDirty(){} + + public abstract void toBytes(MCDataOutput buf); + + public abstract void fromBytes(MCDataInput buf); + + public abstract Tag toTag(); + + public abstract void fromTag(Tag tag); + + public boolean isSameValue(T newValue) { + return Objects.equals(value, newValue); + } +} + diff --git a/src/main/java/codechicken/lib/inventory/container/data/BooleanData.java b/src/main/java/codechicken/lib/inventory/container/data/BooleanData.java new file mode 100644 index 00000000..23bc32c0 --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/BooleanData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class BooleanData extends AbstractDataStore { + + public BooleanData() { + super(false); + } + + public BooleanData(boolean defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeBoolean(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readBoolean(); + } + + @Override + public Tag toTag() { + return ByteTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsByte() != 0; + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/ByteData.java b/src/main/java/codechicken/lib/inventory/container/data/ByteData.java new file mode 100644 index 00000000..c438282e --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/ByteData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class ByteData extends AbstractDataStore { + + public ByteData() { + super((byte) 0); + } + + public ByteData(byte defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeByte(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readByte(); + } + + @Override + public Tag toTag() { + return ByteTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsByte(); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/DoubleData.java b/src/main/java/codechicken/lib/inventory/container/data/DoubleData.java new file mode 100644 index 00000000..990f9475 --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/DoubleData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class DoubleData extends AbstractDataStore { + + public DoubleData() { + super(0D); + } + + public DoubleData(double defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeDouble(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readDouble(); + } + + @Override + public Tag toTag() { + return DoubleTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsDouble(); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/FloatData.java b/src/main/java/codechicken/lib/inventory/container/data/FloatData.java new file mode 100644 index 00000000..6a135bca --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/FloatData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class FloatData extends AbstractDataStore { + + public FloatData() { + super(0F); + } + + public FloatData(float defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeFloat(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readFloat(); + } + + @Override + public Tag toTag() { + return FloatTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsFloat(); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/FluidData.java b/src/main/java/codechicken/lib/inventory/container/data/FluidData.java new file mode 100644 index 00000000..5a5bf64c --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/FluidData.java @@ -0,0 +1,53 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.fluids.FluidStack; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class FluidData extends AbstractDataStore { + + public FluidData() { + super(FluidStack.EMPTY); + } + + public FluidData(FluidStack defaultValue) { + super(defaultValue); + } + + @Override + public void set(FluidStack value) { + this.value = value.copy(); + markDirty(); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeFluidStack(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readFluidStack(); + } + + @Override + public Tag toTag() { + return value.writeToNBT(new CompoundTag()); + } + + @Override + public void fromTag(Tag tag) { + value = FluidStack.loadFluidStackFromNBT((CompoundTag) tag); + } + + @Override + public boolean isSameValue(FluidStack newValue) { + return value.equals(newValue); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/IntData.java b/src/main/java/codechicken/lib/inventory/container/data/IntData.java new file mode 100644 index 00000000..fe43551a --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/IntData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class IntData extends AbstractDataStore { + + public IntData() { + super(0); + } + + public IntData(int defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeVarInt(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readVarInt(); + } + + @Override + public Tag toTag() { + return IntTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsInt(); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/LongData.java b/src/main/java/codechicken/lib/inventory/container/data/LongData.java new file mode 100644 index 00000000..9b928d9a --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/LongData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class LongData extends AbstractDataStore { + + public LongData() { + super(0L); + } + + public LongData(long defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeVarLong(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readVarLong(); + } + + @Override + public Tag toTag() { + return LongTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsLong(); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/data/ShortData.java b/src/main/java/codechicken/lib/inventory/container/data/ShortData.java new file mode 100644 index 00000000..9a29d0dd --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/data/ShortData.java @@ -0,0 +1,42 @@ +package codechicken.lib.inventory.container.data; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Created by brandon3055 on 09/09/2023 + */ +public class ShortData extends AbstractDataStore { + + public ShortData() { + super((short) 0); + } + + public ShortData(short defaultValue) { + super(defaultValue); + } + + @Override + public void toBytes(MCDataOutput buf) { + buf.writeShort(value); + } + + @Override + public void fromBytes(MCDataInput buf) { + value = buf.readShort(); + } + + @Override + public Tag toTag() { + return ShortTag.valueOf(value); + } + + @Override + public void fromTag(Tag tag) { + value = ((NumericTag) tag).getAsShort(); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/modular/ModularGuiContainerMenu.java b/src/main/java/codechicken/lib/inventory/container/modular/ModularGuiContainerMenu.java new file mode 100644 index 00000000..f6f043cc --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/modular/ModularGuiContainerMenu.java @@ -0,0 +1,321 @@ +package codechicken.lib.inventory.container.modular; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.gui.modular.elements.GuiSlots; +import codechicken.lib.gui.modular.lib.container.ContainerScreenAccess; +import codechicken.lib.gui.modular.lib.container.DataSync; +import codechicken.lib.gui.modular.lib.container.SlotGroup; +import codechicken.lib.gui.modular.lib.geometry.GuiParent; +import codechicken.lib.internal.network.CCLNetwork; +import codechicken.lib.packet.PacketCustom; +import codechicken.lib.vec.Vector3; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static codechicken.lib.internal.network.CCLNetwork.*; + +/** + * The base abstract ContainerMenu for all modular gui containers. + *

+ * Created by brandon3055 on 08/09/2023 + */ +public abstract class ModularGuiContainerMenu extends AbstractContainerMenu { + private static final Logger LOGGER = LogManager.getLogger(); + + public final Inventory inventory; + public final List slotGroups = new ArrayList<>(); + public final Map slotGroupMap = new HashMap<>(); + public final Map> zonedSlots = new HashMap<>(); + public final List> dataSyncs = new ArrayList<>(); + + protected ModularGuiContainerMenu(@Nullable MenuType menuType, int containerId, Inventory inventory) { + super(menuType, containerId); + this.inventory = inventory; + } + + /** + * Creates and returns a new slot group for this container. + * You can then add your inventory slots to this slot group, similar to how you would normally add slots to the container. + * With one big exception! You do not need to worry about setting slot positions! (Just use 0, 0) + *

+ * Make sure to save your slot groups to accessible fields in your container menu class. + * You will need to pass these to appropriate {@link GuiSlots} / {@link GuiSlots#singleSlot(GuiParent, ContainerScreenAccess, SlotGroup, int)} elements. + * The gui elements will handle positioning and rendering the slots. + *

+ * As far as splitting a containers slots into multiple groups, Typically the players main inventory and hot bar would be added as two separate groups. + * How you handle the containers slots is up to you, For something like a machine with several spread out slots, + * you can still add all the slots to a single group, then pass each individual slot from the group to a single {@link GuiSlots#singleSlot(GuiParent, ContainerScreenAccess, SlotGroup, int)} element. + * + * @param zoneId Used for quick-move (shift click) operations. Each group has a zone id, and you can specify which zones a group can quick-move to. + * Multiple groups can have the same zone id. Quick move work though the groups in a zone in the order the groups here added. + * @param quickMoveTo List of zones this group can quick-move to. + */ + protected SlotGroup createSlotGroup(int zoneId, int... quickMoveTo) { + SlotGroup group = new SlotGroup(this, zoneId, quickMoveTo); + slotGroups.add(group); + return group; + } + + /** + * Convenience method to create a slot group for player slots. + * Configured to quick-move to {@link #remoteSlotGroup()} groups. + * + * @see #createSlotGroup(int, int[]) + */ + protected SlotGroup playerSlotGroup() { + return createSlotGroup(0, 1); + } + + /** + * Convenience method to create a slot group for the 'other side' of the inventory + * So the Block/tile or whatever this inventory is attached to. + * Configured to quick-move to {@link #playerSlotGroup()} groups. + * + * @see #createSlotGroup(int, int[]) + */ + protected SlotGroup remoteSlotGroup() { + return createSlotGroup(1, 0); + } + + //=== Network ===// + /** + * Send a packet to the client side container. + * + * @param packetId message id, Can be any value from 0 to 254, 255 is used by the {@link DataSync} system. + * @param packetWriter Use this callback to write your data to the packet. + */ + public void sendPacketToClient(int packetId, Consumer packetWriter) { + if (inventory.player instanceof ServerPlayer serverPlayer) { + PacketCustom packet = new PacketCustom(CCLNetwork.NET_CHANNEL, C_GUI_SYNC); + packet.writeByte(containerId); + packet.writeByte((byte) packetId); + packetWriter.accept(packet); + packet.sendToPlayer(serverPlayer); + } + } + + /** + * Send a packet to the server side container. + * + * @param packetId message id, Can be any value from 0 to 255 + * @param packetWriter Use this callback to write your data to the packet. + */ + public void sendPacketToServer(int packetId, Consumer packetWriter) { + PacketCustom packet = new PacketCustom(CCLNetwork.NET_CHANNEL, S_GUI_SYNC); + packet.writeByte(containerId); + packet.writeByte((byte) packetId); + packetWriter.accept(packet); + packet.sendToServer(); + } + + public static void handlePacketFromClient(Player player, MCDataInput packet) { + int containerId = packet.readByte(); + int packetId = packet.readByte() & 0xFF; + if (player.containerMenu instanceof ModularGuiContainerMenu menu && menu.containerId == containerId) { + menu.handlePacketFromClient(player, packetId, packet); + } + } + + /** + * Override this in your container menu implementation in order to receive packets sent via {@link #sendPacketToServer(int, Consumer)} + */ + public void handlePacketFromClient(Player player, int packetId, MCDataInput packet) { + + } + + public static void handlePacketFromServer(Player player, MCDataInput packet) { + int containerId = packet.readByte(); + int packetId = packet.readByte() & 0xFF; + if (player.containerMenu instanceof ModularGuiContainerMenu menu && menu.containerId == containerId) { + menu.handlePacketFromServer(player, packetId, packet); + } + } + + /** + * Override this in your container menu implementation in order to receive packets sent via {@link #sendPacketToServer(int, Consumer)} + *

+ * Don't forget to call super if you plan on using the {@link DataSync} system. + */ + public void handlePacketFromServer(Player player, int packetId, MCDataInput packet) { + if (packetId == 255) { + int index = packet.readByte() & 0xFF; + if (dataSyncs.size() > index) { + dataSyncs.get(index).handleSyncPacket(packet); + } + } + } + + //=== Quick Move ===/// + + /** + * Determines if two @link {@link ItemStack} match and can be merged into a single slot + */ + public static boolean canStacksMerge(ItemStack stack1, ItemStack stack2) { + if (stack1.isEmpty() || stack2.isEmpty()) return false; + return ItemStack.matches(stack1, stack2); + } + + /** + * Transfers to the next zone in order, and will loop around to the lowest zone. + * TODO, Would be nice to have better control over quick-move + * Maybe just the ability to specify which zones each group quick-moves to... + */ + @Override + public ItemStack quickMoveStack(@NotNull Player player, int slotIndex) { + Slot slot = getSlot(slotIndex); + if (slot == null || !slot.hasItem()) { + return ItemStack.EMPTY; + } + + SlotGroup group = slotGroupMap.get(slot); + if (group == null) { + return ItemStack.EMPTY; + } + + ItemStack stack = slot.getItem(); + ItemStack result = stack.copy(); + + boolean movedAnything = false; + for (Integer zone : group.quickMoveTo) { + if (!zonedSlots.containsKey(zone)) { + LOGGER.warn("Attempted to quick move to zone id {} but there are no slots assigned to this zone! This is a bug!", zone); + continue; + } + if (moveItemStackTo(stack, zonedSlots.get(zone), false)) { + movedAnything = true; + break; + } + } + + if (!movedAnything) { + return ItemStack.EMPTY; + } + + if (stack.isEmpty()) { + slot.set(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + slot.onTake(player, stack); + return result; + } + + protected boolean moveItemStackTo(ItemStack stack, List targets, boolean reverse) { + int start = 0; + int end = targets.size(); + boolean moved = false; + int position = start; + if (reverse) { + position = end - 1; + } + + Slot slot; + ItemStack itemStack2; + if (stack.isStackable()) { + while (!stack.isEmpty()) { + if (reverse) { + if (position < start) { + break; + } + } else if (position >= end) { + break; + } + + slot = targets.get(position); + itemStack2 = slot.getItem(); + if (!itemStack2.isEmpty() && ItemStack.isSameItemSameTags(stack, itemStack2)) { + int l = itemStack2.getCount() + stack.getCount(); + if (l <= stack.getMaxStackSize()) { + stack.setCount(0); + itemStack2.setCount(l); + slot.setChanged(); + moved = true; + } else if (itemStack2.getCount() < stack.getMaxStackSize()) { + stack.shrink(stack.getMaxStackSize() - itemStack2.getCount()); + itemStack2.setCount(stack.getMaxStackSize()); + slot.setChanged(); + moved = true; + } + } + + if (reverse) { + --position; + } else { + ++position; + } + } + } + + if (!stack.isEmpty()) { + if (reverse) { + position = end - 1; + } else { + position = start; + } + + while (true) { + if (reverse) { + if (position < start) { + break; + } + } else if (position >= end) { + break; + } + + slot = targets.get(position); + itemStack2 = slot.getItem(); + if (itemStack2.isEmpty() && slot.mayPlace(stack)) { + if (stack.getCount() > slot.getMaxStackSize()) { + slot.set(stack.split(slot.getMaxStackSize())); + } else { + slot.set(stack.split(stack.getCount())); + } + + slot.setChanged(); + moved = true; + break; + } + + if (reverse) { + --position; + } else { + ++position; + } + } + } + + return moved; + } + + //=== Internal Methods ===// + + public void mapSlot(Slot slot, SlotGroup slotGroup) { + slotGroupMap.put(slot, slotGroup); + zonedSlots.computeIfAbsent(slotGroup.zone, e -> new ArrayList<>()).add(slot); + } + + @Override + public void broadcastChanges() { + super.broadcastChanges(); + dataSyncs.forEach(DataSync::detectAndSend); + } +} diff --git a/src/main/java/codechicken/lib/inventory/container/modular/ModularSlot.java b/src/main/java/codechicken/lib/inventory/container/modular/ModularSlot.java new file mode 100644 index 00000000..bfa02872 --- /dev/null +++ b/src/main/java/codechicken/lib/inventory/container/modular/ModularSlot.java @@ -0,0 +1,121 @@ +package codechicken.lib.inventory.container.modular; + +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import java.util.function.*; + +/** + * A fully configurable inventory slot. + * If there is anything this slot can not do... Let me know. + *

+ * Created by brandon3055 on 10/09/2023 + */ +public class ModularSlot extends Slot { + private boolean canPlace = true; + private boolean checkContainer = true; + private Supplier enabled = () -> true; + private Predicate validator = stack -> true; + private Function stackLimit = stack -> Integer.MAX_VALUE; + private BiPredicate canRemove = (player, stack) -> true; + private BiConsumer onSet = (oldStack, newStack) -> {}; + + public ModularSlot(Container container, int index) { + this(container, index, 0, 0); + } + + public ModularSlot(Container container, int index, int xPos, int yPos) { + super(container, index, xPos, yPos); + } + + /** + * Configure this slot as an output only slot. + * Items can not be placed in this slot by the player. + */ + public ModularSlot output() { + canPlace = false; + return this; + } + + /** + * Do not use the containers canPlaceItem when checking if an item can be placed. + */ + public ModularSlot noCheck() { + checkContainer = false; + return this; + } + + /** + * Allows you to attach a validator to control what items are allowed in this slot. + * You can also limit a slots allowed contents via the {@link Container#canPlaceItem(int, ItemStack)} method of the container. + * + * @param validator The validator predicate, If the predicate returns false for a stack, the stack will not be placed. + */ + public ModularSlot setValidator(Predicate validator) { + this.validator = validator; + return this; + } + + /** + * Allows you to get a callback when the slot contents are set. + * Parameters given are Old stack then New stack. + */ + public ModularSlot onSet(BiConsumer onSet) { + this.onSet = onSet; + return this; + } + + /** + * Allows you to apply a stack size limit that (if smaller) will override the container and the item stack limits. + */ + public ModularSlot setStackLimit(Function stackLimit) { + this.stackLimit = stackLimit; + return this; + } + + /** + * Allows you to attach a "can remove" predicate that can block removal of a stack from the slot by the player. + */ + public ModularSlot setCanRemove(BiPredicate canRemove) { + this.canRemove = canRemove; + return this; + } + + public ModularSlot setEnabled(Supplier enabled) { + this.enabled = enabled; + return this; + } + + public ModularSlot setEnabled(boolean enabled) { + this.enabled = () -> enabled; + return this; + } + + @Override + public boolean mayPlace(ItemStack itemStack) { + return canPlace && validator.test(itemStack) && (!checkContainer || container.canPlaceItem(getContainerSlot(), itemStack)); + } + + @Override + public boolean mayPickup(Player player) { + return canRemove.test(player, getItem()); + } + + @Override + public void set(ItemStack itemStack) { + onSet.accept(getItem(), itemStack); + super.set(itemStack); + } + + @Override + public boolean isActive() { + return enabled.get(); + } + + @Override + public int getMaxStackSize(ItemStack itemStack) { + return Math.min(super.getMaxStackSize(itemStack), stackLimit.apply(itemStack)); + } +} diff --git a/src/main/java/codechicken/lib/render/CCRenderEventHandler.java b/src/main/java/codechicken/lib/render/CCRenderEventHandler.java index 7bd13335..1f25a147 100644 --- a/src/main/java/codechicken/lib/render/CCRenderEventHandler.java +++ b/src/main/java/codechicken/lib/render/CCRenderEventHandler.java @@ -1,11 +1,13 @@ package codechicken.lib.render; +import codechicken.lib.gui.modular.lib.CursorHelper; import codechicken.lib.raytracer.VoxelShapeBlockHitResult; import codechicken.lib.vec.Matrix4; import net.minecraft.world.phys.BlockHitResult; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.client.event.RenderHighlightEvent; +import net.minecraftforge.client.event.ScreenEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.EventPriority; diff --git a/src/main/java/codechicken/lib/util/FormatUtil.java b/src/main/java/codechicken/lib/util/FormatUtil.java new file mode 100644 index 00000000..e3395419 --- /dev/null +++ b/src/main/java/codechicken/lib/util/FormatUtil.java @@ -0,0 +1,46 @@ +package codechicken.lib.util; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * Created by brandon3055 on 31/10/2023 + */ +public class FormatUtil { + + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###,###,###,###,###", DecimalFormatSymbols.getInstance(Locale.ROOT)); + + public static String formatNumber(double value) { + if (Math.abs(value) < 1000D) return String.valueOf(value); + else if (Math.abs(value) < 1000000D) return addCommas((int) value); //I mean whats the point of displaying 1.235K instead of 1,235? + else if (Math.abs(value) < 1000000000D) return Math.round(value / 1000D) / 1000D + "M"; + else if (Math.abs(value) < 1000000000000D) return Math.round(value / 1000000D) / 1000D + "G"; + else return Math.round(value / 1000000000D) / 1000D + "T"; + } + + public static String formatNumber(long value) { + if (value == Long.MIN_VALUE) value = Long.MAX_VALUE; + if (Math.abs(value) < 1000L) return String.valueOf(value); + else if (Math.abs(value) < 1000000L) return addCommas(value); + else if (Math.abs(value) < 1000000000L) return Math.round((double)(value / 100000L)) / 10D + "M"; + else if (Math.abs(value) < 1000000000000L) return Math.round((double)(value / 100000000L)) / 10D + "G"; + else if (Math.abs(value) < 1000000000000000L) return Math.round((double)(value / 1000000000L)) / 1000D + "T"; + else if (Math.abs(value) < 1000000000000000000L) return Math.round((double)(value / 1000000000000L)) / 1000D + "P"; + else return Math.round((double) (value / 1000000000000000L)) / 1000D + "E"; + } + + /** + * Add commas to a number e.g. 161253126 > 161,253,126 + */ + public static String addCommas(int value) { + return DECIMAL_FORMAT.format(value); + } + + /** + * Add commas to a number e.g. 161253126 > 161,253,126 + */ + public static String addCommas(long value) { + return DECIMAL_FORMAT.format(value); + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 03a57caa..0cd041ee 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -63,3 +63,27 @@ public-f com.mojang.blaze3d.shaders.Uniform m_5679_(Lorg/joml/Matrix4f;)V # set public-f com.mojang.blaze3d.shaders.Uniform m_200759_(Lorg/joml/Matrix3f;)V # set public com.mojang.blaze3d.shaders.Program (Lcom/mojang/blaze3d/shaders/Program$Type;ILjava/lang/String;)V # public com.mojang.blaze3d.shaders.Program$Type m_85571_()I # getGlType + +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen m_280092_(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/inventory/Slot;)V # renderSlot +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen m_280211_(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/item/ItemStack;IILjava/lang/String;)V # renderFloatingItem +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen m_97744_(DD)Lnet/minecraft/world/inventory/Slot; # findSlot +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen m_97818_()V # recalculateQuickCraftRemaining +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97706_ # clickedSlot +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97711_ # draggingItem +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97710_ # isSplittingStack +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97717_ # quickCraftingType +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97720_ # quickCraftingRemainder +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97715_ # snapbackItem +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97714_ # snapbackTime +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97707_ # snapbackEnd +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97712_ # snapbackStartX +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97713_ # snapbackStartY +public-f net.minecraft.world.inventory.Slot f_40220_ # x +public-f net.minecraft.world.inventory.Slot f_40221_ # y +public net.minecraft.world.inventory.AbstractContainerMenu m_38897_(Lnet/minecraft/world/inventory/Slot;)Lnet/minecraft/world/inventory/Slot; # addSlot + +public net.minecraft.client.renderer.texture.TextureAtlas m_276092_()I # getWidth +public net.minecraft.client.renderer.texture.TextureAtlas m_276095_()I # getHeight +public net.minecraft.client.gui.GuiGraphics (Lnet/minecraft/client/Minecraft;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource$BufferSource;)V # +public net.minecraft.client.gui.GuiGraphics m_286081_()V # flushIfUnmanaged +public net.minecraft.client.gui.GuiGraphics m_287246_()V # flushIfManaged \ No newline at end of file diff --git a/src/main/resources/assets/codechickenlib/atlases/gui.json b/src/main/resources/assets/codechickenlib/atlases/gui.json new file mode 100644 index 00000000..5da82735 --- /dev/null +++ b/src/main/resources/assets/codechickenlib/atlases/gui.json @@ -0,0 +1,9 @@ +{ + "sources": [ + { + "type": "directory", + "source": "gui", + "prefix": "gui/" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/codechickenlib/textures/gui/cursors/drag.png b/src/main/resources/assets/codechickenlib/textures/gui/cursors/drag.png new file mode 100644 index 00000000..1e8c3266 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/cursors/drag.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_diag_tlbr.png b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_diag_tlbr.png new file mode 100644 index 00000000..2714f3f8 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_diag_tlbr.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_diag_trbl.png b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_diag_trbl.png new file mode 100644 index 00000000..b306c6ba Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_diag_trbl.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_h.png b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_h.png new file mode 100644 index 00000000..07baa61c Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_h.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_v.png b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_v.png new file mode 100644 index 00000000..363aca47 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/cursors/resize_v.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_borderless.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_borderless.png new file mode 100644 index 00000000..f1484440 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_borderless.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_borderless_pressed.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_borderless_pressed.png new file mode 100644 index 00000000..7273c079 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_borderless_pressed.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight.png new file mode 100644 index 00000000..11877644 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight_borderless.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight_borderless.png new file mode 100644 index 00000000..677d04c1 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight_borderless.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight_pressed.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight_pressed.png new file mode 100644 index 00000000..02dd3d8b Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_highlight_pressed.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_pressed.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_pressed.png new file mode 100644 index 00000000..aab7096a Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_pressed.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_vanilla.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_vanilla.png new file mode 100644 index 00000000..5e64e34b Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_vanilla.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_vanilla_disabled.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_vanilla_disabled.png new file mode 100644 index 00000000..aa29d997 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/button_vanilla_disabled.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/gui_borderless.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/gui_borderless.png new file mode 100644 index 00000000..87a64d8b Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/gui_borderless.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/dynamic/gui_vanilla.png b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/gui_vanilla.png new file mode 100644 index 00000000..3962fe08 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/dynamic/gui_vanilla.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/widgets/energy_empty.png b/src/main/resources/assets/codechickenlib/textures/gui/widgets/energy_empty.png new file mode 100644 index 00000000..bf499615 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/widgets/energy_empty.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/widgets/energy_full.png b/src/main/resources/assets/codechickenlib/textures/gui/widgets/energy_full.png new file mode 100644 index 00000000..4af15eef Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/widgets/energy_full.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/widgets/progress_arrow_empty.png b/src/main/resources/assets/codechickenlib/textures/gui/widgets/progress_arrow_empty.png new file mode 100644 index 00000000..7ade208c Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/widgets/progress_arrow_empty.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/widgets/progress_arrow_full.png b/src/main/resources/assets/codechickenlib/textures/gui/widgets/progress_arrow_full.png new file mode 100644 index 00000000..87b54615 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/widgets/progress_arrow_full.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/widgets/slot.png b/src/main/resources/assets/codechickenlib/textures/gui/widgets/slot.png new file mode 100644 index 00000000..65ab3081 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/widgets/slot.png differ diff --git a/src/main/resources/assets/codechickenlib/textures/gui/widgets/slot_large.png b/src/main/resources/assets/codechickenlib/textures/gui/widgets/slot_large.png new file mode 100644 index 00000000..61c28017 Binary files /dev/null and b/src/main/resources/assets/codechickenlib/textures/gui/widgets/slot_large.png differ