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:
1 parent
767727efb7
commit
7b80b0e314
19 files changed
+397
-79
No files matched your search
@@ -148,7 +148,7 @@ mod tests;
|
||||
pub use message::GroupMessage;
|
||||
pub use state::{demo_groups, Groups, States};
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::widget::{button, column};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
@@ -156,6 +156,8 @@ use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Lang, LangChoice};
|
||||
|
||||
use crate::app::components::buttons::Button;
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::layout::{FrameMetrics, PageLayout};
|
||||
@@ -165,6 +167,36 @@ use super::Message;
|
||||
pub(super) use registry::Group;
|
||||
use registry::{AI_GROUP, GENERAL_GROUP, GROUPS, GROUP_LABELS, JAVA_GROUP, SELECTED, UPDATE_GROUP};
|
||||
|
||||
/// 这一帧某个控件**实际要画**的交互态:`forced`(`--force-state`)优先,否则退回观测到的 `status`。
|
||||
///
|
||||
/// **全仓唯一的 resolve 点(M285)**:语言档 / 主题档 / 日志 `action_button` / 外观 `choice` 四个站点
|
||||
/// 一律调它。谁在自己那个样式闭包里再写一遍 `unwrap_or_else`,就是把这行语义复制成四份——下一次
|
||||
/// 只改三处就等着漂。
|
||||
///
|
||||
/// 语义与 `pages/instances.rs` 的 `forced_state`(`Button::state(…)`,`Some` 覆盖、`None` 不动)相同;
|
||||
/// 差别只在 iced 的裸 `button` 没有 `.state()`:样式闭包的签名固定收 `button::Status`,所以覆盖只能
|
||||
/// 发生在闭包**里面**(换掉它拿到的那一态),而不是控件上。
|
||||
pub(super) fn resolve_state(forced: Option<Interaction>, status: button::Status) -> Interaction {
|
||||
forced.unwrap_or_else(|| Interaction::from_status(status))
|
||||
}
|
||||
|
||||
/// 把一个 `components::buttons::Button` 钉到 `forced` 那一态(`pages/instances.rs` 的 `forced_state`
|
||||
/// 在设置页的这一份)。
|
||||
///
|
||||
/// **这是第二份,不是抽象**(M285):实例页那条线的控件本身就是 `components::buttons::Button`
|
||||
///(有 `state(Interaction)`),而设置页的 `choice` / `chip` / `action_button` 是裸 `iced::button`——
|
||||
/// 覆盖只能落在样式闭包里(见 [`resolve_state`])。三次法则现在到 2;第三次真出现时落点是
|
||||
/// `components/interaction/`,不是这里。
|
||||
pub(super) fn forced_button<'a, M>(
|
||||
button: Button<'a, M>,
|
||||
forced: Option<Interaction>,
|
||||
) -> Button<'a, M> {
|
||||
match forced {
|
||||
Some(state) => button.state(state),
|
||||
None => button,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页 General 分组要的两件(M106):用户选的档位 + 这一次生效的语言。
|
||||
///
|
||||
/// 为什么不塞进 `pages::Model`:那是页面与 presenter 之间的**展示契约**(由快照派生),而档位是
|
||||
@@ -194,12 +226,17 @@ impl Default for Language {
|
||||
///
|
||||
/// `states` 是「画一帧时那份只读状态」:[`States::appearance`] 是 M93 接进来的外观运行期状态
|
||||
/// (`None` = harness / 演示路径,设置页仍画占位行),[`States::groups`] 是各组自己的状态。
|
||||
///
|
||||
/// `forced` 是 `pages::Model::forced`(`--force-state` 的落点,M285):一路递到各组的控件,由
|
||||
/// [`resolve_state`] 决定「这一帧画哪一态」。`None` = 照旧按控件自己的观测态画——**产品路径恒为
|
||||
/// `None`**(除了取证启动),所以默认行为逐字节不变。
|
||||
pub fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
metrics: FrameMetrics,
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
// 选中分组:有真实外观状态时看状态(点左列可切),否则退回演示值 0。
|
||||
let selected = states
|
||||
@@ -212,10 +249,10 @@ pub fn view<'a>(
|
||||
frame::group_nav(ui, selected, language.effective),
|
||||
frame::left_aligned(
|
||||
metrics.column,
|
||||
form(ui, Some(selected), states, language, update),
|
||||
form(ui, Some(selected), states, language, update, forced),
|
||||
),
|
||||
),
|
||||
_ => frame::single_column(metrics, form(ui, None, states, language, update)),
|
||||
_ => frame::single_column(metrics, form(ui, None, states, language, update, forced)),
|
||||
};
|
||||
|
||||
frame::page(ui, metrics, frame::header(ui, language.effective), body)
|
||||
@@ -240,10 +277,11 @@ fn form<'a>(
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut stack = column![].spacing(ui.space(Space::X6)).width(Length::Fill);
|
||||
|
||||
for section in section_columns(ui, only, states, language, update) {
|
||||
for section in section_columns(ui, only, states, language, update, forced) {
|
||||
stack = stack.push(section);
|
||||
}
|
||||
|
||||
@@ -258,6 +296,7 @@ fn section_columns<'a>(
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
forced: Option<Interaction>,
|
||||
) -> Vec<Element<'a, Message>> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut columns = Vec::new();
|
||||
@@ -274,20 +313,20 @@ fn section_columns<'a>(
|
||||
// 档位与生效语言不在 [`States`] 里,只能从 `pages::Language` 进来;它与 `appearance` 在不在场
|
||||
// 无关(harness 那条路径也画得出真控件)。
|
||||
let card = if index == GENERAL_GROUP {
|
||||
general::body(ui, states, language)
|
||||
general::body(ui, states, language, forced)
|
||||
} else if index == UPDATE_GROUP {
|
||||
// M159:更新组读 presenter 快照(`view` 的 `update` 形参),与 General 同一条按下标分发。
|
||||
update::body(ui, update, language.effective)
|
||||
update::body(ui, update, language.effective, forced)
|
||||
} else if index == AI_GROUP {
|
||||
// M174:助手组要 i18n 文案与「测试连接」结论,与 General / Update 同一条按下标分发。
|
||||
ai::view::body(ui, states, language.effective)
|
||||
ai::view::body(ui, states, language.effective, forced)
|
||||
} else if index == JAVA_GROUP {
|
||||
// M175:Java 组要 i18n 文案 + 扫描快照,同样按下标分发(`GROUP.body` 保持 `None`)。
|
||||
java::view::body(ui, states, language.effective)
|
||||
java::view::body(ui, states, language.effective, forced)
|
||||
} else {
|
||||
group
|
||||
.body
|
||||
.and_then(|body| body(ui, states, language.effective))
|
||||
.and_then(|body| body(ui, states, language.effective, forced))
|
||||
.unwrap_or_else(|| frame::demo_card(ui, group.rows))
|
||||
};
|
||||
|
||||
@@ -313,3 +352,50 @@ fn section_columns<'a>(
|
||||
|
||||
columns
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod resolve_tests {
|
||||
use super::*;
|
||||
|
||||
/// `forced` 必须**覆盖**观测到的 status;不给 `forced` 时必须**退回**观测值(M285)。
|
||||
///
|
||||
/// 抓得住:把 `Some` 分支改成忽略 forced(直接 `Interaction::from_status(status)`)——第一条立刻红,
|
||||
/// 而那正好等于「`--force-state` 在四个控件上没接线」。
|
||||
/// 抓不住:forced 真的画进了 PNG(那是 `pages_demo --force-state=hover|pressed` 的像素判据)。
|
||||
/// `forced_button` 只做一件事:`Some` 钉态、`None` 原样(M285)。
|
||||
///
|
||||
/// `Button::state` 是私有字段、没有 getter,所以断 **derived Debug** 里的那一格。
|
||||
/// 抓得住:`Some` 分支被改成恒 `None`(第一条红);`None` 分支也顺手 `.state(..)`(第二条红)。
|
||||
/// 抓不住:按钮真画成那色(那是出图的事)。
|
||||
#[test]
|
||||
fn forced_button_forces_only_when_asked() {
|
||||
let plain = Button::new("x", ());
|
||||
let forced = format!(
|
||||
"{:?}",
|
||||
forced_button(plain.clone(), Some(Interaction::Hovered))
|
||||
);
|
||||
assert!(
|
||||
forced.contains("state: Some(Hovered)"),
|
||||
"forced 必须钉到那一态:{forced}"
|
||||
);
|
||||
let untouched = format!("{:?}", forced_button(plain, None));
|
||||
assert!(
|
||||
untouched.contains("state: None"),
|
||||
"没给 forced 时必须原样返回:{untouched}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_state_wins_over_the_observed_status() {
|
||||
assert_eq!(
|
||||
resolve_state(Some(Interaction::Hovered), button::Status::Pressed),
|
||||
Interaction::Hovered,
|
||||
"forced 必须覆盖观测到的 status,否则 --force-state 在这四个控件上等于没接线"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_state(None, button::Status::Pressed),
|
||||
Interaction::Pressed,
|
||||
"没给 forced 时必须退回观测值(既有行为不变)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user