迁移后首次跑 test 时这条自测断言写错:函数行为与原 ui_bind 的实现逐字一致, 错的是期望值。修完 `cargo test -p unlucky-presenter` = 122 passed。
166 lines
5.9 KiB
Rust
166 lines
5.9 KiB
Rust
//! 主题与外观的**纯**判据:轮询节拍、背景图缓存键与量化、系统主题取值。
|
||
//!
|
||
//! 这一层原本混在 `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),
|
||
}
|
||
);
|
||
}
|
||
}
|