feat(app-iced): M93 —— 外观接线:整窗 α / 面板 α / 背景图 / 配色来源
把 appearance.* 那批键接到窗口与像素上(M91 只落了键与判据,零调用方)。
整窗 α(D34/D36)
- `ensure_layered(hwnd)` 落在唯一 unsafe 文件 `platform/win32/transparency.rs`
(`WS_EX_LAYERED` 只有 Get/SetWindowLongPtrW 能加,且 CI 的 unsafe-scope 是集合相等);
`app/boot.rs` 只负责开窗,句柄到手后由 `Message::WindowHandle` → `window_alpha::dispatch`
先补样式再下发。本机实测 `layered=ok outcome=Applied { byte: 205 }`(α=0.8)。
- 三条入口都会重算并下发:启动(句柄到达)、设置页改动、配置文件热切。
- `layered` 日志有三态:`n/a`(α≥1,压根不需要)/ `ok` / `FAILED`,避免把「没用到」写成假红。
面板 α 与 CPU 档(D36)
- 面板 α 独立于整窗 α:只作用到外壳层(根容器 / 标题栏 / 导航轨)与外观卡片;
CPU 档强制 1.0、玻璃强制关(`pages/appearance/alpha.rs` 的真值表 + 单测)。
- 色卡档继续走令牌 baseline,只有取色档(①②级)才换 `Ui` 的 scheme(`Ui::from_scheme`)。
背景图(D38)
- 读盘解码(png)→ cover/contain/tile 三种填充几何 → `bg_dim` 向底色退 → 36dp 网格马赛克;
iced 的 `image` widget 需要 `iced/image` feature(Cargo.toml 不在 scope),故为已登记降级。
- 读盘失败静默回落 + 一条可读诊断(设置页错误行 + 日志),配色经 `Provenance` 回落色卡。
设置页「外观」分组
- 整窗透明度滑块(下限 0.08)/ 面板透明度(CPU 档禁用并写明 55%→100% 的降级)/
背景图开关与路径 / 填充三档 / 暗化 / 配色来源三档;改完即时生效并写回:
bridge 覆盖的 5 个键走 `SettingsBridge::persist`,`window_opacity` / `palette_source`
走 `AppCommand::SetSetting`(每个键只有一个写者)。
- `SettingsBridge::load_base()`(presenter)让 app-iced 不需要直接依赖 core 就能读回
`window_opacity` / `palette_source`。
取证
- `ULL_APPEARANCE_*` / `ULL_START_ROUTE` 只影响本次进程(语义同 ULL_TICK_TRACE),
截图通道不读用户配置(D30)。
This commit is contained in:
1 parent
eae7ae0ff4
commit
b8ab46662c
24 files changed
+3119
-48
No files matched your search
@@ -48,6 +48,24 @@ impl App {
|
||||
Message::Boot => self.on_boot(),
|
||||
Message::WindowReady(id) => {
|
||||
self.window = id;
|
||||
// M93:整窗 α 要 Win32 句柄,而 iced 只给 `window::Id`。`window::run` 是唯一能把
|
||||
// `&dyn Window`(实现了 `HasWindowHandle`)拿出来的入口。
|
||||
match id {
|
||||
Some(id) => {
|
||||
window::run(id, super::window_alpha::probe_hwnd).map(Message::WindowHandle)
|
||||
}
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
Message::WindowHandle(hwnd) => {
|
||||
self.hwnd = hwnd;
|
||||
match hwnd {
|
||||
Some(_) => self.apply_window_alpha("boot"),
|
||||
None => {
|
||||
eprintln!("[appearance] hwnd=none(拿不到窗口句柄:整窗 α 无法下发)");
|
||||
self.log_appearance("boot", None);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::SystemTheme(mode) => {
|
||||
@@ -69,6 +87,8 @@ impl App {
|
||||
Message::Window(event) => {
|
||||
if let window::Event::Resized(size) = event {
|
||||
self.width = size.width;
|
||||
// 高度是背景马赛克的视口判据之一(宽度早就有),以前只记宽是因为没有背景层。
|
||||
self.height = size.height;
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
}
|
||||
@@ -122,6 +142,12 @@ impl App {
|
||||
// 截图通道**不读设置文件**(D30 可复现:同一份代码在两台机器上要出同一张图)。
|
||||
self.apply_config_theme();
|
||||
|
||||
// M93 外观取证开关(`ULL_APPEARANCE_*`)+ 启动那一行数值日志。
|
||||
// 必须在 `refresh_view` 之前:设置页要画的就是这份状态。
|
||||
self.appearance.apply_env();
|
||||
self.rebuild_ui();
|
||||
self.log_appearance("boot", None);
|
||||
|
||||
// 交互模式且没显式指定主题 → 探系统;截图模式永不等(见 cli 模块文档)
|
||||
if self.options.wants_system_theme() {
|
||||
tasks.push(iced::system::theme().map(Message::SystemTheme));
|
||||
@@ -129,6 +155,16 @@ impl App {
|
||||
|
||||
// 「重新扫描实例目录 + 全部页面快照」:真 host 在这里发首批事件,mock 在构造时已预置
|
||||
self.dispatch(AppCommand::RefreshAll);
|
||||
|
||||
// 取证开关 `ULL_START_ROUTE`:截图/冒烟直接落到某一页(`cli.rs` 不在本 mission 的 scope,
|
||||
// 而「外观」分组只在设置页上,出矩阵必须能直达)。未知值忽略。
|
||||
if let Some(route) = env_start_route() {
|
||||
eprintln!("[boot] ULL_START_ROUTE → {route:?}");
|
||||
self.state.navigate(route);
|
||||
// 直达某一页时不要再被「自动选中第一条实例」拉回详情页。
|
||||
self.auto_selected = true;
|
||||
}
|
||||
|
||||
self.auto_select();
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
@@ -209,8 +245,10 @@ impl App {
|
||||
let reloaded =
|
||||
!self.has_screenshot() && poll_due && self.bridge.reload_if_changed().is_some();
|
||||
if reloaded {
|
||||
eprintln!("[settings] 配置文件变了,重读主题");
|
||||
eprintln!("[settings] 配置文件变了,重读主题 + 外观");
|
||||
self.apply_config_theme();
|
||||
// M93:外观也热切(面板 α / 背景图 / 配色来源 / 整窗 α 都在里面)。
|
||||
self.reload_appearance();
|
||||
}
|
||||
|
||||
if self.view_dirty {
|
||||
@@ -245,6 +283,7 @@ impl App {
|
||||
}
|
||||
pages::Message::Launch(id) => self.dispatch(AppCommand::LaunchInstance(id)),
|
||||
pages::Message::Stop(id) => self.dispatch(AppCommand::StopInstance(id)),
|
||||
pages::Message::Appearance(change) => self.on_appearance(change),
|
||||
// 还没接后端的占位动作:只打日志(哪几个、为什么,见 pages 模块文档的偏离登记)
|
||||
pages::Message::Action(action) => {
|
||||
eprintln!("[page] 占位动作 {action:?} pressed(没有对应 AppCommand)");
|
||||
@@ -253,7 +292,7 @@ impl App {
|
||||
}
|
||||
|
||||
/// 派发一条指令并**立刻**抽干它带出来的事件(不本地回显,等价于 Slint 版的 `dispatch`)。
|
||||
fn dispatch(&mut self, command: AppCommand) {
|
||||
pub(super) fn dispatch(&mut self, command: AppCommand) {
|
||||
self.backend.dispatch(command);
|
||||
// 观察窗:命令发出后 3 秒内保持帧订阅,等后端把第一条事件交回来
|
||||
// (真 host 是异步的,派发瞬间状态还没变;不设这个窗口,空闲态的帧订阅永远不会打开)。
|
||||
@@ -330,3 +369,17 @@ fn heartbeat() -> Task<Message> {
|
||||
Message::Heartbeat
|
||||
})
|
||||
}
|
||||
|
||||
/// 取证开关 `ULL_START_ROUTE` 的取值解析(`settings` / `instances` / `download` / `logs`)。
|
||||
///
|
||||
/// 为什么是环境变量:`crates/app-iced/src/cli.rs` 不在 M93 的 scope,而外观矩阵必须在**设置页**上
|
||||
/// 出图。语义与 `ULL_APPEARANCE_*` / `ULL_TICK_TRACE` 一致:只影响这一次进程。
|
||||
fn env_start_route() -> Option<Route> {
|
||||
match std::env::var("ULL_START_ROUTE").ok()?.trim() {
|
||||
"instances" => Some(Route::Instances),
|
||||
"download" => Some(Route::Download),
|
||||
"logs" => Some(Route::Logs),
|
||||
"settings" => Some(Route::Settings),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user