feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate, Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。 - 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`), workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、 iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。 - `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、 prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、 mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等), 内部实现零改动。 - 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS / SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport / system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。 - crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge| toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*` (64 处),backend.rs 的 config_path/build_backend 提为 pub。 - crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。 - prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
This commit is contained in:
1 parent
00f132d8ce
commit
f159529936
69 files changed
+441
-129
No files matched your search
@@ -0,0 +1,165 @@
|
||||
//! 主题与外观的**纯**判据:轮询节拍、背景图缓存键与量化、系统主题取值。
|
||||
//!
|
||||
//! 这一层原本混在 `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, 768));
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user