From a9cdc7537886b5edb70df9457e53a772b4d85842 Mon Sep 17 00:00:00 2001 From: akar1881 Date: Thu, 9 Apr 2026 15:10:55 +0300 Subject: [PATCH] new fancy status bars --- .../fancybars/BorderRadiusDialog.java | 137 ++ .../skyblock/fancybars/EditBarWidget.java | 626 +++++---- .../skyblock/fancybars/FancyStatusBars.java | 796 +++++++----- .../skyblock/fancybars/StatusBar.java | 1068 ++++++++------- .../fancybars/StatusBarsConfigScreen.java | 1143 ++++++++++++----- .../skyblock/fancybars/TipsScreen.java | 152 +++ .../assets/skyblocker/lang/en_us.json | 13 + 7 files changed, 2511 insertions(+), 1424 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/fancybars/BorderRadiusDialog.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/fancybars/TipsScreen.java diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BorderRadiusDialog.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BorderRadiusDialog.java new file mode 100644 index 00000000000..d2096607fda --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BorderRadiusDialog.java @@ -0,0 +1,137 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import de.hysky.skyblocker.utils.render.gui.FilteredEditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; + +import java.util.function.IntConsumer; + +/** + * A small dialog that lets the user type a border-radius value (0–20 px). + * Opened from the right-click edit panel on any status bar. + */ +public class BorderRadiusDialog extends Screen { + + private static final int MAX_RADIUS = 20; + + private final Screen parent; + private final int currentRadius; + private final IntConsumer onConfirm; + + private FilteredEditBox inputBox; + private Button confirmButton; + private Component errorMsg = Component.empty(); + + public BorderRadiusDialog(Screen parent, int currentRadius, IntConsumer onConfirm) { + super(Component.translatable("skyblocker.bars.config.borderRadius.dialog.title")); + this.parent = parent; + this.currentRadius = currentRadius; + this.onConfirm = onConfirm; + } + + @Override + protected void init() { + int cx = width / 2; + int cy = height / 2; + + inputBox = new FilteredEditBox(font, cx - 50, cy - 10, 100, 20, + Component.translatable("skyblocker.bars.config.borderRadius")); + inputBox.setMaxLength(3); + inputBox.setValue(String.valueOf(currentRadius)); + inputBox.setFilter(s -> s.isEmpty() || s.matches("\\d{0,3}")); + inputBox.setResponder(text -> { + errorMsg = validate(text) == -1 + ? Component.translatable("skyblocker.bars.config.borderRadius.dialog.error", MAX_RADIUS) + : Component.empty(); + if (confirmButton != null) confirmButton.active = (validate(text) != -1); + }); + addRenderableWidget(inputBox); + + confirmButton = addRenderableWidget(Button.builder( + Component.translatable("skyblocker.bars.config.borderRadius.dialog.confirm"), + _ -> confirm()) + .bounds(cx - 52, cy + 16, 50, 16) + .build()); + + addRenderableWidget(Button.builder( + Component.translatable("skyblocker.bars.config.borderRadius.dialog.cancel"), + _ -> minecraft.setScreen(parent)) + .bounds(cx + 2, cy + 16, 50, 16) + .build()); + + setFocused(inputBox); + } + + /** Returns the clamped value, or -1 if the text is invalid. */ + private int validate(String text) { + if (text == null || text.isBlank()) return 0; + try { + int v = Integer.parseInt(text.trim()); + if (v < 0 || v > MAX_RADIUS) return -1; + return v; + } catch (NumberFormatException _) { + return -1; + } + } + + private void confirm() { + int v = validate(inputBox.getValue()); + if (v == -1) return; + onConfirm.accept(v); + minecraft.setScreen(parent); + } + + @Override + public boolean keyPressed(KeyEvent keyEvent) { + int key = keyEvent.key(); + if (key == GLFW.GLFW_KEY_ENTER || key == GLFW.GLFW_KEY_KP_ENTER) { + confirm(); + return true; + } + if (key == GLFW.GLFW_KEY_ESCAPE) { + minecraft.setScreen(parent); + return true; + } + return super.keyPressed(keyEvent); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + extractTransparentBackground(context); + + int cx = width / 2; + int cy = height / 2; + + // Draw a simple background panel + int panelW = 160, panelH = 80; + context.fill(cx - panelW / 2, cy - panelH / 2, cx + panelW / 2, cy + panelH / 2, 0xCC000000); + context.fill(cx - panelW / 2, cy - panelH / 2, cx + panelW / 2, cy - panelH / 2 + 1, 0xFF555555); + context.fill(cx - panelW / 2, cy + panelH / 2 - 1, cx + panelW / 2, cy + panelH / 2, 0xFF555555); + context.fill(cx - panelW / 2, cy - panelH / 2, cx - panelW / 2 + 1, cy + panelH / 2, 0xFF555555); + context.fill(cx + panelW / 2 - 1, cy - panelH / 2, cx + panelW / 2, cy + panelH / 2, 0xFF555555); + + // Title + context.centeredText(font, getTitle(), cx, cy - panelH / 2 + 5, 0xFFFFFF); + + // Prompt + context.centeredText(font, + Component.translatable("skyblocker.bars.config.borderRadius.dialog.prompt", MAX_RADIUS), + cx, cy - 22, 0xAAAAAA); + + // Error + if (!errorMsg.getString().isEmpty()) { + context.centeredText(font, errorMsg, cx, cy + 36, 0xFF5555); + } + + super.extractRenderState(context, mouseX, mouseY, delta); + } + + @Override + public boolean isPauseScreen() { + return false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java index 51a214de0b3..25b1a99af2b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java @@ -1,6 +1,5 @@ package de.hysky.skyblocker.skyblock.fancybars; -import de.hysky.skyblocker.utils.EnumUtils; import de.hysky.skyblocker.utils.render.GuiHelper; import it.unimi.dsi.fastutil.booleans.BooleanConsumer; import java.awt.Color; @@ -10,8 +9,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.components.AbstractContainerWidget; import net.minecraft.client.gui.components.AbstractScrollArea; +import net.minecraft.client.gui.components.AbstractContainerWidget; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -26,332 +25,391 @@ public class EditBarWidget extends AbstractContainerWidget { - private final EnumCyclingOption iconOption; - private final EnumCyclingOption textOption; - - private final BooleanOption showMaxOption; - private final BooleanOption showOverflowOption; - - private final ColorOption color1; - private final ColorOption color2; - private final ColorOption textColor; - - private final RunnableOption hideOption; - - private final StringWidget nameWidget; - - private final List options; - - private int contentsWidth = 0; - - public EditBarWidget(int x, int y, Screen parent) { - super(x, y, 100, 99, Component.literal("Edit bar"), AbstractScrollArea.defaultSettings(4)); - - Font textRenderer = Minecraft.getInstance().font; - - nameWidget = new StringWidget(Component.empty(), textRenderer); - - MutableComponent translatable = Component.translatable("skyblocker.bars.config.icon"); - iconOption = new EnumCyclingOption<>(0, 11, getWidth(), translatable, StatusBar.IconPosition.class); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + iconOption.getLongestOptionWidth() + 10); - - translatable = Component.translatable("skyblocker.bars.config.text"); - textOption = new EnumCyclingOption<>(0, 22, getWidth(), translatable, StatusBar.TextPosition.class); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + textOption.getLongestOptionWidth() + 10); - - translatable = Component.translatable("skyblocker.bars.config.showMax"); - showMaxOption = new BooleanOption(0, 33, getWidth(), translatable); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); - - translatable = Component.translatable("skyblocker.bars.config.showOverflow"); - showOverflowOption = new BooleanOption(0, 44, getWidth(), translatable); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); - - // COLO(u)RS - translatable = Component.translatable("skyblocker.bars.config.mainColor"); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); - color1 = new ColorOption(0, 55, getWidth(), translatable, parent); - - translatable = Component.translatable("skyblocker.bars.config.overflowColor"); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); - color2 = new ColorOption(0, 66, getWidth(), translatable, parent); - - translatable = Component.translatable("skyblocker.bars.config.textColor"); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); - textColor = new ColorOption(0, 77, getWidth(), translatable, parent); - - translatable = Component.translatable("skyblocker.bars.config.hide"); - contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); - hideOption = new RunnableOption(0, 88, getWidth(), translatable); - - options = List.of(iconOption, textOption, showMaxOption, showOverflowOption, color1, color2, textColor, hideOption); - - setWidth(contentsWidth); - } - - @Override - public List children() { - return options; - } - - public int insideMouseX = 0; - public int insideMouseY = 0; - - @Override - protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - if (isHovered()) { - insideMouseX = mouseX; - insideMouseY = mouseY; - } else { - int i = mouseX - insideMouseX; - int j = mouseY - insideMouseY; - if (i * i + j * j > 30 * 30) visible = false; - } - Matrix3x2fStack matrices = graphics.pose(); - matrices.pushMatrix(); - matrices.translate(getX(), getY()); - TooltipRenderUtil.extractTooltipBackground(graphics, 0, 0, getWidth(), getHeight(), null); - nameWidget.extractRenderState(graphics, mouseX, mouseY, a); - for (AbstractWidget option : options) option.extractRenderState(graphics, mouseX - getX(), mouseY - getY(), a); - matrices.popMatrix(); - } - - @Override - protected void updateWidgetNarration(NarrationElementOutput builder) { - } - - @Override - public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { - if (!visible) return false; - if (!isHovered()) visible = false; - return super.mouseClicked(new MouseButtonEvent(click.x() - getX(), click.y() - getY(), click.buttonInfo()), doubled); - } - - public void setStatusBar(StatusBar statusBar) { - iconOption.setCurrent(statusBar.getIconPosition()); - iconOption.setOnChange(statusBar::setIconPosition); - textOption.setCurrent(statusBar.getTextPosition()); - textOption.setOnChange(statusBar::setTextPosition); - - color1.setCurrent(statusBar.getColors()[0].getRGB()); - color1.setOnChange(color -> statusBar.getColors()[0] = color); - - showMaxOption.active = statusBar.hasMax(); - showMaxOption.setCurrent(statusBar.showMax); - showOverflowOption.active = statusBar.hasOverflow(); - showOverflowOption.setCurrent(statusBar.showOverflow); - showMaxOption.setOnChange(showMax -> statusBar.showMax = showMax); - showOverflowOption.setOnChange(showOverflow -> statusBar.showOverflow = showOverflow); - - 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); - hideOption.active = statusBar.enabled; - hideOption.setRunnable(() -> { - if (statusBar.anchor != null) - FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar); - statusBar.enabled = false; - FancyStatusBars.updatePositions(true); - }); - - MutableComponent formatted = statusBar.getName().copy().withStyle(ChatFormatting.BOLD); - nameWidget.setMessage(formatted); - setWidth(Math.max(Minecraft.getInstance().font.width(formatted), contentsWidth)); - } - - @Override - public void setWidth(int width) { - super.setWidth(width); - for (AbstractWidget option : options) option.setWidth(width); - nameWidget.setWidth(width); - - } - - public class RunnableOption extends AbstractWidget { - - private Runnable runnable; - - public RunnableOption(int x, int y, int width, Component message) { - super(x, y, width, 11, message); - } + private final EnumCyclingOption iconOption; + private final EnumCyclingOption textOption; - public void setRunnable(Runnable runnable) { - this.runnable = runnable; - } + private final BooleanOption showMaxOption; + private final BooleanOption showOverflowOption; - @Override - protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - if (isMouseOver(mouseX, mouseY)) { - graphics.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); - } - Font textRenderer = Minecraft.getInstance().font; - graphics.text(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? CommonColors.WHITE : CommonColors.GRAY, true); - } + private final ColorOption color1; + private final ColorOption color2; + private final ColorOption textColor; - @Override - public void onClick(MouseButtonEvent click, boolean doubled) { - super.onClick(click, doubled); - EditBarWidget.this.visible = false; - if (runnable != null) runnable.run(); - } + private final RunnableOption hideOption; + private final RunnableOption borderRadiusOption; + private final RunnableOption resetBarOption; - @Override - protected void updateWidgetNarration(NarrationElementOutput builder) {} - } + private final StringWidget nameWidget; - public static class EnumCyclingOption> extends AbstractWidget { + private final List options; - private T current; - private final T[] values; - private Consumer onChange = null; + private int contentsWidth = 0; + private final Screen parent; - public EnumCyclingOption(int x, int y, int width, Component message, Class enumClass) { - super(x, y, width, 11, message); - values = enumClass.getEnumConstants(); - current = values[0]; + @SuppressWarnings("unchecked") + public EditBarWidget(int x, int y, Screen parent) { + super(x, y, 100, 121, Component.literal("Edit bar"), AbstractScrollArea.defaultSettings(4)); + this.parent = parent; + + Font textRenderer = Minecraft.getInstance().font; + + nameWidget = new StringWidget(Component.empty(), textRenderer); + + MutableComponent translatable = Component.translatable("skyblocker.bars.config.icon"); + // Icon cycles: OFF → LEFT → RIGHT → CUSTOM + StatusBar.IconPosition[] iconValues = { + StatusBar.IconPosition.OFF, + StatusBar.IconPosition.LEFT, + StatusBar.IconPosition.RIGHT, + StatusBar.IconPosition.CUSTOM + }; + iconOption = new EnumCyclingOption<>(0, 11, getWidth(), translatable, iconValues); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + iconOption.getLongestOptionWidth() + 10); + + translatable = Component.translatable("skyblocker.bars.config.text"); + // Text cycles: OFF → BAR LEFT → BAR CENTER → BAR RIGHT → CUSTOM (CENTER excluded) + StatusBar.TextPosition[] textValues = { + StatusBar.TextPosition.OFF, + StatusBar.TextPosition.LEFT, + StatusBar.TextPosition.BAR_CENTER, + StatusBar.TextPosition.RIGHT, + StatusBar.TextPosition.CUSTOM + }; + textOption = new EnumCyclingOption<>(0, 22, getWidth(), translatable, textValues); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + textOption.getLongestOptionWidth() + 10); + + translatable = Component.translatable("skyblocker.bars.config.showMax"); + showMaxOption = new BooleanOption(0, 33, getWidth(), translatable); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + + translatable = Component.translatable("skyblocker.bars.config.showOverflow"); + showOverflowOption = new BooleanOption(0, 44, getWidth(), translatable); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + + // COLO(u)RS + translatable = Component.translatable("skyblocker.bars.config.mainColor"); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + color1 = new ColorOption(0, 55, getWidth(), translatable, parent); + + translatable = Component.translatable("skyblocker.bars.config.overflowColor"); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + color2 = new ColorOption(0, 66, getWidth(), translatable, parent); + + translatable = Component.translatable("skyblocker.bars.config.textColor"); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + textColor = new ColorOption(0, 77, getWidth(), translatable, parent); + + // Account for both Hide and Show labels width + contentsWidth = Math.max(contentsWidth, textRenderer.width(Component.translatable("skyblocker.bars.config.hide")) + 9 + 10); + contentsWidth = Math.max(contentsWidth, textRenderer.width(Component.translatable("skyblocker.bars.config.show")) + 9 + 10); + hideOption = new RunnableOption(0, 88, getWidth(), Component.translatable("skyblocker.bars.config.hide")); + + translatable = Component.translatable("skyblocker.bars.config.borderRadius"); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + borderRadiusOption = new RunnableOption(0, 99, getWidth(), translatable); + + translatable = Component.translatable("skyblocker.bars.config.resetBar"); + contentsWidth = Math.max(contentsWidth, textRenderer.width(translatable) + 9 + 10); + resetBarOption = new RunnableOption(0, 110, getWidth(), translatable); + + options = List.of(iconOption, textOption, showMaxOption, showOverflowOption, color1, color2, textColor, hideOption, borderRadiusOption, resetBarOption); + + setWidth(contentsWidth); } @Override - protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { - if (isMouseOver(mouseX, mouseY)) { - graphics.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); - } - Font textRenderer = Minecraft.getInstance().font; - graphics.text(textRenderer, getMessage(), getX() + 1, getY() + 1, CommonColors.WHITE, true); - String string = current.toString(); - graphics.text(textRenderer, string, getRight() - textRenderer.width(string) - 1, getY() + 1, CommonColors.WHITE, true); + public List children() { + return options; } - public void setCurrent(T current) { - this.current = current; - } + public int insideMouseX = 0; + public int insideMouseY = 0; @Override - public void onClick(MouseButtonEvent click, boolean doubled) { - current = EnumUtils.cycle(current); - if (onChange != null) onChange.accept(current); - super.onClick(click, doubled); + protected void extractWidgetRenderState(GuiGraphicsExtractor 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; + } + Matrix3x2fStack matrices = context.pose(); + matrices.pushMatrix(); + matrices.translate(getX(), getY()); + TooltipRenderUtil.extractTooltipBackground(context, 0, 0, getWidth(), getHeight(), null); + nameWidget.extractRenderState(context, mouseX, mouseY, delta); + for (AbstractWidget option : options) option.extractRenderState(context, mouseX - getX(), mouseY - getY(), delta); + matrices.popMatrix(); } @Override protected void updateWidgetNarration(NarrationElementOutput builder) { } - public void setOnChange(Consumer onChange) { - this.onChange = onChange; + @Override + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + if (!visible) return false; + if (!isHovered()) visible = false; + return super.mouseClicked(new MouseButtonEvent(click.x() - getX(), click.y() - getY(), click.buttonInfo()), doubled); } - int getLongestOptionWidth() { - int m = 0; - for (T value : values) { - int i = Minecraft.getInstance().font.width(value.toString()); - m = Math.max(m, i); - } - return m; - } - } - - public static class BooleanOption extends AbstractWidget { - - private boolean current = false; - private BooleanConsumer onChange = null; - - public BooleanOption(int x, int y, int width, Component message) { - super(x, y, width, 11, message); + public void setStatusBar(StatusBar statusBar) { + iconOption.setCurrent(statusBar.getIconPosition()); + iconOption.setOnChange(statusBar::setIconPosition); + textOption.setCurrent(statusBar.getTextPosition()); + textOption.setOnChange(statusBar::setTextPosition); + + color1.setCurrent(statusBar.getColors()[0].getRGB()); + color1.setOnChange(color -> statusBar.getColors()[0] = color); + + showMaxOption.active = statusBar.hasMax(); + showMaxOption.setCurrent(statusBar.showMax); + showOverflowOption.active = statusBar.hasOverflow(); + showOverflowOption.setCurrent(statusBar.showOverflow); + showMaxOption.setOnChange(showMax -> statusBar.showMax = showMax); + showOverflowOption.setOnChange(showOverflow -> statusBar.showOverflow = showOverflow); + + 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); + + // Toggle between Hide and Show based on current enabled state + hideOption.active = true; + if (statusBar.enabled) { + hideOption.setMessage(Component.translatable("skyblocker.bars.config.hide")); + hideOption.setRunnable(() -> { + if (statusBar.anchor != null) + FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar); + statusBar.enabled = false; + FancyStatusBars.updatePositions(true); + }); + } else { + hideOption.setMessage(Component.translatable("skyblocker.bars.config.show")); + hideOption.setRunnable(() -> { + statusBar.enabled = true; + statusBar.anchor = null; // make free-floating + if (statusBar.width <= 0) statusBar.width = 0.2f; + FancyStatusBars.updatePositions(true); + }); + } + + borderRadiusOption.active = true; + borderRadiusOption.setRunnable(() -> { + Minecraft.getInstance().setScreen( + new BorderRadiusDialog(parent, statusBar.borderRadius, radius -> { + statusBar.borderRadius = radius; + })); + }); + + resetBarOption.active = true; + resetBarOption.setRunnable(() -> FancyStatusBars.resetSingleBar(statusBar)); + + MutableComponent formatted = statusBar.getName().copy().withStyle(ChatFormatting.BOLD); + nameWidget.setMessage(formatted); + setWidth(Math.max(Minecraft.getInstance().font.width(formatted), contentsWidth)); } @Override - protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - if (isMouseOver(mouseX, mouseY)) { - graphics.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); - } - Font textRenderer = Minecraft.getInstance().font; - graphics.text(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? -1 : CommonColors.GRAY, true); - GuiHelper.border(graphics, getRight() - 10, getY() + 1, 9, 9, active ? -1 : CommonColors.GRAY); - if (current && active) graphics.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, CommonColors.WHITE); - } + public void setWidth(int width) { + super.setWidth(width); + for (AbstractWidget option : options) option.setWidth(width); + nameWidget.setWidth(width); - @Override - public void onClick(MouseButtonEvent click, boolean doubled) { - current = !current; - if (onChange != null) onChange.accept(current); - super.onClick(click, doubled); } - @Override - protected void updateWidgetNarration(NarrationElementOutput builder) { - } + public class RunnableOption extends AbstractWidget { - public void setCurrent(boolean current) { - this.current = current; - } + private Runnable runnable; - public void setOnChange(BooleanConsumer onChange) { - this.onChange = onChange; - } - } + public RunnableOption(int x, int y, int width, Component message) { + super(x, y, width, 11, message); + } - public static class ColorOption extends AbstractWidget { + public void setRunnable(Runnable runnable) { + this.runnable = runnable; + } - public void setCurrent(int current) { - this.current = current; - } + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + Font textRenderer = Minecraft.getInstance().font; + context.text(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? CommonColors.WHITE : CommonColors.GRAY, true); + } - private int current = 0; - private Consumer onChange = null; - private final Screen parent; + @Override + public void onClick(MouseButtonEvent click, boolean doubled) { + super.onClick(click, doubled); + EditBarWidget.this.visible = false; + if (runnable != null) runnable.run(); + } - public ColorOption(int x, int y, int width, Component message, Screen parent) { - super(x, y, width, 11, message); - this.parent = parent; + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) {} } - @Override - protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - if (isMouseOver(mouseX, mouseY)) { - graphics.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); - } - Font textRenderer = Minecraft.getInstance().font; - graphics.text(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? -1 : CommonColors.GRAY, true); - GuiHelper.border(graphics, getRight() - 10, getY() + 1, 9, 9, active ? -1 : CommonColors.GRAY); - graphics.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, active ? current : CommonColors.GRAY); + public static class EnumCyclingOption> extends AbstractWidget { + + private T current; + private final T[] values; + private Consumer onChange = null; + + /** Constructor using a custom (possibly filtered) set of values to cycle through. */ + public EnumCyclingOption(int x, int y, int width, Component message, T[] values) { + super(x, y, width, 11, message); + this.values = values; + current = values[0]; + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + Font textRenderer = Minecraft.getInstance().font; + context.text(textRenderer, getMessage(), getX() + 1, getY() + 1, CommonColors.WHITE, true); + String string = current.toString(); + context.text(textRenderer, string, getRight() - textRenderer.width(string) - 1, getY() + 1, CommonColors.WHITE, true); + } + + public void setCurrent(T current) { + this.current = current; + } + + @Override + public void onClick(MouseButtonEvent click, boolean doubled) { + // Cycle only within the filtered values array + int idx = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] == current) { idx = i; break; } + } + current = values[(idx + 1) % values.length]; + if (onChange != null) onChange.accept(current); + super.onClick(click, doubled); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) { + } + + public void setOnChange(Consumer onChange) { + this.onChange = onChange; + } + + int getLongestOptionWidth() { + int m = 0; + for (T value : values) { + int i = Minecraft.getInstance().font.width(value.toString()); + m = Math.max(m, i); + } + return m; + } } - @Override - public void onClick(MouseButtonEvent click, boolean doubled) { - super.onClick(click, doubled); - Minecraft.getInstance().setScreen(new EditBarColorPopup(Component.literal("Edit ").append(getMessage()), parent, this::set)); + public static class BooleanOption extends AbstractWidget { + + private boolean current = false; + private BooleanConsumer onChange = null; + + public BooleanOption(int x, int y, int width, Component message) { + super(x, y, width, 11, message); + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + Font textRenderer = Minecraft.getInstance().font; + context.text(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? -1 : CommonColors.GRAY, true); + GuiHelper.border(context, getRight() - 10, getY() + 1, 9, 9, active ? -1 : CommonColors.GRAY); + if (current && active) context.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, CommonColors.WHITE); + } + + @Override + public void onClick(MouseButtonEvent click, boolean doubled) { + current = !current; + if (onChange != null) onChange.accept(current); + super.onClick(click, doubled); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) { + } + + public void setCurrent(boolean current) { + this.current = current; + } + + public void setOnChange(BooleanConsumer onChange) { + this.onChange = onChange; + } } - private void set(Color color) { - current = color.getRGB(); - if (onChange != null) onChange.accept(color); + public static class ColorOption extends AbstractWidget { + + public void setCurrent(int current) { + this.current = current; + } + + private int current = 0; + private Consumer onChange = null; + private final Screen parent; + + public ColorOption(int x, int y, int width, Component message, Screen parent) { + super(x, y, width, 11, message); + this.parent = parent; + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + Font textRenderer = Minecraft.getInstance().font; + context.text(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? -1 : CommonColors.GRAY, true); + GuiHelper.border(context, getRight() - 10, getY() + 1, 9, 9, active ? -1 : CommonColors.GRAY); + context.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, active ? current : CommonColors.GRAY); + } + + @Override + public void onClick(MouseButtonEvent click, boolean doubled) { + super.onClick(click, doubled); + Minecraft.getInstance().setScreen(new EditBarColorPopup(Component.literal("Edit ").append(getMessage()), parent, this::set)); + } + + private void set(Color color) { + current = color.getRGB(); + if (onChange != null) onChange.accept(color); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) { + + } + + public void setOnChange(Consumer onChange) { + this.onChange = onChange; + } } @Override - protected void updateWidgetNarration(NarrationElementOutput builder) { - + protected int contentHeight() { + return 0; } - public void setOnChange(Consumer onChange) { - this.onChange = onChange; + @Override + protected double scrollRate() { + return 0; } - } - - @Override - protected int contentHeight() { - return 0; - } - - @Override - protected double scrollRate() { - return 0; - } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java index f87f39bc84a..d89d753d86a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java @@ -10,8 +10,8 @@ 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.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.ClientCommands; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElement; import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; @@ -46,367 +46,489 @@ import java.util.function.Function; public class FancyStatusBars { - private static final Identifier HUD_LAYER = SkyblockerMod.id("fancy_status_bars"); - private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("status_bars.json"); - private static final Logger LOGGER = LoggerFactory.getLogger(FancyStatusBars.class); - - public static BarPositioner barPositioner = new BarPositioner(); - public static Map statusBars = new EnumMap<>(StatusBarType.class); - private static boolean updatePositionsNextFrame; - - public static boolean isHealthFancyBarEnabled() { - return isBarEnabled(StatusBarType.HEALTH); - } - - public static boolean isExperienceFancyBarEnabled() { - return isBarEnabled(StatusBarType.EXPERIENCE); - } - - public static boolean isBarEnabled(StatusBarType type) { - StatusBar statusBar = statusBars.get(type); - return Debug.isTestEnvironment() || statusBar.enabled || statusBar.inMouse; - } - - @SuppressWarnings("deprecation") - @Init - public static void init() { - Function hideIfFancyStatusBarsEnabled = hudElement -> { - if (Utils.isOnSkyblock() && isEnabled()) - return (_, _) -> {}; - return hudElement; - }; - - HudElementRegistry.replaceElement(VanillaHudElements.HEALTH_BAR, hudElement -> { - if (!Utils.isOnSkyblock() || !isEnabled()) return hudElement; - if (isHealthFancyBarEnabled()) { - return (_, _) -> {}; - } else if (isExperienceFancyBarEnabled()) { - return (context, tickCounter) -> { - Matrix3x2fStack pose = context.pose(); - pose.pushMatrix(); - pose.translate(0, 6); - hudElement.extractRenderState(context, tickCounter); - pose.popMatrix(); + private static final Identifier HUD_LAYER = SkyblockerMod.id("fancy_status_bars"); + private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("status_bars.json"); + private static final Logger LOGGER = LoggerFactory.getLogger(FancyStatusBars.class); + + public static BarPositioner barPositioner = new BarPositioner(); + public static Map statusBars = new EnumMap<>(StatusBarType.class); + private static boolean updatePositionsNextFrame; + + public static boolean isHealthFancyBarEnabled() { + return isBarEnabled(StatusBarType.HEALTH); + } + + public static boolean isExperienceFancyBarEnabled() { + return isBarEnabled(StatusBarType.EXPERIENCE); + } + + public static boolean isBarEnabled(StatusBarType type) { + StatusBar statusBar = statusBars.get(type); + return Debug.isTestEnvironment() || statusBar.enabled || statusBar.inMouse; + } + + @SuppressWarnings("deprecation") + @Init + public static void init() { + Function hideIfFancyStatusBarsEnabled = hudElement -> { + if (Utils.isOnSkyblock() && isEnabled()) + return (_, _) -> {}; + return hudElement; }; - } - return hudElement; - }); - HudElementRegistry.replaceElement(VanillaHudElements.EXPERIENCE_LEVEL, hudElement -> { - if (!Utils.isOnSkyblock() || !isEnabled() || !isExperienceFancyBarEnabled()) return hudElement; - return (_, _) -> {}; - }); - HudElementRegistry.replaceElement(VanillaHudElements.INFO_BAR, hudElement -> { - if (!Utils.isOnSkyblock() || !isEnabled() || !isExperienceFancyBarEnabled()) return hudElement; - return (_, _) -> {}; - }); - HudElementRegistry.replaceElement(VanillaHudElements.ARMOR_BAR, hideIfFancyStatusBarsEnabled); - HudElementRegistry.replaceElement(VanillaHudElements.MOUNT_HEALTH, hideIfFancyStatusBarsEnabled); - HudElementRegistry.replaceElement(VanillaHudElements.FOOD_BAR, hideIfFancyStatusBarsEnabled); - HudElementRegistry.replaceElement(VanillaHudElements.AIR_BAR, hideIfFancyStatusBarsEnabled); - - HudElementRegistry.attachElementAfter(VanillaHudElements.HOTBAR, HUD_LAYER, (context, _) -> { - if (Utils.isOnSkyblock()) extractRenderState(context, Minecraft.getInstance()); - }); - - statusBars.put(StatusBarType.HEALTH, StatusBarType.HEALTH.newStatusBar()); - statusBars.put(StatusBarType.INTELLIGENCE, StatusBarType.INTELLIGENCE.newStatusBar()); - statusBars.put(StatusBarType.DEFENSE, StatusBarType.DEFENSE.newStatusBar()); - statusBars.put(StatusBarType.EXPERIENCE, StatusBarType.EXPERIENCE.newStatusBar()); - statusBars.put(StatusBarType.SPEED, StatusBarType.SPEED.newStatusBar()); - statusBars.put(StatusBarType.AIR, StatusBarType.AIR.newStatusBar()); - - // Fetch from old status bar config - int[] counts = new int[3]; // counts for RIGHT, LAYER1, LAYER2 - UIAndVisualsConfig.LegacyBarPositions barPositions = SkyblockerConfigManager.get().uiAndVisuals.bars.barPositions; - initBarPosition(statusBars.get(StatusBarType.HEALTH), counts, barPositions.healthBarPosition); - initBarPosition(statusBars.get(StatusBarType.INTELLIGENCE), counts, barPositions.manaBarPosition); - initBarPosition(statusBars.get(StatusBarType.DEFENSE), counts, barPositions.defenceBarPosition); - initBarPosition(statusBars.get(StatusBarType.EXPERIENCE), counts, barPositions.experienceBarPosition); - initBarPosition(statusBars.get(StatusBarType.SPEED), counts, UIAndVisualsConfig.LegacyBarPosition.RIGHT); - initBarPosition(statusBars.get(StatusBarType.AIR), counts, UIAndVisualsConfig.LegacyBarPosition.RIGHT); - - CompletableFuture.supplyAsync(FancyStatusBars::loadBarConfig, Executors.newVirtualThreadPerTaskExecutor()).thenAccept(object -> { - if (object != null) { - for (String s : object.keySet()) { - StatusBarType type = StatusBarType.from(s); - if (statusBars.containsKey(type)) { - try { - statusBars.get(type).loadFromJson(object.get(s).getAsJsonObject()); - } catch (Exception e) { - LOGGER.error("[Skyblocker] Failed to load {} status bar", s, e); + + HudElementRegistry.replaceElement(VanillaHudElements.HEALTH_BAR, hudElement -> { + if (!Utils.isOnSkyblock() || !isEnabled()) return hudElement; + if (isHealthFancyBarEnabled()) { + return (_, _) -> {}; + } else if (isExperienceFancyBarEnabled()) { + return (context, tickCounter) -> { + Matrix3x2fStack pose = context.pose(); + pose.pushMatrix(); + pose.translate(0, 6); + hudElement.extractRenderState(context, tickCounter); + pose.popMatrix(); + }; } - } 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(_ -> saveBarConfig()); - - ClientCommandRegistrationCallback.EVENT.register((dispatcher, _) -> dispatcher.register( - ClientCommands.literal(SkyblockerMod.NAMESPACE) - .then(ClientCommands.literal("bars").executes(Scheduler.queueOpenScreenCommand(StatusBarsConfigScreen::new))))); - - SkyblockEvents.LOCATION_CHANGE.register(_ -> updatePositionsNextFrame = true); - } - - /** - * Loads the bar position from the old config. Should be used to initialize new bars too. - * - * @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 - */ - @SuppressWarnings("incomplete-switch") - private static void initBarPosition(StatusBar bar, int[] counts, UIAndVisualsConfig.LegacyBarPosition 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; + return hudElement; + }); + HudElementRegistry.replaceElement(VanillaHudElements.EXPERIENCE_LEVEL, hudElement -> { + if (!Utils.isOnSkyblock() || !isEnabled() || !isExperienceFancyBarEnabled()) return hudElement; + return (_, _) -> {}; + }); + HudElementRegistry.replaceElement(VanillaHudElements.INFO_BAR, hudElement -> { + if (!Utils.isOnSkyblock() || !isEnabled() || !isExperienceFancyBarEnabled()) return hudElement; + return (_, _) -> {}; + }); + HudElementRegistry.replaceElement(VanillaHudElements.ARMOR_BAR, hideIfFancyStatusBarsEnabled); + HudElementRegistry.replaceElement(VanillaHudElements.MOUNT_HEALTH, hideIfFancyStatusBarsEnabled); + HudElementRegistry.replaceElement(VanillaHudElements.FOOD_BAR, hideIfFancyStatusBarsEnabled); + HudElementRegistry.replaceElement(VanillaHudElements.AIR_BAR, hideIfFancyStatusBarsEnabled); + + HudElementRegistry.attachElementAfter(VanillaHudElements.HOTBAR, HUD_LAYER, (context, _) -> { + if (Utils.isOnSkyblock()) render(context, Minecraft.getInstance()); + }); + + statusBars.put(StatusBarType.HEALTH, StatusBarType.HEALTH.newStatusBar()); + statusBars.put(StatusBarType.INTELLIGENCE, StatusBarType.INTELLIGENCE.newStatusBar()); + statusBars.put(StatusBarType.DEFENSE, StatusBarType.DEFENSE.newStatusBar()); + statusBars.put(StatusBarType.EXPERIENCE, StatusBarType.EXPERIENCE.newStatusBar()); + statusBars.put(StatusBarType.SPEED, StatusBarType.SPEED.newStatusBar()); + statusBars.put(StatusBarType.AIR, StatusBarType.AIR.newStatusBar()); + + // Fetch from old status bar config + int[] counts = new int[3]; // counts for RIGHT, LAYER1, LAYER2 + UIAndVisualsConfig.LegacyBarPositions barPositions = SkyblockerConfigManager.get().uiAndVisuals.bars.barPositions; + initBarPosition(statusBars.get(StatusBarType.HEALTH), counts, barPositions.healthBarPosition); + initBarPosition(statusBars.get(StatusBarType.INTELLIGENCE), counts, barPositions.manaBarPosition); + initBarPosition(statusBars.get(StatusBarType.DEFENSE), counts, barPositions.defenceBarPosition); + initBarPosition(statusBars.get(StatusBarType.EXPERIENCE), counts, barPositions.experienceBarPosition); + initBarPosition(statusBars.get(StatusBarType.SPEED), counts, UIAndVisualsConfig.LegacyBarPosition.RIGHT); + initBarPosition(statusBars.get(StatusBarType.AIR), counts, UIAndVisualsConfig.LegacyBarPosition.RIGHT); + + CompletableFuture.supplyAsync(FancyStatusBars::loadBarConfig, Executors.newVirtualThreadPerTaskExecutor()).thenAccept(object -> { + if (object != null) { + for (String s : object.keySet()) { + StatusBarType type = StatusBarType.from(s); + if (statusBars.containsKey(type)) { + try { + statusBars.get(type).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); + } + } + } else { + // No saved config — apply preferred default layout for first-time users + for (StatusBarType type : StatusBarType.values()) { + StatusBar bar = statusBars.get(type); + if (bar != null) applyPreferredBarDefaults(type, bar); + } + } + placeBarsInPositioner(); + configLoaded = true; + }).exceptionally(throwable -> { + LOGGER.error("[Skyblocker] Failed reading status bars config", throwable); + return null; + }); + ClientLifecycleEvents.CLIENT_STOPPING.register(_ -> saveBarConfig()); + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, _) -> dispatcher.register( + ClientCommands.literal(SkyblockerMod.NAMESPACE) + .then(ClientCommands.literal("bars").executes(Scheduler.queueOpenScreenCommand(StatusBarsConfigScreen::new))))); + + SkyblockEvents.LOCATION_CHANGE.register(_ -> updatePositionsNextFrame = true); } - } - - private static boolean configLoaded = false; - - @VisibleForTesting - public static void placeBarsInPositioner() { - barPositioner.clear(); - for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { - List barList = statusBars.values().stream().filter(bar -> bar.anchor == barAnchor) - .sorted(Comparator.comparingInt(bar -> bar.gridY).thenComparingInt(bar -> bar.gridX)).toList(); - if (barList.isEmpty()) continue; - - int y = -1; - int rowNum = -1; - for (StatusBar statusBar : barList) { - if (statusBar.gridY > y) { - barPositioner.addRow(barAnchor); - rowNum++; - y = statusBar.gridY; + + /** + * Loads the bar position from the old config. Should be used to initialize new bars too. + * + * @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 + */ + @SuppressWarnings("incomplete-switch") + private static void initBarPosition(StatusBar bar, int[] counts, UIAndVisualsConfig.LegacyBarPosition 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; } - barPositioner.addBar(barAnchor, rowNum, statusBar); - } } - } - - public static @Nullable JsonObject loadBarConfig() { - try (BufferedReader reader = Files.newBufferedReader(FILE)) { - return SkyblockerMod.GSON.fromJson(reader, JsonObject.class); - } catch (NoSuchFileException _) { - LOGGER.warn("[Skyblocker] No status bar config file found, using defaults"); - } catch (Exception e) { - LOGGER.error("[Skyblocker] Failed to load status bars config", e); + + private static boolean configLoaded = false; + + /** + * Applies the preferred default layout for a single bar using hotbar-relative anchoring. + * Pixel offsets are in GUI pixels (scale-independent) so the layout tracks the hotbar + * correctly at every GUI scale. + */ + private static void applyPreferredBarDefaults(StatusBarType type, StatusBar bar) { + bar.anchor = null; + bar.hotbarRelative = true; + bar.gridX = 0; bar.gridY = 0; + bar.enabled = true; + bar.visible = true; + bar.barHeight = 9; + bar.showMax = false; + bar.showOverflow = false; + bar.textCustomScale = 1.0f; + bar.iconCustomOffX = 0; bar.iconCustomOffY = 0; + bar.iconCustomW = StatusBar.ICON_SIZE; bar.iconCustomH = StatusBar.ICON_SIZE; + bar.setIconPosition(StatusBar.IconPosition.LEFT); + bar.setColors(type.getColors().clone()); + bar.setTextColor(type.getTextColor()); + // Hotbar-relative layout (GUI pixels from hotbar top-centre). + // offX from centre, offY from hotbar top (negative = above hotbar). + // Stat bars (H/I/D) tile across the hotbar width with 1px gaps. + // XP spans full hotbar width, 1px above hotbar. Speed/Air flank below. + switch (type) { + case HEALTH -> { + // left bar: spans -91 to -31 (width 60, 1px gap before intel) + bar.hotbarRelOffX = -91; bar.hotbarRelOffY = -21; bar.hotbarPixelWidth = 60; + bar.borderRadius = 10; + bar.setTextPosition(StatusBar.TextPosition.CUSTOM); + bar.textCustomOffX = 23; bar.textCustomOffY = -3; + } + case INTELLIGENCE -> { + // centre bar: spans -30 to 30 (width 60, 1px gaps either side) + bar.hotbarRelOffX = -30; bar.hotbarRelOffY = -21; bar.hotbarPixelWidth = 60; + bar.borderRadius = 10; + bar.setTextPosition(StatusBar.TextPosition.CUSTOM); + bar.textCustomOffX = 25; bar.textCustomOffY = -3; + } + case DEFENSE -> { + // right bar: spans 31 to 91 (width 60, 1px gap after intel) + bar.hotbarRelOffX = 31; bar.hotbarRelOffY = -21; bar.hotbarPixelWidth = 60; + bar.borderRadius = 10; + bar.setTextPosition(StatusBar.TextPosition.CUSTOM); + bar.textCustomOffX = 22; bar.textCustomOffY = -3; + } + case EXPERIENCE -> { + // full hotbar width, 1px above hotbar top (bar height 9 → offY = -(9+1) = -10) + bar.hotbarRelOffX = -91; bar.hotbarRelOffY = -10; bar.hotbarPixelWidth = 182; + bar.borderRadius = 0; + bar.setTextPosition(StatusBar.TextPosition.BAR_CENTER); + bar.textCustomOffX = 0; bar.textCustomOffY = 0; + } + case SPEED -> { + // below-hotbar, 2px left of hotbar left edge (-91 - 2 - 60 = -153) + bar.hotbarRelOffX = -153; bar.hotbarRelOffY = 6; bar.hotbarPixelWidth = 60; + bar.borderRadius = 10; + bar.setTextPosition(StatusBar.TextPosition.CUSTOM); + bar.textCustomOffX = 28; bar.textCustomOffY = -3; + } + case AIR -> { + // below-hotbar, 2px right of hotbar right edge (91 + 2 = 93) + bar.hotbarRelOffX = 93; bar.hotbarRelOffY = 6; bar.hotbarPixelWidth = 60; + bar.borderRadius = 10; + bar.setTextPosition(StatusBar.TextPosition.CUSTOM); + bar.textCustomOffX = 29; bar.textCustomOffY = -2; + } + } } - return null; - } - - public static void saveBarConfig() { - JsonObject output = new JsonObject(); - statusBars.forEach((s, statusBar) -> output.add(s.getSerializedName(), 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 resetToDefaults() { + barPositioner.clear(); + for (StatusBarType type : StatusBarType.values()) { + StatusBar bar = statusBars.get(type); + if (bar == null) continue; + applyPreferredBarDefaults(type, bar); + } + placeBarsInPositioner(); + updatePositions(true); } - } - - public static void updatePositions(boolean ignoreVisibility) { - if (!configLoaded) return; - final int width = Minecraft.getInstance().getWindow().getGuiScaledWidth(); - final int height = Minecraft.getInstance().getWindow().getGuiScaledHeight(); - - // Put these in the corner for the config screen - int offset = 0; - for (StatusBar statusBar : statusBars.values()) { - if (!statusBar.enabled) { - statusBar.setX(5); - statusBar.setY(50 + offset); - statusBar.setWidth(30); - offset += statusBar.getHeight(); - } else if (statusBar.anchor == null) { - statusBar.width = Math.clamp(statusBar.width, 30f / width, 1); - statusBar.x = Math.clamp(statusBar.x, 0, 1 - statusBar.width); - statusBar.y = Math.clamp(statusBar.y, 0, 1 - (float) statusBar.getHeight() / height); - statusBar.setX((int) (statusBar.x * width)); - statusBar.setY((int) (statusBar.y * height)); - statusBar.setWidth((int) (statusBar.width * width)); - } + + /** + * Resets one bar to its default layout and visual settings without touching any other bar. + * Rebuilds the positioner so the bar re-occupies its default grid slot. + */ + public static void resetSingleBar(StatusBar target) { + StatusBarType type = null; + for (java.util.Map.Entry e : statusBars.entrySet()) { + if (e.getValue() == target) { type = e.getKey(); break; } + } + if (type == null) return; + + // Remove from positioner first (in case bar was anchored) + if (target.anchor != null) barPositioner.removeBar(target.anchor, target.gridY, target); + + // Apply preferred defaults for this bar + applyPreferredBarDefaults(type, target); + + placeBarsInPositioner(); + updatePositions(true); } - for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { - ScreenPosition anchorPosition = barAnchor.getAnchorPosition(width, height); - BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule(); - - int targetSize = sizeRule.targetSize(); - boolean visibleHealthMove = barAnchor == BarPositioner.BarAnchor.HOTBAR_TOP && !isHealthFancyBarEnabled(); - if (visibleHealthMove) { - targetSize /= 2; - } - - if (sizeRule.isTargetSize()) { - for (int row = 0; row < barPositioner.getRowCount(barAnchor); row++) { - LinkedList barRow = barPositioner.getRow(barAnchor, row); - if (barRow.isEmpty()) continue; - - // FIX SIZES - int totalSize = 0; - for (StatusBar statusBar : barRow) - totalSize += (statusBar.size = Math.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize())); - - whileLoop: - while (totalSize != targetSize) { - if (totalSize > targetSize) { - for (StatusBar statusBar : barRow) { - if (statusBar.size > sizeRule.minSize()) { - statusBar.size--; - totalSize--; - if (totalSize == targetSize) break whileLoop; - } - } - } else { - for (StatusBar statusBar : barRow) { - if (statusBar.size < sizeRule.maxSize()) { - statusBar.size++; - totalSize++; - if (totalSize == targetSize) break whileLoop; + @VisibleForTesting + public static void placeBarsInPositioner() { + barPositioner.clear(); + for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { + List barList = statusBars.values().stream().filter(bar -> bar.anchor == barAnchor) + .sorted(Comparator.comparingInt(bar -> bar.gridY).thenComparingInt(bar -> bar.gridX)).toList(); + if (barList.isEmpty()) continue; + + 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 @Nullable JsonObject loadBarConfig() { + try (BufferedReader reader = Files.newBufferedReader(FILE)) { + return SkyblockerMod.GSON.fromJson(reader, JsonObject.class); + } catch (NoSuchFileException _) { + 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; + } - int row = 0; - for (int i = 0; i < barPositioner.getRowCount(barAnchor); i++) { - List barRow = new ArrayList<>(barPositioner.getRow(barAnchor, i)); - barRow.removeIf(statusBar -> !statusBar.visible && !ignoreVisibility); - if (barRow.isEmpty()) continue; + public static void saveBarConfig() { + JsonObject output = new JsonObject(); + statusBars.forEach((s, statusBar) -> output.add(s.getSerializedName(), 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(boolean ignoreVisibility) { + if (!configLoaded) return; + final int width = Minecraft.getInstance().getWindow().getGuiScaledWidth(); + final int height = Minecraft.getInstance().getWindow().getGuiScaledHeight(); + + // Put these in the corner for the config screen + int offset = 0; + for (StatusBar statusBar : statusBars.values()) { + if (!statusBar.enabled) { + statusBar.setX(5); + statusBar.setY(50 + offset); + statusBar.setWidth(30); + offset += statusBar.getHeight(); + } else if (statusBar.hotbarRelative) { + // Hotbar-relative: pixel offsets from hotbar top-centre — scale-independent + int hotbarTopY = height - 22; + int hotbarCentX = width / 2; + statusBar.setX(hotbarCentX + statusBar.hotbarRelOffX); + statusBar.setY(hotbarTopY + statusBar.hotbarRelOffY); + statusBar.setWidth(statusBar.hotbarPixelWidth); + } else if (statusBar.anchor == null) { + statusBar.width = Math.clamp(statusBar.width, 30f / width, 1); + statusBar.x = Math.clamp(statusBar.x, 0, 1 - statusBar.width); + statusBar.y = Math.clamp(statusBar.y, 0, 1 - (float) statusBar.getHeight() / height); + statusBar.setX((int) (statusBar.x * width)); + statusBar.setY((int) (statusBar.y * height)); + statusBar.setWidth((int) (statusBar.width * width)); + } + } - // Update the positions - float widthPerSize; - if (sizeRule.isTargetSize()) { - int size = 0; - for (StatusBar bar : barRow) size += bar.size; - widthPerSize = (float) sizeRule.totalWidth() / size; + for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { + ScreenPosition anchorPosition = barAnchor.getAnchorPosition(width, height); + BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule(); - } - else - widthPerSize = sizeRule.widthPerSize(); - - if (visibleHealthMove) widthPerSize /= 2; - - int currSize = 0; - int rowSize = barRow.size(); - for (int j = 0; j < rowSize; j++) { - // A bit of a padding - int offsetX = 0; - int lessWidth = 0; - if (!sizeRule.isTargetSize()) { - offsetX = 1; - lessWidth = 2; - } else if (rowSize > 1) { // Technically bars in the middle of 3+ bars will be smaller than the 2 side ones but shh - if (j == 0) lessWidth = 1; - else if (j == rowSize - 1) { - lessWidth = 1; - offsetX = 1; - } else { - lessWidth = 2; - offsetX = 1; + int targetSize = sizeRule.targetSize(); + boolean visibleHealthMove = barAnchor == BarPositioner.BarAnchor.HOTBAR_TOP && !isHealthFancyBarEnabled(); + if (visibleHealthMove) { + targetSize /= 2; } - } - StatusBar statusBar = barRow.get(j); - statusBar.size = Math.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize()); - - float x = barAnchor.isRight() ? - anchorPosition.x() + (visibleHealthMove ? sizeRule.totalWidth() / 2.f : 0) + currSize * widthPerSize : - anchorPosition.x() - currSize * widthPerSize - statusBar.size * widthPerSize; - statusBar.setX(Mth.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(Mth.floor(statusBar.size * widthPerSize) - lessWidth); - currSize += statusBar.size; - } - if (currSize > 0) row++; - } - } - } + if (sizeRule.isTargetSize()) { + for (int row = 0; row < barPositioner.getRowCount(barAnchor); row++) { + LinkedList barRow = barPositioner.getRow(barAnchor, row); + if (barRow.isEmpty()) continue; + + // FIX SIZES + int totalSize = 0; + for (StatusBar statusBar : barRow) + totalSize += (statusBar.size = Math.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize())); + + whileLoop: + while (totalSize != targetSize) { + if (totalSize > targetSize) { + for (StatusBar statusBar : barRow) { + if (statusBar.size > sizeRule.minSize()) { + statusBar.size--; + totalSize--; + if (totalSize == targetSize) break whileLoop; + } + } + } else { + for (StatusBar statusBar : barRow) { + if (statusBar.size < sizeRule.maxSize()) { + statusBar.size++; + totalSize++; + if (totalSize == targetSize) break whileLoop; + } + } + } + } + + } + } - public static boolean isEnabled() { - return SkyblockerConfigManager.get().uiAndVisuals.bars.enableBars && (!Utils.isInTheRift() || SkyblockerConfigManager.get().uiAndVisuals.bars.enableBarsRift); - } + int row = 0; + for (int i = 0; i < barPositioner.getRowCount(barAnchor); i++) { + List barRow = new ArrayList<>(barPositioner.getRow(barAnchor, i)); + barRow.removeIf(statusBar -> !statusBar.visible && !ignoreVisibility); + if (barRow.isEmpty()) continue; - public static boolean extractRenderState(GuiGraphicsExtractor graphics, Minecraft client) { - LocalPlayer player = client.player; - if (!isEnabled() || player == null) return false; - Collection barCollection = statusBars.values(); - for (StatusBar statusBar : barCollection) { - if (!statusBar.enabled || !statusBar.visible) continue; - statusBar.extractBar(graphics); - } - for (StatusBar statusBar : barCollection) { - if (!statusBar.enabled || !statusBar.visible) continue; - statusBar.extractText(graphics); - } + // Update the positions + float widthPerSize; + if (sizeRule.isTargetSize()) { + int size = 0; + for (StatusBar bar : barRow) size += bar.size; + widthPerSize = (float) sizeRule.totalWidth() / size; - if (Utils.isInTheRift()) { - final int div = SkyblockerConfigManager.get().uiAndVisuals.bars.riftHealthHP ? 1 : 2; - statusBars.get(StatusBarType.HEALTH).updateValues(Math.round(player.getHealth()) / player.getMaxHealth(), 0, Math.round(player.getHealth()) / div, Math.round(player.getMaxHealth()) / div, null); - statusBars.get(StatusBarType.DEFENSE).visible = false; - } else { - StatusBarTracker.Resource health = StatusBarTracker.getHealth(); - statusBars.get(StatusBarType.HEALTH).updateWithResource(health); - int defense = StatusBarTracker.getDefense(); - StatusBar defenseBar = statusBars.get(StatusBarType.DEFENSE); - defenseBar.visible = true; - defenseBar.updateValues(defense / (defense + 100.f), 0, defense, null, null); + } + else + widthPerSize = sizeRule.widthPerSize(); + + if (visibleHealthMove) widthPerSize /= 2; + + int currSize = 0; + int rowSize = barRow.size(); + for (int j = 0; j < rowSize; j++) { + // A bit of a padding + int offsetX = 0; + int lessWidth = 0; + if (!sizeRule.isTargetSize()) { + offsetX = 1; + lessWidth = 2; + } else if (rowSize > 1) { // Technically bars in the middle of 3+ bars will be smaller than the 2 side ones but shh + if (j == 0) lessWidth = 1; + else if (j == rowSize - 1) { + lessWidth = 1; + offsetX = 1; + } else { + lessWidth = 2; + offsetX = 1; + } + } + StatusBar statusBar = barRow.get(j); + statusBar.size = Math.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize()); + + float x = barAnchor.isRight() ? + anchorPosition.x() + (visibleHealthMove ? sizeRule.totalWidth() / 2.f : 0) + currSize * widthPerSize : + anchorPosition.x() - currSize * widthPerSize - statusBar.size * widthPerSize; + statusBar.setX(Mth.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(Mth.floor(statusBar.size * widthPerSize) - lessWidth); + currSize += statusBar.size; + } + if (currSize > 0) row++; + } + + } } - StatusBarTracker.Resource intelligence = StatusBarTracker.getMana(); - if (SkyblockerConfigManager.get().uiAndVisuals.bars.intelligenceDisplay == UIAndVisualsConfig.IntelligenceDisplay.ACCURATE) { - float totalIntelligence = (float) intelligence.max() + intelligence.overflow(); - statusBars.get(StatusBarType.INTELLIGENCE).updateValues(intelligence.value() / totalIntelligence + intelligence.overflow() / totalIntelligence, intelligence.overflow() / totalIntelligence, intelligence.value(), intelligence.max(), intelligence.overflow()); - } else statusBars.get(StatusBarType.INTELLIGENCE).updateWithResource(intelligence); - - StatusBarTracker.Resource speed = StatusBarTracker.getSpeed(); - statusBars.get(StatusBarType.SPEED).updateWithResource(speed); - statusBars.get(StatusBarType.EXPERIENCE).updateValues(player.experienceProgress, 0, player.experienceLevel, null, null); - StatusBarTracker.Resource air = StatusBarTracker.getAir(); - StatusBar airBar = statusBars.get(StatusBarType.AIR); - airBar.updateWithResource(air); - if (player.isUnderWater() != airBar.visible) { - airBar.visible = player.isUnderWater(); - updatePositionsNextFrame = true; + public static boolean isEnabled() { + return SkyblockerConfigManager.get().uiAndVisuals.bars.enableBars && (!Utils.isInTheRift() || SkyblockerConfigManager.get().uiAndVisuals.bars.enableBarsRift); } - if (updatePositionsNextFrame) { - updatePositions(false); - updatePositionsNextFrame = false; + + public static boolean render(GuiGraphicsExtractor context, Minecraft client) { + LocalPlayer player = client.player; + if (!isEnabled() || player == null) return false; + + Collection barCollection = statusBars.values(); + for (StatusBar statusBar : barCollection) { + if (!statusBar.enabled || !statusBar.visible) continue; + statusBar.renderBar(context); + } + // Custom-positioned icons render AFTER all bars so they appear on top of everything + for (StatusBar statusBar : barCollection) { + if (!statusBar.enabled || !statusBar.visible) continue; + if (statusBar.getIconPosition() == StatusBar.IconPosition.CUSTOM) { + statusBar.renderCustomIcon(context); + } + } + for (StatusBar statusBar : barCollection) { + if (!statusBar.enabled || !statusBar.visible) continue; + statusBar.renderText(context); + } + + if (Utils.isInTheRift()) { + final int div = SkyblockerConfigManager.get().uiAndVisuals.bars.riftHealthHP ? 1 : 2; + statusBars.get(StatusBarType.HEALTH).updateValues(Math.round(player.getHealth()) / player.getMaxHealth(), 0, Math.round(player.getHealth()) / div, Math.round(player.getMaxHealth()) / div, null); + statusBars.get(StatusBarType.DEFENSE).visible = false; + } else { + StatusBarTracker.Resource health = StatusBarTracker.getHealth(); + statusBars.get(StatusBarType.HEALTH).updateWithResource(health); + int defense = StatusBarTracker.getDefense(); + StatusBar defenseBar = statusBars.get(StatusBarType.DEFENSE); + defenseBar.visible = true; + defenseBar.updateValues(defense / (defense + 100.f), 0, defense, null, null); + } + + StatusBarTracker.Resource intelligence = StatusBarTracker.getMana(); + if (SkyblockerConfigManager.get().uiAndVisuals.bars.intelligenceDisplay == UIAndVisualsConfig.IntelligenceDisplay.ACCURATE) { + float totalIntelligence = (float) intelligence.max() + intelligence.overflow(); + statusBars.get(StatusBarType.INTELLIGENCE).updateValues(intelligence.value() / totalIntelligence + intelligence.overflow() / totalIntelligence, intelligence.overflow() / totalIntelligence, intelligence.value(), intelligence.max(), intelligence.overflow()); + } else statusBars.get(StatusBarType.INTELLIGENCE).updateWithResource(intelligence); + + StatusBarTracker.Resource speed = StatusBarTracker.getSpeed(); + statusBars.get(StatusBarType.SPEED).updateWithResource(speed); + statusBars.get(StatusBarType.EXPERIENCE).updateValues(player.experienceProgress, 0, player.experienceLevel, null, null); + StatusBarTracker.Resource air = StatusBarTracker.getAir(); + StatusBar airBar = statusBars.get(StatusBarType.AIR); + airBar.updateWithResource(air); + if (player.isUnderWater() != airBar.visible) { + airBar.visible = player.isUnderWater(); + updatePositionsNextFrame = true; + } + if (updatePositionsNextFrame) { + updatePositions(false); + updatePositionsNextFrame = false; + } + return true; } - 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 index d6dafd619f6..c370431a67a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java @@ -33,494 +33,656 @@ import net.minecraft.resources.Identifier; import net.minecraft.util.CommonColors; import net.minecraft.util.StringRepresentable; -import net.minecraft.world.effect.MobEffects; public class StatusBar implements LayoutElement, Renderable, GuiEventListener, NarratableEntry { - private static final Identifier BAR_FILL = SkyblockerMod.id("bars/bar_fill"); - private static final Identifier BAR_BACK = SkyblockerMod.id("bars/bar_back"); - - public static final int ICON_SIZE = 9; - - private final Identifier icon; - private final StatusBarType type; - private Color[] colors; - private @Nullable Color textColor; - - public Color[] getColors() { - return colors; - } - - public boolean hasOverflow() { - return type.hasOverflow(); - } - - public boolean hasMax() { - return type.hasMax(); - } - - public @Nullable Color getTextColor() { - return textColor; - } - - public void setTextColor(@Nullable Color textColor) { - this.textColor = textColor; - } - - public Component getName() { - return type.getName(); - } - - private @Nullable OnClick onClick = null; - public int gridX = 0; - public int gridY = 0; - public float x = 0; - public float y = 0; - public float width = 0; - public BarPositioner.@Nullable BarAnchor anchor = null; - - public int size = 1; - - public float fill = 0; - public float overflowFill = 0; - public boolean inMouse = false; - /** - * Used to hide the bar dynamically, like the oxygen bar - */ - public boolean visible = true; - public boolean enabled = true; - - private Object value = "???"; - private @Nullable Object max = "???"; - private @Nullable Object overflow = "???"; - - private int renderX = 0; - private int renderY = 0; - private int renderWidth = 0; - - private IconPosition iconPosition = IconPosition.LEFT; - private TextPosition textPosition = TextPosition.BAR_CENTER; - - public boolean showMax = false; - public boolean showOverflow = false; - - public StatusBar(StatusBarType type) { - this.icon = SkyblockerMod.id("bars/icons/" + type.getSerializedName()); - this.colors = type.getColors(); - this.textColor = type.getTextColor(); - this.type = type; - } - - protected int transparency(int color) { - if (inMouse) return (color & 0x00FFFFFF) | 0x44_000000; - return color; - } - - @Override - public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - extractBar(graphics); - if (enabled) extractText(graphics); - } - - protected Identifier getIcon() { - return icon; - } - - @SuppressWarnings("incomplete-switch") - public void extractBar(GuiGraphicsExtractor graphics) { - if (renderWidth <= 0) return; - int transparency = transparency(-1); - switch (iconPosition) { - case LEFT -> graphics.blitSprite(RenderPipelines.GUI_TEXTURED, getIcon(), renderX, renderY, ICON_SIZE, ICON_SIZE, transparency); - case RIGHT -> graphics.blitSprite(RenderPipelines.GUI_TEXTURED, getIcon(), renderX + renderWidth - ICON_SIZE, renderY, ICON_SIZE, ICON_SIZE, transparency); - } - - int barWidth = iconPosition.equals(IconPosition.OFF) ? renderWidth : renderWidth - ICON_SIZE - 1; - int barX = iconPosition.equals(IconPosition.LEFT) ? renderX + ICON_SIZE + 1 : renderX; - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, BAR_BACK, barX, renderY + 1, barWidth, 7, transparency); - extractBarFill(graphics, barX, barWidth); - //context.drawText(MinecraftClient.getInstance().textRenderer, gridX + " " + gridY + " s:" + size , x, y-9, Colors.WHITE, true); - } - - protected void extractBarFill(GuiGraphicsExtractor graphics, int barX, int barWith) { - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, renderY + 2, (int) ((barWith - 2) * fill), 5, transparency(colors[0].getRGB())); - - if (hasOverflow() && overflowFill > 0) { - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, renderY + 2, (int) ((barWith - 2) * Math.min(overflowFill, 1)), 5, transparency(colors[1].getRGB())); - } - } - - public void updateValues(float fill, float overflowFill, Object text, @Nullable Object max, @Nullable Object overflow) { - this.value = text; - this.fill = Math.clamp(fill, 0, 1); - this.overflowFill = Math.clamp(overflowFill, 0, 1); - this.max = max; - this.overflow = overflow; - } - - public void updateWithResource(StatusBarTracker.Resource resource) { - updateValues(resource.value() / (float) resource.max(), resource.overflow() / (float) resource.max(), resource.value(), resource.max(), resource.overflow() > 0 ? resource.overflow() : null); - } - - public void extractText(GuiGraphicsExtractor graphics) { - if (!showText()) return; - Font textRenderer = Minecraft.getInstance().font; - int barWidth = iconPosition.equals(IconPosition.OFF) ? renderWidth : renderWidth - ICON_SIZE - 1; - int barX = iconPosition.equals(IconPosition.LEFT) ? renderX + ICON_SIZE + 2 : renderX; - String stringValue = this.value.toString(); - MutableComponent text = Component.literal(stringValue).withStyle(style -> style.withColor((textColor == null ? colors[0] : textColor).getRGB())); - - if (hasMax() && showMax && max != null) { - text.append("/").append(max.toString()); - } - if (hasOverflow() && showOverflow && overflow != null) { - MutableComponent literal = Component.literal(" + ").withStyle(style -> style.withColor(colors[1].getRGB())); - literal.append(overflow.toString()); - text.append(literal); - } - - int textWidth = textRenderer.width(text); - int x; - switch (textPosition) { - case RIGHT -> x = barX + barWidth - textWidth; - case CENTER -> x = this.renderX + (renderWidth - textWidth) / 2; - case BAR_CENTER -> x = barX + (barWidth - textWidth) / 2; - default -> x = barX; // Put on the left by default because I said so. - } - int y = this.renderY - 3; - - int color = transparency((textColor == null ? colors[0] : textColor).getRGB()); - int outlineColor = transparency(CommonColors.BLACK); - - GuiHelper.outlinedText(graphics, Component.translationArg(text), x, y, color, outlineColor); - } - - public void extractCursor(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - int temp_x = renderX; - int temp_y = renderY; - boolean temp_ghost = inMouse; - - renderX = mouseX; - renderY = mouseY; - inMouse = false; - - extractRenderState(graphics, mouseX, mouseY, a); - - renderX = temp_x; - renderY = temp_y; - inMouse = temp_ghost; - } - - // GUI shenanigans - - @Override - public void setX(int x) { - this.renderX = x; - } - - @Override - public void setY(int y) { - this.renderY = y; - } - - @Override - public int getX() { - return renderX; - } - - @Override - public int getY() { - return renderY; - } - - @Override - public int getWidth() { - return renderWidth; - } - - public void setWidth(int width) { - this.renderWidth = width; - } - - @Override - public int getHeight() { - return 9; - } - - @Override - public ScreenRectangle getRectangle() { - return LayoutElement.super.getRectangle(); - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - return mouseX >= renderX && mouseX <= renderX + getWidth() && mouseY >= renderY && mouseY <= renderY + getHeight(); - } - - @Override - public void visitWidgets(Consumer consumer) { - } - - @Override - public void setFocused(boolean focused) { - } - - @Override - public boolean isFocused() { - return false; - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.NONE; - } - - @Override - public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { - if (!isMouseOver(click.x(), click.y())) return false; - if (onClick != null) { - onClick.onClick(this, click); - } - return true; - } - - public void setOnClick(@Nullable OnClick onClick) { - this.onClick = onClick; - } - - @Override - public void updateNarration(NarrationElementOutput builder) { - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("name", getName()) - .append("gridX", gridX) - .append("gridY", gridY) - .append("size", size) - .append("x", renderX) - .append("y", renderY) - .append("width", renderWidth) - .append("anchor", anchor) - .toString(); - } - - public IconPosition getIconPosition() { - return iconPosition; - } - - public void setIconPosition(IconPosition iconPosition) { - this.iconPosition = iconPosition; - } - - public boolean showText() { - return textPosition != TextPosition.OFF; - } - - public TextPosition getTextPosition() { - return textPosition; - } - - public void setTextPosition(TextPosition textPosition) { - this.textPosition = textPosition; - } - - public enum IconPosition implements StringRepresentable { - LEFT, - RIGHT, - OFF; + public static final int ICON_SIZE = 9; + public static final int BAR_HEIGHT = 14; + public static final int MIN_BAR_HEIGHT = 9; + private static final int BAR_BORDER_COLOR = 0xFF1A1A1A; + private static final int BAR_BACKGROUND_COLOR = 0xFF2D2D2D; + + private final Identifier icon; + private final StatusBarType type; + private Color[] colors; + private @Nullable Color textColor; + + public Color[] getColors() { + return colors; + } + + public void setColors(Color[] colors) { + this.colors = colors; + } + + public boolean hasOverflow() { + return type.hasOverflow(); + } + + public boolean hasMax() { + return type.hasMax(); + } + + public @Nullable Color getTextColor() { + return textColor; + } + + public void setTextColor(@Nullable Color textColor) { + this.textColor = textColor; + } + + public Component getName() { + return type.getName(); + } + + private @Nullable OnClick onClick = null; + public int gridX = 0; + public int gridY = 0; + public float x = 0; + public float y = 0; + public float width = 0; + public BarPositioner.@Nullable BarAnchor anchor = null; + + public int size = 1; + public int barHeight = BAR_HEIGHT; + public int borderRadius = 0; + + /** + * When true the bar is positioned relative to the hotbar top-centre in + * GUI-pixel offsets that are scale-independent. Once the user drags the + * bar this flag is cleared and the bar becomes free-floating (anchor = null). + */ + public boolean hotbarRelative = false; + /** GUI-pixel offset from hotbar top-centre X (screenWidth/2). */ + public int hotbarRelOffX = 0; + /** GUI-pixel offset from hotbar top Y (screenHeight - 22). Negative = above. */ + public int hotbarRelOffY = 0; + /** Bar width in GUI pixels when in hotbar-relative mode. */ + public int hotbarPixelWidth = 60; + + public float fill = 0; + public float overflowFill = 0; + public boolean inMouse = false; + /** + * Used to hide the bar dynamically, like the oxygen bar + */ + public boolean visible = true; + public boolean enabled = true; + + private Object value = "???"; + private @Nullable Object max = "???"; + private @Nullable Object overflow = "???"; + + private int renderX = 0; + private int renderY = 0; + private int renderWidth = 0; + + private IconPosition iconPosition = IconPosition.LEFT; + private TextPosition textPosition = TextPosition.BAR_CENTER; + + // Custom sub-element offsets (pixels, relative to bar origin) + public int textCustomOffX = 0; + public int textCustomOffY = 0; + public int iconCustomOffX = 0; + public int iconCustomOffY = 0; + // Custom sub-element scale / size + public float textCustomScale = 1.0f; + public int iconCustomW = ICON_SIZE; + public int iconCustomH = ICON_SIZE; + + public boolean showMax = false; + public boolean showOverflow = false; + + public StatusBar(StatusBarType type) { + this.icon = SkyblockerMod.id("bars/icons/" + type.getSerializedName()); + this.colors = type.getColors(); + this.textColor = type.getTextColor(); + this.type = type; + } + + protected int transparency(int color) { + if (inMouse) return (color & 0x00FFFFFF) | 0x44_000000; + return color; + } @Override - public String getSerializedName() { - return name(); + public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + renderBar(context); + if (iconPosition == IconPosition.CUSTOM) renderCustomIcon(context); + if (enabled) renderText(context); + } + + protected Identifier getIcon() { + return icon; + } + + @SuppressWarnings("incomplete-switch") + public void renderBar(GuiGraphicsExtractor context) { + if (renderWidth <= 0) return; + int transparency = transparency(-1); + int iconY = renderY + (barHeight - ICON_SIZE) / 2; + switch (iconPosition) { + case LEFT -> context.blitSprite(RenderPipelines.GUI_TEXTURED, getIcon(), renderX, iconY, ICON_SIZE, ICON_SIZE, transparency); + case RIGHT -> context.blitSprite(RenderPipelines.GUI_TEXTURED, getIcon(), renderX + renderWidth - ICON_SIZE, iconY, ICON_SIZE, ICON_SIZE, transparency); + // CUSTOM: rendered separately via renderCustomIcon() so it appears above all bars + } + + boolean iconTakesSpace = iconPosition == IconPosition.LEFT || iconPosition == IconPosition.RIGHT; + int barWidth = iconTakesSpace ? renderWidth - ICON_SIZE - 1 : renderWidth; + int barX = iconPosition == IconPosition.LEFT ? renderX + ICON_SIZE + 1 : renderX; + + int r = Math.min(borderRadius, Math.min(barWidth, barHeight) / 2); + fillRounded(context, barX, renderY, barWidth, barHeight, r, transparency(BAR_BORDER_COLOR)); + fillRounded(context, barX + 1, renderY + 1, barWidth - 2, barHeight - 2, Math.max(0, r - 1), transparency(BAR_BACKGROUND_COLOR)); + drawBarFill(context, barX, barWidth); + } + + /** Renders the icon at its custom position. Called AFTER all bars render, so it appears on top. */ + public void renderCustomIcon(GuiGraphicsExtractor context) { + if (renderWidth <= 0 || iconPosition != IconPosition.CUSTOM) return; + context.blitSprite(RenderPipelines.GUI_TEXTURED, getIcon(), + renderX + iconCustomOffX, renderY + iconCustomOffY, iconCustomW, iconCustomH, transparency(-1)); + } + + protected void drawBarFill(GuiGraphicsExtractor context, int barX, int barWidth) { + int innerW = barWidth - 2; + int innerH = barHeight - 2; + int ir = Math.max(0, Math.min(borderRadius, Math.min(barWidth, barHeight) / 2) - 1); + int fillPx = (int) (innerW * fill); + if (fillPx > 0) { + fillRoundedClipped(context, barX + 1, getY() + 1, innerW, innerH, ir, fillPx, transparency(colors[0].getRGB())); + } + if (hasOverflow() && overflowFill > 0) { + int overflowPx = (int) (innerW * Math.min(overflowFill, 1)); + if (overflowPx > 0) { + fillRoundedClipped(context, barX + 1, getY() + 1, innerW, innerH, ir, overflowPx, transparency(colors[1].getRGB())); + } + } + } + + // ────────────── Rounded fill helpers ────────────── + + /** + * Draws a filled rounded rectangle. When r==0 falls back to a plain fill. + * Each row is computed from the circle formula so corners are pixel-perfect. + */ + protected static void fillRounded(GuiGraphicsExtractor ctx, int x, int y, int w, int h, int r, int color) { + if (w <= 0 || h <= 0) return; + if (r <= 0) { ctx.fill(x, y, x + w, y + h, color); return; } + int cr = Math.min(r, Math.min(w, h) / 2); + for (int row = 0; row < h; row++) { + double py = row + 0.5; + int xOff = 0; + if (py < cr) { + double cy = cr - py; + xOff = (int) (cr - Math.sqrt(Math.max(0.0, (double) cr * cr - cy * cy)) + 0.5); + } else if (py > h - cr) { + double cy = py - (h - cr); + xOff = (int) (cr - Math.sqrt(Math.max(0.0, (double) cr * cr - cy * cy)) + 0.5); + } + int rx1 = x + xOff, rx2 = x + w - xOff; + if (rx1 < rx2) ctx.fill(rx1, y + row, rx2, y + row + 1, color); + } + } + + /** + * Draws a rounded rectangle clipped to fillW pixels wide. The right edge is + * a straight cut when fillW < w, and rounds naturally when fillW ≥ w. + * Used so bar fills respect rounded corners without over-drawing. + */ + protected static void fillRoundedClipped(GuiGraphicsExtractor ctx, int x, int y, int w, int h, int r, int fillW, int color) { + if (w <= 0 || h <= 0 || fillW <= 0) return; + int cr = Math.min(r, Math.min(w, h) / 2); + for (int row = 0; row < h; row++) { + double py = row + 0.5; + int xOff = 0; + if (py < cr) { + double cy = cr - py; + xOff = (int) (cr - Math.sqrt(Math.max(0.0, (double) cr * cr - cy * cy)) + 0.5); + } else if (py > h - cr) { + double cy = py - (h - cr); + xOff = (int) (cr - Math.sqrt(Math.max(0.0, (double) cr * cr - cy * cy)) + 0.5); + } + int rx1 = x + xOff; + // Right edge: whichever is smaller — the fill level or the rounded right boundary + int rx2 = Math.min(x + fillW, x + w - xOff); + if (rx1 < rx2) ctx.fill(rx1, y + row, rx2, y + row + 1, color); + } + } + + public void updateValues(float fill, float overflowFill, Object text, @Nullable Object max, @Nullable Object overflow) { + this.value = text; + this.fill = Math.clamp(fill, 0, 1); + this.overflowFill = Math.clamp(overflowFill, 0, 1); + this.max = max; + this.overflow = overflow; + } + + public void updateWithResource(StatusBarTracker.Resource resource) { + updateValues(resource.value() / (float) resource.max(), resource.overflow() / (float) resource.max(), resource.value(), resource.max(), resource.overflow() > 0 ? resource.overflow() : null); + } + + public void renderText(GuiGraphicsExtractor context) { + if (!showText()) return; + Font textRenderer = Minecraft.getInstance().font; + + boolean iconTakesSpace = iconPosition == IconPosition.LEFT || iconPosition == IconPosition.RIGHT; + int barWidth = iconTakesSpace ? renderWidth - ICON_SIZE - 1 : renderWidth; + int barX = iconPosition == IconPosition.LEFT ? renderX + ICON_SIZE + 2 : renderX; + + String stringValue = this.value.toString(); + // Use white text inside the bar for maximum contrast; fall back to type text color + int textArgb = textColor != null ? textColor.getRGB() : 0xFFFFFFFF; + MutableComponent text = Component.literal(stringValue).withStyle(style -> style.withColor(textArgb)); + + if (hasMax() && showMax && max != null) { + text.append("/").append(max.toString()); + } + if (hasOverflow() && showOverflow && overflow != null) { + MutableComponent literal = Component.literal(" + ").withStyle(style -> style.withColor(colors[1].getRGB())); + literal.append(overflow.toString()); + text.append(literal); + } + + int textWidth = textRenderer.width(text); + int x; + int y; + switch (textPosition) { + case RIGHT -> { x = barX + barWidth - textWidth - 2; y = renderY + (barHeight - 9) / 2 + 1; } + case BAR_CENTER -> { x = barX + (barWidth - textWidth) / 2; y = renderY + (barHeight - 9) / 2 + 1; } + case CUSTOM -> { x = renderX + textCustomOffX; y = renderY + textCustomOffY; } + default -> { x = barX + 2; y = renderY + (barHeight - 9) / 2 + 1; } // LEFT is the default + } + + int color = transparency(textArgb); + int outlineColor = transparency(CommonColors.BLACK); + + if (textPosition == TextPosition.CUSTOM && textCustomScale != 1.0f) { + context.pose().pushMatrix(); + context.pose().translate((float) x, (float) y); + context.pose().scale(textCustomScale, textCustomScale); + GuiHelper.outlinedText(context, Component.translationArg(text), 0, 0, color, outlineColor); + context.pose().popMatrix(); + } else { + GuiHelper.outlinedText(context, Component.translationArg(text), x, y, color, outlineColor); + } + } + + public void renderCursor(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + int temp_x = renderX; + int temp_y = renderY; + boolean temp_ghost = inMouse; + + renderX = mouseX; + renderY = mouseY; + inMouse = false; + + extractRenderState(context, mouseX, mouseY, delta); + + renderX = temp_x; + renderY = temp_y; + inMouse = temp_ghost; } + // GUI shenanigans + @Override - public String toString() { - return I18n.get("skyblocker.bars.config.commonPosition." + name()); + public void setX(int x) { + this.renderX = x; } - } - public enum TextPosition implements StringRepresentable { - LEFT, - CENTER, - BAR_CENTER, - RIGHT, - OFF; + @Override + public void setY(int y) { + this.renderY = y; + } @Override - public String getSerializedName() { - return name(); + public int getX() { + return renderX; } @Override - public String toString() { - if (this == CENTER || this == BAR_CENTER) return I18n.get("skyblocker.bars.config.textPosition." + name()); - return I18n.get("skyblocker.bars.config.commonPosition." + name()); - } - } - - @FunctionalInterface - public interface OnClick { - void onClick(StatusBar statusBar, MouseButtonEvent click); - } - - 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); - if (!object.has("enabled")) { - enabled = anchor != null; - } else enabled = object.get("enabled").getAsBoolean(); - if (anchor != null) { - this.size = object.get("size").getAsInt(); - this.gridX = object.get("x").getAsInt(); - this.gridY = object.get("y").getAsInt(); - } else { - this.width = object.get("size").getAsFloat(); - this.x = object.get("x").getAsFloat(); - this.y = object.get("y").getAsFloat(); - } - // these are optional too, why not - if (object.has("icon_position")) this.iconPosition = IconPosition.valueOf(object.get("icon_position").getAsString().trim()); - // backwards compat teehee - if (object.has("show_text")) this.textPosition = object.get("show_text").getAsBoolean() ? TextPosition.BAR_CENTER : TextPosition.OFF; - if (object.has("text_position")) this.textPosition = TextPosition.valueOf(object.get("text_position").getAsString().trim()); - if (object.has("show_max")) this.showMax = object.get("show_max").getAsBoolean(); - if (object.has("show_overflow")) this.showOverflow = object.get("show_overflow").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)); - } - if (anchor != null) { - object.addProperty("anchor", anchor.toString()); - } else object.addProperty("anchor", "null"); - if (anchor != null) { - object.addProperty("x", gridX); - object.addProperty("y", gridY); - object.addProperty("size", size); - } else { - object.addProperty("size", width); - object.addProperty("x", x); - object.addProperty("y", y); - } - object.addProperty("icon_position", iconPosition.getSerializedName()); - object.addProperty("text_position", textPosition.getSerializedName()); - object.addProperty("show_max", showMax); - object.addProperty("show_overflow", showOverflow); - object.addProperty("enabled", enabled); - return object; - } - - public static class ManaStatusBar extends StatusBar { - - public ManaStatusBar(StatusBarType type) { - super(type); + public int getY() { + return renderY; } @Override - protected void extractBarFill(GuiGraphicsExtractor graphics, int barX, int barWith) { - if (hasOverflow() && overflowFill > 0) { - if (overflowFill > fill && SkyblockerConfigManager.get().uiAndVisuals.bars.intelligenceDisplay == UIAndVisualsConfig.IntelligenceDisplay.IN_FRONT) { - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWith - 2) * Math.min(overflowFill, 1)), 5, transparency(getColors()[1].getRGB())); - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWith - 2) * fill), 5, transparency(getColors()[0].getRGB())); - } else { - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWith - 2) * fill), 5, transparency(getColors()[0].getRGB())); - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWith - 2) * Math.min(overflowFill, 1)), 5, transparency(getColors()[1].getRGB())); - } - } else { - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWith - 2) * fill), 5, transparency(getColors()[0].getRGB())); - } + public int getWidth() { + return renderWidth; + } + + public void setWidth(int width) { + this.renderWidth = width; } @Override - public void updateValues(float fill, float overflowFill, Object text, @Nullable Object max, @Nullable Object overflow) { - super.updateValues(fill, overflowFill, StatusBarTracker.isManaEstimated() ? "~" + text : text, max, overflow); + public int getHeight() { + return barHeight; } - } - public static class ExperienceStatusBar extends StatusBar { - private static final Identifier CLOCK_ICON = SkyblockerMod.id("bars/icons/rift_time"); - public ExperienceStatusBar(StatusBarType type) { - super(type); + @Override + public ScreenRectangle getRectangle() { + return LayoutElement.super.getRectangle(); } @Override - protected Identifier getIcon() { - return Utils.isInTheRift() ? CLOCK_ICON : super.getIcon(); + public boolean isMouseOver(double mouseX, double mouseY) { + return mouseX >= renderX && mouseX <= renderX + getWidth() && mouseY >= renderY && mouseY <= renderY + getHeight(); } @Override - public void updateValues(float fill, float overflowFill, Object text, @Nullable Object max, @Nullable Object overflow) { - if (Utils.isInTheRift() && text instanceof Integer time) { - text = time < 60 ? time + "s" : String.format("%dm%02ds", time / 60, time % 60); - } - super.updateValues(fill, overflowFill, text, max, overflow); + public void visitWidgets(Consumer consumer) { } - } - public static class HealthStatusBar extends StatusBar { - private static final Color WITHER_COLOR = new Color(76, 48, 57); - private static final Color POISON_COLOR = new Color(94, 78, 18); - private static final Identifier WITHER_ICON = SkyblockerMod.id("bars/icons/health_wither"); - private static final Identifier POISON_ICON = SkyblockerMod.id("bars/icons/health_poison"); + @Override + public void setFocused(boolean focused) { + } - public HealthStatusBar(StatusBarType type) { - super(type); + @Override + public boolean isFocused() { + return false; } @Override - protected void extractBarFill(GuiGraphicsExtractor graphics, int barX, int barWidth) { - Minecraft client = Minecraft.getInstance(); - boolean withering = client.player != null && client.player.hasEffect(MobEffects.WITHER); - boolean poisoned = client.player != null && client.player.hasEffect(MobEffects.POISON); - - int fillColor; - if (withering) { - fillColor = WITHER_COLOR.getRGB(); - } else if (poisoned) { - fillColor = POISON_COLOR.getRGB(); - } else { - fillColor = getColors()[0].getRGB(); - } - - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWidth - 2) * fill), 5, transparency(fillColor)); - if (hasOverflow() && overflowFill > 0) { - GuiHelper.nineSliceColored(graphics, BAR_FILL, barX + 1, getY() + 2, (int) ((barWidth - 2) * Math.min(overflowFill, 1)), 5, transparency(getColors()[1].getRGB())); - } + public NarrationPriority narrationPriority() { + return NarrationPriority.NONE; } @Override - protected Identifier getIcon() { - Minecraft client = Minecraft.getInstance(); - if (client.player != null) { - if (client.player.hasEffect(MobEffects.WITHER)) return WITHER_ICON; - else if (client.player.hasEffect(MobEffects.POISON)) return POISON_ICON; - } - return super.getIcon(); - } - } + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + if (!isMouseOver(click.x(), click.y())) return false; + if (onClick != null) { + onClick.onClick(this, click); + } + return true; + } + + public void setOnClick(@Nullable OnClick onClick) { + this.onClick = onClick; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("gridX", gridX) + .append("gridY", gridY) + .append("size", size) + .append("x", renderX) + .append("y", renderY) + .append("width", renderWidth) + .append("anchor", anchor) + .toString(); + } + + public IconPosition getIconPosition() { + return iconPosition; + } + + public void setIconPosition(IconPosition iconPosition) { + this.iconPosition = iconPosition; + } + + public boolean showText() { + return textPosition != TextPosition.OFF; + } + + public TextPosition getTextPosition() { + return textPosition; + } + + public void setTextPosition(TextPosition textPosition) { + this.textPosition = textPosition; + } + + /** + * Exact visible bounds of the custom text element. Used for outline drawing in the config screen. + */ + public ScreenRectangle getTextVisualArea(Font font) { + int tx = renderX + textCustomOffX; + int ty = renderY + textCustomOffY; + String sample = value.toString() + (showMax && max != null ? "/" + max : ""); + int tw = Math.max(8, (int) (font.width(sample) * textCustomScale)); + int th = Math.max(4, (int) (9 * textCustomScale)); + return new ScreenRectangle(tx, ty, tw, th); + } + + /** + * Exact visible bounds of the custom icon element. Used for outline drawing in the config screen. + */ + public ScreenRectangle getIconVisualArea() { + return new ScreenRectangle(renderX + iconCustomOffX, renderY + iconCustomOffY, + Math.max(4, iconCustomW), Math.max(4, iconCustomH)); + } + + /** Expanded hit area for clicking the custom text element (visual area + 8px padding on each side). */ + public ScreenRectangle getTextHitArea(Font font) { + ScreenRectangle v = getTextVisualArea(font); + int pad = 8; + return new ScreenRectangle(v.position().x() - pad, v.position().y() - pad, + v.width() + pad * 2, v.height() + pad * 2); + } + + /** Expanded hit area for clicking the custom icon element (visual area + 8px padding on each side). */ + public ScreenRectangle getIconHitArea() { + ScreenRectangle v = getIconVisualArea(); + int pad = 8; + return new ScreenRectangle(v.position().x() - pad, v.position().y() - pad, + v.width() + pad * 2, v.height() + pad * 2); + } + + public enum IconPosition implements StringRepresentable { + LEFT, + RIGHT, + OFF, + CUSTOM; + + @Override + public String getSerializedName() { + return name(); + } + + @Override + public String toString() { + if (this == CUSTOM) return I18n.get("skyblocker.bars.config.commonPosition.CUSTOM"); + return I18n.get("skyblocker.bars.config.commonPosition." + name()); + } + } + + public enum TextPosition implements StringRepresentable { + LEFT, + CENTER, + BAR_CENTER, + RIGHT, + OFF, + CUSTOM; + + @Override + public String getSerializedName() { + return name(); + } + + @Override + public String toString() { + return switch (this) { + case BAR_CENTER -> I18n.get("skyblocker.bars.config.textPosition.BAR_CENTER"); + case LEFT -> I18n.get("skyblocker.bars.config.textPosition.LEFT"); + case RIGHT -> I18n.get("skyblocker.bars.config.textPosition.RIGHT"); + case CUSTOM -> I18n.get("skyblocker.bars.config.textPosition.CUSTOM"); + case CENTER -> I18n.get("skyblocker.bars.config.textPosition.BAR_CENTER"); // legacy + default -> I18n.get("skyblocker.bars.config.commonPosition." + name()); + }; + } + } + + @FunctionalInterface + public interface OnClick { + void onClick(StatusBar statusBar, MouseButtonEvent click); + } + + 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(); + if (maybeAnchor.equals("HOTBAR_RELATIVE")) { + this.anchor = null; + this.hotbarRelative = true; + this.hotbarRelOffX = object.has("hotbar_off_x") ? object.get("hotbar_off_x").getAsInt() : 0; + this.hotbarRelOffY = object.has("hotbar_off_y") ? object.get("hotbar_off_y").getAsInt() : 0; + this.hotbarPixelWidth = object.has("hotbar_pixel_w") ? object.get("hotbar_pixel_w").getAsInt() : 60; + enabled = object.has("enabled") ? object.get("enabled").getAsBoolean() : true; + } else { + this.hotbarRelative = false; + this.anchor = maybeAnchor.equals("null") ? null : BarPositioner.BarAnchor.valueOf(maybeAnchor); + if (!object.has("enabled")) { + enabled = anchor != null; + } else enabled = object.get("enabled").getAsBoolean(); + if (anchor != null) { + this.size = object.get("size").getAsInt(); + this.gridX = object.get("x").getAsInt(); + this.gridY = object.get("y").getAsInt(); + } else { + this.width = object.get("size").getAsFloat(); + this.x = object.get("x").getAsFloat(); + this.y = object.get("y").getAsFloat(); + } + } + // these are optional too, why not + if (object.has("icon_position")) this.iconPosition = IconPosition.valueOf(object.get("icon_position").getAsString().trim()); + // backwards compat teehee + if (object.has("show_text")) this.textPosition = object.get("show_text").getAsBoolean() ? TextPosition.BAR_CENTER : TextPosition.OFF; + if (object.has("text_position")) { + TextPosition tp = TextPosition.valueOf(object.get("text_position").getAsString().trim()); + this.textPosition = tp == TextPosition.CENTER ? TextPosition.BAR_CENTER : tp; + } + if (object.has("show_max")) this.showMax = object.get("show_max").getAsBoolean(); + if (object.has("show_overflow")) this.showOverflow = object.get("show_overflow").getAsBoolean(); + if (object.has("bar_height")) this.barHeight = Math.max(MIN_BAR_HEIGHT, object.get("bar_height").getAsInt()); + if (object.has("border_radius")) this.borderRadius = Math.max(0, object.get("border_radius").getAsInt()); + if (object.has("text_custom_off_x")) this.textCustomOffX = object.get("text_custom_off_x").getAsInt(); + if (object.has("text_custom_off_y")) this.textCustomOffY = object.get("text_custom_off_y").getAsInt(); + if (object.has("icon_custom_off_x")) this.iconCustomOffX = object.get("icon_custom_off_x").getAsInt(); + if (object.has("icon_custom_off_y")) this.iconCustomOffY = object.get("icon_custom_off_y").getAsInt(); + if (object.has("text_custom_scale")) this.textCustomScale = Math.max(0.5f, Math.min(4.0f, object.get("text_custom_scale").getAsFloat())); + if (object.has("icon_custom_w")) this.iconCustomW = Math.max(4, Math.min(64, object.get("icon_custom_w").getAsInt())); + if (object.has("icon_custom_h")) this.iconCustomH = Math.max(4, Math.min(64, object.get("icon_custom_h").getAsInt())); + } + + 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)); + } + if (hotbarRelative) { + object.addProperty("anchor", "HOTBAR_RELATIVE"); + object.addProperty("hotbar_off_x", hotbarRelOffX); + object.addProperty("hotbar_off_y", hotbarRelOffY); + object.addProperty("hotbar_pixel_w", hotbarPixelWidth); + } else if (anchor != null) { + object.addProperty("anchor", anchor.toString()); + object.addProperty("x", gridX); + object.addProperty("y", gridY); + object.addProperty("size", size); + } else { + object.addProperty("anchor", "null"); + object.addProperty("size", width); + object.addProperty("x", x); + object.addProperty("y", y); + } + object.addProperty("icon_position", iconPosition.getSerializedName()); + object.addProperty("text_position", textPosition.getSerializedName()); + object.addProperty("show_max", showMax); + object.addProperty("show_overflow", showOverflow); + object.addProperty("enabled", enabled); + object.addProperty("bar_height", barHeight); + object.addProperty("border_radius", borderRadius); + if (iconPosition == IconPosition.CUSTOM) { + object.addProperty("icon_custom_off_x", iconCustomOffX); + object.addProperty("icon_custom_off_y", iconCustomOffY); + object.addProperty("icon_custom_w", iconCustomW); + object.addProperty("icon_custom_h", iconCustomH); + } + if (textPosition == TextPosition.CUSTOM) { + object.addProperty("text_custom_off_x", textCustomOffX); + object.addProperty("text_custom_off_y", textCustomOffY); + object.addProperty("text_custom_scale", textCustomScale); + } + return object; + } + + public static class HealthStatusBar extends StatusBar { + public HealthStatusBar(StatusBarType type) { + super(type); + } + } + + public static class ManaStatusBar extends StatusBar { + + public ManaStatusBar(StatusBarType type) { + super(type); + } + + @Override + protected void drawBarFill(GuiGraphicsExtractor context, int barX, int barWith) { + int innerW = barWith - 2; + int innerH = barHeight - 2; + int ir = Math.max(0, Math.min(borderRadius, Math.min(barWith, barHeight) / 2) - 1); + int bx = barX + 1; + int by = getY() + 1; + if (hasOverflow() && overflowFill > 0) { + if (overflowFill > fill && SkyblockerConfigManager.get().uiAndVisuals.bars.intelligenceDisplay == UIAndVisualsConfig.IntelligenceDisplay.IN_FRONT) { + int ovPx = (int) (innerW * Math.min(overflowFill, 1)); + if (ovPx > 0) fillRoundedClipped(context, bx, by, innerW, innerH, ir, ovPx, transparency(getColors()[1].getRGB())); + int fillPx = (int) (innerW * fill); + if (fillPx > 0) fillRoundedClipped(context, bx, by, innerW, innerH, ir, fillPx, transparency(getColors()[0].getRGB())); + } else { + int fillPx = (int) (innerW * fill); + if (fillPx > 0) fillRoundedClipped(context, bx, by, innerW, innerH, ir, fillPx, transparency(getColors()[0].getRGB())); + int ovPx = (int) (innerW * Math.min(overflowFill, 1)); + if (ovPx > 0) fillRoundedClipped(context, bx, by, innerW, innerH, ir, ovPx, transparency(getColors()[1].getRGB())); + } + } else { + int fillPx = (int) (innerW * fill); + if (fillPx > 0) fillRoundedClipped(context, bx, by, innerW, innerH, ir, fillPx, transparency(getColors()[0].getRGB())); + } + } + + @Override + public void updateValues(float fill, float overflowFill, Object text, @Nullable Object max, @Nullable Object overflow) { + super.updateValues(fill, overflowFill, StatusBarTracker.isManaEstimated() ? "~" + text : text, max, overflow); + } + } + + public static class ExperienceStatusBar extends StatusBar { + private static final Identifier CLOCK_ICON = SkyblockerMod.id("bars/icons/rift_time"); + public ExperienceStatusBar(StatusBarType type) { + super(type); + } + + @Override + protected Identifier getIcon() { + return Utils.isInTheRift() ? CLOCK_ICON : super.getIcon(); + } + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java index e824b7a8ef7..57978517e10 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java @@ -12,13 +12,11 @@ import java.util.Collection; import java.util.HashMap; -import java.util.LinkedList; import java.util.Map; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.PopupScreen; -import net.minecraft.client.gui.navigation.ScreenAxis; import net.minecraft.client.gui.navigation.ScreenDirection; +import net.minecraft.client.input.KeyEvent; import net.minecraft.client.gui.navigation.ScreenPosition; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.Screen; @@ -28,375 +26,820 @@ import net.minecraft.resources.Identifier; public class StatusBarsConfigScreen extends Screen { - private static final Identifier HOTBAR_TEXTURE = Identifier.withDefaultNamespace("hud/hotbar"); - private static final int HOTBAR_WIDTH = 182; - private static final float RESIZE_THRESHOLD = 0.75f; - private static final int BAR_MINIMUM_WIDTH = 30; - // prioritize left and right cuz they are much smaller than up and down - private static final ScreenDirection[] DIRECTION_CHECK_ORDER = new ScreenDirection[]{ScreenDirection.LEFT, ScreenDirection.RIGHT, ScreenDirection.UP, ScreenDirection.DOWN}; - - private final Map> rectToBar = new HashMap<>(); - /** - * Contains the hovered bar and a boolean that is true if hovering the right side or false otherwise. - */ - private final ObjectBooleanPair<@Nullable StatusBar> resizeHover = new ObjectBooleanMutablePair<>(null, false); - private final Pair<@Nullable StatusBar, @Nullable StatusBar> resizedBars = ObjectObjectMutablePair.of(null, null); - - private @Nullable StatusBar cursorBar = null; - private ScreenPosition cursorOffset = new ScreenPosition(0, 0); - private BarLocation currentInsertLocation = new BarLocation(null, 0, 0); - - private boolean resizing = false; - private EditBarWidget editBarWidget; - - public StatusBarsConfigScreen() { - super(Component.nullToEmpty("Status Bars Config")); - } - - - @Override - public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - super.extractRenderState(graphics, mouseX, mouseY, a); - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_TEXTURE, width / 2 - HOTBAR_WIDTH / 2, height - 22, HOTBAR_WIDTH, 22); - editBarWidget.extractRenderState(graphics, mouseX, mouseY, a); - - Window window = minecraft.getWindow(); - int scaleFactor = window.calculateScale(0, minecraft.isEnforceUnicode()) - window.getGuiScale() + 3; - if ((scaleFactor & 2) == 0) scaleFactor++; - - ScreenRectangle mouseRect = new ScreenRectangle(new ScreenPosition(mouseX - scaleFactor / 2, mouseY - scaleFactor / 2), scaleFactor, scaleFactor); - - if (cursorBar != null) { - cursorBar.extractCursor(graphics, mouseX + cursorOffset.x(), mouseY + cursorOffset.y(), a); - boolean inserted = false; - boolean updatePositions = false; - rectLoop: - for (ScreenRectangle screenRect : rectToBar.keySet()) { - for (ScreenDirection direction : DIRECTION_CHECK_ORDER) { - boolean overlaps = screenRect.getBorder(direction).step(direction).overlaps(mouseRect); - - if (overlaps) { - Pair barPair = rectToBar.get(screenRect); - BarLocation barSnap = barPair.right(); - if (barSnap.barAnchor() == null) break; - if (direction.getAxis().equals(ScreenAxis.VERTICAL)) { - int neighborInsertY = getNeighborInsertY(barSnap, !direction.isPositive()); - inserted = true; - 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); - updatePositions = true; - } - } else { - int neighborInsertX = getNeighborInsertX(barSnap, direction.isPositive()); - inserted = true; - 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); - updatePositions = true; - } - } - break rectLoop; - } + private static final Identifier HOTBAR_TEXTURE = Identifier.withDefaultNamespace("hud/hotbar"); + private static final int HOTBAR_WIDTH = 182; + private static final float RESIZE_THRESHOLD = 0.75f; + private static final int BAR_MINIMUM_WIDTH = 30; + private static final int DRAG_THRESHOLD = 5; + private static final int EDGE_TOLERANCE = 5; + /** How far outside bar bounds the arrows extend (used for "extended zone" hit test). */ + private static final int ARROW_EXT = 14; + + /** Cyan: CUSTOM element handles */ + private static final int HANDLE_COLOR = 0xFF55FFFF; + /** Yellow: selected bar outline */ + private static final int BAR_SEL_COLOR = 0xFFFFFF55; + + private final Map> rectToBar = new HashMap<>(); + /** Hovered bar + boolean: true = right edge, false = left edge */ + private final ObjectBooleanPair<@Nullable StatusBar> resizeHover = new ObjectBooleanMutablePair<>(null, false); + /** Hovered bar + boolean: true = top edge, false = bottom edge (height resize) */ + private final ObjectBooleanPair<@Nullable StatusBar> resizeHeightHover = new ObjectBooleanMutablePair<>(null, false); + private final Pair<@Nullable StatusBar, @Nullable StatusBar> resizedBars = ObjectObjectMutablePair.of(null, null); + + private @Nullable StatusBar cursorBar = null; + /** The currently "active" bar (for outline / right-click menu). */ + private @Nullable StatusBar selectedBar = null; + /** + * Which sub-element within the selected bar is selected. + * null = the bar itself; "text" = custom text element; "icon" = custom icon element. + * When non-null the yellow bar outline is hidden and only cyan handles for that element are shown. + */ + private @Nullable String selectedSubElement = null; + + private ScreenPosition cursorOffset = new ScreenPosition(0, 0); + + private boolean resizing = false; + private boolean mouseButtonHeld = false; + private int dragStartX = 0; + private int dragStartY = 0; + + // ── Height resize (bars) ── + private boolean resizingHeight = false; + private boolean heightResizeFromTop = false; + private @Nullable StatusBar heightResizeBar = null; + private int heightResizeInitialY = 0; + private int heightResizeInitialHeight = 0; + + // ── CUSTOM sub-element move drag ── + private boolean draggingSubElement = false; + private boolean draggingText = false; + private int subDragStartMouseX = 0; + private int subDragStartMouseY = 0; + private int subDragStartOffX = 0; + private int subDragStartOffY = 0; + + // ── Position overlay timer (shown after keyboard nudge) ── + private int nudgeOverlayTimer = 0; + + // ── CUSTOM sub-element resize ── + private enum SubElementEdge { NONE, TEXT_RIGHT, ICON_RIGHT, ICON_BOTTOM } + private SubElementEdge subElementEdgeHover = SubElementEdge.NONE; + private @Nullable StatusBar subElementEdgeBar = null; + + private boolean resizingSubElement = false; + private boolean resizeSubIsText = false; + private boolean resizeSubIsHoriz = true; + private @Nullable StatusBar resizeSubBar = null; + private int resizeSubStartMouse = 0; + private float resizeSubStartScale = 1.0f; + private int resizeSubStartPx = 0; + + private EditBarWidget editBarWidget; + + public StatusBarsConfigScreen() { + super(Component.nullToEmpty("Status Bars Config")); + } + + // ─────────────────────── Helpers ─────────────────────── + + /** True if (x, y) is inside the bar body + the outer arrow zone. */ + private static boolean isInBarExtendedZone(StatusBar bar, int x, int y) { + return x >= bar.getX() - ARROW_EXT && x <= bar.getX() + bar.getWidth() + ARROW_EXT + && y >= bar.getY() - ARROW_EXT && y <= bar.getY() + bar.getHeight() + ARROW_EXT; + } + + /** True if (x, y) is inside the bar body only (not in the arrow gutter). */ + private static boolean isInBarBody(StatusBar bar, int x, int y) { + return x >= bar.getX() && x <= bar.getX() + bar.getWidth() + && y >= bar.getY() && y <= bar.getY() + bar.getHeight(); + } + + private void clearSelection() { + selectedBar = null; + selectedSubElement = null; + editBarWidget.visible = false; + } + + // ─────────────────────── Drag helpers ─────────────────────── + + private void startBarDrag(StatusBar statusBar) { + cursorBar = statusBar; + cursorBar.inMouse = true; + cursorBar.enabled = true; + if (statusBar.anchor != null) + FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar); + // Detach from hotbar-relative anchor → convert current pixel position to screen fractions + if (statusBar.hotbarRelative) { + statusBar.hotbarRelative = false; + statusBar.x = (float) statusBar.getX() / this.width; + statusBar.y = (float) statusBar.getY() / this.height; + statusBar.width = (float) statusBar.getWidth() / this.width; + } else if (statusBar.getWidth() > 0) { + statusBar.width = (float) statusBar.getWidth() / this.width; } - } - if (updatePositions) { + statusBar.anchor = null; FancyStatusBars.updatePositions(true); - return; - } - // check for hovering empty anchors - for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { - ScreenRectangle anchorHitbox = barAnchor.getAnchorHitbox(barAnchor.getAnchorPosition(width, height)); - if (FancyStatusBars.barPositioner.getRowCount(barAnchor) != 0) { - // this fixes flickering - if (FancyStatusBars.barPositioner.getRowCount(barAnchor) == 1) { - LinkedList row = FancyStatusBars.barPositioner.getRow(barAnchor, 0); - if (row.size() == 1 && row.getFirst() == cursorBar && anchorHitbox.overlaps(mouseRect)) inserted = true; - } - continue; + cursorBar.setX(width + 5); + updateScreenRects(); + editBarWidget.visible = false; + } + + /** Tries to start a sub-element RESIZE at (cx, cy). Returns true if started. */ + private boolean tryStartSubElementResize(StatusBar bar, int cx, int cy) { + if (subElementEdgeHover != SubElementEdge.NONE && subElementEdgeBar == bar) { + resizingSubElement = true; + resizeSubBar = bar; + mouseButtonHeld = true; + editBarWidget.visible = false; + switch (subElementEdgeHover) { + case TEXT_RIGHT -> { + resizeSubIsText = true; + resizeSubIsHoriz = true; + resizeSubStartMouse = cx; + resizeSubStartScale = bar.textCustomScale; + } + case ICON_RIGHT -> { + resizeSubIsText = false; + resizeSubIsHoriz = true; + resizeSubStartMouse = cx; + resizeSubStartPx = bar.iconCustomW; + } + case ICON_BOTTOM -> { + resizeSubIsText = false; + resizeSubIsHoriz = false; + resizeSubStartMouse = cy; + resizeSubStartPx = bar.iconCustomH; + } + default -> { resizingSubElement = false; return false; } + } + return true; } + return false; + } - graphics.fill(anchorHitbox.left(), anchorHitbox.top(), anchorHitbox.right(), anchorHitbox.bottom(), 0x99FFFFFF); - if (anchorHitbox.overlaps(mouseRect)) { - inserted = true; - if (currentInsertLocation.barAnchor() == barAnchor) continue; - 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(true); + // ─────────────────────── Render ─────────────────────── + + @Override + public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + super.extractRenderState(context, mouseX, mouseY, delta); + + // Sub-element MOVE drag + if (draggingSubElement && selectedBar != null && mouseButtonHeld) { + int dx = mouseX - subDragStartMouseX; + int dy = mouseY - subDragStartMouseY; + if (draggingText) { + selectedBar.textCustomOffX = subDragStartOffX + dx; + selectedBar.textCustomOffY = subDragStartOffY + dy; + } else { + selectedBar.iconCustomOffX = subDragStartOffX + dx; + selectedBar.iconCustomOffY = subDragStartOffY + dy; + } } - } - if (!inserted) { - if (cursorBar.anchor != null) FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar); - currentInsertLocation = BarLocation.NULL; - FancyStatusBars.updatePositions(true); - cursorBar.setX(width + 5); - } - } else { // Not dragging around a bar - if (resizing) { // actively resizing one or 2 bars - int middleX; // the point between the 2 bars - - StatusBar rightBar = resizedBars.right(); - StatusBar leftBar = resizedBars.left(); - boolean hasRight = rightBar != null; - boolean hasLeft = leftBar != null; - BarPositioner.BarAnchor barAnchor; - if (!hasRight) { - barAnchor = leftBar.anchor; - middleX = leftBar.getX() + leftBar.getWidth(); - } else { - barAnchor = rightBar.anchor; - middleX = rightBar.getX(); + + // Sub-element RESIZE drag + if (resizingSubElement && resizeSubBar != null && mouseButtonHeld) { + if (resizeSubIsText) { + int d = mouseX - resizeSubStartMouse; + resizeSubBar.textCustomScale = Math.max(0.5f, Math.min(4.0f, resizeSubStartScale + d * 0.02f)); + } else if (resizeSubIsHoriz) { + int d = mouseX - resizeSubStartMouse; + resizeSubBar.iconCustomW = Math.max(4, Math.min(64, resizeSubStartPx + d)); + } else { + int d = mouseY - resizeSubStartMouse; + resizeSubBar.iconCustomH = Math.max(4, Math.min(64, resizeSubStartPx + d)); + } } - if (barAnchor != null) { // If is on an anchor - BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule(); - boolean doResize = true; - - 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 / RESIZE_THRESHOLD) { - if (hasRight) { - if (rightBar.size + 1 > sizeRule.maxSize()) doResize = false; - } - if (hasLeft) { - if (leftBar.size - 1 < sizeRule.minSize()) doResize = false; - } - - if (doResize) { - if (hasRight) rightBar.size++; - if (hasLeft) leftBar.size--; - FancyStatusBars.updatePositions(true); - } + // Bar drag threshold check (only when bar itself is selected, not a sub-element) + if (mouseButtonHeld && selectedBar != null && selectedSubElement == null + && cursorBar == null && !resizing && !resizingHeight + && !draggingSubElement && !resizingSubElement) { + double dx = mouseX - dragStartX; + double dy = mouseY - dragStartY; + if (dx * dx + dy * dy > (double) DRAG_THRESHOLD * DRAG_THRESHOLD) { + startBarDrag(selectedBar); } - } else { // towards the right - if (mouseX - middleX > widthPerSize / RESIZE_THRESHOLD) { - if (hasRight) { - if (rightBar.size - 1 < sizeRule.minSize()) doResize = false; - } - if (hasLeft) { - if (leftBar.size + 1 > sizeRule.maxSize()) doResize = false; - } - - if (doResize) { - if (hasRight) rightBar.size--; - if (hasLeft) leftBar.size++; - FancyStatusBars.updatePositions(true); - } + } + + context.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_TEXTURE, width / 2 - HOTBAR_WIDTH / 2, height - 22, HOTBAR_WIDTH, 22); + editBarWidget.extractRenderState(context, mouseX, mouseY, delta); + + Window window = minecraft.getWindow(); + int scaleFactor = window.calculateScale(0, minecraft.isEnforceUnicode()) - window.getGuiScale() + 3; + if ((scaleFactor & 2) == 0) scaleFactor++; + ScreenRectangle mouseRect = new ScreenRectangle(new ScreenPosition(mouseX - scaleFactor / 2, mouseY - scaleFactor / 2), scaleFactor, scaleFactor); + + // Draw selection visuals + if (selectedBar != null && cursorBar == null && selectedBar.enabled) { + if (selectedSubElement == null) { + // Bar selected: yellow outline with arrows + drawBarOutlineWithArrows(context, selectedBar); + } else if ("text".equals(selectedSubElement) + && selectedBar.getTextPosition() == StatusBar.TextPosition.CUSTOM) { + // Text sub-element selected: cyan handles only + drawCustomElementHandles(context, selectedBar.getTextVisualArea(minecraft.font), true); + } else if ("icon".equals(selectedSubElement) + && selectedBar.getIconPosition() == StatusBar.IconPosition.CUSTOM) { + // Icon sub-element selected: cyan handles only + drawCustomElementHandles(context, selectedBar.getIconVisualArea(), false); } - } - } else { // Freely moving around - if (hasLeft) { - leftBar.setWidth(Math.max(BAR_MINIMUM_WIDTH, mouseX - leftBar.getX())); - } else if (hasRight) { - int endX = rightBar.getX() + rightBar.getWidth(); - rightBar.setX(Math.min(endX - BAR_MINIMUM_WIDTH, mouseX)); - rightBar.setWidth(endX - rightBar.getX()); - } } - } else { // hovering bars - rectLoop: - for (ScreenRectangle screenRect : rectToBar.keySet()) { - for (ScreenDirection direction : new ScreenDirection[]{ScreenDirection.LEFT, ScreenDirection.RIGHT}) { - boolean overlaps = screenRect.getBorder(direction).step(direction).overlaps(mouseRect); - - if (overlaps && !editBarWidget.isMouseOver(mouseX, mouseY)) { - Pair barPair = rectToBar.get(screenRect); - BarLocation barLocation = barPair.right(); - StatusBar bar = barPair.left(); - if (!bar.enabled) break; - boolean right = direction.equals(ScreenDirection.RIGHT); - if (barLocation.barAnchor() != null) { - if (barLocation.barAnchor().getSizeRule().isTargetSize() && !FancyStatusBars.barPositioner.hasNeighbor(barLocation.barAnchor(), barLocation.y(), barLocation.x(), right)) { - break; + // Decrement nudge overlay timer each frame + if (nudgeOverlayTimer > 0) nudgeOverlayTimer--; + + if (cursorBar != null) { + int bx = mouseX + cursorOffset.x(); + int by = mouseY + cursorOffset.y(); + cursorBar.renderCursor(context, bx, by, delta); + + // Position overlay: X from left, Y from bottom-left (bottom of bar = Y 0) + int posX = bx; + int posY = this.height - by - cursorBar.getHeight(); + String posText = "X: " + posX + " Y: " + posY; + int labelX = bx + cursorBar.getWidth() / 2 - minecraft.font.width(posText) / 2; + int labelY = by - 14; + // Dark background for readability + context.fill(labelX - 2, labelY - 1, labelX + minecraft.font.width(posText) + 2, labelY + 10, 0xAA000000); + context.text(minecraft.font, posText, labelX, labelY, BAR_SEL_COLOR, false); + + } else if (draggingSubElement && selectedBar != null) { + // Position overlay while dragging a text or icon sub-element + String overlayText; + int centerX, aboveY; + if (draggingText) { + overlayText = "offX: " + selectedBar.textCustomOffX + " offY: " + selectedBar.textCustomOffY; + ScreenRectangle vis = selectedBar.getTextVisualArea(minecraft.font); + centerX = vis.position().x() + vis.width() / 2; + aboveY = vis.position().y() - 14; + } else { + overlayText = "offX: " + selectedBar.iconCustomOffX + " offY: " + selectedBar.iconCustomOffY; + ScreenRectangle vis = selectedBar.getIconVisualArea(); + centerX = vis.position().x() + vis.width() / 2; + aboveY = vis.position().y() - 14; + } + int lx = centerX - minecraft.font.width(overlayText) / 2; + context.fill(lx - 2, aboveY - 1, lx + minecraft.font.width(overlayText) + 2, aboveY + 10, 0xAA000000); + context.text(minecraft.font, overlayText, lx, aboveY, BAR_SEL_COLOR, false); + + } else if (nudgeOverlayTimer > 0 && selectedBar != null) { + // Position overlay after a keyboard nudge — show for ~80 frames then fade out + String overlayText; + int centerX, aboveY; + if (selectedSubElement == null) { + // Bar position + overlayText = "X: " + selectedBar.getX() + " Y: " + (this.height - selectedBar.getY() - selectedBar.getHeight()); + centerX = selectedBar.getX() + selectedBar.getWidth() / 2; + aboveY = selectedBar.getY() - 14; + } else if ("text".equals(selectedSubElement)) { + overlayText = "offX: " + selectedBar.textCustomOffX + " offY: " + selectedBar.textCustomOffY; + ScreenRectangle vis = selectedBar.getTextVisualArea(minecraft.font); + centerX = vis.position().x() + vis.width() / 2; + aboveY = vis.position().y() - 14; + } else { + overlayText = "offX: " + selectedBar.iconCustomOffX + " offY: " + selectedBar.iconCustomOffY; + ScreenRectangle vis = selectedBar.getIconVisualArea(); + centerX = vis.position().x() + vis.width() / 2; + aboveY = vis.position().y() - 14; + } + int lx = centerX - minecraft.font.width(overlayText) / 2; + context.fill(lx - 2, aboveY - 1, lx + minecraft.font.width(overlayText) + 2, aboveY + 10, 0xAA000000); + context.text(minecraft.font, overlayText, lx, aboveY, BAR_SEL_COLOR, false); + + } else { + if (resizing) { + int middleX; + StatusBar rightBar = resizedBars.right(); + StatusBar leftBar = resizedBars.left(); + boolean hasRight = rightBar != null; + boolean hasLeft = leftBar != null; + BarPositioner.BarAnchor barAnchor; + if (!hasRight) { barAnchor = leftBar.anchor; middleX = leftBar.getX() + leftBar.getWidth(); } + else { barAnchor = rightBar.anchor; middleX = rightBar.getX(); } + + if (barAnchor != null) { + BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule(); + boolean doResize = true; + float widthPerSize = sizeRule.isTargetSize() + ? (float) sizeRule.totalWidth() / sizeRule.targetSize() + : sizeRule.widthPerSize(); + if (mouseX < middleX) { + if (middleX - mouseX > widthPerSize / RESIZE_THRESHOLD) { + if (hasRight && rightBar.size + 1 > sizeRule.maxSize()) doResize = false; + if (hasLeft && leftBar.size - 1 < sizeRule.minSize()) doResize = false; + if (doResize) { if (hasRight) rightBar.size++; if (hasLeft) leftBar.size--; FancyStatusBars.updatePositions(true); } + } + } else { + if (mouseX - middleX > widthPerSize / RESIZE_THRESHOLD) { + if (hasRight && rightBar.size - 1 < sizeRule.minSize()) doResize = false; + if (hasLeft && leftBar.size + 1 > sizeRule.maxSize()) doResize = false; + if (doResize) { if (hasRight) rightBar.size--; if (hasLeft) leftBar.size++; FancyStatusBars.updatePositions(true); } + } + } + } else { + if (hasLeft) leftBar.setWidth(Math.max(BAR_MINIMUM_WIDTH, mouseX - leftBar.getX())); + else if (hasRight) { int endX = rightBar.getX() + rightBar.getWidth(); rightBar.setX(Math.min(endX - BAR_MINIMUM_WIDTH, mouseX)); rightBar.setWidth(endX - rightBar.getX()); } } - if (!barLocation.barAnchor().getSizeRule().isTargetSize() && barLocation.x() == 0 && barLocation.barAnchor().isRight() != right) - break; - } - resizeHover.first(bar); - resizeHover.right(right); - graphics.requestCursor(CursorTypes.RESIZE_EW); - break rectLoop; + + } else if (resizingHeight && heightResizeBar != null) { + if (heightResizeFromTop) { + int bottom = heightResizeInitialY + heightResizeInitialHeight; + int newTop = Math.min(bottom - StatusBar.MIN_BAR_HEIGHT, mouseY); + heightResizeBar.setY(newTop); + heightResizeBar.barHeight = bottom - newTop; + heightResizeBar.y = (float) newTop / height; + } else { + heightResizeBar.barHeight = Math.max(StatusBar.MIN_BAR_HEIGHT, mouseY - heightResizeBar.getY()); + } + + } else if (resizingSubElement) { + context.requestCursor(resizeSubIsHoriz ? CursorTypes.RESIZE_EW : CursorTypes.RESIZE_NS); + } else { - resizeHover.first(null); + // Hover detection: sub-element resize edges (highest priority when sub-element selected) + subElementEdgeHover = SubElementEdge.NONE; + subElementEdgeBar = null; + if (selectedBar != null && selectedBar.enabled && selectedSubElement != null) { + if ("text".equals(selectedSubElement) && selectedBar.getTextPosition() == StatusBar.TextPosition.CUSTOM) { + ScreenRectangle vis = selectedBar.getTextVisualArea(minecraft.font); + int rx = vis.position().x() + vis.width(); + int ly = vis.position().y(), by_ = ly + vis.height(); + if (Math.abs(mouseX - rx) <= EDGE_TOLERANCE && mouseY >= ly && mouseY <= by_) { + subElementEdgeHover = SubElementEdge.TEXT_RIGHT; + subElementEdgeBar = selectedBar; + context.requestCursor(CursorTypes.RESIZE_EW); + } + } + if (subElementEdgeHover == SubElementEdge.NONE && "icon".equals(selectedSubElement) + && selectedBar.getIconPosition() == StatusBar.IconPosition.CUSTOM) { + ScreenRectangle vis = selectedBar.getIconVisualArea(); + int rx = vis.position().x() + vis.width(); + int byx = vis.position().y() + vis.height(); + int lx = vis.position().x(), ly = vis.position().y(); + if (Math.abs(mouseX - rx) <= EDGE_TOLERANCE && mouseY >= ly && mouseY <= byx) { + subElementEdgeHover = SubElementEdge.ICON_RIGHT; subElementEdgeBar = selectedBar; + context.requestCursor(CursorTypes.RESIZE_EW); + } else if (Math.abs(mouseY - byx) <= EDGE_TOLERANCE && mouseX >= lx && mouseX <= rx) { + subElementEdgeHover = SubElementEdge.ICON_BOTTOM; subElementEdgeBar = selectedBar; + context.requestCursor(CursorTypes.RESIZE_NS); + } + } + } + + if (subElementEdgeHover == SubElementEdge.NONE) { + // Bar horizontal resize edges + boolean foundHorizontal = false; + rectLoop: + for (ScreenRectangle screenRect : rectToBar.keySet()) { + for (ScreenDirection direction : new ScreenDirection[]{ScreenDirection.LEFT, ScreenDirection.RIGHT}) { + if (screenRect.getBorder(direction).step(direction).overlaps(mouseRect) && !editBarWidget.isMouseOver(mouseX, mouseY)) { + Pair barPair = rectToBar.get(screenRect); + BarLocation barLocation = barPair.right(); + StatusBar bar = barPair.left(); + if (!bar.enabled) break; + // Only resize the selected bar + if (bar != selectedBar || selectedSubElement != null) { resizeHover.first(null); break; } + boolean right = direction.equals(ScreenDirection.RIGHT); + if (barLocation.barAnchor() != null) { + 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(bar); resizeHover.right(right); + resizeHeightHover.first(null); + foundHorizontal = true; + context.requestCursor(CursorTypes.RESIZE_EW); + break rectLoop; + } else { resizeHover.first(null); } + } + } + + if (!foundHorizontal) { + resizeHover.first(null); + heightLoop: + for (ScreenRectangle screenRect : rectToBar.keySet()) { + Pair barPair = rectToBar.get(screenRect); + StatusBar bar = barPair.left(); + if (!bar.enabled || bar.anchor != null) continue; + // Only resize the selected bar + if (bar != selectedBar || selectedSubElement != null) continue; + for (ScreenDirection direction : new ScreenDirection[]{ScreenDirection.UP, ScreenDirection.DOWN}) { + if (screenRect.getBorder(direction).step(direction).overlaps(mouseRect) && !editBarWidget.isMouseOver(mouseX, mouseY)) { + resizeHeightHover.first(bar); + resizeHeightHover.right(direction.equals(ScreenDirection.UP)); + context.requestCursor(CursorTypes.RESIZE_NS); + break heightLoop; + } else { resizeHeightHover.first(null); } + } + } + } else { resizeHeightHover.first(null); } + } } - } } - } } - } - - 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; + + // ─────────────────────── Outline / arrow drawing ─────────────────────── + + /** Yellow selection outline + directional move arrows around the bar. */ + private void drawBarOutlineWithArrows(GuiGraphicsExtractor ctx, StatusBar bar) { + int x = bar.getX(), y = bar.getY(); + int w = bar.getWidth(), h = bar.getHeight(); + // 1px inner border (drawn ON the bar edge, bar still fully visible underneath) + ctx.fill(x, y, x + w, y + 1, BAR_SEL_COLOR); + ctx.fill(x, y + h - 1, x + w, y + h, BAR_SEL_COLOR); + ctx.fill(x, y, x + 1, y + h, BAR_SEL_COLOR); + ctx.fill(x + w - 1, y, x + w, y + h, BAR_SEL_COLOR); + // Arrows at midpoints of each edge (outside the bar) + int mx = x + w / 2, my = y + h / 2; + fillLeftArrow(ctx, x - 7, my, BAR_SEL_COLOR); + fillRightArrow(ctx, x + w + 6, my, BAR_SEL_COLOR); + fillUpArrow(ctx, mx, y - 7, BAR_SEL_COLOR); + fillDownArrow(ctx, mx, y + h + 6, BAR_SEL_COLOR); } - } - - 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; + + /** + * Cyan border + outward move arrows for a CUSTOM text or icon sub-element. + * Also shows white resize handle squares at resize edges. + * @param isText true=text (right-edge resize only), false=icon (right + bottom resize) + */ + private void drawCustomElementHandles(GuiGraphicsExtractor ctx, ScreenRectangle area, boolean isText) { + int x = area.position().x(), y = area.position().y(); + int w = area.width(), h = area.height(); + // 1px border + ctx.fill(x, y, x + w, y + 1, HANDLE_COLOR); + ctx.fill(x, y + h - 1, x + w, y + h, HANDLE_COLOR); + ctx.fill(x, y, x + 1, y + h, HANDLE_COLOR); + ctx.fill(x + w - 1, y, x + w, y + h, HANDLE_COLOR); + // Move arrows + int mx = x + w / 2, my = y + h / 2; + fillLeftArrow(ctx, x - 7, my, HANDLE_COLOR); + fillRightArrow(ctx, x + w + 6, my, HANDLE_COLOR); + fillUpArrow(ctx, mx, y - 7, HANDLE_COLOR); + fillDownArrow(ctx, mx, y + h + 6, HANDLE_COLOR); + // Right-edge resize handle (white square) + ctx.fill(x + w + 2, my - 3, x + w + 7, my + 3, 0xFFFFFFFF); + // Bottom-edge resize handle (icon only) + if (!isText) ctx.fill(mx - 3, y + h + 2, mx + 3, y + h + 7, 0xFFFFFFFF); } - } - - @Override - protected void init() { - super.init(); - FancyStatusBars.updatePositions(true); - editBarWidget = new EditBarWidget(0, 0, this); - editBarWidget.visible = false; - addWidget(editBarWidget); // rendering separately to have it above hotbar - Collection values = FancyStatusBars.statusBars.values(); - values.forEach(this::setup); - updateScreenRects(); - this.addRenderableWidget(Button.builder(Component.literal("?"), - _ -> minecraft.setScreen(new PopupScreen.Builder(this, Component.translatable("skyblocker.bars.config.explanationTitle")) - .addButton(Component.translatable("gui.ok"), PopupScreen::onClose) - .addMessage(Component.translatable("skyblocker.bars.config.explanation")) - .build())) - .bounds(width - 20, (height - 15) / 2, 15, 15) - .build()); - } - - private void setup(StatusBar statusBar) { - this.addRenderableWidget(statusBar); - statusBar.setOnClick(this::onBarClick); - } - - @Override - public void removed() { - super.removed(); - FancyStatusBars.statusBars.values().forEach(statusBar -> statusBar.setOnClick(null)); - if (cursorBar != null) cursorBar.inMouse = false; - FancyStatusBars.updatePositions(false); - FancyStatusBars.saveBarConfig(); - } - - @Override - public boolean isPauseScreen() { - return false; - } - - private void onBarClick(StatusBar statusBar, MouseButtonEvent click) { - if (click.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) { - cursorOffset = new ScreenPosition((int) (statusBar.getX() - click.x()), (int) (statusBar.getY() - click.y())); - cursorBar = statusBar; - cursorBar.inMouse = true; - cursorBar.enabled = true; - currentInsertLocation = BarLocation.of(cursorBar); - if (statusBar.anchor != null) - FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar); - FancyStatusBars.updatePositions(true); - cursorBar.setX(width + 5); // send it to limbo lol - updateScreenRects(); - } else if (click.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT) { - int x = (int) Math.min(click.x() - 1, width - editBarWidget.getWidth()); - int y = (int) Math.min(click.y() - 1, height - editBarWidget.getHeight()); - editBarWidget.visible = true; - editBarWidget.setStatusBar(statusBar); - editBarWidget.setX(x); - editBarWidget.setY(y); + + private static void fillLeftArrow(GuiGraphicsExtractor ctx, int tipX, int midY, int color) { + ctx.fill(tipX, midY, tipX + 1, midY + 1, color); + ctx.fill(tipX + 1, midY - 1, tipX + 2, midY + 2, color); + ctx.fill(tipX + 2, midY - 2, tipX + 5, midY + 3, color); } - } - - private void updateScreenRects() { - rectToBar.clear(); - FancyStatusBars.statusBars.values().forEach(statusBar1 -> { - if (!statusBar1.enabled) return; - rectToBar.put( - new ScreenRectangle(new ScreenPosition(statusBar1.getX(), statusBar1.getY()), statusBar1.getWidth(), statusBar1.getHeight()), - Pair.of(statusBar1, BarLocation.of(statusBar1))); - }); - } - - @Override - public boolean mouseReleased(MouseButtonEvent click) { - if (cursorBar != null) { - cursorBar.inMouse = false; - if (currentInsertLocation == BarLocation.NULL) { - cursorBar.x = (float) ((click.x() + cursorOffset.x()) / width); - cursorBar.y = (float) ((click.y() + cursorOffset.y()) / height); - cursorBar.width = Math.clamp(cursorBar.width, (float) BAR_MINIMUM_WIDTH / width, 1); - } - currentInsertLocation = BarLocation.NULL; - cursorBar = null; - FancyStatusBars.updatePositions(true); - updateScreenRects(); - return true; - } else if (resizing) { - resizing = false; - - // update x and width if bar has no anchor - StatusBar bar = null; - if (resizedBars.left() != null) bar = resizedBars.left(); - else if (resizedBars.right() != null) bar = resizedBars.right(); - if (bar != null && bar.anchor == null) { - bar.x = (float) bar.getX() / width; - bar.width = (float) bar.getWidth() / width; - } - resizedBars.left(null); - resizedBars.right(null); - updateScreenRects(); - return true; + private static void fillRightArrow(GuiGraphicsExtractor ctx, int tipX, int midY, int color) { + ctx.fill(tipX, midY, tipX + 1, midY + 1, color); + ctx.fill(tipX - 1, midY - 1, tipX, midY + 2, color); + ctx.fill(tipX - 4, midY - 2, tipX - 1, midY + 3, color); + } + private static void fillUpArrow(GuiGraphicsExtractor ctx, int midX, int tipY, int color) { + ctx.fill(midX, tipY, midX + 1, tipY + 1, color); + ctx.fill(midX - 1, tipY + 1, midX + 2, tipY + 2, color); + ctx.fill(midX - 2, tipY + 2, midX + 3, tipY + 5, color); + } + private static void fillDownArrow(GuiGraphicsExtractor ctx, int midX, int tipY, int color) { + ctx.fill(midX, tipY, midX + 1, tipY + 1, color); + ctx.fill(midX - 1, tipY - 1, midX + 2, tipY, color); + ctx.fill(midX - 2, tipY - 4, midX + 3, tipY - 1, color); + } + + // ─────────────────────── Screen lifecycle ─────────────────────── + + @Override + protected void init() { + super.init(); + FancyStatusBars.updatePositions(true); + editBarWidget = new EditBarWidget(0, 0, this); + editBarWidget.visible = false; + addWidget(editBarWidget); + Collection values = FancyStatusBars.statusBars.values(); + values.forEach(this::setup); + updateScreenRects(); + this.addRenderableWidget(Button.builder(Component.literal("?"), + _ -> minecraft.setScreen(new TipsScreen(this))) + .bounds(width - 20, (height - 15) / 2, 15, 15) + .build()); + this.addRenderableWidget(Button.builder(Component.translatable("skyblocker.bars.config.resetToDefault"), + _ -> { + FancyStatusBars.resetToDefaults(); + clearSelection(); + updateScreenRects(); + }) + .bounds(5, 5, 110, 14) + .build()); + } + + private void setup(StatusBar statusBar) { + this.addRenderableWidget(statusBar); + statusBar.setOnClick(this::onBarClick); + } + + @Override + public void removed() { + super.removed(); + FancyStatusBars.statusBars.values().forEach(sb -> sb.setOnClick(null)); + if (cursorBar != null) cursorBar.inMouse = false; + FancyStatusBars.updatePositions(false); + FancyStatusBars.saveBarConfig(); } - return super.mouseReleased(click); - } - - @Override - public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { - StatusBar first = resizeHover.first(); - // want the right click thing to have priority - if (!editBarWidget.isMouseOver(click.x(), click.y()) && click.button() == 0 && first != null) { - BarPositioner.BarAnchor barAnchor = first.anchor; - if (barAnchor != null) { - if (resizeHover.rightBoolean()) { - resizedBars.left(first); - - if (FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.gridY, first.gridX, true)) { - resizedBars.right(FancyStatusBars.barPositioner.getBar(barAnchor, first.gridY, first.gridX + (barAnchor.isRight() ? 1 : -1))); - } else resizedBars.right(null); - } else { - resizedBars.right(first); - if (FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.gridY, first.gridX, false)) { - resizedBars.left(FancyStatusBars.barPositioner.getBar(barAnchor, first.gridY, first.gridX + (barAnchor.isRight() ? -1 : 1))); - } else resizedBars.left(null); + @Override + public boolean isPauseScreen() { return false; } + + // ─────────────────────── Click handlers ─────────────────────── + + private void onBarClick(StatusBar statusBar, MouseButtonEvent click) { + if (click.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + int cx = (int) click.x(), cy = (int) click.y(); + ScreenRectangle clickRect = new ScreenRectangle(cx, cy, 1, 1); + + // ── Check for sub-element resize edge (only when that sub-element is already selected) ── + if (selectedBar == statusBar && selectedSubElement != null) { + if (tryStartSubElementResize(statusBar, cx, cy)) return; + } + + // ── Check if click lands on a CUSTOM text element ── + if (statusBar.getTextPosition() == StatusBar.TextPosition.CUSTOM + && statusBar.getTextHitArea(minecraft.font).overlaps(clickRect)) { + boolean alreadySelected = selectedBar == statusBar && "text".equals(selectedSubElement); + selectedBar = statusBar; + selectedSubElement = "text"; + editBarWidget.visible = false; + if (alreadySelected) { + mouseButtonHeld = true; + subDragStartMouseX = cx; subDragStartMouseY = cy; + subDragStartOffX = statusBar.textCustomOffX; subDragStartOffY = statusBar.textCustomOffY; + draggingSubElement = true; draggingText = true; + } + return; + } + + // ── Check if click lands on a CUSTOM icon element ── + if (statusBar.getIconPosition() == StatusBar.IconPosition.CUSTOM + && statusBar.getIconHitArea().overlaps(clickRect)) { + boolean alreadySelected = selectedBar == statusBar && "icon".equals(selectedSubElement); + selectedBar = statusBar; + selectedSubElement = "icon"; + editBarWidget.visible = false; + if (alreadySelected) { + mouseButtonHeld = true; + subDragStartMouseX = cx; subDragStartMouseY = cy; + subDragStartOffX = statusBar.iconCustomOffX; subDragStartOffY = statusBar.iconCustomOffY; + draggingSubElement = true; draggingText = false; + } + return; + } + + // ── Click on bar body → select if not yet; drag only if already selected ── + boolean alreadyBarSelected = selectedBar == statusBar && selectedSubElement == null; + selectedBar = statusBar; + selectedSubElement = null; + editBarWidget.visible = false; + if (alreadyBarSelected) { + mouseButtonHeld = true; + dragStartX = cx; dragStartY = cy; + cursorOffset = new ScreenPosition(statusBar.getX() - cx, statusBar.getY() - cy); + } + + } else if (click.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT) { + selectedBar = statusBar; + selectedSubElement = null; + int x = (int) Math.min(click.x() + 5, width - editBarWidget.getWidth()); + int y = (int) Math.min(click.y() + 5, height - editBarWidget.getHeight()); + editBarWidget.insideMouseX = (int) click.x(); + editBarWidget.insideMouseY = (int) click.y(); + editBarWidget.visible = true; + editBarWidget.setStatusBar(statusBar); + editBarWidget.setX(x); editBarWidget.setY(y); } - } else { // if they have no anchor no need to do any checking - if (resizeHover.rightBoolean()) { - resizedBars.left(first); - resizedBars.right(null); - } else { - resizedBars.right(first); - resizedBars.left(null); + } + + private void updateScreenRects() { + rectToBar.clear(); + FancyStatusBars.statusBars.values().forEach(sb -> { + if (!sb.enabled) return; + rectToBar.put(new ScreenRectangle(new ScreenPosition(sb.getX(), sb.getY()), sb.getWidth(), sb.getHeight()), + Pair.of(sb, BarLocation.of(sb))); + }); + } + + @Override + public boolean mouseReleased(MouseButtonEvent click) { + mouseButtonHeld = false; + + if (resizingSubElement) { + resizingSubElement = false; resizeSubBar = null; + return true; + } + if (draggingSubElement) { + draggingSubElement = false; + return true; + } + + if (cursorBar != null) { + cursorBar.inMouse = false; + cursorBar.anchor = null; + cursorBar.x = (float) ((click.x() + cursorOffset.x()) / width); + cursorBar.y = (float) ((click.y() + cursorOffset.y()) / height); + cursorBar.width = Math.clamp(cursorBar.width, (float) BAR_MINIMUM_WIDTH / width, 1); + cursorBar = null; + FancyStatusBars.updatePositions(true); + updateScreenRects(); + return true; + } else if (resizing) { + resizing = false; + StatusBar bar = resizedBars.left() != null ? resizedBars.left() : resizedBars.right(); + if (bar != null && bar.anchor == null) { bar.x = (float) bar.getX() / width; bar.width = (float) bar.getWidth() / width; } + resizedBars.left(null); resizedBars.right(null); + updateScreenRects(); + return true; + } else if (resizingHeight) { + resizingHeight = false; + if (heightResizeBar != null) { heightResizeBar.y = (float) heightResizeBar.getY() / height; heightResizeBar = null; } + updateScreenRects(); + return true; } - } - resizing = true; - return true; + return super.mouseReleased(click); + } + + // ─────────────────────── Keyboard nudge ─────────────────────── + + @Override + public boolean keyPressed(KeyEvent keyEvent) { + int key = keyEvent.key(); + boolean arrow = key == GLFW.GLFW_KEY_LEFT || key == GLFW.GLFW_KEY_RIGHT + || key == GLFW.GLFW_KEY_UP || key == GLFW.GLFW_KEY_DOWN; + + int mods = keyEvent.modifiers(); + boolean shiftHeld = (mods & GLFW.GLFW_MOD_SHIFT) != 0; + boolean altHeld = (mods & GLFW.GLFW_MOD_ALT) != 0; + + if (arrow && selectedBar != null && (shiftHeld || altHeld)) { + boolean shift = shiftHeld; + boolean alt = altHeld; + boolean left = key == GLFW.GLFW_KEY_LEFT; + boolean right = key == GLFW.GLFW_KEY_RIGHT; + boolean up = key == GLFW.GLFW_KEY_UP; + boolean down = key == GLFW.GLFW_KEY_DOWN; + + if (selectedSubElement == null) { + // ── Bar ── + if (shift && selectedBar.anchor == null) { + // Nudge position (floating bars only) + if (left) selectedBar.x -= 1.0f / width; + if (right) selectedBar.x += 1.0f / width; + if (up) selectedBar.y -= 1.0f / height; + if (down) selectedBar.y += 1.0f / height; + } + if (alt) { + // Resize bar + if ((left || right) && selectedBar.anchor == null) { + int newW = selectedBar.getWidth() + (right ? 1 : -1); + selectedBar.setWidth(Math.max(BAR_MINIMUM_WIDTH, newW)); + selectedBar.width = (float) selectedBar.getWidth() / width; + } + if (up || down) { + selectedBar.barHeight = Math.max(StatusBar.MIN_BAR_HEIGHT, + selectedBar.barHeight + (down ? 1 : -1)); + } + } + } else if ("text".equals(selectedSubElement)) { + // ── Custom text ── + if (shift) { + if (left) selectedBar.textCustomOffX--; + if (right) selectedBar.textCustomOffX++; + if (up) selectedBar.textCustomOffY--; + if (down) selectedBar.textCustomOffY++; + } + if (alt) { + // LEFT/RIGHT scales text; UP/DOWN currently unused for text + float step = 0.05f; + if (left) selectedBar.textCustomScale = Math.max(0.5f, selectedBar.textCustomScale - step); + if (right) selectedBar.textCustomScale = Math.min(4.0f, selectedBar.textCustomScale + step); + } + } else if ("icon".equals(selectedSubElement)) { + // ── Custom icon ── + if (shift) { + if (left) selectedBar.iconCustomOffX--; + if (right) selectedBar.iconCustomOffX++; + if (up) selectedBar.iconCustomOffY--; + if (down) selectedBar.iconCustomOffY++; + } + if (alt) { + if (left) selectedBar.iconCustomW = Math.max(4, selectedBar.iconCustomW - 1); + if (right) selectedBar.iconCustomW = Math.min(64, selectedBar.iconCustomW + 1); + if (up) selectedBar.iconCustomH = Math.max(4, selectedBar.iconCustomH - 1); + if (down) selectedBar.iconCustomH = Math.min(64, selectedBar.iconCustomH + 1); + } + } + + FancyStatusBars.updatePositions(true); + updateScreenRects(); + nudgeOverlayTimer = 80; + return true; + } + + return super.keyPressed(keyEvent); + } + + @Override + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + int cx = (int) click.x(), cy = (int) click.y(); + + // Sub-element edge resize click (highest priority, only for selected sub-element) + if (click.button() == 0 && subElementEdgeHover != SubElementEdge.NONE && subElementEdgeBar != null) { + if (tryStartSubElementResize(subElementEdgeBar, cx, cy)) return true; + } + + // Height resize click + StatusBar heightBar = resizeHeightHover.first(); + if (!editBarWidget.isMouseOver(click.x(), click.y()) && click.button() == 0 && heightBar != null) { + resizingHeight = true; + heightResizeFromTop = resizeHeightHover.rightBoolean(); + heightResizeBar = heightBar; + heightResizeInitialY = heightBar.getY(); + heightResizeInitialHeight = heightBar.barHeight; + mouseButtonHeld = false; + return true; + } + + // Width resize click + StatusBar first = resizeHover.first(); + if (!editBarWidget.isMouseOver(click.x(), click.y()) && click.button() == 0 && first != null) { + BarPositioner.BarAnchor barAnchor = first.anchor; + if (barAnchor != null) { + if (resizeHover.rightBoolean()) { + resizedBars.left(first); + resizedBars.right(FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.gridY, first.gridX, true) + ? FancyStatusBars.barPositioner.getBar(barAnchor, first.gridY, first.gridX + (barAnchor.isRight() ? 1 : -1)) : null); + } else { + resizedBars.right(first); + resizedBars.left(FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.gridY, first.gridX, false) + ? FancyStatusBars.barPositioner.getBar(barAnchor, first.gridY, first.gridX + (barAnchor.isRight() ? -1 : 1)) : null); + } + } else { + if (resizeHover.rightBoolean()) { resizedBars.left(first); resizedBars.right(null); } + else { resizedBars.right(first); resizedBars.left(null); } + } + resizing = true; + return true; + } + + // Global CUSTOM element hit detection (text/icon may be outside bar bounds) + if (click.button() == 0 && !editBarWidget.isMouseOver(click.x(), click.y())) { + ScreenRectangle clickRect = new ScreenRectangle(cx, cy, 1, 1); + for (StatusBar bar : FancyStatusBars.statusBars.values()) { + if (!bar.enabled) continue; + if (bar.getTextPosition() == StatusBar.TextPosition.CUSTOM + && bar.getTextHitArea(minecraft.font).overlaps(clickRect) + && !isInBarBody(bar, cx, cy)) { + boolean alreadyTextSelected = selectedBar == bar && "text".equals(selectedSubElement); + if (alreadyTextSelected && tryStartSubElementResize(bar, cx, cy)) return true; + selectedBar = bar; selectedSubElement = "text"; + editBarWidget.visible = false; + if (alreadyTextSelected) { + mouseButtonHeld = true; + subDragStartMouseX = cx; subDragStartMouseY = cy; + subDragStartOffX = bar.textCustomOffX; subDragStartOffY = bar.textCustomOffY; + draggingSubElement = true; draggingText = true; + } + return true; + } + if (bar.getIconPosition() == StatusBar.IconPosition.CUSTOM + && bar.getIconHitArea().overlaps(clickRect) + && !isInBarBody(bar, cx, cy)) { + boolean alreadyIconSelected = selectedBar == bar && "icon".equals(selectedSubElement); + if (alreadyIconSelected && tryStartSubElementResize(bar, cx, cy)) return true; + selectedBar = bar; selectedSubElement = "icon"; + editBarWidget.visible = false; + if (alreadyIconSelected) { + mouseButtonHeld = true; + subDragStartMouseX = cx; subDragStartMouseY = cy; + subDragStartOffX = bar.iconCustomOffX; subDragStartOffY = bar.iconCustomOffY; + draggingSubElement = true; draggingText = false; + } + return true; + } + } + } + + boolean handled = super.mouseClicked(click, doubled); + + // If nothing handled the click, check if we should keep or clear selection + if (!handled && !editBarWidget.isMouseOver(click.x(), click.y()) && click.button() == 0) { + if (selectedBar != null && isInBarExtendedZone(selectedBar, cx, cy)) { + // Click is in the arrow zone of the selected bar — keep selection and start drag + if (selectedSubElement == null) { + mouseButtonHeld = true; + dragStartX = cx; dragStartY = cy; + cursorOffset = new ScreenPosition(selectedBar.getX() - cx, selectedBar.getY() - cy); + } + // (sub-element already selected — do nothing special, just don't deselect) + } else { + clearSelection(); + } + } + + return handled; } - return super.mouseClicked(click, doubled); - } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/TipsScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/TipsScreen.java new file mode 100644 index 00000000000..fd8055d38aa --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/TipsScreen.java @@ -0,0 +1,152 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; + +import java.util.List; + +/** + * Paginated tips dialog for the FancyStatusBars config screen. + * Shows one tip at a time with ← / → navigation and a Close button. + */ +public class TipsScreen extends Screen { + + private static final int DIALOG_W = 300; + private static final int DIALOG_H = 190; + private static final int PAD = 10; + + private static final int COLOR_BG = 0xFF111122; + private static final int COLOR_BORDER = 0xFFFFFF55; + private static final int COLOR_DIV = 0xFF444466; + private static final int COLOR_TITLE = 0xFFFFFF55; + private static final int COLOR_PAGE = 0xFFAAAAAA; + private static final int COLOR_TEXT = 0xFFEEEEEE; + + private static final List TIPS = List.of( + "LEFT-CLICK a bar to select it — a yellow outline with arrows appears. " + + "Hold and drag to pick it up and place it anywhere on screen.", + + "RIGHT-CLICK any bar to open its options panel. " + + "You can change colors, bar height, border radius, text/icon position, " + + "show-max, show-overflow, and more.", + + "Set Text or Icon position to CUSTOM in the options panel. " + + "Then LEFT-CLICK that text or icon element (cyan outline) to select it " + + "and drag it anywhere — even outside the bar.", + + "Hold SHIFT + Arrow Keys to nudge the selected bar, text, or icon " + + "exactly 1 pixel at a time. Great for pixel-perfect alignment without touching the mouse.", + + "Hold ALT + Arrow Keys to resize the selected element by 1 pixel: " + + "LEFT / RIGHT changes width (or text scale for Custom text), " + + "UP / DOWN changes height.", + + "When a CUSTOM text or icon is selected (cyan outline), drag the white " + + "resize square on its right edge to scale / resize. " + + "Icons also have a bottom-edge handle for height.", + + "Hover over the border between two side-by-side bars until a resize " + + "cursor appears, then drag to redistribute their widths.", + + "While dragging a bar, a yellow label shows its live position. " + + "X = pixels from the left edge. Y = pixels from the bottom " + + "(Y = 0 means the bar's bottom is at the very bottom of the screen).", + + "Use the 'Reset to Default' button to instantly restore every bar " + + "to its original position, size, colors, and all other settings." + ); + + private final Screen parent; + private int currentTip = 0; + private Button prevButton; + private Button nextButton; + + public TipsScreen(Screen parent) { + super(Component.literal("Tips & Tricks")); + this.parent = parent; + } + + private int dlgX() { return (width - DIALOG_W) / 2; } + private int dlgY() { return (height - DIALOG_H) / 2; } + + @Override + protected void init() { + super.init(); + int dx = dlgX(), dy = dlgY(); + int btnY = dy + DIALOG_H - 24; + + prevButton = addRenderableWidget( + Button.builder(Component.literal("←"), _ -> navigate(-1)) + .bounds(dx + PAD, btnY, 26, 16) + .build()); + + addRenderableWidget( + Button.builder(Component.literal("Close"), _ -> minecraft.setScreen(parent)) + .bounds(dx + DIALOG_W / 2 - 26, btnY, 52, 16) + .build()); + + nextButton = addRenderableWidget( + Button.builder(Component.literal("→"), _ -> navigate(+1)) + .bounds(dx + DIALOG_W - PAD - 26, btnY, 26, 16) + .build()); + + updateNavButtons(); + } + + private void navigate(int delta) { + currentTip = Math.floorMod(currentTip + delta, TIPS.size()); + updateNavButtons(); + } + + private void updateNavButtons() { + boolean multi = TIPS.size() > 1; + prevButton.active = multi; + nextButton.active = multi; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta) { + extractTransparentBackground(ctx); + + int dx = dlgX(), dy = dlgY(); + + // Outer border (yellow, 1px) + ctx.fill(dx - 1, dy - 1, dx + DIALOG_W + 1, dy + DIALOG_H + 1, COLOR_BORDER); + // Background panel + ctx.fill(dx, dy, dx + DIALOG_W, dy + DIALOG_H, COLOR_BG); + + // Title + ctx.centeredText(font, "Tips & Tricks", dx + DIALOG_W / 2, dy + PAD, COLOR_TITLE); + + // Divider under title + ctx.fill(dx + PAD, dy + 22, dx + DIALOG_W - PAD, dy + 23, COLOR_DIV); + + // Page indicator + String pageStr = "Tip " + (currentTip + 1) + " of " + TIPS.size(); + ctx.centeredText(font, pageStr, dx + DIALOG_W / 2, dy + 27, COLOR_PAGE); + + // Tip text — word-wrapped + List lines = font.split( + Component.literal(TIPS.get(currentTip)), DIALOG_W - PAD * 2); + int textY = dy + 40; + for (FormattedCharSequence line : lines) { + ctx.text(font, line, dx + PAD, textY, COLOR_TEXT, false); + textY += font.lineHeight + 2; + } + + // Render buttons on top + super.extractRenderState(ctx, mouseX, mouseY, delta); + } + + @Override + public boolean isPauseScreen() { return false; } + + @Override + public boolean shouldCloseOnEsc() { return true; } + + @Override + public void onClose() { minecraft.setScreen(parent); } +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index dc24c02045c..4ea375e1276 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -38,6 +38,13 @@ "skyblocker.api.token.noProfileKeys": "Failed to get your profile keys! Some features of the mod may not work temporarily :( (Has your game been open for more than 24 hours?). To reactivate these features, restart the game.", "skyblocker.bars.config.air": "Air", + "skyblocker.bars.config.borderRadius": "Border Radius", + "skyblocker.bars.config.borderRadius.dialog.cancel": "Cancel", + "skyblocker.bars.config.borderRadius.dialog.confirm": "Confirm", + "skyblocker.bars.config.borderRadius.dialog.error": "Must be 0–%s", + "skyblocker.bars.config.borderRadius.dialog.prompt": "Enter radius (0–%s px):", + "skyblocker.bars.config.borderRadius.dialog.title": "Set Border Radius", + "skyblocker.bars.config.commonPosition.CUSTOM": "Custom", "skyblocker.bars.config.commonPosition.LEFT": "Left", "skyblocker.bars.config.commonPosition.OFF": "Off", "skyblocker.bars.config.commonPosition.RIGHT": "Right", @@ -51,6 +58,9 @@ "skyblocker.bars.config.intelligence": "Intelligence", "skyblocker.bars.config.mainColor": "Main Color", "skyblocker.bars.config.overflowColor": "Overflow Color", + "skyblocker.bars.config.resetBar": "Reset This Bar", + "skyblocker.bars.config.resetToDefault": "Reset to Default", + "skyblocker.bars.config.show": "Show", "skyblocker.bars.config.showMax": "Show Max", "skyblocker.bars.config.showOverflow": "Show Overflow", "skyblocker.bars.config.speed": "Speed", @@ -58,6 +68,9 @@ "skyblocker.bars.config.textColor": "Text Color", "skyblocker.bars.config.textPosition.BAR_CENTER": "Bar Center", "skyblocker.bars.config.textPosition.CENTER": "Center", + "skyblocker.bars.config.textPosition.CUSTOM": "Custom", + "skyblocker.bars.config.textPosition.LEFT": "Bar Left", + "skyblocker.bars.config.textPosition.RIGHT": "Bar Right", "skyblocker.chat.confirmationPromptNotification": "Click anywhere on screen within 60 seconds to accept the prompt.",