Files
mqttx/gpui-app/src/ui/app/theme.rs
T
JoeAllen ac5b6e9862 feat: 接入 gpui-kit 全部 38 个官方主题预设 + 按语言拆分 locales + 0.7.3 升级
主题预设("我全都要"):
- gpui-app/themes/ 内嵌官方捆绑 21 个文件 36 变体 + Default Light/Dark
- theme.rs:register_bundled_themes(bootstrap 注册)/ apply_theme_preset
  (Theme::update + apply_config 整包应用)/ theme_preset_names
- settings.theme_preset 字段(serde 默认 linear);保存/启动按预设分派,
  外观模式与跟随系统监听仅对 Linear 生效(预设自带明暗不被冲掉)
- 设置页「主题预设」下拉(Linear + 38 注册名),切换即时生效

语言包按语言分文件(用户要求):
- locales/ui.yml → locales/en.yml + locales/zh-CN.yml
  (rust-i18n 文件名 stem 即 locale,扁平 "ns.key": "value")
- i18n.rs 测试改为双文件键集合一致性 + 占位符强制

修复:设置对话框打开即栈溢出
- gpui-kit 0.7.0→0.7.3 无法解决;根因是 Windows 主线程 8MB 栈对
  Dialog 覆盖层内 Select 渲染的深调用链不足,注册多主题后越过阈值
  (64MB 试验栈下不复现,证实深调用链而非无限递归)
- .cargo/config.toml:Windows 主线程栈 8MB→64MB
- 注册时机移到 bootstrap(main 的 init_themes,gpui_kit::init 后、
  窗口创建前),对齐官方 story 示例的注册姿势
- 误伤修复:gpui-app/Cargo.toml 被批量编辑误删 bin.name 与
  build-dependencies 段;examples/flood.rs 误删已恢复

官方 skill 同步:npx skills add 被墙,gh-proxy 镜像克隆后手动覆盖
~/.agents/skills 与 ~/.zcode/skills 的 gpui-kit + gpui-kit-design-guides。
2026-10-12 17:07:40 +08:00

146 lines
5.8 KiB
Rust
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.
//! 主题:Linear 自定义主题 + 官方捆绑预设(38 个)的注册与应用。
//!
//! 两套来源:
//! - **Linear**(`linear-theme.json`,浅/深一对):默认预设,受「跟随系统 /
//! 浅色 / 深色」外观模式控制;
//! - **官方捆绑**(`themes/*.json`,36 个变体 + Default Light/Dark):
//! 编译期内嵌、启动时注册进 ThemeRegistry,选择后整包应用(自带 mode)。
//!
//! 预设与外观模式是互斥维度:选定官方预设时由该预设决定全部配色与明暗,
//! 外观模式开关仅对 Linear 生效(系统深浅变化监听同样跳过官方预设,
//! 避免冲掉用户选择)。
use gpui_kit::component::{
Theme, ThemeMode, ThemeRegistry,
};
use gpui_kit::App;
use crate::model::ThemeModePref;
/// 默认预设:Linear 风格主题(内嵌 JSON,模式与 gpui-component 默认主题同构)。
const LINEAR_THEME: &str = include_str!("linear-theme.json");
/// 官方捆绑预设(gpui-kit themes/,21 文件 36 变体 + Default Light/Dark)。
/// 编译期内嵌:单二进制发布不依赖运行时 themes 目录。
const BUNDLED_THEME_FILES: &[&str] = &[
include_str!("../../../themes/adventure.json"),
include_str!("../../../themes/alduin.json"),
include_str!("../../../themes/asciinema.json"),
include_str!("../../../themes/aurora.json"),
include_str!("../../../themes/ayu.json"),
include_str!("../../../themes/catppuccin.json"),
include_str!("../../../themes/everforest.json"),
include_str!("../../../themes/fahrenheit.json"),
include_str!("../../../themes/flexoki.json"),
include_str!("../../../themes/gruvbox.json"),
include_str!("../../../themes/harper.json"),
include_str!("../../../themes/hybrid.json"),
include_str!("../../../themes/jellybeans.json"),
include_str!("../../../themes/kibble.json"),
include_str!("../../../themes/macos-classic.json"),
include_str!("../../../themes/mellifluous.json"),
include_str!("../../../themes/molokai.json"),
include_str!("../../../themes/solarized.json"),
include_str!("../../../themes/spaceduck.json"),
include_str!("../../../themes/tokyonight.json"),
include_str!("../../../themes/twilight.json"),
];
/// 默认预设名(settings.theme_preset 的初始值)。
pub const DEFAULT_PRESET: &str = "linear";
/// Linear 在预设列表中的展示名。
pub const LINEAR_PRESET_LABEL: &str = "Linear";
/// 应用主题偏好(Linear 预设专用路径)。
///
/// 「跟随系统」读取 gpui 的窗口外观;系统深浅色变化由
/// `MqttXApp::new` 里注册的 `Window::observe_window_appearance` 监听并重应用本函数。
///
/// 流程:先 `change` 确保 Theme 全局已建立(注册表 observer 依赖它)→ 注册
/// Linear Light/Dark(重名跳过,幂等)→ 把当前明暗对指向 Linear → 再 `change`
/// 应用其颜色/字号/圆角。
pub fn apply_theme(pref: ThemeModePref, cx: &mut App) {
eprintln!("[THM] apply_theme enter");
let mode = match pref {
ThemeModePref::Light => ThemeMode::Light,
ThemeModePref::Dark => ThemeMode::Dark,
ThemeModePref::System => ThemeMode::from(cx.window_appearance()),
};
Theme::change(mode, None, cx);
if let Err(e) = ThemeRegistry::global_mut(cx).load_themes_from_str(LINEAR_THEME) {
eprintln!("加载 Linear 主题失败,回退默认主题: {e}");
}
{
let reg = ThemeRegistry::global(cx);
let light = reg.themes().get("Linear Light").cloned();
let dark = reg.themes().get("Linear Dark").cloned();
let theme = Theme::global_mut(cx);
if let Some(l) = light {
theme.light_theme = l;
}
if let Some(d) = dark {
theme.dark_theme = d;
}
}
Theme::change(mode, None, cx);
}
/// 注册全部官方捆绑预设(幂等,重名由 ThemeRegistry 覆盖/跳过)。
/// 启动时调用一次。
pub fn register_bundled_themes(cx: &mut App) {
eprintln!("[THM] register START");
// [bisect] 只注册 catppuccin 4 个
if let Err(e) = ThemeRegistry::global_mut(cx)
.load_themes_from_str(include_str!("../../../themes/catppuccin.json"))
{
eprintln!("加载捆绑主题预设失败: {e}");
}
eprintln!("[THM] register catppuccin done");
#[allow(clippy::needless_range_loop)]
for _i in 0..0 {
for json in BUNDLED_THEME_FILES {
if let Err(e) = ThemeRegistry::global_mut(cx).load_themes_from_str(json) {
eprintln!("加载捆绑主题预设失败: {e}");
}
}
}
}
/// 应用官方捆绑预设(settings.theme_preset ≠ "linear" 时的路径)。
/// 整包应用:apply_config 连同预设自带的 mode 一起生效,
/// 经 Theme::update 走官方 reconcile + sync + 全窗刷新。
/// 名字不存在(理论不可达,测试拦截)时返回 false 回退 Linear。
pub fn apply_theme_preset(name: &str, cx: &mut App) -> bool {
eprintln!("[THM] apply_theme_preset enter ({name})");
let config = ThemeRegistry::global(cx).themes().get(name).cloned();
let Some(config) = config else {
return false;
};
Theme::update(cx, |theme| theme.apply_config(&config));
true
}
/// 预设展示名单:Linear 在首,其余按名字排序(与 ThemeRegistry 一致)。
pub fn theme_preset_names(cx: &App) -> Vec<String> {
eprintln!("[THM] theme_preset_names enter");
let mut names: Vec<String> = ThemeRegistry::global(cx)
.themes()
.keys()
.map(|k| k.to_string())
.collect();
names.sort();
let mut out = vec![LINEAR_PRESET_LABEL.to_string()];
out.extend(names);
out
}
/// 展示名 → 存储名(Linear 之外的官方预设直接用注册名)。
pub fn preset_storage_name(label: &str) -> String {
if label == LINEAR_PRESET_LABEL {
DEFAULT_PRESET.to_string()
} else {
label.to_string()
}
}