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

+283
View File
@@ -0,0 +1,283 @@
// 右侧 AI 常驻栏(D16 §1):默认 320px 可折叠;可「展开为页面」变成全宽独立页,
// 两形态共享同一份会话模型(由 Rust 侧 AiSession 持有,这里只渲染,切换不丢状态)。
// 栏顶承载 YOLO 快捷开关(D4 补充)与端点标签。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiChatMessage, ChatRole } from "../models.slint";
import { UiText, TextRole } from "label.slint";
import { AppIcon, IconKind } from "icons.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
import { PlainInput } from "../widgets/inputs.slint";
import { GlassBase } from "glass.slint";
import { ListView } from "std-widgets.slint";
// 一条会话消息:user 靠右(accent-soft 块)/ assistant 靠左 / tool 用状态边框 / system 居中弱化
component ChatBubble inherits Rectangle {
in property <UiChatMessage> message;
property <bool> is-tool: message.role == ChatRole.tool;
property <bool> is-system: message.role == ChatRole.system;
property <length> text-width: root.width - 2 * Metrics.gap-2;
property <length> tool-row: is-tool ? 16px : 0px;
height: text-node.preferred-height + 2 * Metrics.gap-2 + tool-row;
background: message.role == ChatRole.user
? Theme.accent-soft
: is-tool ? Theme.bg-canvas
: is-system ? transparent
: Theme.bg-elevated;
border-radius: Metrics.radius-md;
border-width: is-tool ? 1px : 0px;
border-color: message.tool_ok ? Theme.state-success : Theme.state-danger;
// 工具调用可视化(D4 要求:工具执行在会话里可见)
if is-tool : AppIcon {
x: Metrics.gap-2;
y: Metrics.gap-2 + 1px;
icon-size: 12px;
kind: message.tool_ok ? IconKind.check : IconKind.error;
tint: message.tool_ok ? Theme.state-success : Theme.state-danger;
}
if is-tool : UiText {
x: Metrics.gap-2 + 16px;
y: Metrics.gap-2;
width: text-width - 16px;
height: 14px;
role: TextRole.caption;
text: "工具 · " + message.tool_name;
color: message.tool_ok ? Theme.state-success : Theme.state-danger;
}
text-node := UiText {
x: Metrics.gap-2;
y: Metrics.gap-2 + tool-row;
width: text-width;
height: text-node.preferred-height;
role: TextRole.body;
text: message.text;
wrap: word-wrap;
overflow: elide;
vertical-alignment: top;
horizontal-alignment: message.role == ChatRole.user ? right : left;
color: is-system ? Theme.fg-secondary : Theme.fg-primary;
}
// 流式未收敛:末尾的 accent 光标块
if message.streaming : Rectangle {
x: 0;
y: root.height - 10px;
width: 8px;
height: 10px;
background: Theme.accent;
}
}
export component AiPanel inherits Rectangle {
in property <[UiChatMessage]> messages;
in-out property <bool> collapsed: false;
/// true = 停在右侧栏形态;false = 已拆成独立全宽页
in property <bool> docked: true;
in property <bool> yolo: false;
in property <bool> streaming: false;
in property <string> endpoint-label: "llama.cpp · 本地";
in-out property <string> draft: "";
callback toggle-collapsed();
callback expand();
callback dock();
callback toggle-yolo();
callback send(string);
callback stop();
// 栏形态用 min/max 钉死宽度(布局里等价于固定宽度),页形态交给父容器铺满
min-width: docked ? (collapsed ? Metrics.ai-rail-width : Metrics.ai-panel-width) : 0px;
max-width: docked ? (collapsed ? Metrics.ai-rail-width : Metrics.ai-panel-width) : 100000px;
horizontal-stretch: docked ? 0 : 1;
height: 100%;
background: Theme.bg-surface;
// 玻璃拟态底(D17 高级特性;关闭态不画东西)——排在内容之前,压在最底层
GlassBase { card-radius: 0px; }
// 左边界线
Rectangle {
x: 0;
y: 0;
width: 1px;
height: root.height;
background: Theme.stroke;
}
// ── 折叠态:icon 轨 ─────────────────────────────────────────
if docked && collapsed : VerticalLayout {
x: 1px;
width: root.width - 1px;
height: root.height;
padding-top: Metrics.gap-2;
padding-bottom: Metrics.gap-2;
spacing: Metrics.gap-2;
AppIcon {
kind: IconKind.ai;
icon-size: 18px;
tint: Theme.accent;
TouchArea {
mouse-cursor: pointer;
clicked => { root.toggle-collapsed(); }
}
}
AppIcon {
kind: IconKind.expand;
icon-size: 14px;
tint: Theme.fg-secondary;
TouchArea {
mouse-cursor: pointer;
clicked => { root.expand(); }
}
}
Rectangle { vertical-stretch: 1; background: transparent; }
// YOLO 指示点:开启时实心 accent
Rectangle {
width: 6px;
height: 6px;
border-radius: 3px;
background: root.yolo ? Theme.accent : Theme.stroke-strong;
}
}
// ── 展开态:栏内 / 独立页共用 ────────────────────────────────
if !(docked && collapsed) : VerticalLayout {
x: 1px;
width: root.width - 1px;
height: root.height;
// 头部
Rectangle {
height: 52px;
background: transparent;
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-2;
spacing: Metrics.gap-2;
AppIcon { kind: IconKind.ai; icon-size: 16px; tint: Theme.accent; }
VerticalLayout {
spacing: 0px;
vertical-stretch: 1;
UiText {
role: TextRole.bodyStrong;
text: "AI 助手";
horizontal-stretch: 1;
}
UiText {
role: TextRole.caption;
text: root.endpoint-label;
horizontal-stretch: 1;
}
}
MetroButton {
kind: root.yolo ? ButtonKind.accent : ButtonKind.subtle;
text: "YOLO";
button-height: 24px;
clicked => { root.toggle-yolo(); }
}
if root.docked : MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.expand;
icon-only: true;
button-height: 24px;
clicked => { root.expand(); }
}
if !root.docked : MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.sidebar;
icon-only: true;
button-height: 24px;
clicked => { root.dock(); }
}
if root.docked : MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.chevronRight;
icon-only: true;
button-height: 24px;
clicked => { root.toggle-collapsed(); }
}
}
}
Rectangle { height: 1px; background: Theme.stroke; }
// 对话流(ListView 行虚拟化,research/01 已确认)
ListView {
vertical-stretch: 1;
for m in root.messages : ChatBubble {
message: m;
width: root.width - 2 * Metrics.gap-3;
}
}
if root.messages.length == 0 : UiText {
role: TextRole.caption;
text: "问点什么:装哪个版本、崩溃怎么修、帮我改设置…";
horizontal-alignment: center;
height: 40px;
}
Rectangle { height: 1px; background: Theme.stroke; }
// 输入区
Rectangle {
height: 56px;
background: transparent;
HorizontalLayout {
padding: Metrics.gap-2;
spacing: Metrics.gap-2;
input-box := Rectangle {
horizontal-stretch: 1;
vertical-stretch: 1;
background: Theme.bg-input;
border-radius: Metrics.radius-sm;
border-width: 1px;
border-color: input-field.focused ? Theme.accent : Theme.stroke;
input-field := PlainInput {
text-padding: Metrics.gap-2;
placeholder: "输入消息,Enter 发送";
text <=> root.draft;
accepted(t) => {
if (t != "") {
root.send(t);
}
}
}
}
MetroButton {
kind: ButtonKind.accent;
icon: root.streaming ? IconKind.stop : IconKind.send;
icon-only: true;
clicked => {
if (root.streaming) {
root.stop();
} else if (root.draft != "") {
root.send(root.draft);
}
}
}
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
// 徽标:MC 版本 / 加载器 / 已装 / 远古版 等短标记。Metro 做法是细边框小方块,不用药丸圆角。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiText, TextRole } from "../components/label.slint";
export component Badge inherits Rectangle {
in property <string> text: "";
in property <color> tint: Theme.fg-secondary;
in property <color> fill: transparent;
height: 18px;
width: label.preferred-width + 2 * Metrics.gap-1 + 2px;
background: fill;
border-width: 1px;
border-color: tint;
border-radius: Metrics.radius-sm;
label := UiText {
role: TextRole.caption;
text: root.text;
color: root.tint;
font-size: 11px;
horizontal-alignment: center;
x: Metrics.gap-1 + 1px;
width: label.preferred-width;
}
}
+59
View File
@@ -0,0 +1,59 @@
// 玻璃拟态卡片的公共底座(09 §103:Slint 没有 backdrop-filter,用「预生成模糊背景层」替代)。
//
// 手法:Rust 侧把背景图降采样成一张极小的图(`Theme.bg-blur`),渲染时被平滑放大 → 天然模糊。
// 卡片里把它按 `absolute-position` **反向偏移**画出来,于是卡片显示的是「自己这一块屏幕区域的
// 模糊影像」,视觉上就是毛玻璃;再叠一层半透明面板色保证文字可读(对比度优先)。
//
// 关闭态(`Theme.glass == false` 或无背景图)不画任何东西:卡片保持 Metro 纯色观感。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
export component GlassBase inherits Rectangle {
/// 卡片圆角(跟随宿主卡片)
in property <length> card-radius: Metrics.radius-md;
/// 叠在模糊层上的半透明面板色(保证前景对比度)
in property <color> tint: Theme.surface-solid;
out property <bool> active: Theme.glass && Theme.bg-blur.width > 0 && Theme.bg-image.width > 0;
x: 0;
y: 0;
width: 100%;
height: 100%;
border-radius: root.card-radius;
clip: true;
background: transparent;
if root.active : Image {
// 整窗模糊层 + 反向偏移 = 只露出「卡片所在那一块」。
// 必须用 `parent`(本组件自身)的绝对位置:用 `self` 会与自身 x 形成绑定环
// (absolute-position 依赖 x,x 又依赖 absolute-position)。
source: Theme.bg-blur;
x: -parent.absolute-position.x;
y: -parent.absolute-position.y;
width: Theme.window-width;
height: Theme.window-height;
image-fit: cover;
}
if root.active : Rectangle {
// 暗化层要**重画一次**:模糊层在卡片内部、盖在窗口的暗化层之上,
// 不补这一层的话玻璃卡片会比周围亮一大截(05 截图验收时的真实问题)。
background: rgba(0, 0, 0, Theme.bg-dim);
border-radius: root.card-radius;
}
if root.active : Rectangle {
// 玻璃染色:面板底色 55% 不透明,字压得住背景(对比度优先于「更透」)
background: root.tint.with-alpha(0.55);
border-radius: root.card-radius;
}
if root.active : Rectangle {
// 上缘一道细高光,玻璃的「厚度感」
y: 0;
height: 1px;
width: root.width;
background: Theme.stroke.with-alpha(0.35);
}
}
+192
View File
@@ -0,0 +1,192 @@
// 图标:18×18 viewbox 的矢量绘制(Metro 要求线性几何图标,不用 emoji/位图)。
// 只用 M/L/Q/Z 命令 + 直线,避免依赖字体字形;描边图标 stroke,实心图标 fill。
import { Theme } from "../theme/tokens.slint";
export enum IconKind {
instances,
download,
logs,
settings,
ai,
play,
stop,
search,
plus,
close,
minimize,
maximize,
chevronLeft,
chevronRight,
chevronDown,
expand,
send,
check,
warn,
error,
more,
sidebar,
restore,
}
export component AppIcon inherits Rectangle {
in property <IconKind> kind: IconKind.instances;
in property <color> tint: Theme.fg-secondary;
in property <length> icon-size: 18px;
width: icon-size;
height: icon-size;
background: transparent;
if kind == IconKind.instances : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 2 2 L 8 2 L 8 8 L 2 8 Z M 10 2 L 16 2 L 16 8 L 10 8 Z M 2 10 L 8 10 L 8 16 L 2 16 Z M 10 10 L 16 10 L 16 16 L 10 16 Z";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
if kind == IconKind.download : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 9 2 L 9 11 M 4 7 L 9 12 L 14 7 M 3 15 L 15 15";
fill: transparent; stroke: root.tint; stroke-width: 1.6px;
}
if kind == IconKind.logs : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 3 4 L 15 4 M 3 9 L 15 9 M 3 14 L 11 14";
fill: transparent; stroke: root.tint; stroke-width: 1.6px;
}
if kind == IconKind.settings : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 3 5 L 15 5 M 3 9 L 15 9 M 3 13 L 15 13";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
if kind == IconKind.settings : Rectangle {
x: 5px;
width: 5px; height: 5px; y: 3px;
border-radius: 2.5px; background: root.tint;
}
if kind == IconKind.settings : Rectangle {
x: 10px; y: 7px; width: 5px; height: 5px;
border-radius: 2.5px; background: root.tint;
}
if kind == IconKind.settings : Rectangle {
x: 3px; y: 11px; width: 5px; height: 5px;
border-radius: 2.5px; background: root.tint;
}
if kind == IconKind.ai : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 9 1.5 L 10.7 7.3 L 16.5 9 L 10.7 10.7 L 9 16.5 L 7.3 10.7 L 1.5 9 L 7.3 7.3 Z";
fill: root.tint; stroke: transparent; stroke-width: 1px;
}
if kind == IconKind.play : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 5 3 L 15 9 L 5 15 Z";
fill: root.tint; stroke: transparent; stroke-width: 1px;
}
if kind == IconKind.stop : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 5 5 L 13 5 L 13 13 L 5 13 Z";
fill: root.tint; stroke: transparent; stroke-width: 1px;
}
if kind == IconKind.search : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 8 3 Q 13 3 13 8 Q 13 13 8 13 Q 3 13 3 8 Q 3 3 8 3 Z M 11.5 11.5 L 16 16";
fill: transparent; stroke: root.tint; stroke-width: 1.5px;
}
if kind == IconKind.plus : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 9 3 L 9 15 M 3 9 L 15 9";
fill: transparent; stroke: root.tint; stroke-width: 1.8px;
}
if kind == IconKind.close : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 4 4 L 14 14 M 14 4 L 4 14";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
if kind == IconKind.minimize : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 4 9 L 14 9";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
if kind == IconKind.maximize : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 4.5 4.5 L 13.5 4.5 L 13.5 13.5 L 4.5 13.5 Z";
fill: transparent; stroke: root.tint; stroke-width: 1.2px;
}
if kind == IconKind.chevronLeft : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 11 3 L 6 9 L 11 15";
fill: transparent; stroke: root.tint; stroke-width: 1.6px;
}
if kind == IconKind.chevronRight : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 7 3 L 12 9 L 7 15";
fill: transparent; stroke: root.tint; stroke-width: 1.6px;
}
if kind == IconKind.chevronDown : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 3 7 L 9 13 L 15 7";
fill: transparent; stroke: root.tint; stroke-width: 1.6px;
}
if kind == IconKind.expand : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 3 7 L 3 3 L 7 3 M 11 3 L 15 3 L 15 7 M 15 11 L 15 15 L 11 15 M 7 15 L 3 15 L 3 11";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
if kind == IconKind.send : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 2 9 L 16 9 M 11 4 L 16 9 L 11 14";
fill: transparent; stroke: root.tint; stroke-width: 1.6px;
}
if kind == IconKind.check : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 3 9 L 7 13 L 15 4";
fill: transparent; stroke: root.tint; stroke-width: 1.8px;
}
if kind == IconKind.warn : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 9 2 L 16 15 L 2 15 Z M 9 6.5 L 9 10.5";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
if kind == IconKind.error : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 9 2 Q 16 2 16 9 Q 16 16 9 16 Q 2 16 2 9 Q 2 2 9 2 Z M 9 5 L 9 10";
fill: transparent; stroke: root.tint; stroke-width: 1.5px;
}
if kind == IconKind.more : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 4 7 L 6 7 L 6 9 L 4 9 Z M 8 7 L 10 7 L 10 9 L 8 9 Z M 12 7 L 14 7 L 14 9 L 12 9 Z";
fill: root.tint; stroke: transparent; stroke-width: 1px;
}
if kind == IconKind.sidebar : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 3 4 L 15 4 L 15 14 L 3 14 Z M 7 4 L 7 14";
fill: transparent; stroke: root.tint; stroke-width: 1.4px;
}
// 还原(已最大化时的标题栏按钮):两层叠放方框
if kind == IconKind.restore : Path {
width: root.width; height: root.height;
viewbox-width: 18; viewbox-height: 18;
commands: "M 6 3 L 15 3 L 15 12 M 3 6 L 12 6 L 12 15 L 3 15 Z";
fill: transparent; stroke: root.tint; stroke-width: 1.2px;
}
}
+34
View File
@@ -0,0 +1,34 @@
// 排版角色封装:所有文字都走 UiText,不在页面里散落 font-size/font-family 字面量。
// 角色 → 字号/字重/默认前景色由 tokens.slint 的 ramp 决定;外部赋值可覆盖(例如强调色标题)。
import { Theme } from "../theme/tokens.slint";
export enum TextRole {
display,
title,
subtitle,
body,
bodyStrong,
caption,
mono,
monoSmall,
}
export component UiText inherits Text {
in property <TextRole> role: TextRole.body;
color: role == TextRole.caption ? Theme.fg-secondary : Theme.fg-primary;
font-family: (role == TextRole.mono || role == TextRole.monoSmall)
? Theme.font-family-mono
: Theme.font-family;
font-size: (role == TextRole.display) ? Theme.font-size-display
: (role == TextRole.title) ? Theme.font-size-title
: (role == TextRole.subtitle) ? Theme.font-size-subtitle
: (role == TextRole.caption || role == TextRole.monoSmall) ? Theme.font-size-caption
: Theme.font-size-body;
font-weight: (role == TextRole.bodyStrong || role == TextRole.title || role == TextRole.display)
? 600
: 400;
vertical-alignment: center;
wrap: no-wrap;
overflow: elide;
}
+182
View File
@@ -0,0 +1,182 @@
// 左侧主导航(D16 §1):图标+文字,选中项 accent 色块;可折叠成纯图标窄条(偏好记忆)。
// 折叠宽度/展开宽度取 Metrics,动画时长取 Theme.motion-normal。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { RouteKind } from "../models.slint";
import { UiText, TextRole } from "label.slint";
import { AppIcon, IconKind } from "icons.slint";
component NavItem inherits Rectangle {
in property <IconKind> icon: IconKind.instances;
in property <string> label: "";
in property <bool> active: false;
in property <bool> collapsed: false;
callback clicked();
height: root.collapsed ? 48px : 64px;
background: root.active
? Theme.accent-soft
: touch.has-hover ? Theme.bg-hover : transparent;
animate background { duration: Theme.motion-fast; }
// 选中态:左侧 accent 竖条(Metro 侧栏语义)
Rectangle {
visible: root.active;
x: 0;
y: 0;
width: 3px;
height: root.height;
background: Theme.accent;
}
AppIcon {
kind: root.icon;
icon-size: 20px;
x: (root.width - self.width) / 2;
y: root.collapsed ? (root.height - self.height) / 2 : 12px;
tint: root.active ? Theme.accent : Theme.fg-secondary;
}
UiText {
visible: !root.collapsed;
role: TextRole.caption;
text: root.label;
color: root.active ? Theme.accent : Theme.fg-secondary;
horizontal-alignment: center;
x: 0;
y: 38px;
width: root.width;
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
accessible-role: button;
accessible-label: root.label;
}
export component Nav inherits Rectangle {
in property <RouteKind> active: RouteKind.instances;
in-out property <bool> collapsed: false;
in property <string> account-name: "未登录";
in property <color> account-hue: #5C2D91;
in property <string> account-initial: "?";
callback navigate(RouteKind);
callback toggle-collapsed();
callback account-clicked();
width: collapsed ? Metrics.nav-collapsed-width : Metrics.nav-expanded-width;
height: 100%;
background: Theme.bg-surface;
animate width { duration: Theme.motion-normal; easing: ease-in-out; }
// 右边界线
Rectangle {
x: root.width - 1px;
y: 0;
width: 1px;
height: root.height;
background: Theme.stroke;
}
VerticalLayout {
x: 0;
y: Metrics.gap-2;
width: root.width;
height: root.height - Metrics.gap-2 - Metrics.gap-2;
NavItem {
icon: IconKind.instances;
label: "实例";
active: root.active == RouteKind.instances || root.active == RouteKind.detail;
collapsed: root.collapsed;
clicked => { root.navigate(RouteKind.instances); }
}
NavItem {
icon: IconKind.download;
label: "下载";
active: root.active == RouteKind.download;
collapsed: root.collapsed;
clicked => { root.navigate(RouteKind.download); }
}
NavItem {
icon: IconKind.logs;
label: "日志";
active: root.active == RouteKind.logs;
collapsed: root.collapsed;
clicked => { root.navigate(RouteKind.logs); }
}
NavItem {
icon: IconKind.settings;
label: "设置";
active: root.active == RouteKind.settings;
collapsed: root.collapsed;
clicked => { root.navigate(RouteKind.settings); }
}
Rectangle { vertical-stretch: 1; background: transparent; }
// 底部:账户头像(点击进设置·账户)
Rectangle {
height: 56px;
background: account-touch.has-hover ? Theme.bg-hover : transparent;
Rectangle {
width: 28px;
height: 28px;
border-radius: 14px;
background: root.account-hue;
x: (root.width - self.width) / 2;
y: 8px;
UiText {
text: root.account-initial;
font-size: 12px;
font-weight: 600;
color: Theme.fg-on-accent;
horizontal-alignment: center;
vertical-alignment: center;
}
}
UiText {
visible: !root.collapsed;
role: TextRole.caption;
text: root.account-name;
horizontal-alignment: center;
x: 0;
y: 38px;
width: root.width;
}
account-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.account-clicked(); }
}
}
// 折叠开关(偏好由 Rust 侧持久化到 ui.nav.collapsed)
Rectangle {
height: 40px;
background: toggle-touch.has-hover ? Theme.bg-hover : transparent;
AppIcon {
kind: root.collapsed ? IconKind.chevronRight : IconKind.chevronLeft;
icon-size: 16px;
x: (root.width - self.width) / 2;
y: (root.height - self.height) / 2;
tint: Theme.fg-secondary;
}
toggle-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.toggle-collapsed(); }
}
accessible-role: button;
accessible-label: root.collapsed ? "展开导航栏" : "折叠导航栏";
}
}
}
@@ -0,0 +1,22 @@
// 启动进度环:磁贴「启动中」状态用(下载/校验/启动三个阶段共用一个环)。
// 用 std-widgets 的 ProgressIndicator:progress <= 0 时转为不确定态(转圈)。
// P1 若要完全贴合 Metro 强调色,需自绘弧线(Slint 无 arc 命令,得用旋转遮罩法)。
import { Theme } from "../theme/tokens.slint";
import { ProgressIndicator } from "std-widgets.slint";
export component ProgressRing inherits Rectangle {
/// 0.0~1.0;<= 0 表示未知进度
in property <float> progress: 0;
in property <length> ring-size: 28px;
width: ring-size;
height: ring-size;
background: transparent;
ProgressIndicator {
width: root.ring-size;
height: root.ring-size;
progress: root.progress;
indeterminate: root.progress <= 0;
}
}
+198
View File
@@ -0,0 +1,198 @@
// 实例磁贴(G1):图标 / 名字 / MC 版本徽标 / 加载器徽标 / 最后游玩 / 大启动键 + 设置键。
// 三档尺寸由 Metrics.tile-* 决定(小磁贴藏徽标与时间,只留名字+启动);
// 启动中把底部按钮行替换成「进度环 + 阶段文案 + 停止」。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiTile, RuntimePhase, LoaderKind } from "../models.slint";
import { UiText, TextRole } from "label.slint";
import { AppIcon, IconKind } from "icons.slint";
import { Badge } from "badge.slint";
import { ProgressRing } from "progress-ring.slint";
import { GlassBase } from "glass.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
component TileIcon inherits Rectangle {
in property <UiTile> data;
in property <length> box-size: 44px;
width: box-size;
height: box-size;
border-radius: Metrics.radius-md;
background: root.data.hue;
if root.data.icon.width > 0 : Image {
x: 0;
y: 0;
width: root.width;
height: root.height;
source: root.data.icon;
image-fit: cover;
}
if root.data.icon.width <= 0 : UiText {
text: root.data.initial;
font-size: 18px;
font-weight: 600;
color: Theme.fg-on-accent;
horizontal-alignment: center;
vertical-alignment: center;
}
}
export component InstanceTile inherits Rectangle {
in property <UiTile> data;
in property <bool> compact: false;
callback open();
callback launch();
callback configure();
/// 启动中/运行中的停止键(dispatch StopInstance)
callback stop();
height: Metrics.tile-height;
background: touch.has-hover ? Theme.bg-elevated : Theme.bg-surface;
border-radius: Metrics.radius-md;
border-width: 1px;
border-color: root.data.phase == RuntimePhase.running ? Theme.accent : Theme.stroke;
drop-shadow-blur: touch.has-hover ? 12px : 0px;
drop-shadow-color: Theme.shadow;
drop-shadow-offset-y: 2px;
animate background { duration: Theme.motion-fast; }
animate drop-shadow-blur { duration: Theme.motion-normal; }
// 玻璃拟态底(关闭态什么都不画,卡片就是 Metro 纯色);必须排在最前,否则盖住 accent 细线
GlassBase {
card-radius: root.border-radius;
}
// 运行中:顶部 accent 细线(Metro 色块语义)
Rectangle {
visible: root.data.phase == RuntimePhase.running;
x: 0;
y: 0;
width: root.width;
height: 2px;
background: Theme.accent;
}
// 磁贴主体点击 → 实例详情(G4);按钮的 TouchArea 后声明,命中优先
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.open(); }
}
VerticalLayout {
padding: Metrics.gap-3;
spacing: Metrics.gap-2;
HorizontalLayout {
spacing: Metrics.gap-2;
TileIcon {
data: root.data;
box-size: Metrics.tile-icon-size;
}
VerticalLayout {
spacing: 2px;
vertical-stretch: 1;
UiText {
role: TextRole.bodyStrong;
text: root.data.name;
horizontal-stretch: 1;
}
UiText {
role: TextRole.caption;
text: root.data.last_played;
visible: !root.compact;
}
}
}
HorizontalLayout {
visible: !root.compact;
spacing: Metrics.gap-1;
Badge {
text: root.data.mc_version;
tint: Theme.fg-secondary;
}
Badge {
text: root.data.loader_label;
tint: root.data.loader == LoaderKind.vanilla ? Theme.fg-secondary : Theme.accent;
}
}
Rectangle { vertical-stretch: 1; background: transparent; }
// 空闲/失败:大启动键 + 设置键
HorizontalLayout {
visible: root.data.phase == RuntimePhase.idle || root.data.phase == RuntimePhase.failed;
spacing: Metrics.gap-2;
MetroButton {
kind: ButtonKind.accent;
text: root.data.phase == RuntimePhase.failed ? "重试" : "启动";
show-icon: true;
icon: IconKind.play;
grow: true;
clicked => { root.launch(); }
}
MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.settings;
icon-only: true;
clicked => { root.configure(); }
}
}
// 启动中:进度环 + 阶段文案 + 停止
HorizontalLayout {
visible: root.data.phase == RuntimePhase.downloading
|| root.data.phase == RuntimePhase.verifying
|| root.data.phase == RuntimePhase.launching;
spacing: Metrics.gap-2;
ProgressRing {
progress: root.data.progress;
ring-size: 22px;
}
UiText {
role: TextRole.caption;
text: root.data.phase_label;
horizontal-stretch: 1;
}
MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.stop;
icon-only: true;
clicked => { root.stop(); }
}
}
// 运行中:状态点 + 停止
HorizontalLayout {
visible: root.data.phase == RuntimePhase.running;
spacing: Metrics.gap-2;
Rectangle {
width: 8px;
height: 8px;
border-radius: 4px;
background: Theme.state-success;
}
UiText {
role: TextRole.caption;
text: "运行中";
color: Theme.state-success;
horizontal-stretch: 1;
}
MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.stop;
icon-only: true;
clicked => { root.stop(); }
}
}
}
}
+126
View File
@@ -0,0 +1,126 @@
// 自绘无边框标题栏(D16 §1):Metro 一体感 + accent 顶线。
// 拖拽/最大化/最小化/关闭只发回调,原生窗口操作由 main.rs 接到 Slint Window 平台上
// (Slint DSL 内无法移动原生窗口)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiText, TextRole } from "label.slint";
import { AppIcon, IconKind } from "icons.slint";
component WindowButton inherits Rectangle {
in property <IconKind> icon: IconKind.minimize;
/// 关闭键:hover 用 Metro 红底白字
in property <bool> danger: false;
callback clicked();
width: Metrics.titlebar-button-width;
height: Metrics.titlebar-height;
background: touch.pressed
? (danger ? Theme.state-danger-hover : Theme.bg-pressed)
: touch.has-hover ? (danger ? Theme.state-danger : Theme.bg-hover) : transparent;
animate background { duration: Theme.motion-fast; }
AppIcon {
kind: root.icon;
icon-size: 12px;
x: (root.width - self.width) / 2;
y: (root.height - self.height) / 2;
tint: root.danger && touch.has-hover ? Theme.fg-on-accent : Theme.fg-primary;
}
touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
}
export component TitleBar inherits Rectangle {
in property <string> title: "Unlucky Launcher";
in property <string> subtitle: "";
in property <bool> maximized: false;
callback toggle-maximize();
callback minimize();
callback request-close();
height: Metrics.titlebar-height;
background: Theme.bg-titlebar;
// accent 顶线(Metro 强调色贯穿)
Rectangle {
x: 0;
y: 0;
width: root.width;
height: Metrics.accent-line;
background: Theme.accent;
}
// 拖拽区:交给原生 `WindowMoveArea`(Slint 1.18 内置,拖过阈值才移动窗口,
// 并保持子元素可交互)。只覆盖左侧,右侧三个窗口按钮留出来。
// 平台不支持「系统级窗口移动」时该元素退化为无操作(Qt/部分 Wayland 组合),
// winit on Windows/macOS/X11 可用——Win7 落在这条路上。
WindowMoveArea {
x: 0;
y: Metrics.accent-line;
width: root.width - 3 * Metrics.titlebar-button-width;
height: root.height - Metrics.accent-line;
}
// 应用标记 + 标题
HorizontalLayout {
x: Metrics.gap-3;
y: Metrics.accent-line;
width: root.width - 3 * Metrics.titlebar-button-width - 2 * Metrics.gap-3;
height: root.height - Metrics.accent-line;
spacing: Metrics.gap-2;
Rectangle {
width: 14px;
height: 14px;
border-radius: Metrics.radius-sm;
background: Theme.accent;
UiText {
text: "U";
font-size: 10px;
font-weight: 700;
color: Theme.fg-on-accent;
horizontal-alignment: center;
vertical-alignment: center;
}
}
UiText {
role: TextRole.caption;
text: root.title;
font-weight: 600;
horizontal-stretch: 1;
}
UiText {
role: TextRole.caption;
text: root.subtitle;
color: Theme.fg-secondary;
}
}
HorizontalLayout {
x: root.width - 3 * Metrics.titlebar-button-width;
y: Metrics.accent-line;
width: 3 * Metrics.titlebar-button-width;
height: root.height - Metrics.accent-line;
spacing: 0px;
WindowButton {
icon: IconKind.minimize;
clicked => { root.minimize(); }
}
WindowButton {
icon: root.maximized ? IconKind.restore : IconKind.maximize;
clicked => { root.toggle-maximize(); }
}
WindowButton {
icon: IconKind.close;
danger: true;
clicked => { root.request-close(); }
}
}
}
+303
View File
@@ -0,0 +1,303 @@
// Unlucky-Launcher · 主入口窗口(P0 阶段 B 接线)
//
// 布局真相源:DevDocs/PlanDoc/09-ui-layout.md(D16:左侧固定导航 + 右侧 AI 常驻栏 + 磁贴实例页)。
// 本文件只做三件事:①无边框窗口与字体/缩放口径 ②实例化 `AppShell` ③把 AppShell 的全部
// 属性/回调原样上抛给 src/main.rs(Slint 只能从根组件访问属性与回调,所以这层转发是必需的)。
//
// 与 M11 骨架的关系:M11 的 `Metro` palette global(accent/surface/sidebar/tile/text/muted/pad/gap)
// 已由 `theme/tokens.slint` 的 `Theme` 继承并扩展——同一「palette global 令牌化」思路,但多了
// 亮/暗双套色板(LightPalette/DarkPalette)、主题三态解析、排版 ramp 与字体栈;M11 的占位
// NavItem/Tile 组件由 `components/nav.slint` 与 `components/tile.slint` 取代,占位内核不再保留。
import { AppShell } from "shell.slint";
import { Theme } from "theme/tokens.slint";
import { Metrics } from "theme/metrics.slint";
import {
RouteKind, TileSize, InstanceView, SettingSection, DownloadCategory, ThemeMode, BgFillKind,
UiTile, UiVersion, UiLogSession, UiLogLine, UiDiagnosis, UiChatMessage, UiDialog, UiToast,
} from "models.slint";
export component MainWindow inherits Window {
title: root.app-title;
// 默认尺寸取 D16 线框口径;最小尺寸保证 5 列磁贴 + 导航 + AI 栏不挤压
width: 1180px;
height: 760px;
// Metro 一体感:自绘标题栏(components/titlebar.slint)
no-frame: true;
// 无边框窗口必须显式给边缘缩放热区,否则用户无法拖拽改窗口大小
resize-border-width: 4px;
default-font-family: Theme.font-family;
default-font-size: Theme.font-size-body;
// 窗口自身底色必须**不透明**:面板半透明度(panel-alpha)只该透出背景图,不该透到桌面
background: Theme.canvas-solid;
// ── 从 main.rs 灌入的状态 ───────────────────────────────────
in-out property <string> app-title: "Unlucky Launcher";
in property <RouteKind> route: RouteKind.instances;
in property <bool> nav-collapsed: false;
in property <string> account-name: "未登录";
in property <string> account-initial: "?";
in property <color> account-hue: #5C2D91;
in-out property <bool> ai-collapsed: false;
in property <[UiChatMessage]> ai-messages;
in property <bool> ai-streaming: false;
in-out property <bool> yolo: false;
in property <string> ai-endpoint-label: "llama.cpp · 本地";
in-out property <string> ai-draft: "";
in property <[UiTile]> tiles;
in property <[UiTile]> running-tiles;
in property <[UiVersion]> versions;
in property <[UiLogSession]> log-sessions;
in property <[UiLogLine]> log-lines;
in property <UiDiagnosis> diagnosis;
in property <bool> has-diagnosis: false;
in property <string> selected-session: "";
in-out property <TileSize> tile-size: TileSize.medium;
in-out property <InstanceView> instance-view: InstanceView.tiles;
in-out property <string> instance-query: "";
in-out property <SettingSection> settings-section: SettingSection.general;
in-out property <ThemeMode> theme-mode: ThemeMode.auto;
// 系统主题(M41):0 = 未知(回落 Slint 后端),1 = 浅色,2 = 深色
in property <int> system-theme: 0;
/// 后端是否认得系统主题(`false` 时 Rust 侧才需要轮询探测兜底)
out property <bool> system-theme-known: Theme.system-theme-known;
// ── 外观(高级)P1 四件(09 §101-104,出厂全部关闭)──────────────
// 图片由 Rust 灌入(`in`);其余是 in-out:设置页控件会写回,回调里再原样上抛给 Rust。
in property <image> background-image;
in property <image> background-blur;
in-out property <bool> bg-image-enabled: false;
in-out property <string> bg-image: "";
in-out property <BgFillKind> bg-fill: BgFillKind.cover;
in-out property <float> bg-dim: 0.35;
in-out property <float> panel-alpha: 1.0;
in-out property <bool> glass: false;
in-out property <color> accent-color: transparent;
/// 设置页「强调色(十六进制)」输入框的内容(Rust 写入规范化后的值)
in-out property <string> accent-hex: "";
/// 强调色输入是否非法(设置页亮 warn 行)
in-out property <bool> accent-invalid: false;
in-out property <DownloadCategory> download-category: DownloadCategory.release;
in-out property <bool> download-installed-only: false;
in-out property <string> download-query: "";
in property <UiDialog> dialog;
in property <bool> dialog-open: false;
in property <int> dialog-queued: 0;
in property <[UiToast]> toasts;
// 主题/外观:handler 里整体赋值给 `Theme` 全局(Slint 不允许在组件体内给全局属性做绑定,
// 但允许在回调/init 里赋值)。auto 分支由 Theme 自己读系统的 Palette.color-scheme。
init => {
Theme.theme-mode = root.theme-mode;
Theme.system-theme = root.system-theme;
Theme.bg-image = root.background-image;
Theme.bg-blur = root.background-blur;
Theme.bg-dim = root.bg-dim;
Theme.panel-alpha = root.panel-alpha;
Theme.glass = root.glass;
Theme.accent-override = root.accent-color;
}
changed theme-mode => { Theme.theme-mode = root.theme-mode; }
changed system-theme => { Theme.system-theme = root.system-theme; }
changed background-image => { Theme.bg-image = root.background-image; }
changed background-blur => { Theme.bg-blur = root.background-blur; }
changed bg-dim => { Theme.bg-dim = root.bg-dim; }
changed panel-alpha => { Theme.panel-alpha = root.panel-alpha; }
changed glass => { Theme.glass = root.glass; }
changed accent-color => { Theme.accent-override = root.accent-color; }
// ── 用户操作(main.rs 转成 AppCommand 或纯 UI 偏好)──────────────
callback toggle-maximize();
callback minimize();
callback request-close();
callback navigate(RouteKind);
callback toggle-nav();
callback toggle-ai();
callback expand-ai();
callback dock-ai();
callback toggle-yolo();
callback ai-send(string);
callback ai-stop();
callback instance-launch(string);
callback instance-configure(string);
callback instance-stop(string);
callback instance-open(string);
callback new-instance();
callback instance-view-changed(InstanceView);
callback tile-size-changed(TileSize);
callback instance-query-changed(string);
callback download-category-changed(DownloadCategory);
callback download-install(string);
callback installed-only-changed(bool);
callback download-query-changed(string);
callback ancient-help();
callback log-select(string);
callback log-ask-ai(string);
callback log-apply-fix();
callback settings-section-changed(SettingSection);
callback theme-changed(ThemeMode);
callback bg-image-enabled-changed(bool);
callback bg-image-changed(string);
callback bg-fill-changed(BgFillKind);
callback bg-dim-changed(float);
callback panel-alpha-changed(float);
callback glass-changed(bool);
callback accent-color-changed(string);
callback accent-swatch-picked(int);
callback yolo-changed(bool);
callback mcp-changed(bool);
callback ai-endpoint-changed(int);
callback dialog-primary();
callback dialog-secondary();
callback dialog-dismissed();
callback toast-dismissed(string);
AppShell {
app-title: root.app-title;
// 直接复用 Window 的内建 `maximized`(后端随窗口状态更新,无需自建属性)
maximized: root.maximized;
route: root.route;
nav-collapsed: root.nav-collapsed;
account-name: root.account-name;
account-initial: root.account-initial;
account-hue: root.account-hue;
ai-collapsed: root.ai-collapsed;
ai-messages: root.ai-messages;
ai-streaming: root.ai-streaming;
yolo: root.yolo;
ai-endpoint-label: root.ai-endpoint-label;
ai-draft: root.ai-draft;
tiles: root.tiles;
running-tiles: root.running-tiles;
versions: root.versions;
log-sessions: root.log-sessions;
log-lines: root.log-lines;
diagnosis: root.diagnosis;
has-diagnosis: root.has-diagnosis;
selected-session: root.selected-session;
tile-size: root.tile-size;
instance-view: root.instance-view;
instance-query: root.instance-query;
settings-section: root.settings-section;
theme-mode: root.theme-mode;
bg-image-enabled: root.bg-image-enabled;
bg-image: root.bg-image;
bg-fill: root.bg-fill;
bg-dim: root.bg-dim;
panel-alpha: root.panel-alpha;
glass: root.glass;
accent-hex: root.accent-hex;
accent-invalid: root.accent-invalid;
download-category: root.download-category;
download-installed-only: root.download-installed-only;
download-query: root.download-query;
dialog: root.dialog;
dialog-open: root.dialog-open;
dialog-queued: root.dialog-queued;
toasts: root.toasts;
toggle-maximize => { root.toggle-maximize(); }
minimize => { root.minimize(); }
request-close => { root.request-close(); }
navigate(r) => { root.navigate(r); }
toggle-nav => { root.toggle-nav(); }
toggle-ai => { root.toggle-ai(); }
expand-ai => { root.expand-ai(); }
dock-ai => { root.dock-ai(); }
toggle-yolo => { root.toggle-yolo(); }
ai-send(text) => { root.ai-send(text); }
ai-stop => { root.ai-stop(); }
instance-launch(id) => { root.instance-launch(id); }
instance-configure(id) => { root.instance-configure(id); }
instance-stop(id) => { root.instance-stop(id); }
instance-open(id) => { root.instance-open(id); }
new-instance => { root.new-instance(); }
instance-view-changed(view) => {
root.instance-view = view;
root.instance-view-changed(view);
}
tile-size-changed(size) => {
root.tile-size = size;
root.tile-size-changed(size);
}
instance-query-changed(query) => { root.instance-query-changed(query); }
download-category-changed(category) => {
root.download-category = category;
root.download-category-changed(category);
}
download-install(id) => { root.download-install(id); }
download-query-changed(query) => {
root.download-query = query;
root.download-query-changed(query);
}
ancient-help => { root.ancient-help(); }
installed-only-changed(only) => {
root.download-installed-only = only;
root.installed-only-changed(only);
}
log-select(id) => { root.log-select(id); }
log-ask-ai(id) => { root.log-ask-ai(id); }
log-apply-fix => { root.log-apply-fix(); }
settings-section-changed(section) => {
root.settings-section = section;
root.settings-section-changed(section);
}
theme-changed(mode) => {
root.theme-mode = mode;
root.theme-changed(mode);
}
bg-image-enabled-changed(v) => {
root.bg-image-enabled = v;
root.bg-image-enabled-changed(v);
}
bg-image-changed(path) => {
root.bg-image = path;
root.bg-image-changed(path);
}
bg-fill-changed(fill) => {
root.bg-fill = fill;
root.bg-fill-changed(fill);
}
bg-dim-changed(v) => {
root.bg-dim = v;
root.bg-dim-changed(v);
}
panel-alpha-changed(v) => {
root.panel-alpha = v;
root.panel-alpha-changed(v);
}
glass-changed(v) => {
root.glass = v;
root.glass-changed(v);
}
accent-color-changed(text) => {
root.accent-hex = text;
root.accent-color-changed(text);
}
accent-swatch-picked(index) => { root.accent-swatch-picked(index); }
yolo-changed(enabled) => { root.yolo-changed(enabled); }
mcp-changed(enabled) => { root.mcp-changed(enabled); }
ai-endpoint-changed(index) => { root.ai-endpoint-changed(index); }
dialog-primary => { root.dialog-primary(); }
dialog-secondary => { root.dialog-secondary(); }
dialog-dismissed => { root.dialog-dismissed(); }
toast-dismissed(id) => { root.toast-dismissed(id); }
}
}
+218
View File
@@ -0,0 +1,218 @@
// UI 视图模型:Rust 的 dto.rs 做单向转换后喂给 Slint(app 不 import core 内部类型,01c §5)。
// 命名一律 kebab-case,Rust 侧 slint-build 会生成 snake_case 字段。
// 约定:这里只放「界面要显示什么」,不放领域语义(版本继承链、下载源健康度等留在 core)。
// ── 共享枚举 ─────────────────────────────────────────────────────
export enum RouteKind {
instances,
download,
logs,
settings,
detail,
agent,
}
export enum TileSize {
small,
medium,
large,
}
export enum InstanceView {
tiles,
list,
}
/// D17 主题三态:auto 跟随系统(Slint Palette.color-scheme 热更),light/dark 钉死。
/// 键值 `appearance.theme_mode ∈ {light, dark, auto}`,出厂 auto。
export enum ThemeMode {
auto,
light,
dark,
}
/// 背景图填充模式(09 §101:cover / contain / tile)。与 `core::setting::appearance::BgFill` 同序。
export enum BgFillKind {
cover,
contain,
tile,
}
export enum LoaderKind {
vanilla,
fabric,
quilt,
forge,
neoforge,
optifine,
liteloader,
}
export enum RuntimePhase {
idle,
downloading,
verifying,
launching,
running,
failed,
}
export enum ChatRole {
user,
assistant,
tool,
system,
}
export enum DialogKind {
confirm,
input,
progress,
error,
}
export enum ToastKind {
info,
success,
warn,
error,
}
export enum VersionKind {
release,
snapshot,
ancientClassic,
ancientAlpha,
ancientBeta,
ancientUnlisted,
modpack,
mod,
}
// 下载页分类侧栏的选中项(远古版把四个子类提升为平级枚值,便于单选高亮)
export enum DownloadCategory {
release,
snapshot,
ancientClassic,
ancientAlpha,
ancientBeta,
ancientUnlisted,
modpack,
mod,
}
export enum LogOutcome {
ok,
crashed,
silentFail,
}
export enum LogLevel {
debug,
info,
warn,
error,
}
export enum SettingSection {
general,
/// D17「外观(高级)」:主题三态 / 背景图 / 半透明度 / 玻璃拟态 / 强调色 / 动画强度
appearance,
data,
download,
java,
account,
ai,
proxy,
about,
}
// ── 磁贴 ────────────────────────────────────────────────────────
// row/col 由 Rust state.rs 的 tile_grid() 按当前磁贴尺寸算好(小 6 / 中 4 / 大 3 列),
// 这样 Slint 侧 GridLayout 只消费整数位置,不必在 DSL 里做除法。
export struct UiTile {
id: string,
name: string,
mc_version: string,
loader: LoaderKind,
loader_version: string,
/// 加载器徽标文字(「原版」/「Fabric 0.16.5」):由 Rust 侧用 `LoaderInfo::badge()` 拼好,
/// DSL 不做枚举→字符串映射(少一处双份真相)。
loader_label: string,
last_played: string,
phase: RuntimePhase,
/// 启动中阶段文案(「下载资源」/「校验文件」/「启动中」),idle 时为空。
phase_label: string,
/// 0.0~1.0;<= 0 表示进度未知(进度环转为不确定态)。
progress: float,
/// 实例图标;空 image 时退回「名字首字 + hue 底色」占位。
icon: image,
hue: color,
/// 名字首字,无图标时的占位字母(Rust 侧取首个字素)。
initial: string,
row: int,
col: int,
}
// ── 下载页 ──────────────────────────────────────────────────────
export struct UiVersion {
id: string,
kind: VersionKind,
released: string,
installed: bool,
/// 远古版(classic/alpha/beta/unlisted)——列表行带徽标 + 页顶兼容性提示。
ancient: bool,
}
// ── 日志页 ──────────────────────────────────────────────────────
export struct UiLogSession {
id: string,
when: string,
instance: string,
outcome: LogOutcome,
duration: string,
}
export struct UiLogLine {
text: string,
level: LogLevel,
}
/// D8 规则引擎结论(core::diagnose 的 DTO,app 只负责展示)。
export struct UiDiagnosis {
rule: string,
title: string,
confidence: int,
hint: string,
has_fix: bool,
}
// ── AI 栏 ───────────────────────────────────────────────────────
export struct UiChatMessage {
role: ChatRole,
text: string,
/// role == tool 时的工具名。
tool_name: string,
tool_ok: bool,
/// 流式未完成时 true(末尾显示光标块)。
streaming: bool,
}
// ── 对话框 / Toast ──────────────────────────────────────────────
export struct UiDialog {
id: string,
kind: DialogKind,
title: string,
body: string,
primary: string,
secondary: string,
progress: float,
input_label: string,
input_value: string,
}
export struct UiToast {
id: string,
kind: ToastKind,
text: string,
}
+270
View File
@@ -0,0 +1,270 @@
// G2 下载页(09 §2 P0·G2 / Q24-4 定案):页内独立左侧分类栏(常驻,非 tab)+ 右侧版本列表。
// 远古版入口显式可达(ULL 对 HMCL「隐藏」行为的纠偏),下挂 classic/alpha/beta/unlisted 子类树,
// 进入远古版分类时顶部提示兼容性 + 每行带「远古版」徽标(防误装)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiVersion, VersionKind, DownloadCategory } from "../models.slint";
import { UiText, TextRole } from "../components/label.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { Badge } from "../components/badge.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
import { SearchField, CheckBox } from "../widgets/inputs.slint";
import { ListView } from "std-widgets.slint";
component CategoryRow inherits Rectangle {
in property <string> label: "";
in property <bool> active: false;
in property <bool> sub: false;
in property <bool> disabled: false;
callback clicked();
height: 34px;
background: root.active ? Theme.accent-soft : (row-touch.has-hover ? Theme.bg-hover : transparent);
if root.active : Rectangle {
x: 0;
y: 0;
width: 3px;
height: root.height;
background: Theme.accent;
}
UiText {
x: root.sub ? Metrics.gap-5 : Metrics.gap-3;
width: root.width - (root.sub ? Metrics.gap-5 : Metrics.gap-3) - Metrics.gap-2;
height: root.height;
role: TextRole.body;
text: root.label;
color: root.disabled ? Theme.fg-disabled
: root.active ? Theme.accent
: Theme.fg-primary;
}
row-touch := TouchArea {
mouse-cursor: root.disabled ? default : pointer;
clicked => {
if (!root.disabled) {
root.clicked();
}
}
}
}
component VersionRow inherits Rectangle {
in property <UiVersion> data;
callback install();
height: Metrics.row-height + 8px;
background: row-touch.has-hover ? Theme.bg-hover : transparent;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-2;
UiText {
role: TextRole.bodyStrong;
text: root.data.id;
width: 160px;
}
Badge {
text: root.data.kind == VersionKind.release ? "正式版"
: root.data.kind == VersionKind.snapshot ? "预览版"
: "远古版";
tint: root.data.ancient ? Theme.state-warn : Theme.fg-secondary;
}
UiText {
role: TextRole.caption;
text: root.data.released;
horizontal-stretch: 1;
}
if root.data.installed : HorizontalLayout {
spacing: Metrics.gap-1;
AppIcon { kind: IconKind.check; icon-size: 12px; tint: Theme.state-success; }
UiText {
role: TextRole.caption;
text: "已装";
color: Theme.state-success;
}
}
MetroButton {
kind: ButtonKind.standard;
text: root.data.installed ? "重装" : "安装";
button-height: 26px;
clicked => { root.install(); }
}
}
row-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.install(); }
}
}
export component DownloadPage inherits Rectangle {
in property <[UiVersion]> versions;
in-out property <DownloadCategory> category: DownloadCategory.release;
in-out property <bool> installed-only: false;
in-out property <string> query: "";
/// 远古版子类树是否展开(默认展开,显式可达)
in-out property <bool> ancient-expanded: true;
callback category-changed(DownloadCategory);
callback install(string);
callback installed-only-changed(bool);
callback query-changed(string);
callback toggle-ancient();
/// 远古版兼容性说明(P1 出说明页;当前由 Rust 侧弹 Toast 占位)
callback ancient-help();
background: Theme.bg-canvas;
HorizontalLayout {
// ── 页内分类侧栏(常驻)───────────────────────────────────
Rectangle {
width: 200px;
background: Theme.bg-surface;
border-width: 1px;
border-color: Theme.stroke;
VerticalLayout {
padding-top: Metrics.gap-2;
CategoryRow {
label: "正式版 release";
active: root.category == DownloadCategory.release;
clicked => { root.category-changed(DownloadCategory.release); }
}
CategoryRow {
label: "预览版 snapshot";
active: root.category == DownloadCategory.snapshot;
clicked => { root.category-changed(DownloadCategory.snapshot); }
}
// 远古版:可展开的父节点 + 子类树
CategoryRow {
label: root.ancient-expanded ? "远古版 ▾" : "远古版 ▸";
active: root.category == DownloadCategory.ancientClassic
|| root.category == DownloadCategory.ancientAlpha
|| root.category == DownloadCategory.ancientBeta
|| root.category == DownloadCategory.ancientUnlisted;
clicked => { root.toggle-ancient(); }
}
if root.ancient-expanded : CategoryRow {
label: "classic(rd-*)";
sub: true;
active: root.category == DownloadCategory.ancientClassic;
clicked => { root.category-changed(DownloadCategory.ancientClassic); }
}
if root.ancient-expanded : CategoryRow {
label: "old_alpha(a1.*)";
sub: true;
active: root.category == DownloadCategory.ancientAlpha;
clicked => { root.category-changed(DownloadCategory.ancientAlpha); }
}
if root.ancient-expanded : CategoryRow {
label: "old_beta(b1.*)";
sub: true;
active: root.category == DownloadCategory.ancientBeta;
clicked => { root.category-changed(DownloadCategory.ancientBeta); }
}
if root.ancient-expanded : CategoryRow {
label: "unlisted 汇总";
sub: true;
active: root.category == DownloadCategory.ancientUnlisted;
clicked => { root.category-changed(DownloadCategory.ancientUnlisted); }
}
CategoryRow {
label: "整合包(P1)";
disabled: true;
}
CategoryRow {
label: "Mod(P1)";
disabled: true;
}
Rectangle { vertical-stretch: 1; background: transparent; }
}
}
// ── 右侧版本列表 ──────────────────────────────────────────
VerticalLayout {
vertical-stretch: 1;
Rectangle {
height: Metrics.page-header-height;
background: Theme.bg-canvas;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-3;
UiText {
role: TextRole.subtitle;
text: "版本获取";
width: 96px;
}
SearchField {
width: 220px;
placeholder: "筛选版本号";
text <=> root.query;
edited(t) => { root.query-changed(t); }
}
CheckBox {
text: "仅已装";
checked: root.installed-only;
toggled(v) => { root.installed-only-changed(v); }
}
Rectangle { horizontal-stretch: 1; background: transparent; }
}
}
// 远古版兼容性提示条(首次进入显示,显式可达的代价告知)
Rectangle {
height: 44px;
visible: root.category == DownloadCategory.ancientClassic
|| root.category == DownloadCategory.ancientAlpha
|| root.category == DownloadCategory.ancientBeta
|| root.category == DownloadCategory.ancientUnlisted;
background: Theme.state-warn-soft;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-2;
AppIcon { kind: IconKind.warn; icon-size: 16px; tint: Theme.state-warn; }
UiText {
role: TextRole.caption;
text: "远古版(classic/alpha/beta)需要旧版 Java 与旧启动参数,装完请先看兼容性说明。";
wrap: word-wrap;
horizontal-stretch: 1;
}
MetroButton {
kind: ButtonKind.subtle;
text: "说明";
button-height: 26px;
clicked => { root.ancient-help(); }
}
}
}
Rectangle { height: 1px; background: Theme.stroke; }
ListView {
vertical-stretch: 1;
for v in root.versions : VersionRow {
data: v;
install => { root.install(v.id); }
}
}
}
}
}
+260
View File
@@ -0,0 +1,260 @@
// G1 实例页(09 §2 P0·G1):顶栏(搜索/新建/视图切换/磁贴尺寸)+ 磁贴网格 + 运行中迷你状态条。
// 网格用 GridLayout:列数由 Rust 按磁贴尺寸算好(小 6 / 中 4 / 大 3),row/col 随 UiTile 一起给。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiTile, InstanceView, TileSize, RuntimePhase } from "../models.slint";
import { UiText, TextRole } from "../components/label.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
import { SearchField, SegmentedControl } from "../widgets/inputs.slint";
import { InstanceTile } from "../components/tile.slint";
import { ListView } from "std-widgets.slint";
// 列表视图的一行(与磁贴共用同一份 UiTile 数据)
component InstanceRow inherits Rectangle {
in property <UiTile> data;
callback open();
callback launch();
height: Metrics.row-height + 8px;
background: row-touch.has-hover ? Theme.bg-hover : transparent;
// 行点击区先声明,按钮后声明 → 按钮命中优先
row-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.open(); }
}
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-3;
Rectangle {
width: 28px;
height: 28px;
border-radius: Metrics.radius-sm;
background: root.data.hue;
UiText {
text: root.data.initial;
font-size: 13px;
font-weight: 600;
color: Theme.fg-on-accent;
horizontal-alignment: center;
vertical-alignment: center;
}
}
UiText {
role: TextRole.body;
text: root.data.name;
horizontal-stretch: 1;
}
UiText {
role: TextRole.caption;
text: root.data.mc_version;
width: 88px;
}
UiText {
role: TextRole.caption;
text: root.data.loader_label;
width: 88px;
}
UiText {
role: TextRole.caption;
text: root.data.last_played;
width: 120px;
}
MetroButton {
kind: ButtonKind.subtle;
icon: IconKind.play;
icon-only: true;
enabled: root.data.phase == RuntimePhase.idle
|| root.data.phase == RuntimePhase.failed;
clicked => { root.launch(); }
}
}
}
export component InstancesPage inherits Rectangle {
in property <[UiTile]> tiles;
in property <[UiTile]> running;
in property <InstanceView> view: InstanceView.tiles;
in-out property <TileSize> tile-size: TileSize.medium;
in-out property <string> query: "";
callback launch(string);
callback configure(string);
callback stop(string);
callback open(string);
callback new-instance();
callback view-changed(InstanceView);
callback tile-size-changed(TileSize);
callback query-changed(string);
background: Theme.bg-canvas;
VerticalLayout {
// ── 顶栏 ────────────────────────────────────────────────
Rectangle {
height: Metrics.page-header-height;
background: Theme.bg-surface;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-2;
UiText {
role: TextRole.subtitle;
text: "实例";
width: 72px;
}
SearchField {
width: 240px;
placeholder: "搜索实例";
text <=> root.query;
edited(t) => { root.query-changed(t); }
}
Rectangle { horizontal-stretch: 1; background: transparent; }
SegmentedControl {
width: 120px;
options: ["磁贴", "列表"];
selected: root.view == InstanceView.tiles ? 0 : 1;
changed(i) => {
root.view-changed(i == 0 ? InstanceView.tiles : InstanceView.list);
}
}
SegmentedControl {
width: 132px;
visible: root.view == InstanceView.tiles;
options: ["小", "中", "大"];
selected: root.tile-size == TileSize.small ? 0
: root.tile-size == TileSize.large ? 2 : 1;
changed(i) => {
root.tile-size-changed(
i == 0 ? TileSize.small : i == 2 ? TileSize.large : TileSize.medium);
}
}
MetroButton {
kind: ButtonKind.accent;
text: "新建实例";
show-icon: true;
icon: IconKind.plus;
clicked => { root.new-instance(); }
}
}
}
Rectangle { height: 1px; background: Theme.stroke; }
// ── 运行中迷你状态条 ──────────────────────────────────────
Rectangle {
height: Metrics.running-strip-height;
visible: root.running.length > 0;
background: Theme.state-info-soft;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-2;
Rectangle {
width: 8px;
height: 8px;
border-radius: 4px;
background: Theme.state-success;
}
UiText {
role: TextRole.caption;
text: root.running.length + " 个实例运行中";
color: Theme.state-success;
horizontal-stretch: 1;
}
for r in root.running : UiText {
role: TextRole.caption;
text: r.name;
}
}
}
// ── 磁贴网格 ─────────────────────────────────────────────
body := Flickable {
vertical-stretch: 1;
visible: root.view == InstanceView.tiles && root.tiles.length > 0;
content-width: self.width;
content-height: grid-content.height;
grid-content := Rectangle {
width: body.width;
height: grid.preferred-height;
background: transparent;
grid := GridLayout {
padding: Metrics.gap-4;
spacing: Metrics.gap-3;
for t in root.tiles : InstanceTile {
row: t.row;
col: t.col;
data: t;
compact: root.tile-size == TileSize.small;
open => { root.open(t.id); }
launch => { root.launch(t.id); }
configure => { root.configure(t.id); }
stop => { root.stop(t.id); }
}
}
}
}
// ── 列表视图 ─────────────────────────────────────────────
ListView {
vertical-stretch: 1;
visible: root.view == InstanceView.list && root.tiles.length > 0;
for t in root.tiles : InstanceRow {
data: t;
open => { root.open(t.id); }
launch => { root.launch(t.id); }
}
}
// ── 空态 ────────────────────────────────────────────────
VerticalLayout {
vertical-stretch: 1;
visible: root.tiles.length == 0;
alignment: center;
spacing: Metrics.gap-3;
UiText {
role: TextRole.title;
text: "还没有实例";
horizontal-alignment: center;
color: Theme.fg-secondary;
}
UiText {
role: TextRole.caption;
text: "新建一个实例,或者先去下载页装个版本";
horizontal-alignment: center;
}
HorizontalLayout {
alignment: center;
MetroButton {
kind: ButtonKind.accent;
text: "新建实例";
show-icon: true;
icon: IconKind.plus;
clicked => { root.new-instance(); }
}
}
}
}
}
+259
View File
@@ -0,0 +1,259 @@
// G3 日志页(09 §2 P0·G3):左侧会话列表(✓正常 / ✗崩溃 / ⚠静默失败)+ 右侧日志流 + 崩溃诊断卡片。
// 诊断卡片 = D8 规则引擎结论(命中规则 + 置信度 + 修复动作 + 跳 AI 栏带上下文)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { UiLogSession, UiLogLine, UiDiagnosis, LogOutcome, LogLevel } from "../models.slint";
import { UiText, TextRole } from "../components/label.slint";
import { AppIcon, IconKind } from "../components/icons.slint";
import { Badge } from "../components/badge.slint";
import { MetroButton, ButtonKind } from "../widgets/button.slint";
import { SearchField } from "../widgets/inputs.slint";
import { ListView } from "std-widgets.slint";
component SessionRow inherits Rectangle {
in property <UiLogSession> data;
in property <bool> active: false;
callback clicked();
height: 52px;
background: root.active ? Theme.accent-soft : (row-touch.has-hover ? Theme.bg-hover : transparent);
if root.active : Rectangle {
x: 0;
y: 0;
width: 3px;
height: root.height;
background: Theme.accent;
}
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-2;
spacing: Metrics.gap-2;
AppIcon {
icon-size: 14px;
kind: root.data.outcome == LogOutcome.ok ? IconKind.check
: root.data.outcome == LogOutcome.crashed ? IconKind.error
: IconKind.warn;
tint: root.data.outcome == LogOutcome.ok ? Theme.state-success
: root.data.outcome == LogOutcome.crashed ? Theme.state-danger
: Theme.state-warn;
}
VerticalLayout {
spacing: 0px;
vertical-stretch: 1;
UiText {
role: TextRole.body;
text: root.data.instance;
horizontal-stretch: 1;
}
UiText {
role: TextRole.caption;
text: root.data.when;
horizontal-stretch: 1;
}
}
UiText {
role: TextRole.caption;
text: root.data.duration;
width: 56px;
horizontal-alignment: right;
}
}
row-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
}
component LogRow inherits Rectangle {
in property <UiLogLine> line;
height: 18px;
background: root.line.level == LogLevel.error
? Theme.state-danger-soft
: transparent;
UiText {
x: Metrics.gap-2;
y: 0;
width: root.width - 2 * Metrics.gap-2;
height: root.height;
role: TextRole.monoSmall;
text: root.line.text;
color: root.line.level == LogLevel.error ? Theme.state-danger
: root.line.level == LogLevel.warn ? Theme.state-warn
: root.line.level == LogLevel.debug ? Theme.fg-disabled
: Theme.fg-primary;
}
}
export component LogsPage inherits Rectangle {
in property <[UiLogSession]> sessions;
in property <[UiLogLine]> lines;
in property <UiDiagnosis> diagnosis;
in property <bool> has-diagnosis: false;
in property <bool> live: false;
in-out property <string> selected: "";
in-out property <string> query: "";
callback select(string);
callback ask-ai(string);
callback apply-fix();
callback query-changed(string);
background: Theme.bg-canvas;
HorizontalLayout {
// ── 左侧会话列表 ─────────────────────────────────────────
Rectangle {
width: 248px;
background: Theme.bg-surface;
VerticalLayout {
Rectangle {
height: 44px;
background: transparent;
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-2;
spacing: Metrics.gap-2;
UiText {
role: TextRole.bodyStrong;
text: "会话";
horizontal-stretch: 1;
}
if root.live : Rectangle {
width: 6px;
height: 6px;
border-radius: 3px;
background: Theme.state-success;
}
}
}
ListView {
vertical-stretch: 1;
for s in root.sessions : SessionRow {
data: s;
active: root.selected == s.id;
clicked => { root.select(s.id); }
}
}
}
}
Rectangle { width: 1px; background: Theme.stroke; }
// ── 右侧日志流 + 诊断卡片 ────────────────────────────────
VerticalLayout {
vertical-stretch: 1;
Rectangle {
height: 44px;
background: Theme.bg-surface;
HorizontalLayout {
padding-left: Metrics.gap-3;
padding-right: Metrics.gap-3;
spacing: Metrics.gap-2;
UiText {
role: TextRole.bodyStrong;
text: root.selected == "" ? "未选择会话" : root.selected;
horizontal-stretch: 1;
}
SearchField {
width: 200px;
placeholder: "筛选日志行";
text <=> root.query;
edited(t) => { root.query-changed(t); }
}
MetroButton {
kind: ButtonKind.subtle;
text: "打开目录";
button-height: 26px;
}
}
}
Rectangle { height: 1px; background: Theme.stroke; }
// 崩溃诊断卡片(D8)
Rectangle {
height: root.has-diagnosis ? 76px : 0px;
visible: root.has-diagnosis;
background: Theme.state-danger-soft;
HorizontalLayout {
padding: Metrics.gap-3;
spacing: Metrics.gap-3;
AppIcon { kind: IconKind.error; icon-size: 20px; tint: Theme.state-danger; }
VerticalLayout {
spacing: 2px;
vertical-stretch: 1;
HorizontalLayout {
spacing: Metrics.gap-2;
UiText {
role: TextRole.bodyStrong;
text: root.diagnosis.title;
horizontal-stretch: 1;
}
Badge {
text: "置信度 " + root.diagnosis.confidence + "%";
tint: Theme.state-danger;
}
}
UiText {
role: TextRole.caption;
text: root.diagnosis.hint;
horizontal-stretch: 1;
}
}
VerticalLayout {
spacing: Metrics.gap-1;
alignment: center;
if root.diagnosis.has_fix : MetroButton {
kind: ButtonKind.standard;
text: "修复建议";
button-height: 26px;
clicked => { root.apply-fix(); }
}
MetroButton {
kind: ButtonKind.subtle;
text: "让 AI 详解";
button-height: 26px;
clicked => { root.ask-ai(root.selected); }
}
}
}
}
// 日志流(行虚拟化)
Rectangle {
vertical-stretch: 1;
background: Theme.bg-canvas;
ListView {
for l in root.lines : LogRow {
line: l;
width: root.width;
}
}
}
}
}
}
@@ -0,0 +1,146 @@
// 设置页「外观」分组(09 §2 G6 + §113 D17 定案):主题三态 + 「外观(高级)」P1 四件。
//
// 从 pages/settings.slint 拆出来只为守住 400 行门槛;控件语义与设置键一一对应:
// theme-mode → appearance.theme_mode / bg-* → appearance.bg_image* / panel-alpha →
// appearance.panel_alpha / glass → appearance.glass / accent-color → appearance.accent_color。
// 09 §111 硬约束:高级特性全部默认关闭,开启背景图时这里会给出对比度提示(warn 行)。
import { Theme } from "../theme/tokens.slint";
import { ThemeMode, BgFillKind } from "../models.slint";
import { SettingItem, SettingGroup, ControlKind } from "../widgets/setting-item.slint";
export component AppearanceSection inherits VerticalLayout {
in-out property <ThemeMode> theme-mode: ThemeMode.auto;
in-out property <bool> bg-image-enabled: false;
in-out property <string> bg-image: "";
in-out property <BgFillKind> bg-fill: BgFillKind.cover;
in-out property <float> bg-dim: 0.35;
in-out property <float> panel-alpha: 1.0;
in-out property <bool> glass: false;
in-out property <string> accent-color: "";
in property <bool> accent-invalid: false;
callback theme-changed(ThemeMode);
callback bg-image-enabled-changed(bool);
callback bg-image-changed(string);
callback bg-fill-changed(BgFillKind);
callback bg-dim-changed(float);
callback panel-alpha-changed(float);
callback glass-changed(bool);
callback accent-color-changed(string);
callback accent-swatch-picked(int);
// ── 外观(D17:主题三态 + 09 §101-104 高级四件)──────
SettingGroup { title: "主题"; }
SettingItem {
label: "主题模式";
description: "跟随系统(Auto,随 OS 热切)/ 浅色 / 深色";
kind: ControlKind.segmented;
options: ["跟随系统", "浅色", "深色"];
selected: root.theme-mode == ThemeMode.auto ? 0
: root.theme-mode == ThemeMode.light ? 1 : 2;
changed(i) => {
root.theme-mode = i == 0 ? ThemeMode.auto
: i == 1 ? ThemeMode.light : ThemeMode.dark;
root.theme-changed(root.theme-mode);
}
}
SettingGroup {
title: "高级特性(出厂全部关闭,开启后请确认前景可读)";
}
// ① 自定义窗口背景图(09 §101)
SettingItem {
label: "自定义窗口背景图";
description: "PNG 图片铺满窗口;开启后所有面板半透明处都会透出它";
kind: ControlKind.toggle;
checked: root.bg-image-enabled;
toggled(v) => {
root.bg-image-enabled = v;
root.bg-image-enabled-changed(v);
}
}
SettingItem {
label: "背景图路径";
description: root.bg-image == "" ? "尚未选择(绝对路径,例:D:/wall/p.png)" : root.bg-image;
kind: ControlKind.text;
button-text: "D:/wall/p.png";
value <=> root.bg-image;
edited(t) => { root.bg-image-changed(t); }
}
SettingItem {
label: "填充模式";
description: "铺满(裁切)/ 完整(留底色)/ 平铺";
kind: ControlKind.segmented;
options: ["铺满", "完整", "平铺"];
selected: root.bg-fill == BgFillKind.cover ? 0
: root.bg-fill == BgFillKind.contain ? 1 : 2;
changed(i) => {
root.bg-fill = i == 0 ? BgFillKind.cover
: i == 1 ? BgFillKind.contain : BgFillKind.tile;
root.bg-fill-changed(root.bg-fill);
}
}
SettingItem {
label: "暗化层强度";
// 09 §111:开启背景图时自动给对比度提示——暗化太低 + 面板太透会伤可读性
description: (root.bg-image-enabled && (root.bg-dim < 0.25 || root.panel-alpha < 0.8))
? "⚠ 对比度偏低:建议暗化 ≥ 25% 或面板不透明度 ≥ 80%"
: "越大背景越暗、前景越清晰(0% = 不压暗)";
warn: root.bg-image-enabled && (root.bg-dim < 0.25 || root.panel-alpha < 0.8);
kind: ControlKind.slider;
min-value: 0.0;
max-value: 0.9;
slider-value: root.bg-dim;
slid(v) => {
root.bg-dim = v;
root.bg-dim-changed(v);
}
}
// ② 全局面板半透明度(09 §102)
SettingItem {
label: "全局面板半透明度";
description: "卡片 / 导航 / AI 栏统一透明度(100% = 出厂 Metro 纯色)";
kind: ControlKind.slider;
min-value: 0.4;
max-value: 1.0;
slider-value: root.panel-alpha;
slid(v) => {
root.panel-alpha = v;
root.panel-alpha-changed(v);
}
}
// ③ 玻璃拟态卡片(09 §103)
SettingItem {
label: "玻璃拟态卡片";
description: "卡片底铺背景图的预生成模糊层(需先开启背景图)";
kind: ControlKind.toggle;
checked: root.glass;
toggled(v) => {
root.glass = v;
root.glass-changed(v);
}
}
// ④ 自定义强调色(09 §104)
SettingItem {
label: "预设强调色";
description: root.accent-color == "" ? "当前:出厂 Metro 蓝" : "当前:" + root.accent-color;
kind: ControlKind.swatches;
swatch-colors: [
#0078D4, #0F6CBD, #107C10, #9D5D00,
#C42B1C, #8764B8, #C239B3, #00B7C3,
];
swatch-current: root.accent-color == "" ? #0078D4 : Theme.accent;
swatch-picked(i) => { root.accent-swatch-picked(i); }
}
SettingItem {
label: "强调色(十六进制)";
description: root.accent-invalid
? "⚠ 需要 #RRGGBB 形式,留空恢复出厂蓝"
: "留空 = 出厂 #0078D4;影响顶线 / 选中态 / 按钮 / 徽标";
warn: root.accent-invalid;
kind: ControlKind.text;
button-text: "#0078D4";
value <=> root.accent-color;
edited(t) => { root.accent-color-changed(t); }
}
}
+363
View File
@@ -0,0 +1,363 @@
// G6 设置页(09 §2 P0·G6):左子导航 + 右表单 + 搜索框(设置项多,防迷路)。
// 「通用 / 外观(D17 三态 + 09 §101-104 高级四件,实现在 settings-appearance.slint)/ 数据 / AI /
// 关于」是真实控件,Java / 账户 / 代理分组仍是入口按钮(等各自 mission 接实现)。
// 关于页内嵌 AboutSlint —— Slint RF 2.0 许可义务(01c §8:关于页放官方部件)。
import { Theme } from "../theme/tokens.slint";
import { Metrics } from "../theme/metrics.slint";
import { SettingSection, TileSize, ThemeMode, BgFillKind } from "../models.slint";
import { UiText, TextRole } from "../components/label.slint";
import { SettingItem, SettingGroup, ControlKind } from "../widgets/setting-item.slint";
import { AppearanceSection } from "settings-appearance.slint";
import { SearchField } from "../widgets/inputs.slint";
import { AboutSlint, ScrollView } from "std-widgets.slint";
component SectionRow inherits Rectangle {
in property <string> label: "";
in property <SettingSection> section: SettingSection.general;
in property <bool> active: false;
callback clicked();
height: 36px;
background: root.active ? Theme.accent-soft : (row-touch.has-hover ? Theme.bg-hover : transparent);
if root.active : Rectangle {
x: 0;
y: 0;
width: 3px;
height: root.height;
background: Theme.accent;
}
UiText {
x: Metrics.gap-3;
y: 0;
width: root.width - Metrics.gap-3 - Metrics.gap-2;
height: root.height;
role: TextRole.body;
text: root.label;
color: root.active ? Theme.accent : Theme.fg-primary;
}
row-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.clicked(); }
}
}
export component SettingsPage inherits Rectangle {
in-out property <SettingSection> section: SettingSection.general;
in-out property <string> query: "";
// 外观(D17:主题三态 + 09 §101-104 高级特性四件)
in-out property <ThemeMode> theme-mode: ThemeMode.auto;
in-out property <bool> bg-image-enabled: false;
in-out property <string> bg-image: "";
in-out property <BgFillKind> bg-fill: BgFillKind.cover;
in-out property <float> bg-dim: 0.35;
in-out property <float> panel-alpha: 1.0;
in-out property <bool> glass: false;
in-out property <string> accent-color: "";
in property <bool> accent-invalid: false;
// 通用
in-out property <TileSize> tile-size: TileSize.medium;
in-out property <bool> close-to-tray: true;
// 数据
in-out property <int> data-mode: 0;
in property <string> data-path: "";
// 下载
in-out property <int> download-concurrency: 1;
in property <string> region-label: "未知";
// 账户
in property <string> account-label: "未登录";
// AI
in-out property <int> ai-endpoint: 0;
in-out property <bool> yolo: false;
in-out property <bool> mcp-enabled: false;
// 代理
in-out property <string> proxy: "";
// 关于
in property <string> version: "";
callback section-changed(SettingSection);
callback query-changed(string);
callback theme-changed(ThemeMode);
callback bg-image-enabled-changed(bool);
callback bg-image-changed(string);
callback bg-fill-changed(BgFillKind);
callback bg-dim-changed(float);
callback panel-alpha-changed(float);
callback glass-changed(bool);
callback accent-color-changed(string);
callback accent-swatch-picked(int);
callback tile-size-changed(TileSize);
callback close-to-tray-changed(bool);
callback data-mode-changed(int);
callback download-concurrency-changed(int);
callback ai-endpoint-changed(int);
callback yolo-changed(bool);
callback mcp-changed(bool);
callback proxy-changed(string);
callback migrate-data();
callback login();
callback manage-java();
callback open-audit-log();
callback detect-region();
background: Theme.bg-canvas;
HorizontalLayout {
// ── 左子导航 ──────────────────────────────────────────────
Rectangle {
width: 200px;
background: Theme.bg-surface;
VerticalLayout {
padding-top: Metrics.gap-2;
SectionRow { label: "通用"; section: SettingSection.general; active: root.section == SettingSection.general; clicked => { root.section-changed(SettingSection.general); } }
SectionRow { label: "外观"; section: SettingSection.appearance; active: root.section == SettingSection.appearance; clicked => { root.section-changed(SettingSection.appearance); } }
SectionRow { label: "数据"; section: SettingSection.data; active: root.section == SettingSection.data; clicked => { root.section-changed(SettingSection.data); } }
SectionRow { label: "下载"; section: SettingSection.download; active: root.section == SettingSection.download; clicked => { root.section-changed(SettingSection.download); } }
SectionRow { label: "Java"; section: SettingSection.java; active: root.section == SettingSection.java; clicked => { root.section-changed(SettingSection.java); } }
SectionRow { label: "账户"; section: SettingSection.account; active: root.section == SettingSection.account; clicked => { root.section-changed(SettingSection.account); } }
SectionRow { label: "AI"; section: SettingSection.ai; active: root.section == SettingSection.ai; clicked => { root.section-changed(SettingSection.ai); } }
SectionRow { label: "代理"; section: SettingSection.proxy; active: root.section == SettingSection.proxy; clicked => { root.section-changed(SettingSection.proxy); } }
SectionRow { label: "关于"; section: SettingSection.about; active: root.section == SettingSection.about; clicked => { root.section-changed(SettingSection.about); } }
Rectangle { vertical-stretch: 1; background: transparent; }
}
}
Rectangle { width: 1px; background: Theme.stroke; }
// ── 右表单 ────────────────────────────────────────────────
VerticalLayout {
vertical-stretch: 1;
Rectangle {
height: Metrics.page-header-height;
background: Theme.bg-surface;
HorizontalLayout {
padding-left: Metrics.gap-4;
padding-right: Metrics.gap-4;
spacing: Metrics.gap-3;
UiText {
role: TextRole.subtitle;
text: "设置";
width: 72px;
}
SearchField {
width: 240px;
placeholder: "搜索设置项";
text <=> root.query;
edited(t) => { root.query-changed(t); }
}
Rectangle { horizontal-stretch: 1; background: transparent; }
}
}
Rectangle { height: 1px; background: Theme.stroke; }
ScrollView {
vertical-stretch: 1;
VerticalLayout {
padding-bottom: Metrics.gap-5;
// 外观(D17:主题三态 + 09 §101-104 高级四件;实现在 settings-appearance.slint)
if root.section == SettingSection.appearance : AppearanceSection {
theme-mode: root.theme-mode;
bg-image-enabled: root.bg-image-enabled;
bg-image: root.bg-image;
bg-fill: root.bg-fill;
bg-dim: root.bg-dim;
panel-alpha: root.panel-alpha;
glass: root.glass;
accent-color: root.accent-color;
accent-invalid: root.accent-invalid;
theme-changed(m) => {
root.theme-mode = m;
root.theme-changed(m);
}
bg-image-enabled-changed(v) => {
root.bg-image-enabled = v;
root.bg-image-enabled-changed(v);
}
bg-image-changed(t) => {
root.bg-image = t;
root.bg-image-changed(t);
}
bg-fill-changed(f) => {
root.bg-fill = f;
root.bg-fill-changed(f);
}
bg-dim-changed(v) => {
root.bg-dim = v;
root.bg-dim-changed(v);
}
panel-alpha-changed(v) => {
root.panel-alpha = v;
root.panel-alpha-changed(v);
}
glass-changed(v) => {
root.glass = v;
root.glass-changed(v);
}
accent-color-changed(t) => {
root.accent-color = t;
root.accent-color-changed(t);
}
accent-swatch-picked(i) => { root.accent-swatch-picked(i); }
}
// ── 通用 ────────────────────────────────────────
if root.section == SettingSection.general : SettingGroup { title: "外观"; }
if root.section == SettingSection.general : SettingItem {
label: "磁贴尺寸";
description: "小 6 列 / 中 4 列(默认)/ 大 3 列";
kind: ControlKind.segmented;
options: ["小", "中", "大"];
selected: root.tile-size == TileSize.small ? 0
: root.tile-size == TileSize.large ? 2 : 1;
changed(i) => {
root.tile-size-changed(
i == 0 ? TileSize.small : i == 2 ? TileSize.large : TileSize.medium);
}
}
if root.section == SettingSection.general : SettingItem {
label: "关闭窗口时最小化到托盘";
description: "避免误关正在运行的游戏";
kind: ControlKind.toggle;
checked: root.close-to-tray;
toggled(v) => { root.close-to-tray-changed(v); }
}
// ── 数据 ────────────────────────────────────────
if root.section == SettingSection.data : SettingGroup { title: "数据目录(D6 三模式)"; }
if root.section == SettingSection.data : SettingItem {
label: "存储模式";
description: "便携(运行目录 .ULL/.minecraft)/ 标准(系统应用数据目录)/ 自定义";
kind: ControlKind.segmented;
options: ["便携", "标准", "自定义"];
selected: root.data-mode;
changed(i) => {
root.data-mode = i;
root.data-mode-changed(i);
}
}
if root.section == SettingSection.data : SettingItem {
label: "当前路径";
description: root.data-path;
kind: ControlKind.button;
button-text: "迁移…";
clicked => { root.migrate-data(); }
}
// ── 下载 ────────────────────────────────────────
if root.section == SettingSection.download : SettingGroup { title: "下载(D7)"; }
if root.section == SettingSection.download : SettingItem {
label: "并发数";
kind: ControlKind.segmented;
options: ["4", "8", "16"];
selected: root.download-concurrency;
changed(i) => {
root.download-concurrency = i;
root.download-concurrency-changed(i);
}
}
if root.section == SettingSection.download : SettingItem {
label: "地区判定";
description: "当前:" + root.region-label + "(决定是否走国内镜像)";
kind: ControlKind.button;
button-text: "重新检测";
clicked => { root.detect-region(); }
}
// ── Java ────────────────────────────────────────
if root.section == SettingSection.java : SettingGroup { title: "Java(D13)"; }
if root.section == SettingSection.java : SettingItem {
label: "Java 管理";
description: "基准 JDK17 自动下载 · 多版本可选 · 不匹配时黄条提示";
kind: ControlKind.button;
button-text: "管理多版本";
clicked => { root.manage-java(); }
}
// ── 账户 ────────────────────────────────────────
if root.section == SettingSection.account : SettingGroup { title: "账户(D9)"; }
if root.section == SettingSection.account : SettingItem {
label: "当前账户";
description: root.account-label;
kind: ControlKind.button;
button-text: "登录 / 切换";
clicked => { root.login(); }
}
// ── AI ──────────────────────────────────────────
if root.section == SettingSection.ai : SettingGroup { title: "AI 助手(D4)"; }
if root.section == SettingSection.ai : SettingItem {
label: "模型端点";
description: "OpenAI 兼容 / 本地 llama.cpp / Anthropic / 自定义";
kind: ControlKind.segmented;
options: ["OpenAI", "llama.cpp", "Anthropic", "自定义"];
selected: root.ai-endpoint;
changed(i) => {
root.ai-endpoint = i;
root.ai-endpoint-changed(i);
}
}
if root.section == SettingSection.ai : SettingItem {
label: "YOLO 模式";
description: "跳过全部确认弹窗(审计照记),账户/凭据操作永不 YOLO";
kind: ControlKind.toggle;
checked: root.yolo;
toggled(v) => { root.yolo-changed(v); }
}
if root.section == SettingSection.ai : SettingItem {
label: "MCP server";
description: "允许外部客户端(如 Claude Desktop)调用工具集";
kind: ControlKind.toggle;
checked: root.mcp-enabled;
toggled(v) => { root.mcp-changed(v); }
}
if root.section == SettingSection.ai : SettingItem {
label: "审计日志";
description: "AI 的所有写操作都有记录";
kind: ControlKind.button;
button-text: "查看";
clicked => { root.open-audit-log(); }
}
// ── 代理 ────────────────────────────────────────
if root.section == SettingSection.proxy : SettingGroup { title: "代理"; }
if root.section == SettingSection.proxy : SettingItem {
label: "HTTP 代理";
description: "留空则使用系统代理";
kind: ControlKind.text;
button-text: "http://127.0.0.1:7890";
value <=> root.proxy;
edited(t) => { root.proxy-changed(t); }
}
// ── 关于 ────────────────────────────────────────
if root.section == SettingSection.about : SettingGroup { title: "关于"; }
if root.section == SettingSection.about : SettingItem {
label: "版本";
description: root.version;
}
if root.section == SettingSection.about : SettingItem {
label: "许可";
description: "Apache-2.0(Slint 走 RF 2.0 轨道)";
}
if root.section == SettingSection.about : Rectangle {
height: 120px;
AboutSlint { }
}
}
}
}
}
}
+373
View File
@@ -0,0 +1,373 @@
// 主窗口框架(D16 §1):无边框标题栏 + 左侧可折叠导航 + 内容区路由 + 右侧 AI 常驻栏 + 对话框/Toast 覆盖层。
// 这里只做「组装 + 路由转发」,状态全在 Rust 侧(state.rs),Slint 不持有业务真相。
// main.slint(阶段 B 接线)只负责实例化 AppShell 并把 Window 的窗口事件接上。
import { Theme } from "theme/tokens.slint";
import { Metrics } from "theme/metrics.slint";
import {
RouteKind, TileSize, InstanceView, SettingSection, DownloadCategory, ThemeMode, BgFillKind,
UiTile, UiVersion, UiLogSession, UiLogLine, UiDiagnosis, UiChatMessage, UiDialog, UiToast,
} from "models.slint";
import { TitleBar } from "components/titlebar.slint";
import { Nav } from "components/nav.slint";
import { AiPanel } from "components/ai-panel.slint";
import { UiText, TextRole } from "components/label.slint";
import { MetroButton, ButtonKind } from "widgets/button.slint";
import { DialogHost } from "widgets/dialog.slint";
import { ToastHost } from "widgets/toast.slint";
import { InstancesPage } from "pages/instances.slint";
import { DownloadPage } from "pages/download.slint";
import { LogsPage } from "pages/logs.slint";
import { SettingsPage } from "pages/settings.slint";
export component AppShell inherits Rectangle {
// ── 窗口 ────────────────────────────────────────────────────
in property <string> app-title: "Unlucky Launcher";
in property <bool> maximized: false;
// ── 路由 ────────────────────────────────────────────────────
in property <RouteKind> route: RouteKind.instances;
// ── 导航 / 账户 ──────────────────────────────────────────────
in property <bool> nav-collapsed: false;
in property <string> account-name: "未登录";
in property <string> account-initial: "?";
in property <color> account-hue: #5C2D91;
// ── AI 栏(双形态共享同一份会话)──────────────────────────────
in property <bool> ai-collapsed: false;
in property <[UiChatMessage]> ai-messages;
in property <bool> ai-streaming: false;
in property <bool> yolo: false;
in property <string> ai-endpoint-label: "llama.cpp · 本地";
in-out property <string> ai-draft: "";
// ── 页面数据 ────────────────────────────────────────────────
in property <[UiTile]> tiles;
in property <[UiTile]> running-tiles;
in property <[UiVersion]> versions;
in property <[UiLogSession]> log-sessions;
in property <[UiLogLine]> log-lines;
in property <UiDiagnosis> diagnosis;
in property <bool> has-diagnosis: false;
in property <string> selected-session: "";
// ── 偏好(持久化键见 SubAgentsTalk/app-DTO通道约定.md)──────────
in-out property <TileSize> tile-size: TileSize.medium;
in-out property <InstanceView> instance-view: InstanceView.tiles;
in-out property <string> instance-query: "";
in-out property <SettingSection> settings-section: SettingSection.general;
/// D17 主题三态(auto 跟随系统;light/dark 钉死)。Rust 侧同时写 Theme 全局。
in-out property <ThemeMode> theme-mode: ThemeMode.auto;
/// 外观(高级)四件的 in-out 通道:MainWindow ↔ SettingsPage(图片本身走 `Theme.bg-image`)
in-out property <bool> bg-image-enabled: false;
in-out property <string> bg-image: "";
in-out property <BgFillKind> bg-fill: BgFillKind.cover;
in-out property <float> bg-dim: 0.35;
in-out property <float> panel-alpha: 1.0;
in-out property <bool> glass: false;
/// 设置页编辑的十六进制串(`accent-color` 是解析后的颜色,只给 Theme 用)
in-out property <string> accent-hex: "";
/// 强调色输入是否非法(设置页亮 warn 行)
in-out property <bool> accent-invalid: false;
in-out property <DownloadCategory> download-category: DownloadCategory.release;
in-out property <bool> download-installed-only: false;
/// 下载页版本号筛选词(输入由页面写回,state.download_query 参与过滤)
in-out property <string> download-query: "";
// ── 对话框队列 / Toast ───────────────────────────────────────
in property <UiDialog> dialog;
in property <bool> dialog-open: false;
in property <int> dialog-queued: 0;
in property <[UiToast]> toasts;
// ── 窗口控制(拖拽走 WindowMoveArea,不需要 Rust 参与)────────
callback toggle-maximize();
callback minimize();
callback request-close();
// ── 导航 / AI ───────────────────────────────────────────────
callback navigate(RouteKind);
callback toggle-nav();
callback toggle-ai();
callback expand-ai();
callback dock-ai();
callback toggle-yolo();
callback ai-send(string);
callback ai-stop();
// ── 实例页 ──────────────────────────────────────────────────
callback instance-launch(string);
callback instance-configure(string);
callback instance-stop(string);
callback instance-open(string);
callback new-instance();
callback instance-view-changed(InstanceView);
callback tile-size-changed(TileSize);
callback instance-query-changed(string);
// ── 下载页 ──────────────────────────────────────────────────
callback download-category-changed(DownloadCategory);
callback download-install(string);
callback installed-only-changed(bool);
callback download-query-changed(string);
callback ancient-help();
// ── 日志页 ──────────────────────────────────────────────────
callback log-select(string);
callback log-ask-ai(string);
callback log-apply-fix();
// ── 设置页 ──────────────────────────────────────────────────
callback settings-section-changed(SettingSection);
callback theme-changed(ThemeMode);
callback bg-image-enabled-changed(bool);
callback bg-image-changed(string);
callback bg-fill-changed(BgFillKind);
callback bg-dim-changed(float);
callback panel-alpha-changed(float);
callback glass-changed(bool);
callback accent-color-changed(string);
callback accent-swatch-picked(int);
callback yolo-changed(bool);
callback mcp-changed(bool);
callback ai-endpoint-changed(int);
// ── 对话框 / Toast ──────────────────────────────────────────
callback dialog-primary();
callback dialog-secondary();
callback dialog-dismissed();
callback toast-dismissed(string);
// 窗口底色交给 main.slint 的 Window(不透明 `canvas-solid`),这里只铺背景图层
background: transparent;
// 玻璃拟态的对齐基准:整窗逻辑尺寸(components/glass.slint 反向偏移用)
init => {
Theme.window-width = root.width;
Theme.window-height = root.height;
}
changed width => { Theme.window-width = root.width; }
changed height => { Theme.window-height = root.height; }
// ── ① 自定义窗口背景图(09 §101)────────────────────────────
// 只在「开关打开且图片已加载」时才画:空 image 的 width 为 0,出厂态零开销。
if Theme.bg-image.width > 0 : Rectangle {
x: 0;
y: 0;
width: root.width;
height: root.height;
clip: true;
background: Theme.canvas-solid;
Image {
source: Theme.bg-image;
x: 0;
y: 0;
width: parent.width;
height: parent.height;
// tile 模式由 Rust 预平铺成整窗画布,故这里等同 cover
image-fit: root.bg-fill == BgFillKind.contain ? contain : cover;
}
}
// ── ② 暗化层(09 §111:保证前景可读;0 = 不压暗)─────────────
if Theme.bg-image.width > 0 && Theme.bg-dim > 0 : Rectangle {
x: 0;
y: 0;
width: root.width;
height: root.height;
background: rgba(0, 0, 0, Theme.bg-dim);
}
VerticalLayout {
TitleBar {
title: root.app-title;
subtitle: "P0 骨架 · Metro";
maximized: root.maximized;
toggle-maximize => { root.toggle-maximize(); }
minimize => { root.minimize(); }
request-close => { root.request-close(); }
}
HorizontalLayout {
// 左侧主导航
Nav {
active: root.route;
collapsed: root.nav-collapsed;
account-name: root.account-name;
account-initial: root.account-initial;
account-hue: root.account-hue;
navigate(r) => { root.navigate(r); }
toggle-collapsed => { root.toggle-nav(); }
account-clicked => { root.navigate(RouteKind.settings); root.settings-section-changed(SettingSection.account); }
}
// 内容区(每页独立滚动,路由切换栈语义在 Rust Router)
Rectangle {
horizontal-stretch: 1;
background: Theme.bg-canvas;
if root.route == RouteKind.instances : InstancesPage {
tiles: root.tiles;
running: root.running-tiles;
view: root.instance-view;
tile-size: root.tile-size;
query: root.instance-query;
launch(id) => { root.instance-launch(id); }
configure(id) => { root.instance-configure(id); }
stop(id) => { root.instance-stop(id); }
open(id) => { root.instance-open(id); }
new-instance => { root.new-instance(); }
view-changed(v) => {
root.instance-view = v;
root.instance-view-changed(v);
}
tile-size-changed(s) => {
root.tile-size = s;
root.tile-size-changed(s);
}
query-changed(q) => { root.instance-query-changed(q); }
}
if root.route == RouteKind.download : DownloadPage {
versions: root.versions;
category: root.download-category;
installed-only: root.download-installed-only;
query: root.download-query;
install(id) => { root.download-install(id); }
query-changed(q) => {
root.download-query = q;
root.download-query-changed(q);
}
ancient-help => { root.ancient-help(); }
category-changed(c) => {
root.download-category = c;
root.download-category-changed(c);
}
installed-only-changed(v) => {
root.download-installed-only = v;
root.installed-only-changed(v);
}
}
if root.route == RouteKind.logs : LogsPage {
sessions: root.log-sessions;
lines: root.log-lines;
diagnosis: root.diagnosis;
has-diagnosis: root.has-diagnosis;
selected: root.selected-session;
select(id) => { root.log-select(id); }
ask-ai(id) => { root.log-ask-ai(id); }
apply-fix => { root.log-apply-fix(); }
}
if root.route == RouteKind.settings : SettingsPage {
section: root.settings-section;
theme-mode: root.theme-mode;
bg-image-enabled: root.bg-image-enabled;
bg-image: root.bg-image;
bg-fill: root.bg-fill;
bg-dim: root.bg-dim;
panel-alpha: root.panel-alpha;
glass: root.glass;
accent-color: root.accent-hex;
accent-invalid: root.accent-invalid;
section-changed(s) => {
root.settings-section = s;
root.settings-section-changed(s);
}
theme-changed(mode) => {
root.theme-mode = mode;
root.theme-changed(mode);
}
bg-image-enabled-changed(v) => { root.bg-image-enabled-changed(v); }
bg-image-changed(path) => { root.bg-image-changed(path); }
bg-fill-changed(fill) => { root.bg-fill-changed(fill); }
bg-dim-changed(v) => { root.bg-dim-changed(v); }
panel-alpha-changed(v) => { root.panel-alpha-changed(v); }
glass-changed(v) => { root.glass-changed(v); }
accent-color-changed(text) => {
root.accent-hex = text;
root.accent-color-changed(text);
}
accent-swatch-picked(index) => { root.accent-swatch-picked(index); }
yolo-changed(v) => { root.yolo-changed(v); }
mcp-changed(v) => { root.mcp-changed(v); }
ai-endpoint-changed(i) => { root.ai-endpoint-changed(i); }
}
// AI 拆成独立全宽页:与右侧栏共用同一份 ai-messages(切换无损)
if root.route == RouteKind.agent : AiPanel {
docked: false;
messages: root.ai-messages;
collapsed: false;
yolo: root.yolo;
streaming: root.ai-streaming;
endpoint-label: root.ai-endpoint-label;
draft <=> root.ai-draft;
dock => { root.dock-ai(); }
toggle-yolo => { root.toggle-yolo(); }
send(t) => { root.ai-send(t); }
stop => { root.ai-stop(); }
}
// 实例详情 G4:P0 只留占位(点击磁贴进入)
if root.route == RouteKind.detail : VerticalLayout {
alignment: center;
spacing: Metrics.gap-3;
UiText {
role: TextRole.title;
text: "实例详情(G4)";
horizontal-alignment: center;
}
UiText {
role: TextRole.caption;
text: "P0 骨架未实现:概要与设置 tab 在 G4 补齐";
horizontal-alignment: center;
}
HorizontalLayout {
alignment: center;
MetroButton {
kind: ButtonKind.standard;
text: "返回实例页";
clicked => { root.navigate(RouteKind.instances); }
}
}
}
}
// 右侧 AI 常驻栏(拆成独立页时让位给内容区)
AiPanel {
visible: root.route != RouteKind.agent;
docked: true;
collapsed: root.ai-collapsed;
messages: root.ai-messages;
yolo: root.yolo;
streaming: root.ai-streaming;
endpoint-label: root.ai-endpoint-label;
draft <=> root.ai-draft;
toggle-collapsed => { root.toggle-ai(); }
expand => { root.expand-ai(); }
toggle-yolo => { root.toggle-yolo(); }
send(t) => { root.ai-send(t); }
stop => { root.ai-stop(); }
}
}
}
// 覆盖层:模态队列(一次一条)+ 右下 Toast
DialogHost {
current: root.dialog;
open: root.dialog-open;
queued: root.dialog-queued;
primary => { root.dialog-primary(); }
secondary => { root.dialog-secondary(); }
dismissed => { root.dialog-dismissed(); }
}
ToastHost {
toasts: root.toasts;
dismissed(id) => { root.toast-dismissed(id); }
}
}
+48
View File
@@ -0,0 +1,48 @@
// 布局度量令牌(D16 定稿的尺寸口径集中在此):
// 导航栏折叠宽度、AI 栏宽度、标题栏高度、磁贴三档尺寸与列数。
import { TileSize } from "../models.slint";
export global Metrics {
// 间距刻度(4 的倍数,Metro 栅格)
out property <length> gap-1: 4px;
out property <length> gap-2: 8px;
out property <length> gap-3: 12px;
out property <length> gap-4: 16px;
out property <length> gap-5: 24px;
out property <length> gap-6: 32px;
out property <length> radius-sm: 2px;
out property <length> radius-md: 4px;
out property <length> radius-lg: 8px;
// 主窗口骨架
out property <length> titlebar-height: 40px;
out property <length> titlebar-button-width: 46px;
out property <length> accent-line: 2px;
out property <length> nav-expanded-width: 88px;
out property <length> nav-collapsed-width: 48px;
out property <length> ai-panel-width: 320px;
out property <length> ai-rail-width: 44px;
out property <length> page-header-height: 56px;
out property <length> running-strip-height: 32px;
out property <length> row-height: 44px;
out property <length> control-height: 32px;
out property <length> touch-target: 32px;
// 磁贴三档(Q24-3:默认中磁贴 4 列,设置可调小 6 / 大 3)
in-out property <TileSize> tile-size: TileSize.medium;
out property <int> tile-columns:
tile-size == TileSize.small ? 6
: tile-size == TileSize.large ? 3
: 4;
out property <length> tile-height:
tile-size == TileSize.small ? 104px
: tile-size == TileSize.large ? 176px
: 136px;
out property <length> tile-icon-size:
tile-size == TileSize.small ? 32px
: tile-size == TileSize.large ? 56px
: 44px;
// 小磁贴只留图标+名字+启动键,藏起徽标与最后游玩时间(空间不足)
out property <bool> tile-compact: tile-size == TileSize.small;
}
+208
View File
@@ -0,0 +1,208 @@
// Metro 主题令牌(D14 + D17 主题三态 + 09 §101-104「外观(高级)」):palette + 排版层级 + 字体栈 +
// 动效时长 + 外观令牌(背景图 / 面板半透明度 / 玻璃拟态 / 强调色)。
//
// 结构约定:
// - `LightPalette` / `DarkPalette` = 两套完整色板(**不是**一套加条件),换肤只改这两处。
// - `Theme` = 解析器:按主题三态(auto/light/dark)挑一套,并持有**共享**的 accent 与字体/排版。
// - auto 跟随系统:`Theme.system-theme` 由 Rust 的 `core::platform::theme_probe` 灌入(启动对齐 +
// 未知时兜底轮询),后端支持时另有 `Palette.color-scheme` 实时热切;两者都不认识就按浅色。
// - 「外观(高级)」四件全部默认关闭:`panel-alpha` 1.0 / `glass` false / `bg-image` 空 /
// `accent-override` 透明——出厂观感与 M17 骨架逐像素一致(09 §111 硬约束)。
// - 组件只准用 `Theme.*`,不得出现裸色字面量(背景图暗化层是唯一例外,它是「遮罩」而非配色)。
import { ThemeMode } from "../models.slint";
import { Palette } from "std-widgets.slint";
// ── 亮色色板 ────────────────────────────────────────────────────
export global LightPalette {
out property <color> bg-canvas: #F3F3F3;
out property <color> bg-surface: #FFFFFF;
out property <color> bg-elevated: #FFFFFF;
out property <color> bg-titlebar: #EDEDED;
out property <color> bg-hover: #E9E9E9;
out property <color> bg-pressed: #DEDEDE;
out property <color> bg-input: #FFFFFF;
out property <color> fg-primary: #1A1A1A;
out property <color> fg-secondary: #5F5F5F;
out property <color> fg-disabled: #9A9A9A;
out property <color> stroke: #E1E1E1;
out property <color> stroke-strong: #C8C8C8;
out property <color> shadow: #00000026;
out property <color> scrim: #00000099;
out property <color> accent-soft: #E5F1FB;
out property <color> accent-hover: #106EBE;
out property <color> accent-pressed: #005A9E;
out property <color> state-success: #107C10;
out property <color> state-success-soft: #DFF6DD;
out property <color> state-warn: #9D5D00;
out property <color> state-warn-soft: #FFF4CE;
out property <color> state-danger: #C42B1C;
out property <color> state-danger-hover: #A82214;
out property <color> state-danger-soft: #FDE7E9;
out property <color> state-info-soft: #E7F1FA;
}
// ── 暗色色板 ────────────────────────────────────────────────────
export global DarkPalette {
out property <color> bg-canvas: #1B1B1B;
out property <color> bg-surface: #232323;
out property <color> bg-elevated: #2B2B2B;
out property <color> bg-titlebar: #161616;
out property <color> bg-hover: #2F2F2F;
out property <color> bg-pressed: #383838;
out property <color> bg-input: #1E1E1E;
out property <color> fg-primary: #F2F2F2;
out property <color> fg-secondary: #A6A6A6;
out property <color> fg-disabled: #6B6B6B;
out property <color> stroke: #3A3A3A;
out property <color> stroke-strong: #4A4A4A;
out property <color> shadow: #000000CC;
out property <color> scrim: #00000099;
out property <color> accent-soft: #0F2A3F;
out property <color> accent-hover: #2B8FE0;
out property <color> accent-pressed: #005A9E;
out property <color> state-success: #4CCB4C;
out property <color> state-success-soft: #16321A;
out property <color> state-warn: #FFD24C;
out property <color> state-warn-soft: #3A2E0F;
out property <color> state-danger: #FF6B6B;
out property <color> state-danger-hover: #FF8A8A;
out property <color> state-danger-soft: #3A1A1A;
out property <color> state-info-soft: #10263A;
}
// ── 解析器 ──────────────────────────────────────────────────────
export global Theme {
// ── 主题三态(D17 设置页「外观」分组,出厂 auto)──────────────────
// D17 主题三态(设置页「外观」分组,出厂 auto)
in-out property <ThemeMode> theme-mode: ThemeMode.auto;
// 系统主题来源(M41):0 = 未知(此时回落到 Slint 后端 `Palette.color-scheme`),
// 1 = 浅色 / 2 = 深色。由 Rust 的 `core::platform::theme_probe` 探测后灌入:
// 启动时给首帧对齐,之后按需轮询兜底(后端不支持系统主题时 auto 仍可用)。
in-out property <int> system-theme: 0;
// 后端是否认得系统主题(`unknown` 表示不支持/无 portal)。
out property <bool> system-theme-known: Palette.color-scheme != ColorScheme.unknown;
// 是否深色:显式钉死(light/dark)优先;auto 时用探测值,探测未知才问后端。
out property <bool> dark: theme-mode == ThemeMode.dark
|| (theme-mode == ThemeMode.auto
&& (system-theme == 2
|| (system-theme != 1 && Palette.color-scheme == ColorScheme.dark)));
// ── 外观(高级)令牌(09 §101-104 / §111:出厂全部关闭)─────────
// 全局面板半透明度:`bg-*` 令牌统一按 `panel-alpha` 重建(见下方「面板底色」段),
// 组件侧零改动即透出背景图。出厂 1.0 = 完全不透明,Metro 纯色外观与 M17 骨架一致。
in-out property <float> panel-alpha: 1.0;
// 自定义窗口背景图(空 image = 纯色背景,出厂态)。
in-out property <image> bg-image;
// 背景图暗化层强度(0~0.95,越大前景越清晰)。Rust 侧夹到 schema 范围。
in-out property <float> bg-dim: 0.35;
// 玻璃拟态:预生成模糊层(Rust 侧降采样 + box blur),卡片内按 `absolute-position`
// 反向偏移对齐整窗,得到 Slint 版「毛玻璃」(无 backdrop-filter 的替代手法)。
in-out property <bool> glass: false;
in-out property <image> bg-blur;
// 玻璃层对齐用的窗口逻辑尺寸(AppShell 在 init/changed 里灌入)。
in-out property <length> window-width: 1180px;
in-out property <length> window-height: 760px;
// ── 强调色(accent 令牌全域生效;两套色板共享)──────────────────
// `accent-override` 的 alpha == 0 表示「未自定义」,走 D14 出厂 Metro 蓝。
// (color 的 `.alpha` 字段是 0~255 整数,不是 0~1 浮点。)
in-out property <color> accent-override: transparent;
out property <bool> accent-custom: accent-override.alpha > 1;
out property <color> accent: accent-custom ? accent-override : #0078D4;
// 自定义时由覆盖色派生 hover/pressed/soft(未自定义仍走双色板常量,出场观感不变)。
out property <color> accent-hover: accent-custom
? (dark ? accent.brighter(0.18) : accent.darker(0.12))
: (dark ? DarkPalette.accent-hover : LightPalette.accent-hover);
out property <color> accent-pressed: accent-custom
? (dark ? accent.brighter(0.28) : accent.darker(0.24))
: (dark ? DarkPalette.accent-pressed : LightPalette.accent-pressed);
out property <color> accent-soft: accent-custom
? accent.mix(surface-solid, 0.82)
: (dark ? DarkPalette.accent-soft : LightPalette.accent-soft);
out property <color> accent-contrast: #FFFFFF;
// accent 上的前景色按相对亮度自动选黑/白(自定义亮黄、浅青时白字会糊)。
// 注意:Slint 的 color 通道字段(`.red` 等)是 0~255 的**整数**。
out property <float> accent-luma: accent.red * 0.299 + accent.green * 0.587 + accent.blue * 0.114;
out property <color> fg-on-accent: accent-luma > 160 ? #1A1A1A : #FFFFFF;
// ── 面板底色(`panel-alpha` 统一透出背景图)────────────────────
// `*-solid` 是不透明原件(窗口自身底色 / accent.mix 的混合基准必须不透明,
// 否则浮点叠加会让 accent-soft 随 panel-alpha 一起变淡)。
// 半透明色用 `rgba(r, g, b, a)` 重建而不是 `.with-alpha()`:后者返回 Brush,
// 赋给 `<color>` 属性会类型不符(Slint 无 Brush → color 隐式回退)。
out property <color> surface-solid: dark ? DarkPalette.bg-surface : LightPalette.bg-surface;
out property <color> canvas-solid: dark ? DarkPalette.bg-canvas : LightPalette.bg-canvas;
out property <color> surface-elevated-solid: dark ? DarkPalette.bg-elevated : LightPalette.bg-elevated;
out property <color> titlebar-solid: dark ? DarkPalette.bg-titlebar : LightPalette.bg-titlebar;
out property <color> hover-solid: dark ? DarkPalette.bg-hover : LightPalette.bg-hover;
out property <color> pressed-solid: dark ? DarkPalette.bg-pressed : LightPalette.bg-pressed;
out property <color> input-solid: dark ? DarkPalette.bg-input : LightPalette.bg-input;
out property <color> bg-canvas: rgba(
canvas-solid.red, canvas-solid.green, canvas-solid.blue, panel-alpha);
out property <color> bg-surface: rgba(
surface-solid.red, surface-solid.green, surface-solid.blue, panel-alpha);
out property <color> bg-elevated: rgba(
surface-elevated-solid.red, surface-elevated-solid.green, surface-elevated-solid.blue, panel-alpha);
out property <color> bg-titlebar: rgba(
titlebar-solid.red, titlebar-solid.green, titlebar-solid.blue, panel-alpha);
out property <color> bg-hover: rgba(
hover-solid.red, hover-solid.green, hover-solid.blue, panel-alpha);
out property <color> bg-pressed: rgba(
pressed-solid.red, pressed-solid.green, pressed-solid.blue, panel-alpha);
out property <color> bg-input: rgba(
input-solid.red, input-solid.green, input-solid.blue, panel-alpha);
out property <color> fg-primary: dark ? DarkPalette.fg-primary : LightPalette.fg-primary;
out property <color> fg-secondary: dark ? DarkPalette.fg-secondary : LightPalette.fg-secondary;
out property <color> fg-disabled: dark ? DarkPalette.fg-disabled : LightPalette.fg-disabled;
out property <color> stroke: dark ? DarkPalette.stroke : LightPalette.stroke;
out property <color> stroke-strong: dark ? DarkPalette.stroke-strong : LightPalette.stroke-strong;
out property <color> shadow: dark ? DarkPalette.shadow : LightPalette.shadow;
out property <color> scrim: dark ? DarkPalette.scrim : LightPalette.scrim;
out property <color> state-success: dark ? DarkPalette.state-success : LightPalette.state-success;
out property <color> state-success-soft: dark ? DarkPalette.state-success-soft : LightPalette.state-success-soft;
out property <color> state-warn: dark ? DarkPalette.state-warn : LightPalette.state-warn;
out property <color> state-warn-soft: dark ? DarkPalette.state-warn-soft : LightPalette.state-warn-soft;
out property <color> state-danger: dark ? DarkPalette.state-danger : LightPalette.state-danger;
out property <color> state-danger-hover: dark ? DarkPalette.state-danger-hover : LightPalette.state-danger-hover;
out property <color> state-danger-soft: dark ? DarkPalette.state-danger-soft : LightPalette.state-danger-soft;
out property <color> state-info-soft: dark ? DarkPalette.state-info-soft : LightPalette.state-info-soft;
// ── 字体 ────────────────────────────────────────────────────
// Segoe UI 不可再分发(微软许可)→ Windows 用系统自带,其他平台用 Selawik
// (微软官方开源、Segoe UI 度量兼容)+ Open Sans 回退栈(01c §7 第 5 条)。
// Slint 的 font-family 只吃单个家族名,回退链由 Rust 侧启动时按平台选择 + 注册字体族实现:
// windows → "Segoe UI",macos/linux → "Selawik"(随包)+ "Open Sans" 兜底。
in-out property <string> font-family: "Selawik";
in-out property <string> font-family-mono: "Cascadia Mono";
// 中文回退(思源黑体/微软雅黑由系统字体匹配,这里仅作显式声明)
in-out property <string> font-family-cjk: "Microsoft YaHei UI";
// 排版层级(Metro typography ramp)
out property <length> font-size-display: 24px;
out property <length> font-size-title: 20px;
out property <length> font-size-subtitle: 16px;
out property <length> font-size-body: 14px;
out property <length> font-size-caption: 12px;
out property <length> line-height-body: 20px;
out property <length> line-height-caption: 16px;
// ── 动效(Metro 快速、克制)───────────────────────────────────
out property <duration> motion-fast: 90ms;
out property <duration> motion-normal: 160ms;
out property <duration> motion-slow: 240ms;
// D17 动画强度倍率(P0 占位,P1 接设置页滑条与「减少动画」无障碍开关)
in-out property <float> motion-scale: 1.0;
}
+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); }
}
}
}
}