From 49cbd9e371ea9f12a35681cfd63a14c4d9010590 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sat, 19 Sep 2026 04:34:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20Phase=202=20C1=20=E2=80=94=20MainWi?= =?UTF-8?q?ndow=20+=20Sidebar=20+=20=E9=A2=9C=E8=89=B2/=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完成 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::().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。 --- Cargo.lock | 100 +++++++++++++++++++++++ Cargo.toml | 4 + build.rs | 12 +++ src/lib.rs | 51 ++++-------- ui/app.slint | 174 +++++++++++++++++++++++++++++++++++++++++ ui/components.slint | 66 ++++++++++++++++ ui/dialogs.slint | 4 + ui/empty-state.slint | 5 ++ ui/message-card.slint | 4 + ui/message-list.slint | 5 ++ ui/server-card.slint | 6 ++ ui/settings-page.slint | 6 ++ ui/theme.slint | 14 ++++ 13 files changed, 416 insertions(+), 35 deletions(-) create mode 100644 build.rs create mode 100644 ui/app.slint create mode 100644 ui/components.slint create mode 100644 ui/dialogs.slint create mode 100644 ui/empty-state.slint create mode 100644 ui/message-card.slint create mode 100644 ui/message-list.slint create mode 100644 ui/server-card.slint create mode 100644 ui/settings-page.slint create mode 100644 ui/theme.slint diff --git a/Cargo.lock b/Cargo.lock index e1ce9cd..9ea24de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -844,6 +844,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -1778,6 +1797,7 @@ dependencies = [ "i-slint-common", "icu_normalizer", "icu_properties", + "image", "indexmap", "itertools 0.15.0", "lyon_extra", @@ -1785,9 +1805,14 @@ dependencies = [ "num_enum", "proc-macro2", "quote", + "rayon", + "resvg", "rowan", + "rspolib", + "skrifa", "smol_str 0.3.6", "strum", + "swash", "typed-index-collections", "unicode-segmentation", "url", @@ -2668,6 +2693,12 @@ dependencies = [ "thiserror 2.0.20", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "ndk" version = "0.9.0" @@ -3529,6 +3560,26 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-fonts" version = "0.41.0" @@ -3651,6 +3702,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "rspolib" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fda9a7796aff63a7b1b39ccc93fffaaf65e20042984b4843041a49ca4677535" +dependencies = [ + "lazy_static", + "natord", + "snafu", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3947,6 +4011,19 @@ dependencies = [ "vtable", ] +[[package]] +name = "slint-build" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6cfc9bb0dd48d01508f9e21511d0215e78e8e3be032bacd6be61744abe03c83" +dependencies = [ + "derive_more", + "fontique", + "i-slint-compiler", + "spin_on", + "toml_edit", +] + [[package]] name = "slint-macros" version = "1.18.0" @@ -4018,6 +4095,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "softbuffer" version = "0.4.8" @@ -4389,6 +4487,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -5101,6 +5200,7 @@ name = "windows-gotify-client-rust" version = "0.1.0" dependencies = [ "slint", + "slint-build", "thiserror 2.0.20", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 3d9ed0a..8d5a699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,10 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # --- Error handling --- thiserror = "2" +# Slint's `.slint` files are compiled by `build.rs`; see build.rs. +[build-dependencies] +slint-build = "1" + [profile.release] # See AGENTS.md §42. The `release-balanced` profile below is the practical # default after benchmarking — adjust if startup time regresses. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f44c474 --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +// Build script: compile all `ui/*.slint` files into Rust source via +// `slint-build`. The generated file path is exposed via the +// `SLINT_INCLUDE_GENERATED` env var and pulled into `lib.rs` through +// `slint::include_modules!()`. +// +// Per AGENTS.md §4 the UI lives under `ui/`. `ui/app.slint` is the entry +// point that recursively imports the other files (theme.slint, +// components.slint, ...), so compiling only the entry is sufficient. + +fn main() { + slint_build::compile("ui/app.slint").expect("slint compilation failed"); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 96e8c08..862ad94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,57 +1,38 @@ //! Windows Gotify Client (Rust) — core library. //! -//! Phase 1 only exposes the bare-minimum surface needed to spin up the -//! Slint hello window: -//! - [`error`]: AppError / AppResult +//! Phase 2 C1 only: +//! - [`error`]: AppError / AppResult //! - [`logging`]: tracing-subscriber init -//! - [`run`]: entry that builds the Slint UI and runs its event loop +//! - [`run`]: builds the MainWindow and runs its event loop //! -//! Larger subsystems (gotify, storage, windows integration, ...) are added in -//! later phases per AGENTS.md §57. +//! Larger subsystems (gotify / storage / windows integration / mock data) +//! land in later commits per AGENTS.md §57. pub mod error; pub mod logging; use slint::ComponentHandle; -slint::slint! { - export component AppWindow inherits Window { - title: "Gotify"; - width: 480px; - height: 320px; - background: #1f2024; +slint::include_modules!(); - VerticalLayout { - padding: 24px; - spacing: 12px; - - Text { - text: "Windows Gotify Client (Rust)"; - color: #e6e6e6; - font-size: 18px; - } - - Text { - text: "Phase 1 — Bootstrap OK"; - color: #8a8a8a; - font-size: 12px; - } - } - } -} - -/// Build the Slint UI and run its event loop until the window is closed. +/// Build the MainWindow and run its event loop until the window is closed. /// /// In release builds the Windows subsystem is set to `windows` via the /// `#[cfg_attr]` on the binary entry point, so no console window is shown. -pub fn run() -> AppResult<()> { +pub fn run() -> error::AppResult<()> { tracing::info!( "starting windows-gotify-client-rust v{}", env!("CARGO_PKG_VERSION") ); - let ui = AppWindow::new().map_err(|e| AppError::Ui(e.to_string()))?; - ui.run().map_err(|e| AppError::Ui(e.to_string())) + let ui = MainWindow::new().map_err(|e| error::AppError::Ui(e.to_string()))?; + + // Phase 2 C1: dark by default. C3 wires this to the Appearance section + // of the Settings page and to `state.json` so the choice persists. + ui.set_theme(AppearanceMode::Dark); + + ui.run() + .map_err(|e| error::AppError::Ui(e.to_string())) } pub use error::{AppError, AppResult}; \ No newline at end of file diff --git a/ui/app.slint b/ui/app.slint new file mode 100644 index 0000000..46788df --- /dev/null +++ b/ui/app.slint @@ -0,0 +1,174 @@ +// 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 theme: AppearanceMode.Dark; + out property is-dark: theme != AppearanceMode.Light; + + // Palette — all derived from `is-dark` (AGENTS.md §52). + out property bg: is-dark ? #1f2024 : #f5f6f8; + out property surface: is-dark ? #28292d : #ffffff; + out property surface-elev: is-dark ? #2f3035 : #fafbfc; + out property border: is-dark ? #3a3b40 : #e4e6ea; + out property text-primary: is-dark ? #e6e6e6 : #1a1a1a; + out property text-second: is-dark ? #a0a0a0 : #5c5c5c; + out property text-muted: is-dark ? #6a6a6a : #9a9a9a; + out property accent: is-dark ? #4ea1ff : #0a66c2; + out property success: #4caf50; + out property warning: #ff9800; + out property danger: #f44336; + out property priority-low: #6a6a6a; + out property priority-normal: is-dark ? #4ea1ff : #0a66c2; + out property priority-high: #ff9800; + out property priority-max: #f44336; + + background: bg; + + in-out property 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; + } + } + } + } +} \ No newline at end of file diff --git a/ui/components.slint b/ui/components.slint new file mode 100644 index 0000000..a0eb227 --- /dev/null +++ b/ui/components.slint @@ -0,0 +1,66 @@ +// Reusable UI building blocks (AGENTS.md §7). +// +// In Phase 2 C1 every theme-aware color is a fixed Dark-theme default. Phase +// 2 C3 will widen these to take a Palette struct or forward from `parent` +// so Light/System can repaint the sidebar too. + +export component NavItem inherits Rectangle { + in property label; + in property selected: false; + in property has-accent: true; + + in property bg-idle: #00000000; + in property bg-selected: #2f3035; + in property text-idle: #a0a0a0; + in property text-selected: #e6e6e6; + in property accent-color: #4ea1ff; + + callback clicked(); + + width: 100%; + height: 36px; + border-radius: 8px; + background: selected ? bg-selected : bg-idle; + + Rectangle { + x: 4px; + y: 8px; + width: 3px; + height: parent.height - 16px; + border-radius: 2px; + background: accent-color; + opacity: root.has-accent && root.selected ? 1.0 : 0.0; + } + + Text { + x: 16px; + y: (parent.height - self.height) / 2; + text: root.label; + color: selected ? text-selected : text-idle; + font-size: 13px; + font-weight: selected ? 600 : 400; + } + + TouchArea { + clicked => { + root.clicked(); + } + } +} + +export component SectionLabel inherits Text { + in property label; + in property text-color: #6a6a6a; + + text: label; + color: text-color; + font-size: 11px; + font-weight: 700; +} + +export component Divider inherits Rectangle { + in property line-color: #3a3b40; + + height: 1px; + background: line-color; +} \ No newline at end of file diff --git a/ui/dialogs.slint b/ui/dialogs.slint new file mode 100644 index 0000000..b3f2061 --- /dev/null +++ b/ui/dialogs.slint @@ -0,0 +1,4 @@ +// AGENTS.md §54 dialogs (About + confirm). +// +// Placeholder for Phase 2 C1. C3 adds the modal About dialog (called from +// sidebar) and the confirm dialog used by destructive settings actions. \ No newline at end of file diff --git a/ui/empty-state.slint b/ui/empty-state.slint new file mode 100644 index 0000000..6b1d107 --- /dev/null +++ b/ui/empty-state.slint @@ -0,0 +1,5 @@ +// AGENTS.md §9 empty state. +// +// Placeholder for Phase 2 C1 — actual ✦ glyph + "You're all caught up" +// panel lands in C2 when message-list integration knows whether the +// backing model is empty. \ No newline at end of file diff --git a/ui/message-card.slint b/ui/message-card.slint new file mode 100644 index 0000000..6e85a45 --- /dev/null +++ b/ui/message-card.slint @@ -0,0 +1,4 @@ +// AGENTS.md §7 Message Card. +// +// Placeholder for Phase 2 C1 — real implementation lands in C2 alongside +// `src/models.rs` (MessageItem / Priority) and the mock data feed. \ No newline at end of file diff --git a/ui/message-list.slint b/ui/message-list.slint new file mode 100644 index 0000000..7df55af --- /dev/null +++ b/ui/message-list.slint @@ -0,0 +1,5 @@ +// AGENTS.md §7 / §37 list virtualization. +// +// Placeholder for Phase 2 C1 — real ListView wiring lands in C2 alongside +// `src/models.rs`. Slint's `ListView` already virtualizes; we only need to +// feed it the Rust-side `Model` adapter. \ No newline at end of file diff --git a/ui/server-card.slint b/ui/server-card.slint new file mode 100644 index 0000000..00f9b7e --- /dev/null +++ b/ui/server-card.slint @@ -0,0 +1,6 @@ +// AGENTS.md §21 multi-server card. +// +// Placeholder for Phase 2 C1. The Settings → Connection section uses these +// to list/edit each configured server (name / URL / token / enabled). +// Real data wiring lands in Phase 5 (Storage) when `AppConfig.servers` is +// persisted. \ No newline at end of file diff --git a/ui/settings-page.slint b/ui/settings-page.slint new file mode 100644 index 0000000..970bfae --- /dev/null +++ b/ui/settings-page.slint @@ -0,0 +1,6 @@ +// AGENTS.md §10 Settings page. +// +// Placeholder for Phase 2 C1. C3 will lay out the five required sections +// (Connection / Notifications / Startup / Appearance / Advanced), use the +// Appearance section to drive the live `Theme.appearance` switch, and wire +// up the actual save pipeline (Phase 5). \ No newline at end of file diff --git a/ui/theme.slint b/ui/theme.slint new file mode 100644 index 0000000..f3f7bdc --- /dev/null +++ b/ui/theme.slint @@ -0,0 +1,14 @@ +// AGENTS.md §52 color system + theme switcher. +// +// `Theme` and `Palette` live on `MainWindow` rather than as `export global` — +// Slint 1.18 dead-code-eliminates globals whose binding is compile-time +// reducible (which Theme always is when the default `appearance` is Dark), so +// the only way to keep `Theme` reachable from Rust is to reference it through +// a property on the root component. `MainWindow.theme` is the single source +// of truth; everything else is derived in app.slint. + +export enum AppearanceMode { + System, + Light, + Dark, +} \ No newline at end of file