feat(app-iced): --force-state 穿过设置页与外观区(M285 FORCEDWIRE)

pages::Model.forced 此前只到得了外壳 + 实例页(唯一消费者 pages/instances.rs:159),
设置页「外观」区的滑块/文本框/裸 iced::button 全都不消费它 ⇒ M284 那四处状态层
(语言档 / 主题档 / 日志 action_button / 外观 choice)只有单测证据,做不出像素判据。

改动:
- settings/mod.rs: resolve_state(forced, status) 与 forced_button(button, forced)
  成为全仓唯一两个「forced 怎么解释」的点;view → form → section_columns 一路
  新增 forced 形参(默认 None ⇒ 今天的行为逐字节不变)。
- Group.body(registry.rs)加第 4 个形参 Option<Interaction>,六组统一签名。
- 四类裸 iced::button 控件把 forced 递进样式闭包并经 resolve_state 落地:
  语言档 chip(general/language.rs)、主题档 chip(general/theme.rs)、
  日志 action_button(logs_row.rs)、外观 choice(appearance/section/mod.rs,
  含 accent/window/bg_image/colour_source 共 10 处调用)。
- components::buttons::Button 的三组(update / ai / java)走 forced_button 的 .state()。
- harness: request.rs 解析 --force-state=rest|hover|pressed(未知值报错,词与产品
  cli.rs 同一套),slug 加 -hover/-pressed;app.rs 把它写进 model.forced,
  截图通道默认 Rest 的纪律保持(显式值优先)。
- pages/mod.rs: 唯一调用点把 model.forced 传给 settings::view(397 → 399 行)。

单测:resolve_state 的 Some 覆盖 / None 退回;forced_button 的 Some 钉态 / None 原样。
This commit is contained in:
AskaEth committed 2026-11-12 23:12:20 +08:00
1 parent 767727efb7
commit 7b80b0e314
19 files changed
+397 -79

No files matched your search

+36 -1
View File
@@ -12,6 +12,7 @@ use std::path::PathBuf;
use unlucky_design::color::SchemeFlavor;
use crate::app::components::interaction::Interaction;
use crate::cli::Backend;
use crate::pages::{layout::WidthClass, Screen};
@@ -47,6 +48,18 @@ pub struct Request {
///
/// 只对 `--screen=settings` 有意义:别的页没有外观分组,静默接受会造出「以为接了线」的假图(U1)。
pub real_appearance: bool,
/// 强制交互态(`--force-state=rest|hover|pressed`):`Some` 时**控件按那一态画**(M285)。
///
/// 为什么这个开关在本文件而不在产品 bin:产品 bin 的 `--screen` 只认 `shell|gallery`,设置页 /
/// 外观区只能走本 harness 出图(`examples/pages_demo.rs`)。M282 / M284 各自撞过一次——
/// `pages::Model.forced` 到不了设置页,于是「hover 与 pressed 必须不同」这条判据**做不出像素证据**,
/// 只能降级成单测。接上之后两条态各自一张图。
///
/// 词与语义与产品的 `cli::ForcedState` 一致(`rest|hover|pressed`),但**刻意不共用那个 enum**:
/// 产品那条通道的默认值与守卫(`--screenshot` 恒钉 `Rest`)是另一套,把两者拧在一起会让
/// 「取证通道的默认值」和「产品窗口的默认值」互相污染。这里存的是下游形状 [`Interaction`],
/// 落点是 `pages::Model.forced`(`harness/app.rs`)。
pub force_state: Option<Interaction>,
/// 截图窗口开**指针穿透**(`window::enable_mouse_passthrough` ⇒ winit `set_cursor_hittest(false)`)。
///
/// 默认:给了 `--screenshot` 就开;**交互模式(没有截图)一律关**——交互窗口收不到鼠标是产品事故,
@@ -68,6 +81,7 @@ impl Default for Request {
backend: Backend::Auto,
scale_factor: None,
real_appearance: false,
force_state: None,
passthrough: false,
}
}
@@ -138,6 +152,20 @@ impl Request {
}
};
}
"--force-state" => {
// 三档词与产品 bin 同一套(`cli.rs` 的 `--force-state`),报错文案也照抄。
request.force_state =
Some(match take(&mut args, inline, "--force-state")?.as_str() {
"rest" => Interaction::Enabled,
"hover" => Interaction::Hovered,
"pressed" => Interaction::Pressed,
other => {
return Err(format!(
"--force-state 只认 rest|hover|pressed,收到 {other:?}"
))
}
});
}
"--passthrough" => {
passthrough = Some(match take(&mut args, inline, "--passthrough")?.as_str() {
"on" => true,
@@ -183,7 +211,7 @@ impl Request {
Ok(request)
}
/// 出图文件名片段(`<screen>[-real]-<class>-<flavor>-w<宽>`,归档用)。
/// 出图文件名片段(`<screen>[-real][-hover|-pressed]-<class>-<flavor>-w<宽>`,归档用)。
pub fn slug(&self) -> String {
let class = WidthClass::of(self.width);
let flavor = if self.flavor.is_dark() {
@@ -198,6 +226,13 @@ impl Request {
} else {
self.screen.slug().to_owned()
};
// 强制态也进文件名(`-hover` / `-pressed`):同一屏在两个 forced 下是**两张不同的证据图**,
// 归档里必须能分开(与 `-real` 同一条理由)。`rest` / 不给 = 不加后缀 ⇒ 既有文件名逐字不变。
let screen = match self.force_state {
Some(Interaction::Hovered) => format!("{screen}-hover"),
Some(Interaction::Pressed) => format!("{screen}-pressed"),
_ => screen,
};
format!("{screen}-{}-{flavor}-w{width}", class.name())
}
}