diff --git a/src/main/java/dev/isxander/controlify/gui/layout/AbstractLayoutComponent.java b/src/main/java/dev/isxander/controlify/gui/layout/AbstractLayoutComponent.java new file mode 100644 index 0000000..30a0d04 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/layout/AbstractLayoutComponent.java @@ -0,0 +1,40 @@ +package dev.isxander.controlify.gui.layout; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbstractLayoutComponent implements RenderComponent { + private final List components = new ArrayList<>(); + + public List getChildComponents() { + return components; + } + + public U insertTop(U area) { + components.add(area); + return area; + } + + public U insertBottom(U area) { + components.add(0, area); + return area; + } + + public U insertAbove(U area, T above) { + int index = components.indexOf(above); + if (index == -1) + throw new IllegalArgumentException("InteractionArea " + above + " is not registered!"); + + components.add(index + 1, area); + return area; + } + + public U insertBelow(U area, T below) { + int index = components.indexOf(below); + if (index == -1) + throw new IllegalArgumentException("InteractionArea " + below + " is not registered!"); + + components.add(index, area); + return area; + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/layout/AnchorPoint.java b/src/main/java/dev/isxander/controlify/gui/layout/AnchorPoint.java new file mode 100644 index 0000000..a1ef488 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/layout/AnchorPoint.java @@ -0,0 +1,29 @@ +package dev.isxander.controlify.gui.layout; + +import org.joml.Vector2f; +import org.joml.Vector2fc; +import org.joml.Vector2i; +import org.joml.Vector2ic; + +public enum AnchorPoint { + TOP_LEFT(0, 0), + TOP_CENTER(0.5f, 0), + TOP_RIGHT(1, 0), + CENTER_LEFT(0, 0.5f), + CENTER(0.5f, 0.5f), + CENTER_RIGHT(0.5f, 1), + BOTTOM_LEFT(0f, 1f), + BOTTOM_CENTER(0.5f, 1f), + BOTTOM_RIGHT(1f, 1f); + + public final float anchorX, anchorY; + + AnchorPoint(float anchorX, float anchorY) { + this.anchorX = anchorX; + this.anchorY = anchorY; + } + + public Vector2i getAnchorPosition(Vector2ic windowSize) { + return new Vector2i((int) (windowSize.x() * anchorX), (int) (windowSize.y() * anchorY)); + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/layout/ColumnLayoutComponent.java b/src/main/java/dev/isxander/controlify/gui/layout/ColumnLayoutComponent.java new file mode 100644 index 0000000..3c306a1 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/layout/ColumnLayoutComponent.java @@ -0,0 +1,160 @@ +package dev.isxander.controlify.gui.layout; + +import com.mojang.blaze3d.vertex.PoseStack; +import org.apache.commons.lang3.Validate; +import org.joml.Vector2i; +import org.joml.Vector2ic; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; + +public class ColumnLayoutComponent extends AbstractLayoutComponent { + private final int componentPaddingVertical; + private final int colPaddingLeft, colPaddingRight, colPaddingTop, colPaddingBottom; + private final ElementPosition elementPosition; + + private ColumnLayoutComponent(Collection elements, + int componentPaddingVertical, + int colPaddingLeft, int colPaddingRight, + int colPaddingTop, int colPaddingBottom, + ElementPosition elementPosition + ) { + for (var element : elements) { + insertTop(element); + } + + this.componentPaddingVertical = componentPaddingVertical; + this.colPaddingLeft = colPaddingLeft; + this.colPaddingRight = colPaddingRight; + this.colPaddingTop = colPaddingTop; + this.colPaddingBottom = colPaddingBottom; + this.elementPosition = elementPosition; + } + + @Override + public void render(PoseStack stack, int x, int y, float deltaTime) { + int width = getMaxChildWidth(); + + if (width == -1) + return; + + int yOffset = 0; + for (var element : getChildComponents()) { + if (!element.isVisible()) + continue; + + element.render( + stack, + x + colPaddingLeft + elementPosition.positionFunction.apply(width, element.size().x()), + y + colPaddingTop + yOffset, + deltaTime + ); + + yOffset += element.size().y() + componentPaddingVertical; + } + } + + @Override + public Vector2ic size() { + return new Vector2i( + getMaxChildWidth() + colPaddingLeft + colPaddingRight, + getSumHeight() + colPaddingTop + colPaddingBottom + ); + } + + private int getSumHeight() { + return this.getChildComponents().stream() + .filter(RenderComponent::isVisible) + .map(RenderComponent::size) + .mapToInt(size -> size.y() + componentPaddingVertical) + .sum() - componentPaddingVertical; + } + + private int getMaxChildWidth() { + return this.getChildComponents().stream() + .filter(RenderComponent::isVisible) + .map(RenderComponent::size) + .mapToInt(Vector2ic::x) + .max().orElse(-1); + } + + public static Builder builder() { + return new Builder<>(); + } + + public enum ElementPosition { + LEFT((rowWidth, elementWidth) -> 0), + RIGHT((rowWidth, elementWidth) -> rowWidth - elementWidth), + MIDDLE((rowWidth, elementWidth) -> rowWidth / 2 - elementWidth / 2); + + public final BiFunction positionFunction; + + ElementPosition(BiFunction positionFunction) { + this.positionFunction = positionFunction; + } + } + + public static class Builder { + private final List elements = new ArrayList<>(); + private int componentPaddingVertical; + private int colPaddingLeft, colPaddingRight, colPaddingTop, colPaddingBottom; + private ElementPosition elementPosition = null; + + public Builder element(T element) { + elements.add(element); + return this; + } + + public Builder elements(T... elements) { + this.elements.addAll(Arrays.asList(elements)); + return this; + } + + public Builder elements(Collection elements) { + this.elements.addAll(elements); + return this; + } + + public Builder elementPadding(int padding) { + this.componentPaddingVertical = padding; + return this; + } + + public Builder colPadding(int left, int right, int top, int bottom) { + this.colPaddingLeft = left; + this.colPaddingRight = right; + this.colPaddingTop = top; + this.colPaddingBottom = bottom; + return this; + } + + public Builder colPadding(int horizontal, int vertical) { + return colPadding(horizontal, horizontal, vertical, vertical); + } + + public Builder colPadding(int padding) { + return colPadding(padding, padding, padding, padding); + } + + public Builder elementPosition(ElementPosition elementPosition) { + this.elementPosition = elementPosition; + return this; + } + + public ColumnLayoutComponent build() { + Validate.notEmpty(elements, "No elements were added to the column!"); + Validate.notNull(elementPosition, "Element position cannot be null!"); + + return new ColumnLayoutComponent<>( + elements, + componentPaddingVertical, + colPaddingLeft, colPaddingRight, + colPaddingTop, colPaddingBottom, + elementPosition + ); + } + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java b/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java new file mode 100644 index 0000000..10c0261 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/layout/PositionedComponent.java @@ -0,0 +1,56 @@ +package dev.isxander.controlify.gui.layout; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import org.joml.Vector2i; +import org.joml.Vector2ic; + +public class PositionedComponent { + private final T component; + + private int x, y; + + private final AnchorPoint windowAnchor; + private final int offsetX, offsetY; + private final AnchorPoint origin; + + public PositionedComponent(T component, AnchorPoint windowAnchor, int offsetX, int offsetY, AnchorPoint origin) { + this.component = component; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.windowAnchor = windowAnchor; + this.origin = origin; + + updatePosition(); + } + + public void updatePosition() { + Vector2ic windowPosition = windowAnchor.getAnchorPosition(windowSize()); + Vector2ic anchoredPosition = origin.getAnchorPosition(component.size()); + + this.x = windowPosition.x() + offsetX - anchoredPosition.x(); + this.y = windowPosition.y() + offsetY - anchoredPosition.y(); + } + + public void render(PoseStack stack, float deltaTime) { + component.render(stack, x, y, deltaTime); + } + + public int x() { + return x; + } + + public int y() { + return y; + } + + public T getComponent() { + return component; + } + + private Vector2i windowSize() { + Window window = Minecraft.getInstance().getWindow(); + return new Vector2i(window.getGuiScaledWidth(), window.getGuiScaledHeight()); + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/layout/RenderComponent.java b/src/main/java/dev/isxander/controlify/gui/layout/RenderComponent.java new file mode 100644 index 0000000..15ebc5c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/layout/RenderComponent.java @@ -0,0 +1,14 @@ +package dev.isxander.controlify.gui.layout; + +import com.mojang.blaze3d.vertex.PoseStack; +import org.joml.Vector2ic; + +public interface RenderComponent { + void render(PoseStack stack, int x, int y, float deltaTime); + + Vector2ic size(); + + default boolean isVisible() { + return true; + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/layout/RowLayoutComponent.java b/src/main/java/dev/isxander/controlify/gui/layout/RowLayoutComponent.java new file mode 100644 index 0000000..f0065b5 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/layout/RowLayoutComponent.java @@ -0,0 +1,157 @@ +package dev.isxander.controlify.gui.layout; + +import com.mojang.blaze3d.vertex.PoseStack; +import org.apache.commons.lang3.Validate; +import org.joml.Vector2i; +import org.joml.Vector2ic; + +import java.util.*; +import java.util.function.BiFunction; + +public class RowLayoutComponent extends AbstractLayoutComponent { + private final int elementPaddingHorizontal; + private final int rowPaddingLeft, rowPaddingRight, rowPaddingTop, rowPaddingBottom; + private final ElementPosition elementPosition; + + private RowLayoutComponent(Collection elements, + int elementPaddingHorizontal, + int rowPaddingLeft, int rowPaddingRight, + int rowPaddingTop, int rowPaddingBottom, + ElementPosition elementPosition + ) { + for (var element : elements) { + insertTop(element); + } + + this.elementPaddingHorizontal = elementPaddingHorizontal; + this.rowPaddingLeft = rowPaddingLeft; + this.rowPaddingRight = rowPaddingRight; + this.rowPaddingTop = rowPaddingTop; + this.rowPaddingBottom = rowPaddingBottom; + this.elementPosition = elementPosition; + } + + @Override + public void render(PoseStack stack, int x, int y, float deltaTime) { + int height = getMaxChildHeight(); + + if (height == -1) + return; + + int xOffset = 0; + for (var element : getChildComponents()) { + if (!element.isVisible()) + continue; + + element.render( + stack, + x + rowPaddingLeft + xOffset, + y + rowPaddingTop + elementPosition.positionFunction.apply(height, element.size().y()), + deltaTime + ); + + xOffset += element.size().x() + elementPaddingHorizontal; + } + } + + @Override + public Vector2ic size() { + return new Vector2i( + getSumWidth() + rowPaddingLeft + rowPaddingRight, + getMaxChildHeight() + rowPaddingTop + rowPaddingBottom + ); + } + + private int getMaxChildHeight() { + return this.getChildComponents().stream() + .filter(RenderComponent::isVisible) + .map(RenderComponent::size) + .mapToInt(Vector2ic::y) + .max().orElse(-1); + } + + private int getSumWidth() { + return this.getChildComponents().stream() + .filter(RenderComponent::isVisible) + .map(RenderComponent::size) + .mapToInt(size -> size.x() + elementPaddingHorizontal) + .sum() - elementPaddingHorizontal; + } + + public static Builder builder() { + return new Builder<>(); + } + + public enum ElementPosition { + TOP((rowHeight, elementHeight) -> 0), + BOTTOM((rowHeight, elementHeight) -> rowHeight - elementHeight), + MIDDLE((rowHeight, elementHeight) -> rowHeight / 2 - elementHeight / 2); + + public final BiFunction positionFunction; + + ElementPosition(BiFunction positionFunction) { + this.positionFunction = positionFunction; + } + } + + public static class Builder { + private final List elements = new ArrayList<>(); + private int elementPaddingHorizontal; + private int rowPaddingLeft, rowPaddingRight, rowPaddingTop, rowPaddingBottom; + private ElementPosition elementPosition = null; + + public Builder element(T element) { + elements.add(element); + return this; + } + + public Builder elements(T... elements) { + this.elements.addAll(Arrays.asList(elements)); + return this; + } + + public Builder elements(Collection elements) { + this.elements.addAll(elements); + return this; + } + + public Builder elementPadding(int padding) { + this.elementPaddingHorizontal = padding; + return this; + } + + public Builder rowPadding(int left, int right, int top, int bottom) { + this.rowPaddingLeft = left; + this.rowPaddingRight = right; + this.rowPaddingTop = top; + this.rowPaddingBottom = bottom; + return this; + } + + public Builder rowPadding(int horizontal, int vertical) { + return rowPadding(horizontal, horizontal, vertical, vertical); + } + + public Builder rowPadding(int padding) { + return rowPadding(padding, padding, padding, padding); + } + + public Builder elementPosition(ElementPosition elementPosition) { + this.elementPosition = elementPosition; + return this; + } + + public RowLayoutComponent build() { + Validate.notEmpty(elements, "No elements were added to the row!"); + Validate.notNull(elementPosition, "Element position cannot be null!"); + + return new RowLayoutComponent<>( + elements, + elementPaddingHorizontal, + rowPaddingLeft, rowPaddingRight, + rowPaddingTop, rowPaddingBottom, + elementPosition + ); + } + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/guide/GuideAction.java b/src/main/java/dev/isxander/controlify/ingame/guide/GuideAction.java index 87f114d..da6a4a4 100644 --- a/src/main/java/dev/isxander/controlify/ingame/guide/GuideAction.java +++ b/src/main/java/dev/isxander/controlify/ingame/guide/GuideAction.java @@ -3,13 +3,13 @@ package dev.isxander.controlify.ingame.guide; import dev.isxander.controlify.api.bind.ControllerBinding; import dev.isxander.controlify.api.ingameguide.ActionLocation; import dev.isxander.controlify.api.ingameguide.ActionPriority; +import dev.isxander.controlify.api.ingameguide.GuideActionNameSupplier; import net.minecraft.network.chat.Component; import org.jetbrains.annotations.NotNull; -public record GuideAction(ControllerBinding binding, Component name, ActionLocation location, - ActionPriority priority) implements Comparable { - public GuideAction(ControllerBinding binding, Component name, ActionLocation location) { - this(binding, name, location, ActionPriority.NORMAL); +public record GuideAction(ControllerBinding binding, GuideActionNameSupplier name, ActionPriority priority) implements Comparable { + public GuideAction(ControllerBinding binding, GuideActionNameSupplier name) { + this(binding, name, ActionPriority.NORMAL); } @Override diff --git a/src/main/java/dev/isxander/controlify/ingame/guide/GuideActionRenderer.java b/src/main/java/dev/isxander/controlify/ingame/guide/GuideActionRenderer.java new file mode 100644 index 0000000..2f57882 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/ingame/guide/GuideActionRenderer.java @@ -0,0 +1,64 @@ +package dev.isxander.controlify.ingame.guide; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.controlify.api.bind.BindRenderer; +import dev.isxander.controlify.api.ingameguide.IngameGuideContext; +import dev.isxander.controlify.gui.DrawSize; +import dev.isxander.controlify.gui.layout.RenderComponent; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.network.chat.Component; +import org.joml.Vector2i; +import org.joml.Vector2ic; + +import java.util.Optional; + +public class GuideActionRenderer implements RenderComponent { + private final GuideAction guideAction; + private final boolean rtl; + + private Optional name = Optional.empty(); + + public GuideActionRenderer(GuideAction action, boolean rtl) { + this.guideAction = action; + this.rtl = rtl; + } + + @Override + public void render(PoseStack stack, int x, int y, float deltaTime) { + if (name.isEmpty()) + return; + + Font font = Minecraft.getInstance().font; + + BindRenderer renderer = guideAction.binding().renderer(); + DrawSize drawSize = renderer.size(); + int textWidth = font.width(name.get()); + + renderer.render(stack, x + (!rtl ? 0 : textWidth + 2), y + drawSize.height() / 2); + + int textX = x + (rtl ? 0 : drawSize.width() + 2); + int textY = y + drawSize.height() / 2 - font.lineHeight / 2; + + GuiComponent.fill(stack, textX - 1, textY - 1, textX + textWidth + 1, textY + font.lineHeight + 1, 0x80000000); + font.draw(stack, name.get(), textX, textY, 0xFFFFFF); + } + + @Override + public Vector2ic size() { + DrawSize bindSize = guideAction.binding().renderer().size(); + Font font = Minecraft.getInstance().font; + + return new Vector2i(bindSize.width() + 2 + name.map(font::width).orElse(-2), Math.max(bindSize.height(), font.lineHeight)); + } + + @Override + public boolean isVisible() { + return name.isPresent(); + } + + public void updateName(IngameGuideContext ctx) { + name = guideAction.name().supply(ctx); + } +} diff --git a/src/main/java/dev/isxander/controlify/ingame/guide/InGameButtonGuide.java b/src/main/java/dev/isxander/controlify/ingame/guide/InGameButtonGuide.java index 96f6243..d821509 100644 --- a/src/main/java/dev/isxander/controlify/ingame/guide/InGameButtonGuide.java +++ b/src/main/java/dev/isxander/controlify/ingame/guide/InGameButtonGuide.java @@ -7,6 +7,9 @@ import dev.isxander.controlify.bindings.ControllerBindingImpl; import dev.isxander.controlify.compatibility.ControlifyCompat; import dev.isxander.controlify.controller.Controller; import dev.isxander.controlify.api.event.ControlifyEvents; +import dev.isxander.controlify.gui.layout.AnchorPoint; +import dev.isxander.controlify.gui.layout.ColumnLayoutComponent; +import dev.isxander.controlify.gui.layout.PositionedComponent; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiComponent; import net.minecraft.client.multiplayer.ClientLevel; @@ -26,17 +29,45 @@ public class InGameButtonGuide implements IngameGuideRegistry { private final LocalPlayer player; private final Minecraft minecraft = Minecraft.getInstance(); - private final List guidePredicates = new ArrayList<>(); - private final List leftGuides = new ArrayList<>(); private final List rightGuides = new ArrayList<>(); + private final PositionedComponent> leftLayout; + private final PositionedComponent> rightLayout; + public InGameButtonGuide(Controller controller, LocalPlayer localPlayer) { this.controller = controller; this.player = localPlayer; registerDefaultActions(); ControlifyEvents.INGAME_GUIDE_REGISTRY.invoker().onRegisterIngameGuide(controller.bindings(), this); + + Collections.sort(leftGuides); + Collections.sort(rightGuides); + + leftLayout = new PositionedComponent<>( + ColumnLayoutComponent.builder() + .elementPadding(2) + .colPadding(4, 4) + .elementPosition(ColumnLayoutComponent.ElementPosition.LEFT) + .elements(leftGuides.stream().map(guide -> new GuideActionRenderer(guide, false)).toList()) + .build(), + AnchorPoint.TOP_LEFT, + 0, 0, + AnchorPoint.TOP_LEFT + ); + + rightLayout = new PositionedComponent<>( + ColumnLayoutComponent.builder() + .elementPadding(2) + .colPadding(4, 4) + .elementPosition(ColumnLayoutComponent.ElementPosition.RIGHT) + .elements(rightGuides.stream().map(guide -> new GuideActionRenderer(guide, true)).toList()) + .build(), + AnchorPoint.TOP_RIGHT, + 0, 0, + AnchorPoint.TOP_RIGHT + ); } public void renderHud(PoseStack poseStack, float tickDelta, int width, int height) { @@ -45,76 +76,20 @@ public class InGameButtonGuide implements IngameGuideRegistry { ControlifyCompat.ifBeginHudBatching(); - { - var offset = 0; - for (var action : leftGuides) { - var renderer = action.binding().renderer(); - - var drawSize = renderer.size(); - if (offset == 0) offset += drawSize.height() / 2; - - int x = 4; - int y = 3 + offset; - - renderer.render(poseStack, x, y); - - int textX = x + drawSize.width() + 2; - int textY = y - minecraft.font.lineHeight / 2; - GuiComponent.fill(poseStack, textX - 1, textY - 1, textX + minecraft.font.width(action.name()) + 1, textY + minecraft.font.lineHeight + 1, 0x80000000); - minecraft.font.draw(poseStack, action.name(), textX, textY, 0xFFFFFF); - - offset += drawSize.height() + 2; - } - } - - { - var offset = 0; - for (var action : rightGuides) { - var renderer = action.binding().renderer(); - - var drawSize = renderer.size(); - if (offset == 0) offset += drawSize.height() / 2; - - int x = width - 4 - drawSize.width(); - int y = 3 + offset; - - renderer.render(poseStack, x, y); - - int textX = x - minecraft.font.width(action.name()) - 2; - int textY = y - minecraft.font.lineHeight / 2; - GuiComponent.fill(poseStack, textX - 1, textY - 1, textX + minecraft.font.width(action.name()) + 1, textY + minecraft.font.lineHeight + 1, 0x80000000); - minecraft.font.draw(poseStack, action.name(), textX, textY, 0xFFFFFF); - - offset += drawSize.height() + 2; - } - } + leftLayout.render(poseStack, tickDelta); + rightLayout.render(poseStack, tickDelta); ControlifyCompat.ifEndHudBatching(); } public void tick() { - leftGuides.clear(); - rightGuides.clear(); + IngameGuideContext context = new IngameGuideContext(Minecraft.getInstance(), player, minecraft.level, calculateHitResult(), controller); - if (!controller.config().showIngameGuide || minecraft.screen != null) - return; + leftLayout.getComponent().getChildComponents().forEach(renderer -> renderer.updateName(context)); + rightLayout.getComponent().getChildComponents().forEach(renderer -> renderer.updateName(context)); - for (var actionPredicate : guidePredicates) { - var action = actionPredicate.supply(Minecraft.getInstance(), player, minecraft.level, calculateHitResult(), controller); - if (action.isEmpty()) - continue; - - GuideAction guideAction = action.get(); - if (!guideAction.binding().isUnbound()) { - if (action.get().location() == ActionLocation.LEFT) - leftGuides.add(action.get()); - else - rightGuides.add(action.get()); - } - } - - Collections.sort(leftGuides); - Collections.sort(rightGuides); + leftLayout.updatePosition(); + rightLayout.updatePosition(); } @Override @@ -124,7 +99,10 @@ public class InGameButtonGuide implements IngameGuideRegistry { @Override public void registerGuideAction(ControllerBinding binding, ActionLocation location, ActionPriority priority, GuideActionNameSupplier supplier) { - guidePredicates.add(new GuideActionSupplier(binding, location, priority, supplier)); + if (location == ActionLocation.LEFT) + leftGuides.add(new GuideAction(binding, supplier, priority)); + else + rightGuides.add(new GuideAction(binding, supplier, priority)); } private void registerDefaultActions() { @@ -248,11 +226,4 @@ public class InGameButtonGuide implements IngameGuideRegistry { return pickResult; } } - - private record GuideActionSupplier(ControllerBinding binding, ActionLocation location, ActionPriority priority, GuideActionNameSupplier nameSupplier) { - public Optional supply(Minecraft client, LocalPlayer player, ClientLevel level, HitResult hitResult, Controller controller) { - return nameSupplier.supply(new IngameGuideContext(client, player, level, hitResult, controller)) - .map(name -> new GuideAction(binding, name, location, priority)); - } - } }