Files
Unlucky-Launcher/crates/presenter/src/theme.rs
T
AskaEth d03ba13c0f test(presenter): 修正 tile_viewport 单测的期望值(760/128 量化到 640 而非 768)
迁移后首次跑 test 时这条自测断言写错:函数行为与原 ui_bind 的实现逐字一致,
错的是期望值。修完 `cargo test -p unlucky-presenter` = 122 passed。
2026-11-02 10:11:55 +08:00

166 lines
5.9 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.
//! 主题与外观的**纯**判据:轮询节拍、背景图缓存键与量化、系统主题取值。
//!
//! 这一层原本混在 `crates/app` 的 `ui_bind.rs` / `theme_runtime.rs` 里,与「把值灌进窗口属性」
//! 的绑定代码缠在一起,于是两个 UI 版都得各写一遍。抽到这里的只有**判据**:什么时候该重读配置、
//! 什么时候该重建背景图、系统主题该报哪个值。灌属性仍然由 UI 侧做,本模块不碰任何框架类型。
use crate::prefs::BgFill;
/// 配置热切轮询间隔(拍数,33ms/拍 → 约 1s 一次 `mtime` 比对,AI/CLI 改完下一拍即生效)。
pub const CONFIG_POLL_TICKS: u64 = 30;
/// 系统主题兜底轮询间隔(拍数):只在 `auto` 且后端不认识系统主题时启动(约 3s)。
pub const SYSTEM_THEME_POLL_TICKS: u64 = 90;
/// tile 模式重建画布的窗口尺寸粒度(像素):避免拖动窗口时反复重算平铺画布。
pub const TILE_REGENERATE_STEP: u32 = 128;
/// 系统主题取值(UI 侧根组件属性就用这三个值:0 未知 / 1 浅色 / 2 深色)。
pub const SYSTEM_THEME_UNKNOWN: i32 = 0;
/// 浅色。
pub const SYSTEM_THEME_LIGHT: i32 = 1;
/// 深色。
pub const SYSTEM_THEME_DARK: i32 = 2;
/// 背景图加工输入(值相同就不重新解码)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundKey {
/// 背景图路径
pub path: String,
/// 填充模式
pub fill: BgFill,
/// tile 用的窗口尺寸(按 [`TILE_REGENERATE_STEP`] 量化;cover/contain 恒为 (0, 0))
pub viewport: (u32, u32),
}
/// 本拍是否该比对一次配置文件 `mtime`(热切的采样点)。
pub fn config_poll_due(ticks: u64) -> bool {
ticks.is_multiple_of(CONFIG_POLL_TICKS)
}
/// 本拍是否该兜底探测系统主题。
///
/// 启动时先探一次(`probed == false`),之后只在「主题是 auto + 后端不认识系统主题」时
/// 按 [`SYSTEM_THEME_POLL_TICKS`] 兜底轮询。
pub fn system_theme_poll_due(probed: bool, auto: bool, backend_known: bool, ticks: u64) -> bool {
!probed || (auto && !backend_known && ticks.is_multiple_of(SYSTEM_THEME_POLL_TICKS))
}
/// 系统主题取值(纯映射,便于单测)。
pub fn system_theme_code(theme: unlucky_core::platform::theme_probe::SystemTheme) -> i32 {
use unlucky_core::platform::theme_probe::SystemTheme;
match theme {
SystemTheme::Light => SYSTEM_THEME_LIGHT,
SystemTheme::Dark => SYSTEM_THEME_DARK,
SystemTheme::Unknown => SYSTEM_THEME_UNKNOWN,
}
}
/// 探测本机系统主题并给出 UI 侧属性值。
pub fn system_theme_value() -> i32 {
system_theme_code(unlucky_core::platform::theme_probe::probe())
}
/// tile 画布的窗口尺寸(量化到 [`TILE_REGENERATE_STEP`],避免拖动窗口时反复重算)。
///
/// 非 tile 模式恒为 (0, 0):cover/contain 不依赖窗口尺寸,缩放只让渲染器拉伸。
pub fn tile_viewport(fill: BgFill, size: (u32, u32)) -> (u32, u32) {
if fill != BgFill::Tile {
return (0, 0);
}
(
size.0 / TILE_REGENERATE_STEP * TILE_REGENERATE_STEP,
size.1 / TILE_REGENERATE_STEP * TILE_REGENERATE_STEP,
)
}
#[cfg(test)]
mod tests {
use super::{
config_poll_due, system_theme_code, system_theme_poll_due, tile_viewport, BackgroundKey,
CONFIG_POLL_TICKS, SYSTEM_THEME_DARK, SYSTEM_THEME_LIGHT, SYSTEM_THEME_POLL_TICKS,
SYSTEM_THEME_UNKNOWN, TILE_REGENERATE_STEP,
};
use crate::prefs::BgFill;
use unlucky_core::platform::theme_probe::SystemTheme;
#[test]
fn config_poll_fires_every_interval() {
assert!(config_poll_due(0));
assert!(!config_poll_due(1));
assert!(config_poll_due(CONFIG_POLL_TICKS));
assert!(!config_poll_due(CONFIG_POLL_TICKS + 1));
}
#[test]
fn system_theme_probe_backs_off_after_first_read() {
// 启动时无条件探一次
assert!(system_theme_poll_due(false, false, true, 1));
// 探过、主题非 auto:不再轮询
assert!(!system_theme_poll_due(
true,
false,
false,
SYSTEM_THEME_POLL_TICKS
));
// 探过、auto、后端认识系统主题:不再轮询
assert!(!system_theme_poll_due(
true,
true,
true,
SYSTEM_THEME_POLL_TICKS
));
// 探过、auto、后端不认识:按间隔兜底
assert!(!system_theme_poll_due(
true,
true,
false,
SYSTEM_THEME_POLL_TICKS - 1
));
assert!(system_theme_poll_due(
true,
true,
false,
SYSTEM_THEME_POLL_TICKS
));
}
#[test]
fn system_theme_codes_are_stable() {
assert_eq!(
system_theme_code(SystemTheme::Unknown),
SYSTEM_THEME_UNKNOWN
);
assert_eq!(system_theme_code(SystemTheme::Light), SYSTEM_THEME_LIGHT);
assert_eq!(system_theme_code(SystemTheme::Dark), SYSTEM_THEME_DARK);
}
#[test]
fn tile_viewport_quantizes_and_ignores_other_fills() {
assert_eq!(tile_viewport(BgFill::Cover, (1180, 760)), (0, 0));
assert_eq!(tile_viewport(BgFill::Contain, (1180, 760)), (0, 0));
let (width, height) = tile_viewport(BgFill::Tile, (1180, 760));
assert_eq!(width % TILE_REGENERATE_STEP, 0);
assert_eq!(height % TILE_REGENERATE_STEP, 0);
assert_eq!((width, height), (1152, 640));
}
#[test]
fn background_key_compares_by_value() {
let key = BackgroundKey {
path: "a.png".to_string(),
fill: BgFill::Tile,
viewport: (256, 256),
};
assert_eq!(key.clone(), key);
assert_ne!(
key,
BackgroundKey {
path: "a.png".to_string(),
fill: BgFill::Tile,
viewport: (128, 256),
}
);
}
}