Unlucky Launcher v0.2.0
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
首个 iced + Material 3 Expressive 版本的源码快照。 完整历史与日常提交在 dev 分支;main 只放阶段性快照。
This commit is contained in:
commit
8a8e5ca0c0
1079 files changed
+208035
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// 新建实例向导(G5,全屏模态):版本 → 加载器 → 名字 → 确认 → 进度。
|
||||
// 步骤推进/校验在 Rust(`crate::wizard`),这里只画当前步与提交按钮。
|
||||
import { Theme } from "../theme/tokens.slint";
|
||||
import { Metrics } from "../theme/metrics.slint";
|
||||
import { UiVersion, VersionKind } from "../models.slint";
|
||||
import { PageState } from "../page_state.slint";
|
||||
import { UiText, TextRole } from "../components/label.slint";
|
||||
import { Badge } from "../components/badge.slint";
|
||||
import { AppIcon, IconKind } from "../components/icons.slint";
|
||||
import { MetroButton, ButtonKind } from "../widgets/button.slint";
|
||||
import { SearchField } from "../widgets/inputs.slint";
|
||||
import { InstallPanel } from "../components/install-panel.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
|
||||
component StepDot inherits Rectangle {
|
||||
in property <int> index: 0;
|
||||
in property <string> label: "";
|
||||
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Metrics.gap-1;
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background: PageState.wizard.step >= root.index ? Theme.accent : Theme.bg-hover;
|
||||
UiText {
|
||||
role: TextRole.caption;
|
||||
text: root.index + 1;
|
||||
horizontal-alignment: center;
|
||||
color: PageState.wizard.step >= root.index ? Theme.fg-on-accent : Theme.fg-secondary;
|
||||
}
|
||||
}
|
||||
UiText {
|
||||
role: TextRole.caption;
|
||||
text: root.label;
|
||||
color: PageState.wizard.step >= root.index ? Theme.fg-primary : Theme.fg-disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component VersionChoice inherits Rectangle {
|
||||
in property <UiVersion> data;
|
||||
callback picked();
|
||||
|
||||
height: 36px;
|
||||
background: PageState.wizard.version == root.data.id ? Theme.accent-soft
|
||||
: (touch.has-hover ? Theme.bg-hover : transparent);
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Metrics.gap-3;
|
||||
padding-right: Metrics.gap-3;
|
||||
spacing: Metrics.gap-2;
|
||||
UiText { role: TextRole.body; text: root.data.id; width: 160px; }
|
||||
Badge {
|
||||
text: root.data.kind == VersionKind.release ? "正式版" : "预览版";
|
||||
tint: Theme.fg-secondary;
|
||||
}
|
||||
UiText { role: TextRole.caption; text: root.data.released; horizontal-stretch: 1; }
|
||||
if PageState.wizard.version == root.data.id : AppIcon {
|
||||
kind: IconKind.check;
|
||||
icon-size: 14px;
|
||||
tint: Theme.accent;
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.picked(); }
|
||||
}
|
||||
}
|
||||
|
||||
component LoaderChoice inherits Rectangle {
|
||||
in property <string> label: "";
|
||||
in property <string> hint: "";
|
||||
in property <int> index: 0;
|
||||
callback picked();
|
||||
|
||||
height: 48px;
|
||||
background: PageState.wizard.loader == root.index ? Theme.accent-soft
|
||||
: (touch.has-hover ? Theme.bg-hover : transparent);
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Metrics.gap-3;
|
||||
padding-right: Metrics.gap-3;
|
||||
spacing: Metrics.gap-2;
|
||||
VerticalLayout {
|
||||
vertical-stretch: 1;
|
||||
spacing: 0px;
|
||||
UiText { role: TextRole.body; text: root.label; }
|
||||
UiText { role: TextRole.caption; text: root.hint; horizontal-stretch: 1; }
|
||||
}
|
||||
if PageState.wizard.loader == root.index : AppIcon {
|
||||
kind: IconKind.check;
|
||||
icon-size: 14px;
|
||||
tint: Theme.accent;
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.picked(); }
|
||||
}
|
||||
}
|
||||
|
||||
export component NewInstanceWizard inherits Rectangle {
|
||||
// 版本列表由 shell 传入(与下载页共用一份,避免二次拉取)
|
||||
in property <[UiVersion]> versions;
|
||||
in-out property <string> query: "";
|
||||
|
||||
visible: PageState.wizard.open;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
|
||||
// 吃掉底层点击(模态)
|
||||
TouchArea { }
|
||||
|
||||
Rectangle {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: min(parent.width - 80px, 720px);
|
||||
height: min(parent.height - 60px, 560px);
|
||||
background: Theme.bg-surface;
|
||||
border-radius: Metrics.radius-md;
|
||||
border-width: 1px;
|
||||
border-color: Theme.stroke;
|
||||
|
||||
VerticalLayout {
|
||||
// 标题栏
|
||||
Rectangle {
|
||||
height: 52px;
|
||||
background: transparent;
|
||||
HorizontalLayout {
|
||||
padding-left: Metrics.gap-4;
|
||||
padding-right: Metrics.gap-3;
|
||||
spacing: Metrics.gap-2;
|
||||
UiText {
|
||||
role: TextRole.subtitle;
|
||||
text: "新建实例 · " + PageState.wizard.step_label;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
MetroButton {
|
||||
kind: ButtonKind.subtle;
|
||||
text: "关闭";
|
||||
button-height: 24px;
|
||||
clicked => { PageState.wizard-close(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤指示
|
||||
HorizontalLayout {
|
||||
padding-left: Metrics.gap-4;
|
||||
spacing: Metrics.gap-3;
|
||||
StepDot { index: 0; label: "版本"; }
|
||||
StepDot { index: 1; label: "加载器"; }
|
||||
StepDot { index: 2; label: "名字"; }
|
||||
StepDot { index: 3; label: "确认"; }
|
||||
StepDot { index: 4; label: "进度"; }
|
||||
}
|
||||
|
||||
Rectangle { height: 1px; background: Theme.stroke; }
|
||||
|
||||
// 步骤内容
|
||||
Rectangle {
|
||||
vertical-stretch: 1;
|
||||
background: transparent;
|
||||
|
||||
// ① 选版本(只列正式版/快照,远古版走下载页的显式入口)
|
||||
if PageState.wizard.step == 0 : VerticalLayout {
|
||||
padding: Metrics.gap-3;
|
||||
spacing: Metrics.gap-2;
|
||||
SearchField {
|
||||
width: 220px;
|
||||
text <=> root.query;
|
||||
placeholder: "筛选版本号";
|
||||
}
|
||||
ScrollView {
|
||||
vertical-stretch: 1;
|
||||
VerticalLayout {
|
||||
for v in root.versions : VersionChoice {
|
||||
visible: !v.ancient;
|
||||
data: v;
|
||||
picked => { PageState.wizard-select-version(v.id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ② 选加载器
|
||||
if PageState.wizard.step == 1 : VerticalLayout {
|
||||
padding: Metrics.gap-3;
|
||||
spacing: 0px;
|
||||
LoaderChoice { index: 0; label: "原版(Vanilla)"; hint: "不装加载器,最快最稳"; picked => { PageState.wizard-select-loader(0); } }
|
||||
LoaderChoice { index: 1; label: "Fabric"; hint: "轻量模组加载器(GUI 可直接装)"; picked => { PageState.wizard-select-loader(1); } }
|
||||
LoaderChoice { index: 2; label: "Quilt"; hint: "Fabric 的分支(GUI 可直接装)"; picked => { PageState.wizard-select-loader(2); } }
|
||||
LoaderChoice { index: 3; label: "Forge"; hint: "需要 installer processors(首装会外调 java,约 1–3 分钟)"; picked => { PageState.wizard-select-loader(3); } }
|
||||
LoaderChoice { index: 4; label: "NeoForge"; hint: "需要 installer processors(首装会外调 java,约 1–3 分钟)"; picked => { PageState.wizard-select-loader(4); } }
|
||||
}
|
||||
|
||||
// ③ 名字
|
||||
if PageState.wizard.step == 2 : VerticalLayout {
|
||||
padding: Metrics.gap-4;
|
||||
spacing: Metrics.gap-2;
|
||||
UiText { role: TextRole.body; text: "实例名(默认用版本号)"; }
|
||||
SearchField {
|
||||
width: 320px;
|
||||
text: PageState.wizard.name;
|
||||
placeholder: "例如:生存 1.20.4";
|
||||
edited(t) => { PageState.wizard-name-changed(t); }
|
||||
}
|
||||
if PageState.wizard.error != "" : UiText {
|
||||
role: TextRole.caption;
|
||||
text: PageState.wizard.error;
|
||||
color: Theme.state-danger;
|
||||
}
|
||||
UiText {
|
||||
role: TextRole.caption;
|
||||
text: "不能包含 / \\ : * ? \" < > |,最多 64 个字符。";
|
||||
}
|
||||
}
|
||||
|
||||
// ④ 确认
|
||||
if PageState.wizard.step == 3 : VerticalLayout {
|
||||
padding: Metrics.gap-4;
|
||||
spacing: Metrics.gap-2;
|
||||
UiText { role: TextRole.bodyStrong; text: "将创建实例"; }
|
||||
UiText { role: TextRole.body; text: "版本 " + PageState.wizard.version; }
|
||||
UiText { role: TextRole.body; text: "加载器 " + PageState.wizard.loader_label; }
|
||||
UiText { role: TextRole.body; text: "实例名 " + PageState.wizard.name; }
|
||||
UiText {
|
||||
role: TextRole.caption;
|
||||
text: "确认后会按 core 安装管线下载:版本 JSON → 客户端 jar → 库 → 资源索引 → 登记实例。";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
if PageState.wizard.error != "" : UiText {
|
||||
role: TextRole.caption;
|
||||
text: PageState.wizard.error;
|
||||
color: Theme.state-danger;
|
||||
}
|
||||
}
|
||||
|
||||
// ⑤ 进度
|
||||
if PageState.wizard.step == 4 : VerticalLayout {
|
||||
padding: Metrics.gap-3;
|
||||
spacing: Metrics.gap-2;
|
||||
InstallPanel {
|
||||
data: PageState.install;
|
||||
cancel => { PageState.install-cancel(); }
|
||||
}
|
||||
UiText {
|
||||
role: TextRole.caption;
|
||||
text: "装完会自动跳到实例页;失败原因会显示在上面。";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 1px; background: Theme.stroke; }
|
||||
|
||||
// 底部按钮
|
||||
Rectangle {
|
||||
height: 56px;
|
||||
background: transparent;
|
||||
HorizontalLayout {
|
||||
padding: Metrics.gap-4;
|
||||
spacing: Metrics.gap-2;
|
||||
Rectangle { horizontal-stretch: 1; background: transparent; }
|
||||
MetroButton {
|
||||
kind: ButtonKind.subtle;
|
||||
text: "上一步";
|
||||
button-height: 30px;
|
||||
enabled: PageState.wizard.step > 0 && PageState.wizard.step < 4;
|
||||
clicked => { PageState.wizard-back(); }
|
||||
}
|
||||
if PageState.wizard.step < 3 : MetroButton {
|
||||
kind: ButtonKind.accent;
|
||||
text: "下一步";
|
||||
button-height: 30px;
|
||||
enabled: PageState.wizard.can-next;
|
||||
clicked => { PageState.wizard-next(); }
|
||||
}
|
||||
if PageState.wizard.step == 3 : MetroButton {
|
||||
kind: ButtonKind.accent;
|
||||
text: "开始安装";
|
||||
button-height: 30px;
|
||||
enabled: PageState.wizard.can-next && !PageState.wizard.busy;
|
||||
clicked => { PageState.wizard-confirm(); }
|
||||
}
|
||||
if PageState.wizard.step == 4 : MetroButton {
|
||||
kind: ButtonKind.standard;
|
||||
text: "后台继续";
|
||||
button-height: 30px;
|
||||
clicked => { PageState.wizard-close(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user