Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s

Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

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); }
}
}
}
}