aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java191
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java308
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java117
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java274
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java288
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java353
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java365
8 files changed, 1706 insertions, 203 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java
deleted file mode 100644
index 3456d1ad..00000000
--- a/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java
+++ /dev/null
@@ -1,191 +0,0 @@
-package de.hysky.skyblocker.skyblock;
-
-import de.hysky.skyblocker.SkyblockerMod;
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.utils.Utils;
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.font.TextRenderer;
-import net.minecraft.client.gui.DrawContext;
-import net.minecraft.util.Identifier;
-
-public class FancyStatusBars {
- private static final Identifier BARS = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/bars.png");
-
- private final MinecraftClient client = MinecraftClient.getInstance();
- private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker;
-
- private final StatusBar[] bars = new StatusBar[]{
- new StatusBar(0, 16733525, 2), // Health Bar
- new StatusBar(1, 5636095, 2), // Intelligence Bar
- new StatusBar(2, 12106180, 1), // Defence Bar
- new StatusBar(3, 8453920, 1), // Experience Bar
- };
-
- // Positions to show the bars
- // 0: Hotbar Layer 1, 1: Hotbar Layer 2, 2: Right of hotbar
- // Anything outside the set values hides the bar
- private final int[] anchorsX = new int[3];
- private final int[] anchorsY = new int[3];
-
- public FancyStatusBars() {
- moveBar(0, 0);
- moveBar(1, 0);
- moveBar(2, 0);
- moveBar(3, 0);
- }
-
- private int fill(int value, int max) {
- return (100 * value) / max;
- }
-
- public boolean render(DrawContext context, int scaledWidth, int scaledHeight) {
- var player = client.player;
- if (!SkyblockerConfigManager.get().general.bars.enableBars || player == null || Utils.isInTheRift())
- return false;
- anchorsX[0] = scaledWidth / 2 - 91;
- anchorsY[0] = scaledHeight - 33;
- anchorsX[1] = anchorsX[0];
- anchorsY[1] = anchorsY[0] - 10;
- anchorsX[2] = (scaledWidth / 2 + 91) + 2;
- anchorsY[2] = scaledHeight - 16;
-
- bars[0].update(statusBarTracker.getHealth());
- bars[1].update(statusBarTracker.getMana());
- int def = statusBarTracker.getDefense();
- bars[2].fill[0] = fill(def, def + 100);
- bars[2].text = def;
- bars[3].fill[0] = (int) (100 * player.experienceProgress);
- bars[3].text = player.experienceLevel;
-
- // Update positions of bars from config
- for (int i = 0; i < 4; i++) {
- int configAnchorNum = switch (i) {
- case 0 -> SkyblockerConfigManager.get().general.bars.barPositions.healthBarPosition.toInt();
- case 1 -> SkyblockerConfigManager.get().general.bars.barPositions.manaBarPosition.toInt();
- case 2 -> SkyblockerConfigManager.get().general.bars.barPositions.defenceBarPosition.toInt();
- case 3 -> SkyblockerConfigManager.get().general.bars.barPositions.experienceBarPosition.toInt();
- default -> 0;
- };
-
- if (bars[i].anchorNum != configAnchorNum)
- moveBar(i, configAnchorNum);
- }
-
- for (var bar : bars) {
- bar.draw(context);
- }
- for (var bar : bars) {
- bar.drawText(context);
- }
- return true;
- }
-
- public void moveBar(int bar, int location) {
- // Set the bar to the new anchor
- bars[bar].anchorNum = location;
-
- // Count how many bars are in each location
- int layer1Count = 0, layer2Count = 0;
- for (int i = 0; i < 4; i++) {
- switch (bars[i].anchorNum) {
- case 0 -> layer1Count++;
- case 1 -> layer2Count++;
- }
- }
-
- // Set the bars width and offsetX according to their anchor and how many bars are on that layer
- int adjustedLayer1Count = 0, adjustedLayer2Count = 0, adjustedRightCount = 0;
- for (int i = 0; i < 4; i++) {
- switch (bars[i].anchorNum) {
- case 0 -> {
- bars[i].bar_width = (172 - ((layer1Count - 1) * 11)) / layer1Count;
- bars[i].offsetX = adjustedLayer1Count * (bars[i].bar_width + 11 + (layer1Count == 3 ? 0 : 1));
- adjustedLayer1Count++;
- }
- case 1 -> {
- bars[i].bar_width = (172 - ((layer2Count - 1) * 11)) / layer2Count;
- bars[i].offsetX = adjustedLayer2Count * (bars[i].bar_width + 11 + (layer2Count == 3 ? 0 : 1));
- adjustedLayer2Count++;
- }
- case 2 -> {
- bars[i].bar_width = 50;
- bars[i].offsetX = adjustedRightCount * (50 + 11);
- adjustedRightCount++;
- }
- }
- }
- }
-
- private class StatusBar {
- public final int[] fill;
- public int offsetX;
- private final int v;
- private final int text_color;
- public int anchorNum;
- public int bar_width;
- public Object text;
-
- private StatusBar(int i, int textColor, int fillNum) {
- this.v = i * 9;
- this.text_color = textColor;
- this.fill = new int[fillNum];
- this.fill[0] = 100;
- this.anchorNum = 0;
- this.text = "";
- }
-
- public void update(StatusBarTracker.Resource resource) {
- int max = resource.max();
- int val = resource.value();
- this.fill[0] = fill(val, max);
- this.fill[1] = fill(resource.overflow(), max);
- this.text = val;
- }
-
- public void draw(DrawContext context) {
- // Dont draw if anchorNum is outside of range
- if (anchorNum < 0 || anchorNum > 2) return;
-
- // Draw the icon for the bar
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX, anchorsY[anchorNum], 0, v, 9, 9);
-
- // Draw the background for the bar
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10, anchorsY[anchorNum], 10, v, 2, 9);
- for (int i = 2; i < bar_width - 2; i += 58) {
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10 + i, anchorsY[anchorNum], 12, v, Math.min(58, bar_width - 2 - i), 9);
- }
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10 + bar_width - 2, anchorsY[anchorNum], 70, v, 2, 9);
-
- // Draw the filled part of the bar
- for (int i = 0; i < fill.length; i++) {
- int fill_width = this.fill[i] * (bar_width - 2) / 100;
- if (fill_width >= 1) {
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11, anchorsY[anchorNum], 72 + i * 60, v, 1, 9);
- }
- for (int j = 1; j < fill_width - 1; j += 58) {
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11 + j, anchorsY[anchorNum], 73 + i * 60, v, Math.min(58, fill_width - 1 - j), 9);
- }
- if (fill_width == bar_width - 2) {
- context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11 + fill_width - 1, anchorsY[anchorNum], 131 + i * 60, v, 1, 9);
- }
- }
- }
-
- public void drawText(DrawContext context) {
- // Dont draw if anchorNum is outside of range
- if (anchorNum < 0 || anchorNum > 2) return;
-
- TextRenderer textRenderer = client.textRenderer;
- String text = this.text.toString();
- int x = anchorsX[anchorNum] + this.offsetX + 11 + (bar_width - textRenderer.getWidth(text)) / 2;
- int y = anchorsY[anchorNum] - 3;
-
- final int[] offsets = new int[]{-1, 1};
- for (int i : offsets) {
- context.drawText(textRenderer, text, x + i, y, 0, false);
- context.drawText(textRenderer, text, x, y + i, 0, false);
- }
- context.drawText(textRenderer, text, x, y, text_color, false);
- }
- }
-}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java
index 6b90b86c..9d460803 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java
@@ -34,18 +34,7 @@ public class EditBidPopup extends AbstractPopupScreen {
super.init();
layout = DirectionalLayoutWidget.vertical();
layout.spacing(8).getMainPositioner().alignHorizontalCenter();
- textFieldWidget = new TextFieldWidget(textRenderer, 120, 15, Text.empty()) {
- @Override
- public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
- if (!super.keyPressed(keyCode, scanCode, modifiers)) {
- if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
- done(null);
- return true;
- }
- } else return true;
- return false;
- }
- };
+ textFieldWidget = new EnterConfirmTextFieldWidget(textRenderer, 120, 15, Text.empty(), () -> done(null));
textFieldWidget.setTextPredicate(this::isStringGood);
layout.add(new TextWidget(Text.literal("- Set Bid -").fillStyle(Style.EMPTY.withBold(true)), textRenderer));
layout.add(textFieldWidget);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java
new file mode 100644
index 00000000..a72b955b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java
@@ -0,0 +1,308 @@
+package de.hysky.skyblocker.skyblock.fancybars;
+
+import net.minecraft.client.gui.ScreenPos;
+import net.minecraft.client.gui.ScreenRect;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+public class BarPositioner {
+
+ private final Map<BarAnchor, LinkedList<LinkedList<StatusBar>>> map = new HashMap<>(BarAnchor.values().length);
+
+ public BarPositioner() {
+ for (BarAnchor value : BarAnchor.values()) {
+ map.put(value, new LinkedList<>());
+ }
+ }
+
+
+ public int getRowCount(@NotNull BarAnchor barAnchor) {
+ return map.get(barAnchor).size();
+ }
+
+ /**
+ * Adds a row to the end of an anchor
+ *
+ * @param barAnchor the anchor
+ */
+ public void addRow(@NotNull BarAnchor barAnchor) {
+ map.get(barAnchor).add(new LinkedList<>());
+ }
+
+ /**
+ * Adds a row at the specified index
+ *
+ * @param barAnchor the anchor
+ * @param row row index
+ */
+ public void addRow(@NotNull BarAnchor barAnchor, int row) {
+ map.get(barAnchor).add(row, new LinkedList<>());
+ }
+
+ /**
+ * adds a bar to the end of a row
+ *
+ * @param barAnchor the anchor
+ * @param row the row
+ * @param bar the bar to add
+ */
+ public void addBar(@NotNull BarAnchor barAnchor, int row, StatusBar bar) {
+ LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row);
+ statusBars.add(bar);
+ bar.gridY = row;
+ bar.gridX = statusBars.lastIndexOf(bar); // optimization baby, start with the end!
+ bar.anchor = barAnchor;
+ }
+
+ /**
+ * adds a bar to the specified x in a row
+ *
+ * @param barAnchor the anchor
+ * @param row the row
+ * @param x the index in the row
+ * @param bar the bar to add
+ */
+ public void addBar(@NotNull BarAnchor barAnchor, int row, int x, StatusBar bar) {
+ LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row);
+ statusBars.add(x, bar);
+ bar.gridY = row;
+ bar.gridX = statusBars.indexOf(bar);
+ bar.anchor = barAnchor;
+ }
+
+ /**
+ * removes the specified bar at x on the row. If it's row is empty after being removed, the row will be auto removed
+ *
+ * @param barAnchor the anchor
+ * @param row dah row
+ * @param x dah x
+ */
+ public void removeBar(@NotNull BarAnchor barAnchor, int row, int x) {
+ LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row);
+ StatusBar remove = statusBars.remove(x);
+ remove.anchor = null;
+ for (int i = x; i < statusBars.size(); i++) {
+ statusBars.get(i).gridX--;
+ }
+ if (statusBars.isEmpty()) removeRow(barAnchor, row);
+ }
+
+ /**
+ * removes the specified bar on the row. If it's row is empty after being removed, the row will be auto removed
+ *
+ * @param barAnchor the anchor
+ * @param row dah row
+ * @param bar dah bar
+ */
+ public void removeBar(@NotNull BarAnchor barAnchor, int row, StatusBar bar) {
+ LinkedList<StatusBar> barRow = map.get(barAnchor).get(row);
+ int x = barRow.indexOf(bar);
+ if (x < 0) return; // probably a bad idea
+
+ barRow.remove(bar);
+ bar.anchor = null;
+ for (int i = x; i < barRow.size(); i++) {
+ barRow.get(i).gridX--;
+ }
+ if (barRow.isEmpty()) removeRow(barAnchor, row);
+ }
+
+ /**
+ * row must be empty
+ *
+ * @param barAnchor the anchor
+ * @param row the row to remove
+ */
+ public void removeRow(@NotNull BarAnchor barAnchor, int row) {
+ LinkedList<StatusBar> barRow = map.get(barAnchor).get(row);
+ if (!barRow.isEmpty())
+ throw new IllegalStateException("Can't remove a non-empty row (" + barAnchor + "," + row + ")");
+ map.get(barAnchor).remove(row);
+ for (int i = row; i < map.get(barAnchor).size(); i++) {
+ for (StatusBar statusBar : map.get(barAnchor).get(i)) {
+ statusBar.gridY--;
+ }
+ }
+ }
+
+
+ public LinkedList<StatusBar> getRow(@NotNull BarAnchor barAnchor, int row) {
+ return map.get(barAnchor).get(row);
+ }
+
+ public StatusBar getBar(@NotNull BarAnchor barAnchor, int row, int x) {
+ return map.get(barAnchor).get(row).get(x);
+ }
+
+ public boolean hasNeighbor(@NotNull BarAnchor barAnchor, int row, int x, boolean right) {
+ LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row);
+ if (barAnchor.isRight()) {
+ return (right && x < statusBars.size() - 1) || (!right && x > 0);
+ } else {
+ return (right && x > 0) || (!right && x < statusBars.size() - 1);
+ }
+ }
+
+
+ public enum BarAnchor {
+ HOTBAR_LEFT(true, false,
+ (scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth / 2 - 91 - 2, scaledHeight - 5),
+ SizeRule.freeSize(25, 2, 6)),
+
+ HOTBAR_RIGHT(true, true,
+ (scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth / 2 + 91 + 2, scaledHeight - 5),
+ SizeRule.freeSize(25, 2, 6)),
+
+ HOTBAR_TOP(true, true,
+ (scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth / 2 - 91, scaledHeight - 23),
+ SizeRule.targetSize(12, 182, 2),
+ anchorPosition -> new ScreenRect(anchorPosition.x(), anchorPosition.y() - 20, 182, 20)),
+
+ SCREEN_TOP_LEFT(false, true,
+ ((scaledWidth, scaledHeight) -> new ScreenPos(5, 5)),
+ SizeRule.freeSize(25, 2, 6)
+ ),
+ SCREEN_TOP_RIGHT(false, false,
+ ((scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth - 5, 5)),
+ SizeRule.freeSize(25, 2, 6)
+ ),
+ SCREEN_BOTTOM_LEFT(true, true,
+ ((scaledWidth, scaledHeight) -> new ScreenPos(5, scaledHeight - 5)),
+ SizeRule.freeSize(25, 2, 6)
+ ),
+ SCREEN_BOTTOM_RIGHT(true, false,
+ ((scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth - 5, scaledHeight - 5)),
+ SizeRule.freeSize(25, 2, 6)
+ );
+
+ private final AnchorPositionProvider positionProvider;
+ private final AnchorHitboxProvider hitboxProvider;
+ private final boolean up;
+ private final boolean right;
+ private final SizeRule sizeRule;
+
+ /**
+ * @param up whether the rows stack towards the top of the screen from the anchor (false is bottom)
+ * @param right whether the bars are line up towards the right of the screen from the anchor (false is left)
+ * @param positionProvider provides the position of the anchor for a give screen size
+ * @param sizeRule the rule the bars should follow. See {@link SizeRule}
+ * @param hitboxProvider provides the hitbox for when the anchor has no bars for the config screen
+ */
+ BarAnchor(boolean up, boolean right, AnchorPositionProvider positionProvider, SizeRule sizeRule, AnchorHitboxProvider hitboxProvider) {
+ this.positionProvider = positionProvider;
+ this.up = up;
+ this.right = right;
+ this.hitboxProvider = hitboxProvider;
+ this.sizeRule = sizeRule;
+ }
+
+ BarAnchor(boolean up, boolean right, AnchorPositionProvider positionProvider, SizeRule sizeRule) {
+ this(up, right, positionProvider, sizeRule,
+ anchorPosition -> new ScreenRect(anchorPosition.x() - (right ? 0 : 20), anchorPosition.y() - (up ? 20 : 0), 20, 20));
+ }
+
+ public ScreenPos getAnchorPosition(int scaledWidth, int scaledHeight) {
+ return positionProvider.getPosition(scaledWidth, scaledHeight);
+ }
+
+ public ScreenRect getAnchorHitbox(ScreenPos anchorPosition) {
+ return hitboxProvider.getHitbox(anchorPosition);
+ }
+
+ /**
+ * whether the rows stack towards the top of the screen from the anchor (false is bottom)
+ *
+ * @return true if towards the top, false otherwise
+ */
+ public boolean isUp() {
+ return up;
+ }
+
+ /**
+ * whether the bars are line up towards the right of the screen from the anchor (false is left)
+ *
+ * @return true if towards the right, false otherwise
+ */
+ public boolean isRight() {
+ return right;
+ }
+
+ public SizeRule getSizeRule() {
+ return sizeRule;
+ }
+
+ private static final List<BarAnchor> cached = List.of(values());
+
+ /**
+ * cached version of {@link BarAnchor#values()}
+ *
+ * @return the list of anchors
+ */
+ public static List<BarAnchor> allAnchors() {
+ return cached;
+ }
+ }
+
+ /**
+ * The rules the bars on an anchor should follow
+ *
+ * @param isTargetSize whether the bars went to fit to a target width
+ * @param targetSize the size of all the bars on a row should add up to this (target size)
+ * @param totalWidth the total width taken by all the bars on the row (target size)
+ * @param widthPerSize the width of each size "unit" (free size)
+ * @param minSize the minimum (free and target size)
+ * @param maxSize the maximum (free and target size, THIS SHOULD BE THE SAME AS {@code targetSize} FOR {@code isTargetSize = true})
+ */
+ public record SizeRule(boolean isTargetSize, int targetSize, int totalWidth, int widthPerSize, int minSize, int maxSize) {
+ public static SizeRule freeSize(int widthPerSize, int minSize, int maxSize) {
+ return new SizeRule(false, -1, -1, widthPerSize, minSize, maxSize);
+ }
+
+ public static SizeRule targetSize(int targetSize, int totalWidth, int minSize) {
+ return new SizeRule(true, targetSize, totalWidth, -1, minSize, targetSize);
+ }
+ }
+
+ /**
+ * A record representing a snapshot of a bar's position
+ *
+ * @param barAnchor
+ * @param x
+ * @param y the row
+ */
+ public record BarLocation(@Nullable BarAnchor barAnchor, int x, int y) {
+
+ public static final BarLocation NULL = new BarLocation(null, -1, -1);
+
+ public static BarLocation of(StatusBar bar) {
+ return new BarLocation(bar.anchor, bar.gridX, bar.gridY);
+ }
+
+ public boolean equals(BarAnchor barAnchor, int x, int y) {
+ return x == this.x && y == this.y && barAnchor == this.barAnchor;
+ }
+ }
+
+ /**
+ * provides the position of the anchor for a give screen size
+ */
+ @FunctionalInterface
+ interface AnchorPositionProvider {
+
+ ScreenPos getPosition(int scaledWidth, int scaledHeight);
+ }
+
+ @FunctionalInterface
+ interface AnchorHitboxProvider {
+
+ /**
+ * The hitbox, as in how large the area of "snapping" is if there are no bars on this anchor
+ *
+ * @param anchorPosition the position of the anchor
+ * @return the rectangle that represents the hitbox
+ */
+ ScreenRect getHitbox(ScreenPos anchorPosition);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java
new file mode 100644
index 00000000..64e79bab
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java
@@ -0,0 +1,117 @@
+package de.hysky.skyblocker.skyblock.fancybars;
+
+import de.hysky.skyblocker.utils.render.gui.AbstractPopupScreen;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.widget.*;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+
+import java.awt.*;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class EditBarColorPopup extends AbstractPopupScreen {
+
+ private final Consumer<Color> setColor;
+
+ private DirectionalLayoutWidget layout = DirectionalLayoutWidget.vertical();
+ private BasicColorSelector colorSelector;
+
+ protected EditBarColorPopup(Text title, Screen backgroundScreen, Consumer<Color> setColor) {
+ super(title, backgroundScreen);
+ this.setColor = setColor;
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ layout = DirectionalLayoutWidget.vertical();
+ layout.spacing(8).getMainPositioner().alignHorizontalCenter();
+ layout.add(new TextWidget(title.copy().fillStyle(Style.EMPTY.withBold(true)), MinecraftClient.getInstance().textRenderer));
+ colorSelector = new BasicColorSelector(0, 0, 150, () -> done(null));
+ layout.add(colorSelector);
+
+ DirectionalLayoutWidget horizontal = DirectionalLayoutWidget.horizontal();
+ ButtonWidget buttonWidget = ButtonWidget.builder(Text.literal("Cancel"), button -> close()).width(80).build();
+ horizontal.add(buttonWidget);
+ horizontal.add(ButtonWidget.builder(Text.literal("Done"), this::done).width(80).build());
+
+ layout.add(horizontal);
+ layout.forEachChild(this::addDrawableChild);
+ this.layout.refreshPositions();
+ SimplePositioningWidget.setPos(layout, this.getNavigationFocus());
+ }
+
+ private void done(Object object) {
+ if (colorSelector.validColor) setColor.accept(new Color(colorSelector.getColor()));
+ close();
+ }
+
+ @Override
+ public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.renderBackground(context, mouseX, mouseY, delta);
+ drawPopupBackground(context, layout.getX(), layout.getY(), layout.getWidth(), layout.getHeight());
+ }
+
+ private static class BasicColorSelector extends ContainerWidget {
+
+ private final EnterConfirmTextFieldWidget textFieldWidget;
+
+ public BasicColorSelector(int x, int y, int width, Runnable onEnter) {
+ super(x, y, width, 15, Text.literal("edit color"));
+ textFieldWidget = new EnterConfirmTextFieldWidget(MinecraftClient.getInstance().textRenderer, getX() + 16, getY(), width - 16, 15, Text.empty(), onEnter);
+ textFieldWidget.setChangedListener(this::onTextChange);
+ textFieldWidget.setTextPredicate(s -> s.length() <= 6);
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of(textFieldWidget);
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ private int color = 0xFF000000;
+ private boolean validColor = false;
+
+ private void onTextChange(String text) {
+ try {
+ color = Integer.parseInt(text, 16) | 0xFF000000;
+ validColor = true;
+ } catch (NumberFormatException e) {
+ color = 0;
+ validColor = false;
+ }
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ context.drawBorder(getX(), getY(), 15, 15, validColor ? -1 : 0xFFDD0000);
+ context.fill(getX() + 1, getY() + 1, getX() + 14, getY() + 14, color);
+ textFieldWidget.renderWidget(context, mouseX, mouseY, delta);
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+
+ }
+
+ @Override
+ public void setX(int x) {
+ super.setX(x);
+ textFieldWidget.setX(getX() + 16);
+ }
+
+ @Override
+ public void setY(int y) {
+ super.setY(y);
+ textFieldWidget.setY(getY());
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java
new file mode 100644
index 00000000..54af7a03
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java
@@ -0,0 +1,274 @@
+package de.hysky.skyblocker.skyblock.fancybars;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.tooltip.TooltipBackgroundRenderer;
+import net.minecraft.client.gui.widget.ClickableWidget;
+import net.minecraft.client.gui.widget.ContainerWidget;
+import net.minecraft.client.gui.widget.TextWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Formatting;
+
+import java.awt.*;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class EditBarWidget extends ContainerWidget {
+
+ private final EnumCyclingOption<StatusBar.IconPosition> iconOption;
+ private final BooleanOption booleanOption;
+
+ private final ColorOption color1;
+ private final ColorOption color2;
+ private final ColorOption textColor;
+ private final TextWidget nameWidget;
+
+ private int contentsWidth = 0;
+
+ public EditBarWidget(int x, int y, Screen parent) {
+ super(x, y, 100, 66, Text.literal("Edit bar"));
+
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+
+ nameWidget = new TextWidget(Text.empty(), textRenderer);
+
+ MutableText translatable = Text.translatable("skyblocker.bars.config.icon");
+ contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + textRenderer.getWidth("RIGHT") + 10);
+ iconOption = new EnumCyclingOption<>(0, 11, getWidth(), translatable, StatusBar.IconPosition.class);
+
+ translatable = Text.translatable("skyblocker.bars.config.showValue");
+ contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10);
+ booleanOption = new BooleanOption(0, 22, getWidth(), translatable);
+
+ // COLO(u)RS
+ translatable = Text.translatable("skyblocker.bars.config.mainColor");
+ contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10);
+ color1 = new ColorOption(0, 33, getWidth(), translatable, parent);
+
+ translatable = Text.translatable("skyblocker.bars.config.overflowColor");
+ contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10);
+ color2 = new ColorOption(0, 44, getWidth(), translatable, parent);
+
+ translatable = Text.translatable("skyblocker.bars.config.textColor");
+ contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10);
+ textColor = new ColorOption(0, 55, getWidth(), translatable, parent);
+
+ setWidth(contentsWidth);
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of(iconOption, booleanOption, color1, color2, textColor);
+ }
+
+ public int insideMouseX = 0;
+ public int insideMouseY = 0;
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (isHovered()) {
+ insideMouseX = mouseX;
+ insideMouseY = mouseY;
+ } else {
+ int i = mouseX - insideMouseX;
+ int j = mouseY - insideMouseY;
+ if (i * i + j * j > 30 * 30) visible = false;
+ }
+ TooltipBackgroundRenderer.render(context, getX(), getY(), getWidth(), getHeight(), 0);
+ MatrixStack matrices = context.getMatrices();
+ matrices.push();
+ matrices.translate(getX(), getY(), 0);
+ nameWidget.render(context, mouseX, mouseY, delta);
+ iconOption.renderWidget(context, mouseX - getX(), mouseY - getY(), delta);
+ booleanOption.renderWidget(context, mouseX - getX(), mouseY - getY(), delta);
+ color1.renderWidget(context, mouseX - getX(), mouseY - getY(), delta);
+ color2.renderWidget(context, mouseX - getX(), mouseY - getY(), delta);
+ textColor.renderWidget(context, mouseX - getX(), mouseY - getY(), delta);
+ matrices.pop();
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (!visible) return false;
+ if (!isHovered()) visible = false;
+ return super.mouseClicked(mouseX - getX(), mouseY - getY(), button);
+ }
+
+ public void setStatusBar(StatusBar statusBar) {
+ iconOption.setCurrent(statusBar.getIconPosition());
+ iconOption.setOnChange(statusBar::setIconPosition);
+ booleanOption.setCurrent(statusBar.showText());
+ booleanOption.setOnChange(statusBar::setShowText);
+
+ color1.setCurrent(statusBar.getColors()[0].getRGB());
+ color1.setOnChange(color -> statusBar.getColors()[0] = color);
+
+ color2.active = statusBar.hasOverflow();
+ if (color2.active) {
+ color2.setCurrent(statusBar.getColors()[1].getRGB());
+ color2.setOnChange(color -> statusBar.getColors()[1] = color);
+ }
+
+ if (statusBar.getTextColor() != null) {
+ textColor.setCurrent(statusBar.getTextColor().getRGB());
+ }
+ textColor.setOnChange(statusBar::setTextColor);
+
+ MutableText formatted = statusBar.getName().copy().formatted(Formatting.BOLD);
+ nameWidget.setMessage(formatted);
+ setWidth(Math.max(MinecraftClient.getInstance().textRenderer.getWidth(formatted), contentsWidth));
+ }
+
+ @Override
+ public void setWidth(int width) {
+ super.setWidth(width);
+ iconOption.setWidth(width);
+ booleanOption.setWidth(width);
+ color1.setWidth(width);
+ color2.setWidth(width);
+ textColor.setWidth(width);
+ nameWidget.setWidth(width);
+
+ }
+
+ public static class EnumCyclingOption<T extends Enum<T>> extends ClickableWidget {
+
+ private T current;
+ private final T[] values;
+ private Consumer<T> onChange = null;
+
+ public EnumCyclingOption(int x, int y, int width, Text message, Class<T> enumClass) {
+ super(x, y, width, 11, message);
+ values = enumClass.getEnumConstants();
+ current = values[0];
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (isMouseOver(mouseX, mouseY)) {
+ context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF);
+ }
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, getMessage(), getX() + 1, getY() + 1, -1, true);
+ String string = current.toString();
+ context.drawText(textRenderer, string, getRight() - textRenderer.getWidth(string) - 1, getY() + 1, -1, true);
+ }
+
+ public void setCurrent(T current) {
+ this.current = current;
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ current = values[(current.ordinal() + 1) % values.length];
+ if (onChange != null) onChange.accept(current);
+ super.onClick(mouseX, mouseY);
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+ }
+
+ public void setOnChange(Consumer<T> onChange) {
+ this.onChange = onChange;
+ }
+ }
+
+ public static class BooleanOption extends ClickableWidget {
+
+ private boolean current = false;
+ private Consumer<Boolean> onChange = null;
+
+ public BooleanOption(int x, int y, int width, Text message) {
+ super(x, y, width, 11, message);
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (isMouseOver(mouseX, mouseY)) {
+ context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF);
+ }
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, getMessage(), getX() + 1, getY() + 1, -1, true);
+ context.drawBorder(getRight() - 10, getY() + 1, 9, 9, -1);
+ if (current) context.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, -1);
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ current = !current;
+ if (onChange != null) onChange.accept(current);
+ super.onClick(mouseX, mouseY);
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+ }
+
+ public void setCurrent(boolean current) {
+ this.current = current;
+ }
+
+ public void setOnChange(Consumer<Boolean> onChange) {
+ this.onChange = onChange;
+ }
+ }
+
+ public static class ColorOption extends ClickableWidget {
+
+ public void setCurrent(int current) {
+ this.current = current;
+ }
+
+ private int current = 0;
+ private Consumer<Color> onChange = null;
+ private final Screen parent;
+
+ public ColorOption(int x, int y, int width, Text message, Screen parent) {
+ super(x, y, width, 11, message);
+ this.parent = parent;
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (isMouseOver(mouseX, mouseY)) {
+ context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF);
+ }
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? -1 : Colors.GRAY, true);
+ context.drawBorder(getRight() - 10, getY() + 1, 9, 9, active ? -1 : Colors.GRAY);
+ context.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, active ? current : Colors.GRAY);
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ super.onClick(mouseX, mouseY);
+ MinecraftClient.getInstance().setScreen(new EditBarColorPopup(Text.literal("Edit ").append(getMessage()), parent, this::set));
+ }
+
+ private void set(Color color) {
+ current = color.getRGB();
+ if (onChange != null) onChange.accept(color);
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+
+ }
+
+ public void setOnChange(Consumer<Color> onChange) {
+ this.onChange = onChange;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java
new file mode 100644
index 00000000..a960c9af
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java
@@ -0,0 +1,288 @@
+package de.hysky.skyblocker.skyblock.fancybars;
+
+import com.google.gson.JsonObject;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.StatusBarTracker;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.ScreenPos;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.glfw.GLFW;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+public class FancyStatusBars {
+ private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("status_bars.json");
+ private static final Logger LOGGER = LoggerFactory.getLogger(FancyStatusBars.class);
+
+ private final MinecraftClient client = MinecraftClient.getInstance();
+ private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker;
+
+ public static BarPositioner barPositioner = new BarPositioner();
+ public static Map<String, StatusBar> statusBars = new HashMap<>();
+
+ public static void init() {
+ statusBars.put("health", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/health"),
+ new Color[]{new Color(255, 0, 0), new Color(255, 220, 0)},
+ true, new Color(255, 85, 85), Text.translatable("skyblocker.bars.config.health")));
+ statusBars.put("intelligence", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/intelligence"),
+ new Color[]{new Color(0, 255, 255), new Color(180, 0, 255)},
+ true, new Color(85, 255, 255), Text.translatable("skyblocker.bars.config.intelligence")));
+ statusBars.put("defense", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/defense"),
+ new Color[]{new Color(255, 255, 255)},
+ false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.defense")));
+ statusBars.put("experience", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/experience"),
+ new Color[]{new Color(100, 230, 70)},
+ false, new Color(128, 255, 32), Text.translatable("skyblocker.bars.config.experience")));
+
+ // Fetch from old status bar config
+ int[] counts = new int[3]; // counts for RIGHT, LAYER1, LAYER2
+ StatusBar health = statusBars.get("health");
+ SkyblockerConfig.OldBarPositions barPositions = SkyblockerConfigManager.get().general.bars.barPositions;
+ loadOldBarPosition(health, counts, barPositions.healthBarPosition);
+ StatusBar intelligence = statusBars.get("intelligence");
+ loadOldBarPosition(intelligence, counts, barPositions.manaBarPosition);
+ StatusBar defense = statusBars.get("defense");
+ loadOldBarPosition(defense, counts, barPositions.defenceBarPosition);
+ StatusBar experience = statusBars.get("experience");
+ loadOldBarPosition(experience, counts, barPositions.experienceBarPosition);
+
+ CompletableFuture.supplyAsync(FancyStatusBars::loadBarConfig).thenAccept(object -> {
+ if (object != null) {
+ for (String s : object.keySet()) {
+ if (statusBars.containsKey(s)) {
+ try {
+ statusBars.get(s).loadFromJson(object.get(s).getAsJsonObject());
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load {} status bar", s, e);
+ }
+ } else {
+ LOGGER.warn("[Skyblocker] Unknown status bar: {}", s);
+ }
+ }
+ }
+ placeBarsInPositioner();
+ configLoaded = true;
+ }).exceptionally(throwable -> {
+ LOGGER.error("[Skyblocker] Failed reading status bars config", throwable);
+ return null;
+ });
+ ClientLifecycleEvents.CLIENT_STOPPING.register((client) -> {
+ saveBarConfig();
+ GLFW.glfwDestroyCursor(StatusBarsConfigScreen.RESIZE_CURSOR);
+ });
+
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(
+ ClientCommandManager.literal(SkyblockerMod.NAMESPACE)
+ .then(ClientCommandManager.literal("bars").executes(Scheduler.queueOpenScreenCommand(StatusBarsConfigScreen::new)))));
+ }
+
+ /**
+ * Loads the bar position from the old config
+ * @param bar the bar to load the position for
+ * @param counts the counts for each bar position (LAYER1, LAYER2, RIGHT)
+ * @param position the position to load
+ */
+ private static void loadOldBarPosition(StatusBar bar, int[] counts, SkyblockerConfig.OldBarPosition position) {
+ switch (position) {
+ case RIGHT:
+ bar.anchor = BarPositioner.BarAnchor.HOTBAR_RIGHT;
+ bar.gridY = 0;
+ bar.gridX = counts[position.ordinal()]++;
+ break;
+ case LAYER1:
+ bar.anchor = BarPositioner.BarAnchor.HOTBAR_TOP;
+ bar.gridY = 0;
+ bar.gridX = counts[position.ordinal()]++;
+ break;
+ case LAYER2:
+ bar.anchor = BarPositioner.BarAnchor.HOTBAR_TOP;
+ bar.gridY = 1;
+ bar.gridX = counts[position.ordinal()]++;
+ break;
+ }
+ }
+
+ private static boolean configLoaded = false;
+
+ private static void placeBarsInPositioner() {
+ List<StatusBar> original = statusBars.values().stream().toList();
+
+ for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) {
+ List<StatusBar> barList = new ArrayList<>(original.stream().filter(bar -> bar.anchor == barAnchor).toList());
+ if (barList.isEmpty()) continue;
+ barList.sort((a, b) -> a.gridY == b.gridY ? Integer.compare(a.gridX, b.gridX) : Integer.compare(a.gridY, b.gridY));
+
+ int y = -1;
+ int rowNum = -1;
+ for (StatusBar statusBar : barList) {
+ if (statusBar.gridY > y) {
+ barPositioner.addRow(barAnchor);
+ rowNum++;
+ y = statusBar.gridY;
+ }
+ barPositioner.addBar(barAnchor, rowNum, statusBar);
+ }
+ }
+ }
+
+ public static JsonObject loadBarConfig() {
+ try (BufferedReader reader = Files.newBufferedReader(FILE)) {
+ return SkyblockerMod.GSON.fromJson(reader, JsonObject.class);
+ } catch (NoSuchFileException e) {
+ LOGGER.warn("[Skyblocker] No status bar config file found, using defaults");
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load status bars config", e);
+ }
+ return null;
+ }
+
+ public static void saveBarConfig() {
+ JsonObject output = new JsonObject();
+ statusBars.forEach((s, statusBar) -> output.add(s, statusBar.toJson()));
+ try (BufferedWriter writer = Files.newBufferedWriter(FILE)) {
+ SkyblockerMod.GSON.toJson(output, writer);
+ LOGGER.info("[Skyblocker] Saved status bars config");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to save status bars config", e);
+ }
+ }
+
+ public static void updatePositions() {
+ if (!configLoaded) return;
+ final int width = MinecraftClient.getInstance().getWindow().getScaledWidth();
+ final int height = MinecraftClient.getInstance().getWindow().getScaledHeight();
+
+ for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) {
+ ScreenPos anchorPosition = barAnchor.getAnchorPosition(width, height);
+ BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule();
+
+ if (sizeRule.isTargetSize()) {
+ for (int row = 0; row < barPositioner.getRowCount(barAnchor); row++) {
+ LinkedList<StatusBar> barRow = barPositioner.getRow(barAnchor, row);
+ if (barRow.isEmpty()) continue;
+
+ // FIX SIZES
+ int totalSize = 0;
+ for (StatusBar statusBar : barRow)
+ totalSize += (statusBar.size = MathHelper.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize()));
+
+ whileLoop:
+ while (totalSize != sizeRule.targetSize()) {
+ if (totalSize > sizeRule.targetSize()) {
+ for (StatusBar statusBar : barRow) {
+ if (statusBar.size > sizeRule.minSize()) {
+ statusBar.size--;
+ totalSize--;
+ if (totalSize == sizeRule.targetSize()) break whileLoop;
+ }
+ }
+ } else {
+ for (StatusBar statusBar : barRow) {
+ if (statusBar.size < sizeRule.maxSize()) {
+ statusBar.size++;
+ totalSize++;
+ if (totalSize == sizeRule.targetSize()) break whileLoop;
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ for (int row = 0; row < barPositioner.getRowCount(barAnchor); row++) {
+ List<StatusBar> barRow = barPositioner.getRow(barAnchor, row);
+ if (barRow.isEmpty()) continue;
+
+
+ // Update the positions
+ float widthPerSize;
+ if (sizeRule.isTargetSize())
+ widthPerSize = (float) sizeRule.totalWidth() / sizeRule.targetSize();
+ else
+ widthPerSize = sizeRule.widthPerSize();
+
+ int currSize = 0;
+ int rowSize = barRow.size();
+ for (int i = 0; i < rowSize; i++) {
+ // A bit of a padding
+ int offsetX = 0;
+ int lessWidth = 0;
+ if (rowSize > 1) { // Technically bars in the middle of 3+ bars will be smaller than the 2 side ones but shh
+ if (i == 0) lessWidth = 1;
+ else if (i == rowSize - 1) {
+ lessWidth = 1;
+ offsetX = 1;
+ } else {
+ lessWidth = 2;
+ offsetX = 1;
+ }
+ }
+ StatusBar statusBar = barRow.get(i);
+ statusBar.size = MathHelper.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize());
+
+ float x = barAnchor.isRight() ?
+ anchorPosition.x() + currSize * widthPerSize :
+ anchorPosition.x() - currSize * widthPerSize - statusBar.size * widthPerSize;
+ statusBar.setX(MathHelper.ceil(x) + offsetX);
+
+ int y = barAnchor.isUp() ?
+ anchorPosition.y() - (row + 1) * (statusBar.getHeight() + 1) :
+ anchorPosition.y() + row * (statusBar.getHeight() + 1);
+ statusBar.setY(y);
+
+ statusBar.setWidth(MathHelper.floor(statusBar.size * widthPerSize) - lessWidth);
+ currSize += statusBar.size;
+ statusBar.gridX = i;
+ statusBar.gridY = row;
+
+ }
+ }
+
+ }
+ }
+
+ public boolean render(DrawContext context, int scaledWidth, int scaledHeight) {
+ var player = client.player;
+ if (!SkyblockerConfigManager.get().general.bars.enableBars || player == null || Utils.isInTheRift())
+ return false;
+
+ Collection<StatusBar> barCollection = statusBars.values();
+ for (StatusBar value : barCollection) {
+ value.render(context, -1, -1, client.getLastFrameDuration());
+ }
+ for (StatusBar statusBar : barCollection) {
+ if (statusBar.showText()) statusBar.renderText(context);
+ }
+ StatusBarTracker.Resource health = statusBarTracker.getHealth();
+ statusBars.get("health").updateValues(health.value() / (float) health.max(), health.overflow() / (float) health.max(), health.value());
+
+ StatusBarTracker.Resource intelligence = statusBarTracker.getMana();
+ statusBars.get("intelligence").updateValues(intelligence.value() / (float) intelligence.max(), intelligence.overflow() / (float) intelligence.max(), intelligence.value());
+ int defense = statusBarTracker.getDefense();
+ statusBars.get("defense").updateValues(defense / (defense + 100.f), 0, defense);
+ statusBars.get("experience").updateValues(player.experienceProgress, 0, player.experienceLevel);
+ return true;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java
new file mode 100644
index 00000000..f8109852
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java
@@ -0,0 +1,353 @@
+package de.hysky.skyblocker.skyblock.fancybars;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.*;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.widget.ClickableWidget;
+import net.minecraft.client.gui.widget.Widget;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+import java.util.function.Consumer;
+
+public class StatusBar implements Widget, Drawable, Element, Selectable {
+
+ private static final Identifier BAR_FILL = new Identifier(SkyblockerMod.NAMESPACE, "bars/bar_fill");
+ private static final Identifier BAR_BACK = new Identifier(SkyblockerMod.NAMESPACE, "bars/bar_back");
+
+
+ /* public static final Codec<StatusBar> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.INT.fieldOf("size").forGetter(bar -> bar.size),
+ Codec.INT.fieldOf("x").forGetter(bar -> bar.gridX),
+ Codec.INT.fieldOf("y").forGetter(bar -> bar.gridY),
+ Codec.STRING.listOf().fieldOf("colors").xmap(
+ strings -> strings.stream().map(s -> Integer.parseInt(s, 16)).map(Color::new).toArray(Color[]::new),
+ colors -> Arrays.stream(colors).map(color -> Integer.toHexString(color.getRGB())).toList())
+ .forGetter(StatusBar::getColors),
+ Codec.STRING.optionalFieldOf("text_color").xmap(
+ s -> {
+ if (s.isPresent()) {
+ return Optional.of(new Color(Integer.parseInt(s.get(), 16)));
+ } else return Optional.empty();
+ },
+ o -> o.map(object -> Integer.toHexString(((Color) object).getRGB())))
+ .forGetter(bar -> {
+ if (bar.getTextColor() != null) {
+ return Optional.of(bar.getTextColor());
+ } else return Optional.empty();
+ }),
+ Codec.BOOL.optionalFieldOf("show_text", true).forGetter(StatusBar::showText),
+ Codec.STRING.fieldOf("icon_position").xmap(
+ IconPosition::valueOf,
+ Enum::toString
+ ).forGetter(bar -> bar.iconPosition)
+ )
+
+ .apply(instance, ));*/
+
+ private final Identifier icon;
+
+ public Color[] getColors() {
+ return colors;
+ }
+
+ public boolean hasOverflow() {
+ return hasOverflow;
+ }
+
+ public @Nullable Color getTextColor() {
+ return textColor;
+ }
+
+ private Color[] colors;
+ private final boolean hasOverflow;
+
+ public void setTextColor(@Nullable Color textColor) {
+ this.textColor = textColor;
+ }
+
+ private @Nullable Color textColor;
+
+ public Text getName() {
+ return name;
+ }
+
+ private final Text name;
+
+ private @Nullable OnClick onClick = null;
+ public int gridX = 0;
+ public int gridY = 0;
+ public @Nullable BarPositioner.BarAnchor anchor = null;
+
+ public int size = 1;
+ private int width = 0;
+
+ public float fill = 0;
+ public float overflowFill = 0;
+ public boolean ghost = false;
+
+ private Object value = "";
+
+ private int x = 0;
+ private int y = 0;
+
+ private IconPosition iconPosition = IconPosition.LEFT;
+ private boolean showText = true;
+
+ public StatusBar(Identifier icon, Color[] colors, boolean hasOverflow, @Nullable Color textColor, Text name) {
+ this.icon = icon;
+ this.colors = colors;
+ this.hasOverflow = hasOverflow;
+ this.textColor = textColor;
+ this.name = name;
+ }
+
+ public StatusBar(Identifier icon, Color[] colors, boolean hasOverflow, @Nullable Color textColor) {
+ this(icon, colors, hasOverflow, textColor, Text.empty());
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (width <= 0) return;
+ // half works lol. only puts transparency on the filler of the bar
+ if (ghost) context.setShaderColor(1f, 1f, 1f, 0.25f);
+ switch (iconPosition) {
+ case LEFT -> context.drawGuiTexture(icon, x, y, 9, 9);
+ case RIGHT -> context.drawGuiTexture(icon, x + width - 9, y, 9, 9);
+ }
+
+ int barWith = iconPosition.equals(IconPosition.OFF) ? width : width - 10;
+ int barX = iconPosition.equals(IconPosition.LEFT) ? x + 10 : x;
+ context.drawGuiTexture(BAR_BACK, barX, y + 1, barWith, 7);
+ RenderHelper.renderNineSliceColored(context, BAR_FILL, barX + 1, y + 2, (int) ((barWith - 2) * fill), 5, colors[0]);
+
+
+ if (hasOverflow && overflowFill > 0) {
+ RenderHelper.renderNineSliceColored(context, BAR_FILL, barX + 1, y + 2, (int) ((barWith - 2) * overflowFill), 5, colors[1]);
+ }
+ if (ghost) context.setShaderColor(1f, 1f, 1f, 1f);
+ //context.drawText(MinecraftClient.getInstance().textRenderer, gridX + " " + gridY + " s:" + size , x, y-9, Colors.WHITE, true);
+ }
+
+ public void updateValues(float fill, float overflowFill, Object text) {
+ this.value = text;
+ this.fill = fill;
+ this.overflowFill = overflowFill;
+ }
+
+ public void renderText(DrawContext context) {
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ int barWith = iconPosition.equals(IconPosition.OFF) ? width : width - 10;
+ int barX = iconPosition.equals(IconPosition.LEFT) ? x + 11 : x;
+ String text = this.value.toString();
+ int x = barX + (barWith - textRenderer.getWidth(text)) / 2;
+ int y = this.y - 3;
+
+ final int[] offsets = new int[]{-1, 1};
+ for (int i : offsets) {
+ context.drawText(textRenderer, text, x + i, y, 0, false);
+ context.drawText(textRenderer, text, x, y + i, 0, false);
+ }
+ context.drawText(textRenderer, text, x, y, (textColor == null ? colors[0] : textColor).getRGB(), false);
+ }
+
+ public void renderCursor(DrawContext context, int mouseX, int mouseY, float delta) {
+ int temp_x = x;
+ int temp_y = y;
+ int temp_width = width;
+ boolean temp_ghost = ghost;
+
+ x = mouseX;
+ y = mouseY;
+ width = 100;
+ ghost = false;
+
+ render(context, mouseX, mouseY, delta);
+
+ x = temp_x;
+ y = temp_y;
+ width = temp_width;
+ ghost = temp_ghost;
+ }
+
+ // GUI shenanigans
+
+ @Override
+ public void setX(int x) {
+ this.x = x;
+ }
+
+ @Override
+ public void setY(int y) {
+ this.y = y;
+ }
+
+ @Override
+ public int getX() {
+ return x;
+ }
+
+ @Override
+ public int getY() {
+ return y;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ @Override
+ public int getHeight() {
+ return 9;
+ }
+
+ @Override
+ public ScreenRect getNavigationFocus() {
+ return Widget.super.getNavigationFocus();
+ }
+
+ @Override
+ public boolean isMouseOver(double mouseX, double mouseY) {
+ return mouseX >= x && mouseX <= x + getWidth() && mouseY >= y && mouseY <= y + getHeight();
+ }
+
+ @Override
+ public void forEachChild(Consumer<ClickableWidget> consumer) {
+ }
+
+ @Override
+ public void setFocused(boolean focused) {
+ }
+
+ @Override
+ public boolean isFocused() {
+ return false;
+ }
+
+ @Override
+ public SelectionType getType() {
+ return SelectionType.NONE;
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (!isMouseOver(mouseX, mouseY)) return false;
+ if (onClick != null) {
+ onClick.onClick(this, button, (int) mouseX, (int) mouseY);
+ }
+ return true;
+ }
+
+ public void setOnClick(@Nullable OnClick onClick) {
+ this.onClick = onClick;
+ }
+
+ @Override
+ public void appendNarrations(NarrationMessageBuilder builder) {
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this)
+ .append("name", name)
+ .append("gridX", gridX)
+ .append("gridY", gridY)
+ .append("size", size)
+ .append("x", x)
+ .append("y", y)
+ .append("width", width)
+ .append("anchor", anchor)
+ .toString();
+ }
+
+ public IconPosition getIconPosition() {
+ return iconPosition;
+ }
+
+ public void setIconPosition(IconPosition iconPosition) {
+ this.iconPosition = iconPosition;
+ }
+
+ public boolean showText() {
+ return showText;
+ }
+
+ public void setShowText(boolean showText) {
+ this.showText = showText;
+ }
+
+ public enum IconPosition {
+ LEFT,
+ RIGHT,
+ OFF
+ }
+
+ @FunctionalInterface
+ public interface OnClick {
+
+ void onClick(StatusBar statusBar, int button, int mouseX, int mouseY);
+ }
+
+ public void loadFromJson(JsonObject object) {
+ // Make colors optional, so it's easy to reset to default
+ if (object.has("colors")) {
+ JsonArray colors1 = object.get("colors").getAsJsonArray();
+ if (colors1.size() < 2 && hasOverflow) {
+ throw new IllegalStateException("Missing second color of bar that has overflow");
+ }
+ Color[] newColors = new Color[colors1.size()];
+ for (int i = 0; i < colors1.size(); i++) {
+ JsonElement jsonElement = colors1.get(i);
+ newColors[i] = new Color(Integer.parseInt(jsonElement.getAsString(), 16));
+ }
+ this.colors = newColors;
+ }
+
+ if (object.has("text_color")) this.textColor = new Color(Integer.parseInt(object.get("text_color").getAsString(), 16));
+
+ String maybeAnchor = object.get("anchor").getAsString().trim();
+ this.anchor = maybeAnchor.equals("null") ? null : BarPositioner.BarAnchor.valueOf(maybeAnchor);
+ this.size = object.get("size").getAsInt();
+ this.gridX = object.get("x").getAsInt();
+ this.gridY = object.get("y").getAsInt();
+ // these are optional too, why not
+ if (object.has("icon_position")) this.iconPosition = IconPosition.valueOf(object.get("icon_position").getAsString().trim());
+ if (object.has("show_text")) this.showText = object.get("show_text").getAsBoolean();
+
+ }
+
+ public JsonObject toJson() {
+ JsonObject object = new JsonObject();
+ JsonArray colors1 = new JsonArray();
+ for (Color color : colors) {
+ colors1.add(Integer.toHexString(color.getRGB()).substring(2));
+ }
+ object.add("colors", colors1);
+ if (textColor != null) {
+ object.addProperty("text_color", Integer.toHexString(textColor.getRGB()).substring(2));
+ }
+ object.addProperty("size", size);
+ if (anchor != null) {
+ object.addProperty("anchor", anchor.toString());
+ } else object.addProperty("anchor", "null");
+ object.addProperty("x", gridX);
+ object.addProperty("y", gridY);
+ object.addProperty("icon_position", iconPosition.toString());
+ object.addProperty("show_text", showText);
+ return object;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java
new file mode 100644
index 00000000..d1009a25
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java
@@ -0,0 +1,365 @@
+package de.hysky.skyblocker.skyblock.fancybars;
+
+import it.unimi.dsi.fastutil.Pair;
+import it.unimi.dsi.fastutil.objects.ObjectBooleanMutablePair;
+import it.unimi.dsi.fastutil.objects.ObjectObjectMutablePair;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.ScreenPos;
+import net.minecraft.client.gui.ScreenRect;
+import net.minecraft.client.gui.navigation.NavigationAxis;
+import net.minecraft.client.gui.navigation.NavigationDirection;
+import net.minecraft.client.gui.screen.PopupScreen;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.util.Window;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.glfw.GLFW;
+import de.hysky.skyblocker.skyblock.fancybars.BarPositioner.BarLocation;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class StatusBarsConfigScreen extends Screen {
+ private static final Identifier HOTBAR_TEXTURE = new Identifier("hud/hotbar");
+
+ public static final long RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_HRESIZE_CURSOR);
+
+ private final Map<ScreenRect, BarLocation> rectToBarLocation = new HashMap<>();
+ private static final int HOTBAR_WIDTH = 182;
+
+ private @Nullable StatusBar cursorBar = null;
+
+ public StatusBarsConfigScreen() {
+ super(Text.of("Status Bars Config"));
+ FancyStatusBars.updatePositions();
+ }
+
+ private BarLocation currentInsertLocation = new BarLocation(null, 0, 0);
+
+ private final Pair<BarLocation, Boolean> resizeHover = new ObjectBooleanMutablePair<>(BarLocation.NULL, false);
+
+ private final Pair<BarLocation, BarLocation> resizedBars = ObjectObjectMutablePair.of(BarLocation.NULL, BarLocation.NULL);
+ private boolean resizing = false;
+
+ private EditBarWidget editBarWidget;
+
+ // prioritize left and right cuz they are much smaller space than up and down
+ private static final NavigationDirection[] DIRECTION_CHECK_ORDER = new NavigationDirection[]{NavigationDirection.LEFT, NavigationDirection.RIGHT, NavigationDirection.UP, NavigationDirection.DOWN};
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ /*for (ScreenRect screenRect : meaningFullName.keySet()) {
+ context.fillGradient(screenRect.position().x(), screenRect.position().y(), screenRect.position().x() + screenRect.width(), screenRect.position().y() + screenRect.height(), 0xFFFF0000, 0xFF0000FF);
+ }*/
+ super.render(context, mouseX, mouseY, delta);
+ context.drawGuiTexture(HOTBAR_TEXTURE, width / 2 - HOTBAR_WIDTH / 2, height - 22, HOTBAR_WIDTH, 22);
+ editBarWidget.render(context, mouseX, mouseY, delta);
+
+ ScreenRect mouseRect = new ScreenRect(new ScreenPos(mouseX - 1, mouseY - 1), 3, 3);
+ assert client != null;
+ Window window = client.getWindow();
+
+ if (cursorBar != null) {
+ cursorBar.renderCursor(context, mouseX, mouseY, delta);
+ boolean inserted = false;
+ rectLoop:
+ for (ScreenRect screenRect : rectToBarLocation.keySet()) {
+ for (NavigationDirection direction : DIRECTION_CHECK_ORDER) {
+ boolean overlaps = screenRect.getBorder(direction).add(direction).overlaps(mouseRect);
+
+ if (overlaps) {
+ BarLocation barSnap = rectToBarLocation.get(screenRect);
+ if (barSnap.barAnchor() == null) break;
+ if (direction.getAxis().equals(NavigationAxis.VERTICAL)) {
+ int neighborInsertY = getNeighborInsertY(barSnap, !direction.isPositive());
+ if (!currentInsertLocation.equals(barSnap.barAnchor(), barSnap.x(), neighborInsertY)) {
+ if (cursorBar.anchor != null)
+ FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar);
+ FancyStatusBars.barPositioner.addRow(barSnap.barAnchor(), neighborInsertY);
+ FancyStatusBars.barPositioner.addBar(barSnap.barAnchor(), neighborInsertY, cursorBar);
+ currentInsertLocation = BarLocation.of(cursorBar);
+ inserted = true;
+ }
+ } else {
+ int neighborInsertX = getNeighborInsertX(barSnap, direction.isPositive());
+ if (!currentInsertLocation.equals(barSnap.barAnchor(), neighborInsertX, barSnap.y())) {
+ if (cursorBar.anchor != null)
+ FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar);
+ FancyStatusBars.barPositioner.addBar(barSnap.barAnchor(), barSnap.y(), neighborInsertX, cursorBar);
+ currentInsertLocation = BarLocation.of(cursorBar);
+ inserted = true;
+ }
+ }
+ break rectLoop;
+ }
+ }
+ }
+ if (inserted) {
+ FancyStatusBars.updatePositions();
+ return;
+ }
+ // check for hovering empty anchors
+ for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) {
+ if (FancyStatusBars.barPositioner.getRowCount(barAnchor) != 0) continue;
+ ScreenRect anchorHitbox = barAnchor.getAnchorHitbox(barAnchor.getAnchorPosition(width, height));
+ context.fill(anchorHitbox.getLeft(), anchorHitbox.getTop(), anchorHitbox.getRight(), anchorHitbox.getBottom(), 0x99FFFFFF);
+ if (anchorHitbox.overlaps(mouseRect) && currentInsertLocation.barAnchor() != barAnchor) {
+ if (cursorBar.anchor != null)
+ FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar);
+ FancyStatusBars.barPositioner.addRow(barAnchor);
+ FancyStatusBars.barPositioner.addBar(barAnchor, 0, cursorBar);
+ currentInsertLocation = BarLocation.of(cursorBar);
+ FancyStatusBars.updatePositions();
+ }
+ }
+ } else {
+ if (resizing) { // actively resizing one or 2 bars
+ int middleX;
+
+ BarLocation left = resizedBars.left();
+ BarLocation right = resizedBars.right();
+ boolean hasRight = right.barAnchor() != null;
+ boolean hasLeft = left.barAnchor() != null;
+ BarPositioner.BarAnchor barAnchor;
+ if (!hasRight) {
+ barAnchor = left.barAnchor();
+ StatusBar bar = FancyStatusBars.barPositioner.getBar(barAnchor, left.y(), left.x());
+ middleX = bar.getX() + bar.getWidth();
+ } else {
+ barAnchor = right.barAnchor();
+ middleX = FancyStatusBars.barPositioner.getBar(barAnchor, right.y(), right.x()).getX();
+ }
+
+ boolean doResize = true;
+ StatusBar rightBar = null;
+ StatusBar leftBar = null;
+
+ BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule();
+
+ float widthPerSize;
+ if (sizeRule.isTargetSize())
+ widthPerSize = (float) sizeRule.totalWidth() / sizeRule.targetSize();
+ else
+ widthPerSize = sizeRule.widthPerSize();
+
+ // resize towards the left
+ if (mouseX < middleX) {
+ if (middleX - mouseX > widthPerSize / .75f) {
+ if (hasRight) {
+ rightBar = FancyStatusBars.barPositioner.getBar(barAnchor, right.y(), right.x());
+ if (rightBar.size + 1 > sizeRule.maxSize()) doResize = false;
+ }
+ if (hasLeft) {
+ leftBar = FancyStatusBars.barPositioner.getBar(barAnchor, left.y(), left.x());
+ if (leftBar.size - 1 < sizeRule.minSize()) doResize = false;
+ }
+
+ if (doResize) {
+ if (hasRight) rightBar.size++;
+ if (hasLeft) leftBar.size--;
+ FancyStatusBars.updatePositions();
+ }
+ }
+ } else { // towards the right
+ if (mouseX - middleX > widthPerSize / .75f) {
+ if (hasRight) {
+ rightBar = FancyStatusBars.barPositioner.getBar(barAnchor, right.y(), right.x());
+ if (rightBar.size - 1 < sizeRule.minSize()) doResize = false;
+ }
+ if (hasLeft) {
+ leftBar = FancyStatusBars.barPositioner.getBar(barAnchor, left.y(), left.x());
+ if (leftBar.size + 1 > sizeRule.maxSize()) doResize = false;
+ }
+
+ if (doResize) {
+ if (hasRight) rightBar.size--;
+ if (hasLeft) leftBar.size++;
+ FancyStatusBars.updatePositions();
+ }
+ }
+ }
+
+ } else { // hovering bars
+ rectLoop:
+ for (ScreenRect screenRect : rectToBarLocation.keySet()) {
+ for (NavigationDirection direction : new NavigationDirection[]{NavigationDirection.LEFT, NavigationDirection.RIGHT}) {
+ boolean overlaps = screenRect.getBorder(direction).add(direction).overlaps(mouseRect);
+
+ if (overlaps && !editBarWidget.isMouseOver(mouseX, mouseY)) {
+ BarLocation barLocation = rectToBarLocation.get(screenRect);
+ if (barLocation.barAnchor() == null) break;
+ boolean right = direction.equals(NavigationDirection.RIGHT);
+ // can't resize on the edge of a target size row!
+ if (barLocation.barAnchor().getSizeRule().isTargetSize() && !FancyStatusBars.barPositioner.hasNeighbor(barLocation.barAnchor(), barLocation.y(), barLocation.x(), right)) {
+ break;
+ }
+ if (!barLocation.barAnchor().getSizeRule().isTargetSize() && barLocation.x() == 0 && barLocation.barAnchor().isRight() != right)
+ break;
+ resizeHover.first(barLocation);
+ resizeHover.right(right);
+ GLFW.glfwSetCursor(window.getHandle(), RESIZE_CURSOR);
+ break rectLoop;
+ } else {
+ resizeHover.first(BarLocation.NULL);
+ GLFW.glfwSetCursor(window.getHandle(), 0);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static int getNeighborInsertX(BarLocation barLocation, boolean right) {
+ BarPositioner.BarAnchor barAnchor = barLocation.barAnchor();
+ int gridX = barLocation.x();
+ if (barAnchor == null) return 0;
+ if (right) {
+ return barAnchor.isRight() ? gridX + 1 : gridX;
+ } else {
+ return barAnchor.isRight() ? gridX : gridX + 1;
+ }
+ }
+
+ private static int getNeighborInsertY(BarLocation barLocation, boolean up) {
+ BarPositioner.BarAnchor barAnchor = barLocation.barAnchor();
+ int gridY = barLocation.y();
+ if (barAnchor == null) return 0;
+ if (up) {
+ return barAnchor.isUp() ? gridY + 1 : gridY;
+ } else {
+ return barAnchor.isUp() ? gridY : gridY + 1;
+ }
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ editBarWidget = new EditBarWidget(0, 0, this);
+ editBarWidget.visible = false;
+ addSelectableChild(editBarWidget); // rendering separately to have it above hotbar
+ Collection<StatusBar> values = FancyStatusBars.statusBars.values();
+ values.forEach(this::setup);
+ checkNullAnchor(values);
+ updateScreenRects();
+ this.addDrawableChild(ButtonWidget.builder(Text.literal("?"),
+ button -> {
+ assert client != null;
+ client.setScreen(new PopupScreen.Builder(this, Text.translatable("skyblocker.bars.config.explanationTitle"))
+ .button(Text.translatable("gui.ok"), PopupScreen::close)
+ .message(Text.translatable("skyblocker.bars.config.explanation"))
+ .build());
+ })
+ .dimensions(width - 20, (height - 15) / 2, 15, 15)
+ .build());
+ }
+
+ private void setup(StatusBar statusBar) {
+ this.addDrawableChild(statusBar);
+ statusBar.setOnClick(this::onBarClick);
+ }
+
+ private static void checkNullAnchor(Iterable<StatusBar> bars) {
+ int offset = 0;
+ for (StatusBar statusBar : bars) {
+ if (statusBar.anchor == null) {
+ statusBar.setX(5);
+ statusBar.setY(50 + offset);
+ statusBar.setWidth(30);
+ offset += statusBar.getHeight();
+ }
+ }
+ }
+
+ @Override
+ public void removed() {
+ super.removed();
+ FancyStatusBars.statusBars.values().forEach(statusBar -> statusBar.setOnClick(null));
+ if (cursorBar != null) cursorBar.ghost = false;
+ FancyStatusBars.updatePositions();
+ assert client != null;
+ GLFW.glfwSetCursor(client.getWindow().getHandle(), 0);
+ FancyStatusBars.saveBarConfig();
+ }
+
+ @Override
+ public boolean shouldPause() {
+ return false;
+ }
+
+ private void onBarClick(StatusBar statusBar, int button, int mouseX, int mouseY) {
+ if (button == 0) {
+ cursorBar = statusBar;
+ cursorBar.ghost = true;
+ if (statusBar.anchor != null)
+ FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar);
+ FancyStatusBars.updatePositions();
+ cursorBar.setX(width + 5); // send it to limbo lol
+ updateScreenRects();
+ } else if (button == 1) {
+ int x = Math.min(mouseX - 1, width - editBarWidget.getWidth());
+ int y = Math.min(mouseY - 1, height - editBarWidget.getHeight());
+ editBarWidget.visible = true;
+ editBarWidget.setStatusBar(statusBar);
+ editBarWidget.setX(x);
+ editBarWidget.setY(y);
+ }
+ }
+
+ private void updateScreenRects() {
+ rectToBarLocation.clear();
+ FancyStatusBars.statusBars.values().forEach(statusBar1 -> {
+ if (statusBar1.anchor == null) return;
+ rectToBarLocation.put(
+ new ScreenRect(new ScreenPos(statusBar1.getX(), statusBar1.getY()), statusBar1.getWidth(), statusBar1.getHeight()),
+ BarLocation.of(statusBar1));
+ });
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ if (cursorBar != null) {
+ cursorBar.ghost = false;
+ cursorBar = null;
+ FancyStatusBars.updatePositions();
+ checkNullAnchor(FancyStatusBars.statusBars.values());
+ updateScreenRects();
+ return true;
+ } else if (resizing) {
+ resizing = false;
+ resizedBars.left(BarLocation.NULL);
+ resizedBars.right(BarLocation.NULL);
+ updateScreenRects();
+ return true;
+ }
+ return super.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ BarLocation first = resizeHover.first();
+ // want the right click thing to have priority
+ if (!editBarWidget.isMouseOver(mouseX, mouseY) && button == 0 && !first.equals(BarLocation.NULL)) {
+ BarPositioner.BarAnchor barAnchor = first.barAnchor();
+ assert barAnchor != null;
+ if (resizeHover.right()) {
+ resizedBars.left(first);
+
+ if (FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.y(), first.x(), true)) {
+ resizedBars.right(new BarLocation(barAnchor, first.x() + (barAnchor.isRight() ? 1 : -1), first.y()));
+ } else resizedBars.right(BarLocation.NULL);
+ } else {
+ resizedBars.right(first);
+
+ if (FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.y(), first.x(), false)) {
+ resizedBars.left(new BarLocation(barAnchor, first.x() + (barAnchor.isRight() ? -1 : 1), first.y()));
+ } else resizedBars.left(BarLocation.NULL);
+ }
+ resizing = true;
+ return true;
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+}