新增: Rust 数据模型 + 颜色纯函数 + ts-rs 类型同步(B1)
- model.rs: 全部数据结构(Deck/Slide/Element/Style/Theme/AiCfg/LibItem/PageTemplate/ChatMessage/Op/RichSegment) 所有结构体 derive Serialize/Deserialize/TS,自动生成 TS 类型到 bindings/types.ts - color.rs: 颜色解析纯函数(resolve_color/resolve_bg/is_dark_bg/shade/hex_to_rgb) 对应 store.ts 的颜色逻辑,23 个单元测试全部通过 - lib.rs: 新增 app_info IPC 命令 - ts-rs 自动生成的 bindings/types.ts 与手写 types.ts 类型一致
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/* =====================================================================
|
||||
* color.rs — 颜色/背景解析纯函数(对应 store.ts 的颜色函数)
|
||||
*
|
||||
* 这些函数供 Rust 端的渲染/序列化使用(B2 store 迁移时调用)。
|
||||
* 当前阶段保持与 TS 版本完全一致的逻辑。
|
||||
* ===================================================================== */
|
||||
|
||||
pub fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
|
||||
let c = hex.trim_start_matches('#');
|
||||
let full = if c.len() == 3 {
|
||||
format!(
|
||||
"{}{}{}{}{}{}",
|
||||
&c[0..1],
|
||||
&c[0..1],
|
||||
&c[1..2],
|
||||
&c[1..2],
|
||||
&c[2..3],
|
||||
&c[2..3]
|
||||
)
|
||||
} else {
|
||||
c.to_string()
|
||||
};
|
||||
if full.len() < 6 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&full[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&full[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&full[4..6], 16).ok()?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
pub fn is_dark_hex(hex: &str) -> bool {
|
||||
match hex_to_rgb(hex) {
|
||||
Some((r, g, b)) => (0.299 * r as f64 + 0.587 * g as f64 + 0.114 * b as f64) < 145.0,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shade(hex: &str, pct: f64) -> String {
|
||||
let (r, g, b) = match hex_to_rgb(hex) {
|
||||
Some(rgb) => rgb,
|
||||
None => return hex.to_string(),
|
||||
};
|
||||
let f = if pct < 0.0 { 0.0 } else { 255.0 };
|
||||
let p = pct.abs() / 100.0;
|
||||
let nr = ((f - r as f64) * p + r as f64).round() as u8;
|
||||
let ng = ((f - g as f64) * p + g as f64).round() as u8;
|
||||
let nb = ((f - b as f64) * p + b as f64).round() as u8;
|
||||
format!("#{:02x}{:02x}{:02x}", nr, ng, nb)
|
||||
}
|
||||
|
||||
pub fn is_dark_bg(bg: &str) -> bool {
|
||||
if bg.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if bg.starts_with('#') {
|
||||
return is_dark_hex(bg);
|
||||
}
|
||||
if bg == "primary" || bg == "accent" {
|
||||
return true;
|
||||
}
|
||||
if bg.starts_with("g-") {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_valid_hex(s: &str) -> bool {
|
||||
s.starts_with('#')
|
||||
&& s.len() >= 4
|
||||
&& s[1..].chars().all(|c| c.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
/// 颜色解析:dark=true 时深色文字键反相为浅色
|
||||
pub fn resolve_color(key: &str, dark: bool, theme: &crate::model::Theme) -> String {
|
||||
if key.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if key.starts_with('#') {
|
||||
if !is_valid_hex(key) {
|
||||
return if dark {
|
||||
"#ffffff".into()
|
||||
} else {
|
||||
theme.text.clone()
|
||||
};
|
||||
}
|
||||
if dark && is_dark_hex(key) {
|
||||
return "#ffffff".into();
|
||||
}
|
||||
return key.into();
|
||||
}
|
||||
if dark {
|
||||
match key {
|
||||
"text" => "#ffffff".into(),
|
||||
"muted" => "rgba(255,255,255,0.72)".into(),
|
||||
"primary" => "#ffffff".into(),
|
||||
"accent" => theme.accent.clone(),
|
||||
_ => "#ffffff".into(),
|
||||
}
|
||||
} else {
|
||||
match key {
|
||||
"primary" => theme.primary.clone(),
|
||||
"accent" => theme.accent.clone(),
|
||||
"text" => theme.text.clone(),
|
||||
"muted" => theme.muted.clone(),
|
||||
_ => theme.text.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 背景解析:渐变键 → CSS gradient
|
||||
pub fn resolve_bg(key: &str, theme: &crate::model::Theme) -> String {
|
||||
if key.is_empty() {
|
||||
return "#ffffff".into();
|
||||
}
|
||||
if key.starts_with('#') {
|
||||
return if is_valid_hex(key) {
|
||||
key.into()
|
||||
} else {
|
||||
"#ffffff".into()
|
||||
};
|
||||
}
|
||||
match key {
|
||||
"g-primary" => format!(
|
||||
"linear-gradient(135deg, {} 0%, {} 100%)",
|
||||
theme.primary, theme.accent
|
||||
),
|
||||
"g-deep" => format!(
|
||||
"linear-gradient(160deg, {} 0%, {} 100%)",
|
||||
shade(&theme.primary, -28.0),
|
||||
theme.primary
|
||||
),
|
||||
"g-soft" => format!(
|
||||
"linear-gradient(135deg, {} 0%, {} 100%)",
|
||||
theme.panel.clone(),
|
||||
theme.bg.clone()
|
||||
),
|
||||
"primary" => theme.primary.clone(),
|
||||
"accent" => theme.accent.clone(),
|
||||
"panel" => theme.panel.clone(),
|
||||
"bg" => theme.bg.clone(),
|
||||
_ => "#ffffff".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hex_to_rgb() {
|
||||
assert_eq!(hex_to_rgb("#4f46e5"), Some((79, 70, 229)));
|
||||
assert_eq!(hex_to_rgb("#fff"), Some((255, 255, 255)));
|
||||
assert_eq!(hex_to_rgb("invalid"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_dark_hex() {
|
||||
assert!(is_dark_hex("#0f172a")); // 深色
|
||||
assert!(is_dark_hex("#4f46e5")); // 青蓝偏深
|
||||
assert!(!is_dark_hex("#ffffff")); // 白色
|
||||
assert!(!is_dark_hex("#f59e0b")); // 琥珀亮色
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_dark_bg() {
|
||||
assert!(is_dark_bg("primary"));
|
||||
assert!(is_dark_bg("g-deep"));
|
||||
assert!(!is_dark_bg("bg"));
|
||||
assert!(!is_dark_bg("panel"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shade() {
|
||||
let result = shade("#000000", 50.0);
|
||||
assert_eq!(result, "#808080"); // 黑色提亮 50% → 灰色
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_color_light() {
|
||||
use crate::model::Theme;
|
||||
let theme = Theme {
|
||||
name: "test".into(),
|
||||
primary: "#4f46e5".into(),
|
||||
accent: "#06b6d4".into(),
|
||||
bg: "#ffffff".into(),
|
||||
panel: "#f8fafc".into(),
|
||||
text: "#1e293b".into(),
|
||||
muted: "#64748b".into(),
|
||||
};
|
||||
assert_eq!(resolve_color("primary", false, &theme), "#4f46e5");
|
||||
assert_eq!(resolve_color("text", false, &theme), "#1e293b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_color_dark() {
|
||||
use crate::model::Theme;
|
||||
let theme = Theme {
|
||||
name: "test".into(),
|
||||
primary: "#4f46e5".into(),
|
||||
accent: "#06b6d4".into(),
|
||||
bg: "#ffffff".into(),
|
||||
panel: "#f8fafc".into(),
|
||||
text: "#1e293b".into(),
|
||||
muted: "#64748b".into(),
|
||||
};
|
||||
// 深色背景下 text 键 → 白色
|
||||
assert_eq!(resolve_color("text", true, &theme), "#ffffff");
|
||||
// primary 键在深色背景下也 → 白色
|
||||
assert_eq!(resolve_color("primary", true, &theme), "#ffffff");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user