Unlucky Launcher v0.2.0
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run

首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
This commit is contained in:
AskaEth committed 2026-11-05 14:22:11 +08:00
commit 8a8e5ca0c0
1079 files changed
+208035

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
// Metro 按钮:扁平 + accent 填充,无圆角渐变,hover 只变底色(D14)。
// 四种语义:accent(主操作)/ standard(次要)/ subtle(工具条)/ danger(危险确认)。
// grow = true 时占满布局剩余宽度(磁贴的启动键),否则保持内容宽度(工具按钮)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { UiText, TextRole } from "../components/label.slint";
export enum ButtonKind {
accent,
standard,
subtle,
danger,
}
export component MetroButton inherits Rectangle {
in property <ButtonKind> kind: ButtonKind.standard;
in property <string> text: "";
in property <IconKind> icon: IconKind.plus;
/// 是否显示图标(纯文字按钮置 false)
in property <bool> show-icon: false;
in property <bool> enabled: true;
/// 图标按钮:不画文字,尺寸压到正方形
in property <bool> icon-only: false;
/// 在布局中占据剩余宽度
in property <bool> grow: false;
in property <length> button-height: Metrics.control-height;
callback clicked();
min-width: icon-only ? button-height : text-layout.preferred-width;
height: button-height;
horizontal-stretch: grow ? 1 : 0;
border-radius: Metrics.radius-sm;
border-width: kind == ButtonKind.standard ? 1px : 0px;
border-color: Theme.stroke-strong;
opacity: enabled ? 1.0 : 0.45;
background: kind == ButtonKind.accent
? (touch.has-hover ? Theme.accent-hover : Theme.accent)
: kind == ButtonKind.danger
? (touch.has-hover ? Theme.state-danger-hover : Theme.state-danger)
: kind == ButtonKind.subtle
? (touch.pressed ? Theme.bg-pressed : touch.has-hover ? Theme.bg-hover : transparent)
: (touch.pressed ? Theme.bg-pressed : touch.has-hover ? Theme.bg-hover : Theme.bg-surface);
animate background { duration: Theme.motion-fast; }
text-layout := HorizontalLayout {
alignment: center;
padding-left: icon-only ? 0px : Metrics.gap-2;
padding-right: icon-only ? 0px : Metrics.gap-2;
spacing: Metrics.gap-1;
AppIcon {
icon-size: Metrics.touch-target / 2;
kind: root.icon;
visible: root.show-icon || root.icon-only;
tint: kind == ButtonKind.accent || kind == ButtonKind.danger
? Theme.fg-on-accent
: Theme.fg-primary;
}
UiText {
role: TextRole.bodyStrong;
text: root.text;
visible: !root.icon-only;
color: kind == ButtonKind.accent || kind == ButtonKind.danger
? Theme.fg-on-accent
: Theme.fg-primary;
horizontal-alignment: center;
}
}
touch := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
root.clicked();
}
}
}
accessible-role: button;
accessible-label: root.text;
}
+122
View File
@@ -0,0 +1,122 @@
// 对话框宿主:HMCL 队列语义(一次只显示一条,来源排序由 Rust DialogQueue 决定)+ Metro 化外观。
// 类型:确认 / 输入 / 进度 / 错误(含诊断入口)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiDialog, DialogKind } from "../models.slint";
import { UiText, TextRole } from "../components/label.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { MetroButton, ButtonKind } from "button.slint";
import { TextField } from "inputs.slint";
export component DialogHost inherits Rectangle {
in property <UiDialog> current;
in property <bool> open: false;
/// 队列中剩余条数(>0 时卡片底部提示「还有 N 条待处理」,语义上是队列而非堆叠)
in property <int> queued: 0;
in-out property <string> input-text: "";
callback primary();
callback secondary();
callback dismissed();
width: 100%;
height: 100%;
background: Theme.scrim;
visible: open;
// 吞掉遮罩上的点击,避免误触下层页面
TouchArea {
clicked => { root.dismissed(); }
}
card := Rectangle {
x: (root.width - card.width) / 2;
y: (root.height - card.height) / 2;
width: 440px;
height: content.preferred-height;
background: Theme.bg-elevated;
border-radius: Metrics.radius-lg;
border-width: 1px;
border-color: Theme.stroke;
drop-shadow-blur: 24px;
drop-shadow-color: Theme.shadow;
drop-shadow-offset-y: 6px;
content := VerticalLayout {
padding: Metrics.gap-5;
spacing: Metrics.gap-3;
HorizontalLayout {
spacing: Metrics.gap-2;
if root.current.kind == DialogKind.error : AppIcon {
kind: IconKind.error;
icon-size: 20px;
tint: Theme.state-danger;
}
if root.current.kind == DialogKind.confirm : AppIcon {
kind: IconKind.warn;
icon-size: 20px;
tint: Theme.state-warn;
}
UiText {
role: TextRole.title;
text: root.current.title;
horizontal-stretch: 1;
}
}
body-text := UiText {
role: TextRole.body;
text: root.current.body;
wrap: word-wrap;
overflow: elide;
color: Theme.fg-secondary;
vertical-alignment: top;
height: body-text.preferred-height;
}
if root.current.kind == DialogKind.input : TextField {
placeholder: root.current.input_label;
text <=> root.input-text;
}
if root.current.kind == DialogKind.progress : track := Rectangle {
height: 6px;
border-radius: 3px;
background: Theme.bg-pressed;
Rectangle {
width: track.width * Math.min(Math.max(root.current.progress, 0), 1);
height: track.height;
border-radius: 3px;
background: Theme.accent;
}
}
HorizontalLayout {
spacing: Metrics.gap-2;
alignment: end;
padding-top: Metrics.gap-2;
UiText {
role: TextRole.caption;
text: root.queued > 0 ? "还有 " + root.queued + " 条待处理" : "";
horizontal-stretch: 1;
horizontal-alignment: left;
}
if root.current.secondary != "" : MetroButton {
kind: ButtonKind.subtle;
text: root.current.secondary;
clicked => { root.secondary(); }
}
MetroButton {
kind: root.current.kind == DialogKind.error ? ButtonKind.danger : ButtonKind.accent;
text: root.current.primary;
enabled: root.current.kind != DialogKind.progress;
clicked => { root.primary(); }
}
}
}
}
}
+300
View File
@@ -0,0 +1,300 @@
// 基础输入件:单行输入(含占位文本)、搜索框、开关、分段控件、复选框。
// 一律自绘(Metro 需要方角 + accent 高亮)。占位文本手绘:内置 TextInput 没有
// placeholder-text(那是 std-widgets 的 LineEdit,外观不可控)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { UiText, TextRole } from "../components/label.slint";
/// 单行输入:内置 TextInput + 占位文本层(Focus 态由外部读 `focused`)。
export component PlainInput inherits Rectangle {
in-out property <string> text: "";
in property <string> placeholder: "";
in property <color> text-color: Theme.fg-primary;
in property <length> text-padding: 0px;
out property <bool> focused: input.has-focus;
callback edited(string);
callback accepted(string);
background: transparent;
input := TextInput {
x: root.text-padding;
y: 0;
width: root.width - 2 * root.text-padding;
height: root.height;
color: root.text-color;
font-family: Theme.font-family;
font-size: Theme.font-size-body;
vertical-alignment: center;
single-line: true;
text <=> root.text;
edited => { root.edited(self.text); }
accepted => { root.accepted(self.text); }
}
if root.text == "" && root.placeholder != "" : UiText {
x: root.text-padding;
y: 0;
width: root.width - 2 * root.text-padding;
height: root.height;
role: TextRole.body;
text: root.placeholder;
color: Theme.fg-disabled;
}
}
export component SearchField inherits Rectangle {
in-out property <string> text: "";
in property <string> placeholder: "搜索";
callback edited(string);
callback accepted(string);
height: Metrics.control-height;
background: Theme.bg-input;
border-radius: Metrics.radius-sm;
border-width: 1px;
border-color: field.focused ? Theme.accent : Theme.stroke;
HorizontalLayout {
padding-left: Metrics.gap-2;
padding-right: Metrics.gap-2;
spacing: Metrics.gap-2;
AppIcon { kind: IconKind.search; icon-size: 14px; }
field := PlainInput {
placeholder: root.placeholder;
text <=> root.text;
edited(t) => { root.edited(t); }
accepted(t) => { root.accepted(t); }
}
}
}
export component TextField inherits Rectangle {
in-out property <string> text: "";
in property <string> placeholder: "";
callback edited(string);
height: Metrics.control-height;
background: Theme.bg-input;
border-radius: Metrics.radius-sm;
border-width: 1px;
border-color: field.focused ? Theme.accent : Theme.stroke;
field := PlainInput {
text-padding: Metrics.gap-2;
placeholder: root.placeholder;
text <=> root.text;
edited(t) => { root.edited(t); }
}
}
export component ToggleSwitch inherits Rectangle {
in-out property <bool> checked: false;
callback toggled(bool);
width: 40px;
height: 20px;
border-radius: 10px;
background: checked ? Theme.accent : Theme.stroke-strong;
animate background { duration: Theme.motion-normal; }
Rectangle {
width: 14px;
height: 14px;
y: 3px;
x: root.checked ? root.width - self.width - 3px : 3px;
border-radius: 7px;
background: Theme.fg-on-accent;
animate x { duration: Theme.motion-normal; easing: ease-in-out; }
}
TouchArea {
mouse-cursor: pointer;
clicked => {
root.checked = !root.checked;
root.toggled(root.checked);
}
}
}
/// 分段控件(视图切换、磁贴尺寸、主题三态共用)
export component SegmentedControl inherits Rectangle {
in property <[string]> options;
in-out property <int> selected: 0;
callback changed(int);
height: Metrics.control-height;
background: Theme.bg-canvas;
border-radius: Metrics.radius-sm;
border-width: 1px;
border-color: Theme.stroke;
HorizontalLayout {
padding: 2px;
spacing: 2px;
for o[i] in options : Rectangle {
border-radius: Metrics.radius-sm;
background: i == root.selected
? Theme.accent
: (seg.has-hover ? Theme.bg-hover : transparent);
animate background { duration: Theme.motion-fast; }
UiText {
role: TextRole.caption;
text: o;
horizontal-alignment: center;
color: i == root.selected ? Theme.fg-on-accent : Theme.fg-primary;
}
seg := TouchArea {
mouse-cursor: pointer;
clicked => {
root.selected = i;
root.changed(i);
}
}
}
}
}
export component CheckBox inherits Rectangle { in-out property <bool> checked: false;
in property <string> text: "";
callback toggled(bool);
height: Metrics.touch-target;
background: transparent;
HorizontalLayout {
spacing: Metrics.gap-2;
Rectangle {
width: 16px;
height: 16px;
border-radius: Metrics.radius-sm;
border-width: 1px;
border-color: root.checked ? Theme.accent : Theme.stroke-strong;
background: root.checked ? Theme.accent : Theme.bg-input;
if root.checked : AppIcon {
kind: IconKind.check;
icon-size: 12px;
x: 2px;
y: 2px;
tint: Theme.fg-on-accent;
}
}
UiText { role: TextRole.body; text: root.text; }
TouchArea {
mouse-cursor: pointer;
clicked => {
root.checked = !root.checked;
root.toggled(root.checked);
}
}
}
}
/// 滑条(09 §102/§101 的「强度滑条」:暗化层强度、面板半透明度共用)。
/// 自绘而非 std-widgets 的 Slider:Metro 需要方头轨道 + accent 填充,且要跟主题令牌一起换肤。
export component MetroSlider inherits Rectangle {
in property <float> minimum: 0.0;
in property <float> maximum: 1.0;
in-out property <float> value: 1.0;
callback changed(float);
out property <float> ratio: root.maximum > root.minimum
? Math.max(0.0, Math.min(1.0, (root.value - root.minimum) / (root.maximum - root.minimum)))
: 0.0;
width: 200px;
height: Metrics.touch-target;
background: transparent;
/// 鼠标位置 → 取值(Slint 的 function 不能声明局部变量,所以拆成纯函数;
/// 函数名不用 kebab-case:`value-at(...)` 会被当成减法解析)
pure function valueForX(x: length) -> float {
return root.minimum
+ Math.max(0.0, Math.min(1.0, x / root.width)) * (root.maximum - root.minimum);
}
// 轨道 + 已填充段
Rectangle {
x: 0;
y: (root.height - 4px) / 2;
width: root.width;
height: 4px;
border-radius: 2px;
background: Theme.stroke-strong;
}
Rectangle {
x: 0;
y: (root.height - 4px) / 2;
width: root.width * root.ratio;
height: 4px;
border-radius: 2px;
background: Theme.accent;
}
// 滑块
Rectangle {
x: (root.width - 14px) * root.ratio;
y: (root.height - 14px) / 2;
width: 14px;
height: 14px;
border-radius: 7px;
background: Theme.accent;
border-width: 2px;
border-color: Theme.surface-solid;
}
area := TouchArea {
mouse-cursor: pointer;
moved => {
root.value = root.valueForX(self.mouse-x);
root.changed(root.value);
}
clicked => {
root.value = root.valueForX(self.mouse-x);
root.changed(root.value);
}
}
accessible-role: slider;
}
/// 预设色板(09 §104 自定义强调色:预设色板 + 取色器,取色器由十六进制输入承担)。
export component ColorSwatches inherits Rectangle {
in property <[color]> palette;
in property <color> current;
/// 传**索引**而不是 color:Slint 没有「颜色 → #RRGGBB」的格式化,
/// 预设色板的十六进制串由 Rust 侧同一张表给出(一处真相)。
callback picked(int);
width: root.palette.length * 28px;
height: 28px;
background: transparent;
HorizontalLayout {
spacing: 4px;
for shade[i] in root.palette : Rectangle {
width: 24px;
height: 24px;
border-radius: Metrics.radius-sm;
background: shade;
border-width: shade == root.current ? 2px : 1px;
border-color: shade == root.current ? Theme.fg-primary : Theme.stroke;
swatch-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.picked(i); }
}
accessible-role: button;
}
}
}
+155
View File
@@ -0,0 +1,155 @@
// 设置项行:左标签+说明,右控件。控件类型用 ControlKind 收敛,避免为每个设置项写一遍布局。
// (Slint 没有 slot 机制,所以用 kind 枚举 + 通用属性把控件种类收在一处。)
//
// M41 增加两种控件:`slider`(强度/透明度滑条)与 `swatches`(预设色板 + 取色器入口),
// 它们服务 09 §101-104 的「外观(高级)」四件——高级特性默认关闭,控件本身常显(关着也能调)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiText, TextRole } from "../components/label.slint";
import { MetroButton, ButtonKind } from "button.slint";
import {
ToggleSwitch, SegmentedControl, TextField, MetroSlider, ColorSwatches,
} from "inputs.slint";
export enum ControlKind {
none,
toggle,
segmented,
button,
text,
slider,
swatches,
}
export component SettingItem inherits Rectangle {
in property <string> label: "";
in property <string> description: "";
in property <ControlKind> kind: ControlKind.none;
in property <[string]> options;
in property <string> button-text: "";
in-out property <int> selected: 0;
in-out property <bool> checked: false;
in-out property <string> value: "";
// 滑条
in property <float> min-value: 0.0;
in property <float> max-value: 1.0;
in-out property <float> slider-value: 1.0;
// 色板
in property <[color]> swatch-colors;
in property <color> swatch-current: transparent;
// 危险/提示色描述(如「对比度偏低」)
in property <bool> warn: false;
callback toggled(bool);
callback changed(int);
callback clicked();
callback edited(string);
callback slid(float);
callback swatch-picked(int);
height: root.description == "" ? 44px : 60px;
background: transparent;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-4;
VerticalLayout {
spacing: 0px;
vertical-stretch: 1;
UiText {
role: TextRole.body;
text: root.label;
horizontal-stretch: 1;
}
UiText {
role: TextRole.caption;
text: root.description;
visible: root.description != "";
color: root.warn ? Theme.state-warn : Theme.fg-secondary;
horizontal-stretch: 1;
}
}
if root.kind == ControlKind.toggle : ToggleSwitch {
checked: root.checked;
toggled(v) => {
root.checked = v;
root.toggled(v);
}
}
if root.kind == ControlKind.segmented : SegmentedControl {
width: root.options.length * 64px;
options: root.options;
selected: root.selected;
changed(i) => {
root.selected = i;
root.changed(i);
}
}
if root.kind == ControlKind.button : MetroButton {
kind: ButtonKind.standard;
text: root.button-text;
button-height: 28px;
clicked => { root.clicked(); }
}
if root.kind == ControlKind.text : TextField {
width: 240px;
placeholder: root.button-text;
text <=> root.value;
edited(t) => { root.edited(t); }
}
if root.kind == ControlKind.slider : HorizontalLayout {
spacing: Metrics.gap-2;
alignment: end;
UiText {
role: TextRole.caption;
text: (root.slider-value * 100).round() + "%";
width: 40px;
horizontal-alignment: right;
color: Theme.fg-secondary;
}
MetroSlider {
minimum: root.min-value;
maximum: root.max-value;
value: root.slider-value;
changed(v) => {
root.slider-value = v;
root.slid(v);
}
}
}
if root.kind == ControlKind.swatches : ColorSwatches {
palette: root.swatch-colors;
current: root.swatch-current;
picked(i) => { root.swatch-picked(i); }
}
}
}
/// 分组标题
export component SettingGroup inherits Rectangle {
in property <string> title: "";
height: 36px;
background: transparent;
UiText {
x: Metrics.gap-4;
y: 0;
width: root.width - 2 * Metrics.gap-4;
height: root.height;
role: TextRole.caption;
text: root.title;
color: Theme.fg-secondary;
font-weight: 600;
}
}
+87
View File
@@ -0,0 +1,87 @@
// Toast:右下角非阻塞提示(下载完成 / 复制成功 / 后台任务结论)。
// 队列与自动消失时长由 Rust ToastCenter 管理(P0 默认 4s),这里只负责渲染与手动关闭。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiToast, ToastKind } from "../models.slint";
import { UiText, TextRole } from "../components/label.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
component ToastCard inherits Rectangle {
in property <UiToast> data;
callback dismissed(string);
width: 300px;
height: body-layout.preferred-height;
background: Theme.bg-elevated;
border-radius: Metrics.radius-md;
border-width: 1px;
border-color: Theme.stroke;
drop-shadow-blur: 16px;
drop-shadow-color: Theme.shadow;
drop-shadow-offset-y: 3px;
// 左侧语义色条:Metro 用色块表达状态,不用图标喧宾夺主
Rectangle {
x: 0px;
y: 0px;
width: 3px;
height: root.height;
background: root.data.kind == ToastKind.success ? Theme.state-success
: root.data.kind == ToastKind.warn ? Theme.state-warn
: root.data.kind == ToastKind.error ? Theme.state-danger
: Theme.accent;
}
body-layout := HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-2;
padding-top: Metrics.gap-2;
padding-bottom: Metrics.gap-2;
spacing: Metrics.gap-2;
UiText {
role: TextRole.body;
text: root.data.text;
wrap: word-wrap;
horizontal-stretch: 1;
}
AppIcon {
kind: IconKind.close;
icon-size: 14px;
tint: Theme.fg-secondary;
TouchArea {
mouse-cursor: pointer;
clicked => { root.dismissed(root.data.id); }
}
}
}
}
export component ToastHost inherits Rectangle {
in property <[UiToast]> toasts;
callback dismissed(string);
width: 100%;
height: 100%;
background: transparent;
stack-box := Rectangle {
x: root.width - stack-box.width - Metrics.gap-4;
y: root.height - stack-box.height - Metrics.gap-4;
width: 300px;
height: stack.preferred-height;
background: transparent;
stack := VerticalLayout {
width: stack-box.width;
spacing: Metrics.gap-2;
for t in root.toasts : ToastCard {
data: t;
dismissed(id) => { root.dismissed(id); }
}
}
}
}
+301
View File
@@ -0,0 +1,301 @@
// 新建实例向导(G5,全屏模态):版本 → 加载器 → 名字 → 确认 → 进度。
// 步骤推进/校验在 Rust(`crate::wizard`),这里只画当前步与提交按钮。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiVersion, VersionKind } from "../models.slint";
import { PageState } from "../page_state.slint";
import { UiText, TextRole } from "../components/label.slint";
import { Badge } from "../components/badge.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
import { SearchField } from "../widgets/inputs.slint";
import { InstallPanel } from "../components/install-panel.slint";
import { ScrollView } from "std-widgets.slint";
component StepDot inherits Rectangle {
in property <int> index: 0;
in property <string> label: "";
height: 28px;
background: transparent;
HorizontalLayout {
spacing: Metrics.gap-1;
alignment: center;
Rectangle {
width: 18px;
height: 18px;
border-radius: 9px;
background: PageState.wizard.step >= root.index ? Theme.accent : Theme.bg-hover;
UiText {
role: TextRole.caption;
text: root.index + 1;
horizontal-alignment: center;
color: PageState.wizard.step >= root.index ? Theme.fg-on-accent : Theme.fg-secondary;
}
}
UiText {
role: TextRole.caption;
text: root.label;
color: PageState.wizard.step >= root.index ? Theme.fg-primary : Theme.fg-disabled;
}
}
}
component VersionChoice inherits Rectangle {
in property <UiVersion> data;
callback picked();
height: 36px;
background: PageState.wizard.version == root.data.id ? Theme.accent-soft
: (touch.has-hover ? Theme.bg-hover : transparent);
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
UiText { role: TextRole.body; text: root.data.id; width: 160px; }
Badge {
text: root.data.kind == VersionKind.release ? "正式版" : "预览版";
tint: Theme.fg-secondary;
}
UiText { role: TextRole.caption; text: root.data.released; horizontal-stretch: 1; }
if PageState.wizard.version == root.data.id : AppIcon {
kind: IconKind.check;
icon-size: 14px;
tint: Theme.accent;
}
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.picked(); }
}
}
component LoaderChoice inherits Rectangle {
in property <string> label: "";
in property <string> hint: "";
in property <int> index: 0;
callback picked();
height: 48px;
background: PageState.wizard.loader == root.index ? Theme.accent-soft
: (touch.has-hover ? Theme.bg-hover : transparent);
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
VerticalLayout {
vertical-stretch: 1;
spacing: 0px;
UiText { role: TextRole.body; text: root.label; }
UiText { role: TextRole.caption; text: root.hint; horizontal-stretch: 1; }
}
if PageState.wizard.loader == root.index : AppIcon {
kind: IconKind.check;
icon-size: 14px;
tint: Theme.accent;
}
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.picked(); }
}
}
export component NewInstanceWizard inherits Rectangle {
// 版本列表由 shell 传入(与下载页共用一份,避免二次拉取)
in property <[UiVersion]> versions;
in-out property <string> query: "";
visible: PageState.wizard.open;
background: rgba(0, 0, 0, 0.45);
// 吃掉底层点击(模态)
TouchArea { }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(parent.width - 80px, 720px);
height: min(parent.height - 60px, 560px);
background: Theme.bg-surface;
border-radius: Metrics.radius-md;
border-width: 1px;
border-color: Theme.stroke;
VerticalLayout {
// 标题栏
Rectangle {
height: 52px;
background: transparent;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
UiText {
role: TextRole.subtitle;
text: "新建实例 · " + PageState.wizard.step_label;
horizontal-stretch: 1;
}
MetroButton {
kind: ButtonKind.subtle;
text: "关闭";
button-height: 24px;
clicked => { PageState.wizard-close(); }
}
}
}
// 步骤指示
HorizontalLayout {
padding-left: Metrics.gap-4;
spacing: Metrics.gap-3;
StepDot { index: 0; label: "版本"; }
StepDot { index: 1; label: "加载器"; }
StepDot { index: 2; label: "名字"; }
StepDot { index: 3; label: "确认"; }
StepDot { index: 4; label: "进度"; }
}
Rectangle { height: 1px; background: Theme.stroke; }
// 步骤内容
Rectangle {
vertical-stretch: 1;
background: transparent;
// ① 选版本(只列正式版/快照,远古版走下载页的显式入口)
if PageState.wizard.step == 0 : VerticalLayout {
padding: Metrics.gap-3;
spacing: Metrics.gap-2;
SearchField {
width: 220px;
text <=> root.query;
placeholder: "筛选版本号";
}
ScrollView {
vertical-stretch: 1;
VerticalLayout {
for v in root.versions : VersionChoice {
visible: !v.ancient;
data: v;
picked => { PageState.wizard-select-version(v.id); }
}
}
}
}
// ② 选加载器
if PageState.wizard.step == 1 : VerticalLayout {
padding: Metrics.gap-3;
spacing: 0px;
LoaderChoice { index: 0; label: "原版(Vanilla)"; hint: "不装加载器,最快最稳"; picked => { PageState.wizard-select-loader(0); } }
LoaderChoice { index: 1; label: "Fabric"; hint: "轻量模组加载器(GUI 可直接装)"; picked => { PageState.wizard-select-loader(1); } }
LoaderChoice { index: 2; label: "Quilt"; hint: "Fabric 的分支(GUI 可直接装)"; picked => { PageState.wizard-select-loader(2); } }
LoaderChoice { index: 3; label: "Forge"; hint: "需要 installer processors(首装会外调 java,约 1–3 分钟)"; picked => { PageState.wizard-select-loader(3); } }
LoaderChoice { index: 4; label: "NeoForge"; hint: "需要 installer processors(首装会外调 java,约 1–3 分钟)"; picked => { PageState.wizard-select-loader(4); } }
}
// ③ 名字
if PageState.wizard.step == 2 : VerticalLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-2;
UiText { role: TextRole.body; text: "实例名(默认用版本号)"; }
SearchField {
width: 320px;
text: PageState.wizard.name;
placeholder: "例如:生存 1.20.4";
edited(t) => { PageState.wizard-name-changed(t); }
}
if PageState.wizard.error != "" : UiText {
role: TextRole.caption;
text: PageState.wizard.error;
color: Theme.state-danger;
}
UiText {
role: TextRole.caption;
text: "不能包含 / \\ : * ? \" < > |,最多 64 个字符。";
}
}
// ④ 确认
if PageState.wizard.step == 3 : VerticalLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-2;
UiText { role: TextRole.bodyStrong; text: "将创建实例"; }
UiText { role: TextRole.body; text: "版本 " + PageState.wizard.version; }
UiText { role: TextRole.body; text: "加载器 " + PageState.wizard.loader_label; }
UiText { role: TextRole.body; text: "实例名 " + PageState.wizard.name; }
UiText {
role: TextRole.caption;
text: "确认后会按 core 安装管线下载:版本 JSON → 客户端 jar → 库 → 资源索引 → 登记实例。";
wrap: word-wrap;
}
if PageState.wizard.error != "" : UiText {
role: TextRole.caption;
text: PageState.wizard.error;
color: Theme.state-danger;
}
}
// ⑤ 进度
if PageState.wizard.step == 4 : VerticalLayout {
padding: Metrics.gap-3;
spacing: Metrics.gap-2;
InstallPanel {
data: PageState.install;
cancel => { PageState.install-cancel(); }
}
UiText {
role: TextRole.caption;
text: "装完会自动跳到实例页;失败原因会显示在上面。";
}
}
}
Rectangle { height: 1px; background: Theme.stroke; }
// 底部按钮
Rectangle {
height: 56px;
background: transparent;
HorizontalLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-2;
Rectangle { horizontal-stretch: 1; background: transparent; }
MetroButton {
kind: ButtonKind.subtle;
text: "上一步";
button-height: 30px;
enabled: PageState.wizard.step > 0 && PageState.wizard.step < 4;
clicked => { PageState.wizard-back(); }
}
if PageState.wizard.step < 3 : MetroButton {
kind: ButtonKind.accent;
text: "下一步";
button-height: 30px;
enabled: PageState.wizard.can-next;
clicked => { PageState.wizard-next(); }
}
if PageState.wizard.step == 3 : MetroButton {
kind: ButtonKind.accent;
text: "开始安装";
button-height: 30px;
enabled: PageState.wizard.can-next && !PageState.wizard.busy;
clicked => { PageState.wizard-confirm(); }
}
if PageState.wizard.step == 4 : MetroButton {
kind: ButtonKind.standard;
text: "后台继续";
button-height: 30px;
clicked => { PageState.wizard-close(); }
}
}
}
}
}
}