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
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:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user