fix(ui): 固定底栏+滚动区布局,彻底解决按钮越界与文字重叠

- 顶部任务选择与底部开始/停止/仅关闭按钮固定在屏幕内,永不出界
- 中间设置区改为可滚动容器(滚轮滚动、右侧滚动条、scissor 裁剪防溢出)
- 面板宽度随窗口自适应(200~400px),窗口缩放不再挤压布局
- 复选框两列放不下时自动上下堆叠,消除文字重叠
- 数值行改为 [-] 标签 ... 值 [+] 结构,标签左对齐、数值右对齐、动态刷新
- 矿物输入框占满面板宽度,可见可编辑
- 空闲任务时显示引导文案而非一堆无关选项
- 开始前校验矿物列表非空
这个提交包含在:
lyizai
2026-08-30 12:36:16 +08:00
父节点 f1d9d2fd30
当前提交 49c6297c41
共修改 2 个文件,包含 278 行新增和 206 行删除
+277 -205
查看文件
@@ -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(响应式 + 滚动布局)。
*
* <p>结构:顶部标题与任务选择、底部操作按钮(开始/停止/仅关闭)固定不动,
* 中间为可滚动设置区(滚轮滚动,右侧有滚动条)。设置区内容按当前任务动态
* 显示:主世界只显示主世界相关项,下界只显示下界相关项,避免无关选项干扰。
* 面板宽度随窗口自适应,标签过长时复选框自动改为上下堆叠,避免文字重叠。</p>
*/
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<ScrollEntry> scrollable = new ArrayList<>();
/** 可滚动的纯文本(分区标题、数值、字段标签),渲染时按滚动偏移绘制 */
private record TextEntry(int x, int virtualY, Supplier<String> text, int color, boolean rightAlign, boolean underline) {}
private final List<TextEntry> 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<Boolean> s1,
String label2, java.util.function.BooleanSupplier g2, Consumer<Boolean> 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<Boolean> set1,
String label2, BooleanSupplier get2, Consumer<Boolean> 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<Boolean> 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<Integer> 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<TextFieldWidget> refSetter, Consumer<String> 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<TextFieldWidget> 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<Integer> 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<String> 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();