From 49c6297c4136dddb6e750a8b1eea646c09882d78 Mon Sep 17 00:00:00 2001
From: lyizai <3945107889@qq.com>
Date: Sun, 30 Aug 2026 12:36:16 +0800
Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E5=9B=BA=E5=AE=9A=E5=BA=95=E6=A0=8F?=
=?UTF-8?q?+=E6=BB=9A=E5=8A=A8=E5=8C=BA=E5=B8=83=E5=B1=80=EF=BC=8C?=
=?UTF-8?q?=E5=BD=BB=E5=BA=95=E8=A7=A3=E5=86=B3=E6=8C=89=E9=92=AE=E8=B6=8A?=
=?UTF-8?q?=E7=95=8C=E4=B8=8E=E6=96=87=E5=AD=97=E9=87=8D=E5=8F=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 顶部任务选择与底部开始/停止/仅关闭按钮固定在屏幕内,永不出界
- 中间设置区改为可滚动容器(滚轮滚动、右侧滚动条、scissor 裁剪防溢出)
- 面板宽度随窗口自适应(200~400px),窗口缩放不再挤压布局
- 复选框两列放不下时自动上下堆叠,消除文字重叠
- 数值行改为 [-] 标签 ... 值 [+] 结构,标签左对齐、数值右对齐、动态刷新
- 矿物输入框占满面板宽度,可见可编辑
- 空闲任务时显示引导文案而非一堆无关选项
- 开始前校验矿物列表非空
---
gradle.properties | 2 +-
.../java/autodig/client/gui/AutoDigGui.java | 482 ++++++++++--------
2 files changed, 278 insertions(+), 206 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index b765588..01ad493 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8
loader_version=0.19.3
# Mod Properties
-mod_version=2.0.0
+mod_version=2.0.1
maven_group=autodig
archives_base_name=autodig
diff --git a/src/main/java/autodig/client/gui/AutoDigGui.java b/src/main/java/autodig/client/gui/AutoDigGui.java
index 1d7b531..ca54fb4 100644
--- a/src/main/java/autodig/client/gui/AutoDigGui.java
+++ b/src/main/java/autodig/client/gui/AutoDigGui.java
@@ -4,41 +4,59 @@ import autodig.client.AutoDigModClient;
import autodig.client.config.DigConfig;
import autodig.client.config.MineTask;
import autodig.client.controller.DigController;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
+import java.util.function.IntSupplier;
+import java.util.function.Supplier;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.CheckboxWidget;
+import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.text.Text;
+import net.minecraft.util.math.MathHelper;
/**
- * AutoDig 游戏内 GUI - 响应式布局,根据任务动态显示选项。
+ * AutoDig 游戏内 GUI(响应式 + 滚动布局)。
+ *
+ *
结构:顶部标题与任务选择、底部操作按钮(开始/停止/仅关闭)固定不动,
+ * 中间为可滚动设置区(滚轮滚动,右侧有滚动条)。设置区内容按当前任务动态
+ * 显示:主世界只显示主世界相关项,下界只显示下界相关项,避免无关选项干扰。
+ * 面板宽度随窗口自适应,标签过长时复选框自动改为上下堆叠,避免文字重叠。
*/
public final class AutoDigGui extends Screen {
private final AutoDigModClient mod;
private final DigConfig config;
private final DigController controller;
+ /** 关闭界面时是否把改动写回 mod 配置("仅关闭"会置 false) */
private boolean applyOnClose = true;
- // UI 组件引用
+ // ---- 布局 ----
+ private int panelLeft;
+ private int panelW;
+ /** 滚动区可视范围(屏幕坐标) */
+ private int viewportTop;
+ private int viewportBottom;
+
+ // ---- 滚动状态 ----
+ private double scrollOffset;
+ private int contentHeight;
+
+ /** 可滚动控件与其虚拟坐标(真实 y = virtualY - scrollOffset) */
+ private record ScrollEntry(ClickableWidget widget, int x, int virtualY) {}
+ private final List scrollable = new ArrayList<>();
+
+ /** 可滚动的纯文本(分区标题、数值、字段标签),渲染时按滚动偏移绘制 */
+ private record TextEntry(int x, int virtualY, Supplier text, int color, boolean rightAlign, boolean underline) {}
+ private final List texts = new ArrayList<>();
+
private TextFieldWidget overworldOresField;
private TextFieldWidget netherOresField;
- // 数值显示按钮
- private ButtonWidget levelBtn;
- private ButtonWidget tunnelBtn;
- private ButtonWidget countBtn;
- private ButtonWidget armorDurBtn;
- private ButtonWidget toolDurBtn;
- private ButtonWidget fleeHBtn;
-
- // 任务按钮(用于刷新高亮)
- private ButtonWidget btnNone;
- private ButtonWidget btnOverworld;
- private ButtonWidget btnNether;
-
public AutoDigGui(AutoDigModClient mod) {
super(Text.literal("AutoDig"));
this.mod = mod;
@@ -49,201 +67,215 @@ public final class AutoDigGui extends Screen {
@Override
protected void init() {
this.clearChildren();
- int centerX = this.width / 2;
- int panelW = Math.min(380, this.width - 40);
- int left = centerX - panelW / 2;
- int right = left + panelW;
- int y = 30;
- int gap = 4;
- int rowH = 20;
- int checkboxGap = 24;
- int sectionGap = 14;
+ this.scrollable.clear();
+ this.texts.clear();
+ this.scrollOffset = 0;
+ this.overworldOresField = null;
+ this.netherOresField = null;
- // ===== 标题 =====
- // 居中已在 render 处理
+ // 面板宽度自适应窗口,最小 200 保证 +/- 与输入框可用
+ this.panelW = MathHelper.clamp(this.width - 16, 200, 400);
+ this.panelLeft = (this.width - this.panelW) / 2;
- // ===== 任务选择 =====
- int btnW = (panelW - gap * 2) / 3;
- this.btnNone = taskButton("空闲", MineTask.NONE, left, y, btnW);
- this.btnOverworld = taskButton("主世界", MineTask.OVERWORLD, left + btnW + gap, y, btnW);
- this.btnNether = taskButton("下界", MineTask.NETHER, left + (btnW + gap) * 2, y, btnW);
- y += rowH + sectionGap;
+ // ===== 固定区:任务选择 =====
+ int taskY = 24;
+ int w3 = (this.panelW - 8) / 3;
+ this.addDrawableChild(this.taskButton("空闲", MineTask.NONE, this.panelLeft, taskY, w3));
+ this.addDrawableChild(this.taskButton("主世界", MineTask.OVERWORLD, this.panelLeft + w3 + 4, taskY, w3));
+ this.addDrawableChild(this.taskButton("下界", MineTask.NETHER, this.panelLeft + (w3 + 4) * 2, taskY, w3));
- // ===== 通用开关(所有任务共用)=====
- y = addSectionHeader(y, left, "通用设置", panelW);
- y = addCheckboxRow(y, left, panelW,
- "保命优先", () -> config.survivalFirst, v -> config.survivalFirst = v,
- "优先绕开岩浆", () -> config.avoidLava, v -> config.avoidLava = v);
- y = addCheckboxRow(y, left, panelW,
- "自动进食", () -> config.autoEat, v -> config.autoEat = v,
- "自动切换工具", () -> config.autoTool, v -> config.autoTool = v);
- y = addCheckboxRow(y, left, panelW,
- "杀戮光环", () -> config.killAura, v -> config.killAura = v,
- "重生后回死亡点", () -> config.respawnReturn, v -> config.respawnReturn = v);
- y = addCheckboxRow(y, left, panelW,
- "避难时绝杀不冒进", () -> config.killAuraSafe, v -> config.killAuraSafe = v,
- "下界协同杀戮光环", () -> config.killAuraInNether, v -> config.killAuraInNether = v);
- y = addCheckboxRow(y, left, panelW,
- "护甲耐久保护", () -> config.armorDurabilityProtection, v -> config.armorDurabilityProtection = v,
- "工具耐久保护", () -> config.toolDurabilityProtection, v -> config.toolDurabilityProtection = v);
- y += sectionGap;
+ // ===== 固定区:底部操作按钮(永远可见,不随内容滚动) =====
+ int actionY = this.height - 44;
+ this.addDrawableChild(this.textButton("开始", b -> this.start(), this.panelLeft, actionY, w3));
+ this.addDrawableChild(this.textButton("停止", b -> this.stop(), this.panelLeft + w3 + 4, actionY, w3));
+ this.addDrawableChild(this.textButton("仅关闭", b -> this.closeOnly(), this.panelLeft + (w3 + 4) * 2, actionY, w3));
- // ===== 主世界专用设置 =====
- if (config.task == MineTask.OVERWORLD) {
- y = addSectionHeader(y, left, "主世界设置", panelW);
- this.levelBtn = numberRow("挖掘层 (Y)", left, y, panelW,
- () -> config.overworldLevel, v -> config.overworldLevel = v, -64, 320);
- y += rowH + gap;
- this.tunnelBtn = numberRow("主干巷道长度", left, y, panelW,
- () -> config.tunnelLength, v -> config.tunnelLength = v, 4, 512);
- y += rowH + gap;
+ this.viewportTop = taskY + 26;
+ this.viewportBottom = actionY - 8;
- // 矿物输入框
- y = addTextFieldRow(y, left, panelW, "目标矿物 (逗号分隔)",
- config.overworldOres, tf -> this.overworldOresField = tf,
- v -> config.overworldOres = v);
- y += sectionGap;
+ // ===== 滚动区内容(虚拟坐标从 0 开始) =====
+ int y = 0;
+ if (this.config.task == MineTask.NONE) {
+ this.texts.add(new TextEntry(this.panelLeft, 4, () -> "请先在上方选择挖矿任务", 0xBBBBBB, false, false));
+ this.texts.add(new TextEntry(this.panelLeft, 18, () -> "主世界:鱼骨巷道挖矿 / 下界:进传送门挖矿并自动返程", 0x888888, false, false));
+ y = 40;
+ } else {
+ y = this.header(y, "通用设置");
+ y = this.checkPair(y, "保命优先", () -> this.config.survivalFirst, v -> this.config.survivalFirst = v,
+ "优先绕开岩浆", () -> this.config.avoidLava, v -> this.config.avoidLava = v);
+ y = this.checkPair(y, "自动进食", () -> this.config.autoEat, v -> this.config.autoEat = v,
+ "自动切换工具", () -> this.config.autoTool, v -> this.config.autoTool = v);
+ y = this.checkPair(y, "重生后回死亡点", () -> this.config.respawnReturn, v -> this.config.respawnReturn = v,
+ "杀戮光环", () -> this.config.killAura, v -> this.config.killAura = v);
+ y = this.checkPair(y, "杀戮光环不冒进", () -> this.config.killAuraSafe, v -> this.config.killAuraSafe = v,
+ "下界协同杀戮光环", () -> this.config.killAuraInNether, v -> this.config.killAuraInNether = v);
+ y = this.checkPair(y, "护甲耐久保护", () -> this.config.armorDurabilityProtection, v -> this.config.armorDurabilityProtection = v,
+ "工具耐久保护", () -> this.config.toolDurabilityProtection, v -> this.config.toolDurabilityProtection = v);
+ y += 10;
+
+ y = this.header(y, "装备耐久阈值(低于即停,百分比)");
+ y = this.numberRow(y, "护甲耐久", () -> this.config.armorDurabilityThreshold,
+ v -> this.config.armorDurabilityThreshold = v, 1, 100);
+ y = this.numberRow(y, "工具耐久", () -> this.config.toolDurabilityThreshold,
+ v -> this.config.toolDurabilityThreshold = v, 1, 100);
+ y += 10;
+
+ if (this.config.task == MineTask.OVERWORLD) {
+ y = this.header(y, "主世界设置");
+ y = this.numberRow(y, "挖掘层 (Y)", () -> this.config.overworldLevel,
+ v -> this.config.overworldLevel = v, -64, 320);
+ y = this.numberRow(y, "巷道长度", () -> this.config.tunnelLength,
+ v -> this.config.tunnelLength = v, 4, 512);
+ y = this.fieldRow(y, "目标矿物(逗号分隔,如 diamond_ore,deepslate_diamond_ore)",
+ this.config.overworldOres, tf -> this.overworldOresField = tf);
+ y += 10;
+ } else if (this.config.task == MineTask.NETHER) {
+ y = this.header(y, "下界设置");
+ y = this.numberRow(y, "目标数量 (-1 无限)", () -> this.config.netherTargetCount,
+ v -> this.config.netherTargetCount = v, -1, 512);
+ y = this.checkPair(y, "挖完/背包满/血量低/耐久低 自动回主世界",
+ () -> this.config.netherAutoReturn, v -> this.config.netherAutoReturn = v,
+ null, null, null);
+ y = this.numberRow(y, "返程血量阈值", () -> this.config.netherFleeHealthThreshold,
+ v -> this.config.netherFleeHealthThreshold = v, 0, 100);
+ y = this.fieldRow(y, "目标矿物(逗号分隔,如 ancient_debris,nether_gold_ore)",
+ this.config.netherOres, tf -> this.netherOresField = tf);
+ y += 10;
+ }
}
- // ===== 下界专用设置 =====
- if (config.task == MineTask.NETHER) {
- y = addSectionHeader(y, left, "下界设置", panelW);
- this.countBtn = numberRow("目标数量 (-1无限)", left, y, panelW,
- () -> config.netherTargetCount, v -> config.netherTargetCount = v, -1, 512);
- y += rowH + gap;
- y = addCheckboxRow(y, left, panelW,
- "挖完/背包满/血量低/耐久低 自动回主世界", () -> config.netherAutoReturn, v -> config.netherAutoReturn = v,
- "", () -> false, v -> {}); // 单列占满
- y += rowH + gap;
- this.fleeHBtn = numberRow("返程血量阈值%", left, y, panelW,
- () -> config.netherFleeHealthThreshold, v -> config.netherFleeHealthThreshold = v, 0, 100);
- y += rowH + gap;
-
- // 矿物输入框
- y = addTextFieldRow(y, left, panelW, "目标矿物 (逗号分隔)",
- config.netherOres, tf -> this.netherOresField = tf,
- v -> config.netherOres = v);
- y += sectionGap;
- }
-
- // ===== 装备耐久阈值(通用,但仅在开启保护时有效)=====
- if (config.armorDurabilityProtection || config.toolDurabilityProtection) {
- y = addSectionHeader(y, left, "装备耐久阈值", panelW);
- this.armorDurBtn = numberRow("护甲耐久 %", left, y, panelW,
- () -> config.armorDurabilityThreshold, v -> config.armorDurabilityThreshold = v, 1, 100);
- y += rowH + gap;
- this.toolDurBtn = numberRow("工具耐久 %", left, y, panelW,
- () -> config.toolDurabilityThreshold, v -> config.toolDurabilityThreshold = v, 1, 100);
- y += sectionGap;
- }
-
- // ===== 底部按钮 =====
- int btnY = Math.max(y, this.height - 48);
- int bW = (panelW - gap * 2) / 3;
- this.addDrawableChild(textButton("开始", b -> start(), left, btnY, bW));
- this.addDrawableChild(textButton("停止", b -> stop(), left + bW + gap, btnY, bW));
- this.addDrawableChild(textButton("仅关闭", b -> closeOnly(), left + (bW + gap) * 2, btnY, bW));
+ this.contentHeight = y;
+ this.clampScroll();
+ this.applyScroll();
}
- // ========== 布局辅助 ==========
+ // ================= 布局辅助 =================
- private int addSectionHeader(int y, int left, String title, int panelW) {
- // 画分隔线
- this.addDrawableChild(ButtonWidget.builder(Text.literal(""), b -> {}).dimensions(left, y + 10, panelW, 1).build());
- // 标题文本会在 render 中绘制,这里只预留空间
- return y + 18;
+ /** 分区标题(金色文本 + 下划线)。 */
+ private int header(int y, String text) {
+ this.texts.add(new TextEntry(this.panelLeft, y, () -> text, 0xFFD700, false, true));
+ return y + 16;
}
- private int addCheckboxRow(int y, int left, int panelW,
- String label1, java.util.function.BooleanSupplier g1, Consumer s1,
- String label2, java.util.function.BooleanSupplier g2, Consumer s2) {
- int halfW = (panelW - 8) / 2;
- CheckboxWidget c1 = CheckboxWidget.builder(Text.literal(label1), this.textRenderer)
- .pos(left, y).checked(g1.getAsBoolean())
- .callback((b, v) -> s1.accept(v)).build();
- c1.setWidth(halfW);
- this.addDrawableChild(c1);
- if (!label2.isEmpty()) {
- CheckboxWidget c2 = CheckboxWidget.builder(Text.literal(label2), this.textRenderer)
- .pos(left + halfW + 8, y).checked(g2.getAsBoolean())
- .callback((b, v) -> s2.accept(v)).build();
- c2.setWidth(halfW);
- this.addDrawableChild(c2);
+ /**
+ * 一行复选框:两列放得下则并排;放不下(窄屏/长文本)自动上下堆叠,
+ * 保证文字不会互相重叠。label2 为 null 时独占一行。
+ */
+ private int checkPair(int y, String label1, BooleanSupplier get1, Consumer set1,
+ String label2, BooleanSupplier get2, Consumer set2) {
+ CheckboxWidget c1 = this.checkbox(label1, get1, set1);
+ if (label2 == null) {
+ this.addScrollable(c1, this.panelLeft, y);
+ return y + 22;
}
+ CheckboxWidget c2 = this.checkbox(label2, get2, set2);
+ if (c1.getWidth() + c2.getWidth() + 12 <= this.panelW) {
+ this.addScrollable(c1, this.panelLeft, y);
+ this.addScrollable(c2, this.panelLeft + this.panelW - c2.getWidth(), y);
+ return y + 22;
+ }
+ // 放不下:上下堆叠
+ this.addScrollable(c1, this.panelLeft, y);
+ this.addScrollable(c2, this.panelLeft, y + 22);
+ return y + 44;
+ }
+
+ private CheckboxWidget checkbox(String label, BooleanSupplier getter, Consumer setter) {
+ return CheckboxWidget.builder(Text.literal(label), this.textRenderer)
+ .pos(0, 0).checked(getter.getAsBoolean())
+ .callback((b, v) -> setter.accept(v)).build();
+ }
+
+ /** 数值行:[-] 标签 ..... 值 [+],标签左对齐、数值右对齐,自适应面板宽度。 */
+ private int numberRow(int y, String label, IntSupplier getter, Consumer setter, int min, int max) {
+ int btn = 20;
+ this.addScrollable(ButtonWidget.builder(Text.literal("-"), b ->
+ setter.accept(MathHelper.clamp(getter.getAsInt() - 1, min, max)))
+ .dimensions(this.panelLeft, 0, btn, btn).build(), this.panelLeft, y);
+ this.addScrollable(ButtonWidget.builder(Text.literal("+"), b ->
+ setter.accept(MathHelper.clamp(getter.getAsInt() + 1, min, max)))
+ .dimensions(this.panelLeft + this.panelW - btn, 0, btn, btn).build(),
+ this.panelLeft + this.panelW - btn, y);
+ this.texts.add(new TextEntry(this.panelLeft + btn + 6, y + 6, () -> label, 0xFFFFFF, false, false));
+ this.texts.add(new TextEntry(this.panelLeft + this.panelW - btn - 6, y + 6,
+ () -> String.valueOf(getter.getAsInt()), 0x55FF55, true, false));
return y + 24;
}
- private int addTextFieldRow(int y, int left, int panelW, String label,
- String initialValue, Consumer refSetter, Consumer onChange) {
- // 标签
- this.addDrawableChild(ButtonWidget.builder(Text.literal(label), b -> {}).dimensions(left, y, panelW, 18).build());
- y += 20;
- // 输入框
- TextFieldWidget tf = new TextFieldWidget(this.textRenderer, left, y, panelW, 20, Text.literal(""));
- tf.setText(initialValue);
- tf.setChangedListener(onChange);
- tf.setMaxLength(200);
- refSetter.accept(tf);
- this.addDrawableChild(tf);
+ /** 文本输入行:标签 + 输入框(占满面板宽度)。 */
+ private int fieldRow(int y, String label, String initial, Consumer ref) {
+ this.texts.add(new TextEntry(this.panelLeft, y + 3, () -> label, 0xBBBBBB, false, false));
+ y += 14;
+ TextFieldWidget tf = new TextFieldWidget(this.textRenderer, this.panelLeft, 0, this.panelW, 18, Text.literal(""));
+ tf.setMaxLength(256);
+ tf.setText(initial);
+ ref.accept(tf);
+ this.addScrollable(tf, this.panelLeft, y);
return y + 24;
}
- private ButtonWidget numberRow(String label, int left, int y, int panelW,
- java.util.function.IntSupplier getter, Consumer setter, int min, int max) {
- int btnSize = 22;
- int labelW = 110;
- int valW = 60;
- int x = left;
- this.addDrawableChild(ButtonWidget.builder(Text.literal("-"), b -> {
- setter.accept(clamp(getter.getAsInt() - 1, min, max));
- refreshNumberButtons();
- }).dimensions(x, y, btnSize, btnSize).build());
- x += btnSize + 4;
- // 标签按钮(不可点击)
- ButtonWidget lbl = ButtonWidget.builder(Text.literal(label), b -> {}).dimensions(x, y, labelW, btnSize).build();
- lbl.active = false;
- this.addDrawableChild(lbl);
- x += labelW + 4;
- // 数值显示
- ButtonWidget val = ButtonWidget.builder(Text.literal("" + getter.getAsInt()), b -> {}).dimensions(x, y, valW, btnSize).build();
- val.active = false;
- this.addDrawableChild(val);
- x += valW + 4;
- this.addDrawableChild(ButtonWidget.builder(Text.literal("+"), b -> {
- setter.accept(clamp(getter.getAsInt() + 1, min, max));
- refreshNumberButtons();
- }).dimensions(x, y, btnSize, btnSize).build());
- return val;
- }
-
- private int clamp(int v, int min, int max) {
- return Math.max(min, Math.min(max, v));
- }
-
- private void refreshNumberButtons() {
- if (levelBtn != null) levelBtn.setMessage(Text.literal("" + config.overworldLevel));
- if (tunnelBtn != null) tunnelBtn.setMessage(Text.literal("" + config.tunnelLength));
- if (countBtn != null) countBtn.setMessage(Text.literal("" + config.netherTargetCount));
- if (armorDurBtn != null) armorDurBtn.setMessage(Text.literal("" + config.armorDurabilityThreshold));
- if (toolDurBtn != null) toolDurBtn.setMessage(Text.literal("" + config.toolDurabilityThreshold));
- if (fleeHBtn != null) fleeHBtn.setMessage(Text.literal("" + config.netherFleeHealthThreshold));
+ /** 注册可滚动控件(真实位置在 applyScroll 中按偏移计算)。 */
+ private void addScrollable(ClickableWidget widget, int x, int virtualY) {
+ widget.setX(x);
+ widget.setY(virtualY);
+ this.scrollable.add(new ScrollEntry(widget, x, virtualY));
+ this.addDrawableChild(widget);
}
private ButtonWidget taskButton(String label, MineTask task, int x, int y, int w) {
- boolean selected = config.task == task;
+ boolean selected = this.config.task == task;
String text = selected ? "■ " + label : "□ " + label;
- ButtonWidget b = textButton(text, button -> {
- config.task = task;
- init(); // 完全重建 UI 以显示/隐藏对应区域
- }, x, y, w);
- this.addDrawableChild(b);
- return b;
+ return this.addDrawableChild(this.textButton(text, b -> {
+ this.config.task = task;
+ this.init(); // 切换任务:重建滚动区,只显示相关设置
+ }, x, y, w));
}
private ButtonWidget textButton(String label, ButtonWidget.PressAction action, int x, int y, int w) {
- return ButtonWidget.builder(Text.literal(label), action).dimensions(x, y, w, 22).build();
+ return ButtonWidget.builder(Text.literal(label), action).dimensions(x, y, w, 20).build();
}
+ // ================= 滚动逻辑 =================
+
+ private double maxScroll() {
+ return Math.max(0, this.contentHeight - (this.viewportBottom - this.viewportTop));
+ }
+
+ private void clampScroll() {
+ this.scrollOffset = MathHelper.clamp(this.scrollOffset, 0, this.maxScroll());
+ }
+
+ /** 按滚动偏移重排控件位置,并把滚出可视区的控件隐藏(不渲染、不可点)。 */
+ private void applyScroll() {
+ for (ScrollEntry e : this.scrollable) {
+ int dy = e.virtualY() - (int) this.scrollOffset;
+ e.widget().setX(e.x());
+ e.widget().setY(dy);
+ e.widget().visible = dy + e.widget().getHeight() > this.viewportTop
+ && dy < this.viewportBottom;
+ }
+ }
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
+ if (super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
+ return true;
+ }
+ double before = this.scrollOffset;
+ this.scrollOffset = MathHelper.clamp(
+ this.scrollOffset - verticalAmount * 24.0, 0, this.maxScroll());
+ if (this.scrollOffset != before) {
+ this.applyScroll();
+ return true;
+ }
+ return false;
+ }
+
+ // ================= 渲染 =================
+
+ /**
+ * GUI 打开时不允许游戏暂停:否则客户端 tick 不执行,控制器停摆。
+ */
@Override
public boolean shouldPause() {
return false;
@@ -251,30 +283,65 @@ public final class AutoDigGui extends Screen {
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
- // 半透明背景
this.renderBackground(context, mouseX, mouseY, delta);
+ // 滚动区底色,增强可读性
+ context.fill(this.panelLeft - 6, this.viewportTop - 4,
+ this.panelLeft + this.panelW + 6, this.viewportBottom + 4, 0x88000000);
super.render(context, mouseX, mouseY, delta);
- // 标题
- context.drawCenteredTextWithShadow(this.textRenderer, "AutoDig 自动挖矿", this.width / 2, 10, 0xFFFFFF);
+ context.drawCenteredTextWithShadow(this.textRenderer, "AutoDig 自动挖矿",
+ this.width / 2, 8, 0xFFFFFF);
- // 当前任务/状态(底部)
+ // 滚动区文本(裁剪绘制,防止溢出到固定区)
+ int offset = (int) this.scrollOffset;
+ context.enableScissor(this.panelLeft - 6, this.viewportTop - 4,
+ this.panelLeft + this.panelW + 6, this.viewportBottom + 4);
+ for (TextEntry t : this.texts) {
+ String text = t.text().get();
+ if (text == null || text.isEmpty()) {
+ continue;
+ }
+ int dy = t.virtualY() - offset;
+ int x = t.rightAlign() ? t.x() - this.textRenderer.getWidth(text) : t.x();
+ context.drawTextWithShadow(this.textRenderer, text, x, dy, t.color());
+ if (t.underline()) {
+ context.fill(this.panelLeft, dy + 11, this.panelLeft + this.panelW, dy + 12, 0x55FFFFFF);
+ }
+ }
+ context.disableScissor();
+
+ // 滚动条
+ double max = this.maxScroll();
+ if (max > 0) {
+ int trackTop = this.viewportTop - 4;
+ int trackBottom = this.viewportBottom + 4;
+ int trackH = trackBottom - trackTop;
+ int thumbH = Math.max(18, (int) (trackH * (double) trackH / Math.max(1, this.contentHeight)));
+ int thumbY = trackTop + (int) ((trackH - thumbH) * (this.scrollOffset / max));
+ int barX = this.panelLeft + this.panelW + 8;
+ context.fill(barX, trackTop, barX + 3, trackBottom, 0x40FFFFFF);
+ context.fill(barX, thumbY, barX + 3, thumbY + thumbH, 0xB0FFFFFF);
+ }
+
+ // 底部状态栏
context.drawTextWithShadow(this.textRenderer,
- "任务: " + config.task.label() + " 状态: " + controller.statusText(),
- 10, this.height - 20, 0xAAAAAA);
-
- // 绘制分组标题(因为用 ButtonWidget 做分隔线不好看,改用直接绘制)
- int centerX = this.width / 2;
- int panelW = Math.min(380, this.width - 40);
- int left = centerX - panelW / 2;
- int y = 30 + 20 + 14; // 任务行 + sectionGap
- // 通用设置标题
- context.drawTextWithShadow(this.textRenderer, "通用设置", left, y - 16, 0xFFD700);
- // 动态计算后续分组标题位置比较麻烦,改在 init 时记录或直接在 render 时估算
- // 简化:只画标题栏背景
+ "任务: " + this.config.task.label() + " 状态: " + this.controller.statusText(),
+ this.panelLeft, this.height - 14, 0xAAAAAA);
}
- /** 把界面临时配置写回 mod 全局配置并保存。 */
+ // ================= 应用与操作 =================
+
+ /** 同步文本输入框的最新值到本地配置副本。 */
+ private void syncFields() {
+ if (this.overworldOresField != null) {
+ this.config.overworldOres = this.overworldOresField.getText();
+ }
+ if (this.netherOresField != null) {
+ this.config.netherOres = this.netherOresField.getText();
+ }
+ }
+
+ /** 把界面临时配置写回 mod 全局配置并保存(供 controller 实时读取)。 */
private void applyToMod() {
DigConfig target = this.mod.config();
target.task = this.config.task;
@@ -301,19 +368,25 @@ public final class AutoDigGui extends Screen {
}
private void start() {
+ this.syncFields();
if (this.config.task == MineTask.NONE) {
this.mod.sendToast("AutoDig: 请先选择一个挖矿任务(主世界 / 下界)");
return;
}
- // 同步输入框最新值
- if (this.overworldOresField != null) this.config.overworldOres = this.overworldOresField.getText();
- if (this.netherOresField != null) this.config.netherOres = this.netherOresField.getText();
+ List ores = this.config.task == MineTask.OVERWORLD
+ ? this.config.getOverworldOreList()
+ : this.config.getNetherOreList();
+ if (ores.isEmpty()) {
+ this.mod.sendToast("AutoDig: 目标矿物列表为空,请填写至少一个矿物 ID");
+ return;
+ }
this.applyToMod();
this.mod.sendToast("AutoDig: 已开始 " + this.config.task.label() + " 任务");
this.close();
}
private void stop() {
+ // 先把本地副本的任务置空再写回,避免 close() 二次应用时把旧任务写回去
this.config.task = MineTask.NONE;
this.applyToMod();
this.controller.bridge().cancelAll();
@@ -321,6 +394,7 @@ public final class AutoDigGui extends Screen {
this.close();
}
+ /** 仅关闭:丢弃未应用的改动。 */
private void closeOnly() {
this.applyOnClose = false;
this.close();
@@ -329,9 +403,7 @@ public final class AutoDigGui extends Screen {
@Override
public void close() {
if (this.applyOnClose) {
- // 同步输入框最新值
- if (this.overworldOresField != null) this.config.overworldOres = this.overworldOresField.getText();
- if (this.netherOresField != null) this.config.netherOres = this.netherOresField.getText();
+ this.syncFields();
this.applyToMod();
}
super.close();