Files
Windows-Gotify-Client-Rust/ui/app.slint
T
KennethCheng 49cbd9e371 feat(ui): Phase 2 C1 — MainWindow + Sidebar + 颜色/主题系统
完成 AGENTS.md §57 Phase 2 的前 3 项:MainWindow 布局、Sidebar、Dark 主题
基础。剩余的 Message list / Empty state / Settings / Light-Switch /
Window state 留到 C2 / C3。

技术栈变化
----------

本次最大的工程变化是把 Slint UI 从 `slint!{}` macro 内嵌迁到独立
`.slint` 文件 + build.rs 编译:

  ui/app.slint           MainWindow + Sidebar 骨架(Phase 2 C1 完整实现)
  ui/theme.slint         export enum AppearanceMode { System, Light, Dark }
                         (不再有 export global Theme / Palette,见下文)
  ui/components.slint    NavItem / SectionLabel / Divider(Dark 默认颜色)
  ui/message-card.slint  占位 stub(C2 实现)
  ui/message-list.slint  占位 stub(C2 实现)
  ui/empty-state.slint   占位 stub(C2 实现)
  ui/settings-page.slint 占位 stub(C3 实现)
  ui/server-card.slint   占位 stub(C3 实现)
  ui/dialogs.slint       占位 stub(C3 实现)
  build.rs               slint_build::compile("ui/app.slint"),
                         把生成的 .rs 路径写入 SLINT_INCLUDE_GENERATED
  src/lib.rs             `slint::include_modules!()` 引入编译产物;
                         `MainWindow::new()` 拿 UI 实例;
                         `ui.set_theme(AppearanceMode::Dark)` 初始化
                         (C3 改成从 state.json 读 + 监听用户切换)

为什么不用 `include_slint!` —— 那是 `slint_interpreter` crate 提供的
API,不在 `slint` 1.18 主 crate 里。官方推荐的"build-time precompile"
路线是 `slint_build::compile()` + `slint::include_modules!()`,文档明确
写了这一步。

为什么不用 export global Palette / Theme
---------------------------------------

最初一版我把 Theme + Palette 都做成 `export global`,期望从 Rust 端
用 `ui.global::<Theme>().set_appearance(...)` 切换 Light/Dark。失败
了:slint-build 1.18 在生成 Rust 代码时把这两个 global 都 dead-code
消除了,generated 的 pub use 列表里只剩 `MainWindow` 和
`SidebarSelection`。原因——global 的所有 binding 在编译期都被简化为
常量(Theme.appearance 默认 Dark,Palette.background 绑定直接 inline 成
`#1f2024`),没有任何 dynamic 引用保留 global 的符号表。

解决方案:把 Theme + Palette 全部搬到 MainWindow 上作为 out properties,
由 `MainWindow.theme` 触发 binding 重新计算。Rust 端用
`ui.set_theme(AppearanceMode::Light)` 切。代价是 sidebar 的 NavItem
暂时只有 Dark 默认色(fixed `#2f3035` 等),Light theme 在 C3 通过
property forwarding 扩展 NavItem 时补齐——这是 C1 阶段明确接受的折衷。

这一节值得写进 commit 而不是悄悄改掉——后面任何看到代码的人都会好奇
"为什么 palette 不直接 export global?",是这个原因。下次有 Phase 3+ 的人
碰这块也省得踩同样的坑。

Slint 版本对齐
--------------

之前 commit 时钉过 `slint = "1"`(让 cargo 选最新 1.x 稳定版),现在
slint-build 1.18.0 引入 build-deps:`slint-build`, `i-slint-compiler` 等。
两者锁到同一个 1.18.0 版本是 Slint 自身的 forward-compat 约束
(slint-build 的 `compat-1-18` feature 在 build.rs 启动时强校验)。

MainWindow / Sidebar 布局细节
-----------------------------

按 AGENTS.md §6 / §7:

- 960×680 preferred,760×520 min —— Slint 1.18 不允许同时设
  `width` + `min-width`,用 `preferred-width`/`preferred-height` 替代
  (之前第一次 build 报 `Cannot specify both 'width' and 'min-width'`,
  这里记录一下)。
- 232px sidebar 宽,含 AGENTS.md §7 列出的 6 个 section
  (logo + All + Servers 占位 + Filters 三个 + Settings + About)
- SidebarSelection enum 驱动右侧 view-switcher,三个 view 都有占位
  文字(Messages / Settings / About),C2 / C3 替换实际内容
- AGENTS.md §52 颜色 token 全部按 dark / light 两套列出,由
  `MainWindow.is-dark` 派生;优先级颜色(§7 message card)也按
  §7 Low/Normal/High/Max 映射

未做(按计划归到后续 commit)
------------------------------

C2:
  - src/models.rs(MessageItem + Priority enum,AGENTS.md §34)
  - src/mock.rs(生成 10 条 mock 消息)
  - ui/message-card.slint 真实内容
  - ui/message-list.slint 真实内容(ListView 接 Rust Model adapter)
  - ui/empty-state.slint 真实内容(§9 "You're all caught up" + ✦ + Test button)
  - main 区 if-switch 真正根据 list.is_empty 选 list / empty-state

C3:
  - src/storage/* —— 持久化 state.json
  - ui/settings-page.slint 真实内容(§10 五个分组 + Advanced 折叠)
  - ui/server-card.slint 真实内容
  - ui/dialogs.slint 真实内容(About + confirm)
  - sidebar NavItem 接 palette forwarding,Light theme 真的换 sidebar 颜色
  - 主题切到 Light/System 实时生效(System 跟随 Windows 主题)

验证
----

  cargo build              exit 0, 5.10s(增量)
  cargo build --release    exit 0(slint + skia + accesskit 全重编,约 8 分钟)
  warnings                 0
  debug   .exe             26.01 MB, subsystem = 3 (WINDOWS_CUI / console)
  release .exe              7.87 MB, subsystem = 2 (WINDOWS_GUI)

Phase 2 C1 验收:能在 Windows 上打开 MainWindow(960×680、Dark、含完整
Sidebar),通过 release profile 的 WINDOWS_GUI subsystem 验证无 console。
Sidebar 切换 Settings/About 时右侧文案占位更新——C2/C3 替换为实际 view。
2026-09-19 04:34:06 +08:00

174 lines
6.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Main window + sidebar (AGENTS.md §6, §7).
//
// Phase 2 C1 only lays out the chrome: 960×680 default, 760×520 minimum,
// 232px sidebar with the seven required sections. The message area /
// settings page / about dialog are still placeholders — content lands in
// C2 (messages) and C3 (settings + dialogs + sidebar Light/System).
import { AppearanceMode } from "./theme.slint";
import { NavItem, SectionLabel } from "./components.slint";
export enum SidebarSelection {
All,
Settings,
About,
}
export component MainWindow inherits Window {
title: "Gotify";
preferred-width: 960px;
preferred-height: 680px;
min-width: 760px;
min-height: 520px;
// Theme switch — Rust-controllable via `ui.set_theme(...)`. Light/System
// is wired through this single property; the sidebar children will start
// following it once C3 widens NavItem to accept palette forwarding.
in-out property <AppearanceMode> theme: AppearanceMode.Dark;
out property <bool> is-dark: theme != AppearanceMode.Light;
// Palette — all derived from `is-dark` (AGENTS.md §52).
out property <color> bg: is-dark ? #1f2024 : #f5f6f8;
out property <color> surface: is-dark ? #28292d : #ffffff;
out property <color> surface-elev: is-dark ? #2f3035 : #fafbfc;
out property <color> border: is-dark ? #3a3b40 : #e4e6ea;
out property <color> text-primary: is-dark ? #e6e6e6 : #1a1a1a;
out property <color> text-second: is-dark ? #a0a0a0 : #5c5c5c;
out property <color> text-muted: is-dark ? #6a6a6a : #9a9a9a;
out property <color> accent: is-dark ? #4ea1ff : #0a66c2;
out property <color> success: #4caf50;
out property <color> warning: #ff9800;
out property <color> danger: #f44336;
out property <color> priority-low: #6a6a6a;
out property <color> priority-normal: is-dark ? #4ea1ff : #0a66c2;
out property <color> priority-high: #ff9800;
out property <color> priority-max: #f44336;
background: bg;
in-out property <SidebarSelection> selection: SidebarSelection.All;
HorizontalLayout {
// §7 Sidebar width: 220-240px.
Rectangle {
width: 232px;
background: surface;
VerticalLayout {
padding: 20px;
spacing: 6px;
// App logo / title block.
Text {
text: "Gotify";
color: text-primary;
font-size: 18px;
font-weight: 700;
}
Text {
text: "Windows Client · Phase 2";
color: text-muted;
font-size: 10px;
}
Rectangle { height: 16px; }
SectionLabel { label: "MESSAGES"; text-color: text-muted; }
NavItem {
label: "All";
selected: root.selection == SidebarSelection.All;
clicked => { root.selection = SidebarSelection.All; }
}
Rectangle { height: 12px; }
SectionLabel { label: "SERVERS"; text-color: text-muted; }
Text {
text: "(no servers configured)";
color: text-muted;
font-size: 11px;
}
Rectangle { height: 12px; }
SectionLabel { label: "FILTERS"; text-color: text-muted; }
NavItem {
label: "High";
selected: false;
has-accent: false;
clicked => { /* TODO Phase 2 C2 */ }
}
NavItem {
label: "Normal";
selected: false;
has-accent: false;
clicked => { /* TODO Phase 2 C2 */ }
}
NavItem {
label: "Low";
selected: false;
has-accent: false;
clicked => { /* TODO Phase 2 C2 */ }
}
Rectangle { height: 12px; }
SectionLabel { label: "APP"; text-color: text-muted; }
NavItem {
label: "Settings";
selected: root.selection == SidebarSelection.Settings;
clicked => { root.selection = SidebarSelection.Settings; }
}
NavItem {
label: "About";
selected: root.selection == SidebarSelection.About;
clicked => { root.selection = SidebarSelection.About; }
}
}
}
// Right pane — placeholder content driven by `selection`.
Rectangle {
background: bg;
if root.selection == SidebarSelection.All: VerticalLayout {
padding: 24px;
spacing: 8px;
Text {
text: "Messages";
color: text-primary;
font-size: 18px;
font-weight: 700;
}
Text {
text: "Message list and empty state land in Phase 2 C2.";
color: text-muted;
font-size: 12px;
}
}
if root.selection == SidebarSelection.Settings: Rectangle {
background: bg;
Text {
x: 24px;
y: 24px;
text: "Settings page arrives in Phase 2 C3.";
color: text-muted;
font-size: 13px;
}
}
if root.selection == SidebarSelection.About: Rectangle {
background: bg;
Text {
x: 24px;
y: 24px;
text: "About dialog arrives in Phase 2 C3.";
color: text-muted;
font-size: 13px;
}
}
}
}
}