feat(app-iced): 页面层把卡片底接进禁用/交互控件(M237)
M234 把 `interaction::Base` 与组件侧的 `.base(..)` 接好了,但页面层仍是默认的画布底: 卡片(`Role::SurfaceContainerLow`)里的禁用按钮 / chip / 开关把 10% / 12% / 38% 叠在画布色上。 M223 记的「卡片里禁用控件 CR 1.05、形丢失」因此只在 gallery 里修好,产品页没修。 - 导出容器底:`pages::frame::BASE = Base::of(MaterialLayer::Card.surface_role())` —— 取的是 `card()` **真正画出来的那一层**(不是「反正是 surface」);`logs::panel::BASE` 是同一个常量的别名(两张卡画的是同一个材质层)。 - 接线(改前一律 `Base::CANVAS`,改后 `frame::BASE`):设置·Java(扫描 / 取消 / 设为默认 chip / 三个输入框 / 添加 / 保存 / 预置项 chip / 下载徽标);设置·通用(语言三档、主题三档、日志行 两个手写按钮 → 状态层叠 `BASE.role()`);实例详情(MODS 开关、SAVES 的重试与打开文件夹、 启动参数区的两个输入框 + 后端 chip + 保存 / 恢复);日志页(诊断卡页脚 `Copy summary`)。 详情头的启动 / 停止 / 动作行**不在卡里**,保持画布底并加注释。 - 判据:`pages/frame/tests.rs` 逐值断言「`BASE` == 卡片画出来的颜色」+ 源码结构盯 `card()` 不许改画别的材质层;`pages/settings/tests/java_shot.rs` 是**页面级像素判据**(读 1180×760 真截图、逐位匹配:卡区里「叠卡底」3136 px vs「叠画布」21 px,深色 3128 / 1); `pages/tests/base_wiring.rs` 数接线点,守像素够不到的页面(实例详情 MODS / SAVES)。 - 变异自证:撤掉「立即扫描」的 `.base(BASE)` ⇒ 像素判据红(73 px vs 3083 px,退出码 101); 撤掉诊断卡页脚的 `.base(..)` ⇒ 接线判据红(0 vs 1,退出码 101)。均已还原。 - 门禁:fmt / check -p unlucky-app-iced --all-targets / test --workspace(3344 通过,0 失败)/ clippy --workspace --all-targets -D warnings / 文件 ≤400(最大 389),全部本地过。 - 已知未尽:面板 α<1 的半透明卡(`Base` 只装一个 `Role`)、GPU 档、实例详情与日志页在 1180×760 出图里没有可量测的禁用控件(夹具缺 running,已报 finding);app_bar / adopt pick / 设置页分组列仍未导出 `BASE`;设置·下载 / 更新 / 助手、账户页、内容库、下载页仍未接线。
This commit is contained in:
1 parent
cf8a951e29
commit
3dcd5fd117
20 files changed
+518
-42
No files matched your search
@@ -335,10 +335,11 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> El
|
||||
let background = if selected {
|
||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||
// M237:这一组整个住在 `frame::card` 里,状态层叠**卡底**(不是画布)。
|
||||
Some(Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
frame::BASE.role(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -120,10 +120,11 @@ fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, mode: ThemeMode) -> Eleme
|
||||
let background = if selected {
|
||||
Some(Background::Color(ui.color(Role::SecondaryContainer)))
|
||||
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||
// M237:主题三档同样住在设置卡里,状态层叠卡底(`frame::BASE`)。
|
||||
Some(Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
frame::BASE.role(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -26,7 +26,7 @@ use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
|
||||
use crate::app::components::selection::{chip, Chip};
|
||||
use crate::pages::frame::{divider, text_role_owned};
|
||||
use crate::pages::frame::{divider, text_role_owned, BASE};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -122,22 +122,24 @@ fn choice_row<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
let title = format!("Java {}", choice.major);
|
||||
let mut badges = row![].spacing(ui.space(Space::X1));
|
||||
// M237:徽标是**禁用配色**(`Chip::badge` = 12% 容器 / 38% 内容),而这个板块整个住在
|
||||
// `frame::card` 里 ⇒ 底必须是卡底,否则徽标会把禁用色叠在画布色上。
|
||||
if choice.lts {
|
||||
badges = badges.push(chip(
|
||||
ui,
|
||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadLts)),
|
||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadLts)).base(BASE),
|
||||
));
|
||||
}
|
||||
if choice.baseline {
|
||||
badges = badges.push(chip(
|
||||
ui,
|
||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadBaseline)),
|
||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadBaseline)).base(BASE),
|
||||
));
|
||||
}
|
||||
if recommended {
|
||||
badges = badges.push(chip(
|
||||
ui,
|
||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadRecommended)),
|
||||
Chip::badge(catalog.text(Key::SettingsJavaDownloadRecommended)).base(BASE),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -256,11 +258,12 @@ fn install_button<'a>(
|
||||
Key::SettingsJavaDownloadAction
|
||||
});
|
||||
if busy {
|
||||
return chip(ui, Chip::badge(label));
|
||||
// 忙时那枚「下载」是**禁用配色**的徽标(不是可点 chip),同样叠在卡底上(M237)。
|
||||
return chip(ui, Chip::badge(label).base(BASE));
|
||||
}
|
||||
chip(
|
||||
ui,
|
||||
Chip::filter(label, false, msg(JavaMessage::Download(major))),
|
||||
Chip::filter(label, false, msg(JavaMessage::Download(major))).base(BASE),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use unlucky_i18n::{Catalog, Key};
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||
use crate::pages::frame::text_role_owned;
|
||||
use crate::pages::frame::{text_role_owned, BASE};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -23,16 +23,19 @@ use super::view::msg;
|
||||
use super::Message as JavaMessage;
|
||||
|
||||
/// 手动添加一行:输入框 + 「添加」按钮 + 原因。
|
||||
///
|
||||
/// M237:这一行住在 `frame::card` 里,输入框与按钮都把底传成 `frame::BASE`(禁用/状态层叠卡底)。
|
||||
pub(super) fn add_row<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
||||
let field = Field::new(&state.add_draft)
|
||||
.variant(FieldVariant::Outlined)
|
||||
.label(catalog.text(Key::SettingsJavaAdd))
|
||||
.placeholder(catalog.text(Key::SettingsJavaAddPlaceholder))
|
||||
.supporting(catalog.text(Key::SettingsJavaAddHint));
|
||||
.supporting(catalog.text(Key::SettingsJavaAddHint))
|
||||
.base(BASE);
|
||||
let add = if state.add_draft.trim().is_empty() || !state.storage {
|
||||
buttons::outlined(
|
||||
ui,
|
||||
Button::disabled(catalog.text(Key::SettingsJavaAddAction)),
|
||||
Button::disabled(catalog.text(Key::SettingsJavaAddAction)).base(BASE),
|
||||
)
|
||||
} else {
|
||||
buttons::outlined(
|
||||
@@ -40,7 +43,8 @@ pub(super) fn add_row<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Ele
|
||||
Button::new(
|
||||
catalog.text(Key::SettingsJavaAddAction),
|
||||
msg(JavaMessage::Add),
|
||||
),
|
||||
)
|
||||
.base(BASE),
|
||||
)
|
||||
};
|
||||
row![
|
||||
@@ -71,7 +75,8 @@ pub(super) fn memory_field<'a>(
|
||||
);
|
||||
let field = Field::new(&state.memory)
|
||||
.variant(FieldVariant::Outlined)
|
||||
.label(catalog.text(Key::SettingsJavaMemory));
|
||||
.label(catalog.text(Key::SettingsJavaMemory))
|
||||
.base(BASE);
|
||||
// 校验不过就把输入框本身标红(不只是下面那行字):用户一眼看到「是这个框有问题」。
|
||||
let field = match state.validate() {
|
||||
Err(Refusal::MemoryEmpty | Refusal::MemoryNotNumber | Refusal::MemoryRange) => {
|
||||
@@ -124,7 +129,8 @@ pub(super) fn jvm_field<'a>(
|
||||
.variant(FieldVariant::Outlined)
|
||||
.label(catalog.text(Key::SettingsJavaJvm))
|
||||
.placeholder(catalog.text(Key::SettingsJavaJvmPlaceholder))
|
||||
.supporting(catalog.text(Key::SettingsJavaJvmHint));
|
||||
.supporting(catalog.text(Key::SettingsJavaJvmHint))
|
||||
.base(BASE);
|
||||
// 两种拒绝都长在这个框上:引号没配对、或者粘了超过 1024 字符的一整段(M199)。
|
||||
let field = if matches!(
|
||||
state.validate(),
|
||||
@@ -165,9 +171,9 @@ pub(super) fn actions_row<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
let label = catalog.text(Key::SettingsJavaSave);
|
||||
if state.can_save() {
|
||||
buttons::filled(ui, Button::new(label, msg(JavaMessage::Save)))
|
||||
buttons::filled(ui, Button::new(label, msg(JavaMessage::Save)).base(BASE))
|
||||
} else {
|
||||
buttons::filled(ui, Button::disabled(label))
|
||||
buttons::filled(ui, Button::disabled(label).base(BASE))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ use unlucky_i18n::{Catalog, Key};
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||
use crate::app::components::selection::{chip, Chip};
|
||||
use crate::pages::frame::{divider, text_role_owned};
|
||||
use crate::pages::frame::{divider, text_role_owned, BASE};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -52,7 +52,8 @@ fn install_root<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
||||
.variant(FieldVariant::Outlined)
|
||||
.label(catalog.text(Key::SettingsJavaInstallRoot))
|
||||
.placeholder(catalog.text(Key::SettingsJavaInstallRootPlaceholder))
|
||||
.supporting(catalog.text(Key::SettingsJavaInstallRootHint));
|
||||
.supporting(catalog.text(Key::SettingsJavaInstallRootHint))
|
||||
.base(BASE);
|
||||
let field = if prefs.validate().is_err() {
|
||||
field.error()
|
||||
} else {
|
||||
@@ -184,7 +185,8 @@ fn vendor_order<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
||||
Button::new(
|
||||
catalog.text(Key::SettingsJavaVendorReset),
|
||||
msg(JavaMessage::ResetPriority),
|
||||
),
|
||||
)
|
||||
.base(BASE),
|
||||
));
|
||||
}
|
||||
list.into()
|
||||
@@ -215,7 +217,8 @@ fn vendor_row<'a>(
|
||||
catalog.text(Key::SettingsJavaVendorUp),
|
||||
false,
|
||||
msg(JavaMessage::MoveVendor { index, up: true }),
|
||||
),
|
||||
)
|
||||
.base(BASE),
|
||||
);
|
||||
let down = chip(
|
||||
ui,
|
||||
@@ -223,7 +226,8 @@ fn vendor_row<'a>(
|
||||
catalog.text(Key::SettingsJavaVendorDown),
|
||||
false,
|
||||
msg(JavaMessage::MoveVendor { index, up: false }),
|
||||
),
|
||||
)
|
||||
.base(BASE),
|
||||
);
|
||||
row![
|
||||
container(
|
||||
@@ -266,7 +270,8 @@ fn auto_install<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
||||
catalog.text(Key::SettingsJavaAutoInstallOn),
|
||||
on,
|
||||
msg(JavaMessage::ToggleAutoInstall(true)),
|
||||
),
|
||||
)
|
||||
.base(BASE),
|
||||
),
|
||||
chip(
|
||||
ui,
|
||||
@@ -274,7 +279,8 @@ fn auto_install<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
||||
catalog.text(Key::SettingsJavaAutoInstallOff),
|
||||
!on,
|
||||
msg(JavaMessage::ToggleAutoInstall(false)),
|
||||
),
|
||||
)
|
||||
.base(BASE),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1));
|
||||
@@ -304,9 +310,12 @@ fn auto_install<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'
|
||||
fn actions<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
||||
let label = catalog.text(Key::SettingsJavaPrefsSave);
|
||||
let save = if state.prefs.dirty() && state.storage {
|
||||
buttons::filled(ui, Button::new(label, msg(JavaMessage::SavePrefs)))
|
||||
buttons::filled(
|
||||
ui,
|
||||
Button::new(label, msg(JavaMessage::SavePrefs)).base(BASE),
|
||||
)
|
||||
} else {
|
||||
buttons::filled(ui, Button::disabled(label))
|
||||
buttons::filled(ui, Button::disabled(label).base(BASE))
|
||||
};
|
||||
let hint = if state.prefs.dirty() {
|
||||
catalog.text(Key::SettingsJavaPrefsUnsaved).to_string()
|
||||
|
||||
@@ -25,7 +25,7 @@ use unlucky_i18n::{Catalog, Key, Lang};
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::app::components::selection::{chip, Chip};
|
||||
use crate::pages::frame::{card, divider, text_role_owned};
|
||||
use crate::pages::frame::{card, divider, text_role_owned, BASE};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -114,10 +114,15 @@ fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Me
|
||||
};
|
||||
|
||||
let scan_label = catalog.text(Key::SettingsJavaScan);
|
||||
// M237:这一整张卡片是 `frame::card` 画的(`Role::SurfaceContainerLow`),所以卡片里的
|
||||
// 控件把底传成 `frame::BASE`——禁用态的 10% / 12% 叠在卡底上,不是画布上。
|
||||
let scan = if scanning || !state.storage {
|
||||
buttons::filled(ui, Button::disabled(scan_label))
|
||||
buttons::filled(ui, Button::disabled(scan_label).base(BASE))
|
||||
} else {
|
||||
buttons::filled(ui, Button::new(scan_label, msg(JavaMessage::Scan)))
|
||||
buttons::filled(
|
||||
ui,
|
||||
Button::new(scan_label, msg(JavaMessage::Scan)).base(BASE),
|
||||
)
|
||||
};
|
||||
// 已经按过取消(线程还没退出)时,「取消」也灰掉:再按一次没有任何意义。
|
||||
let cancel = buttons::tonal(
|
||||
@@ -129,7 +134,8 @@ fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Me
|
||||
)
|
||||
} else {
|
||||
Button::disabled(catalog.text(Key::SettingsJavaCancel))
|
||||
},
|
||||
}
|
||||
.base(BASE),
|
||||
);
|
||||
|
||||
// 「扫描已取消」那句只在线程**真退出之后**出现;退出之前状态行本身写着「正在取消…」。
|
||||
@@ -227,7 +233,7 @@ fn runtime_row<'a>(
|
||||
None => (String::new(), ui.color(Role::OnSurfaceVariant)),
|
||||
};
|
||||
|
||||
let action = chip(ui, action_chip(catalog, state, runtime));
|
||||
let action = chip(ui, action_chip(catalog, state, runtime).base(BASE));
|
||||
|
||||
let mut block = column![row![
|
||||
container(
|
||||
|
||||
@@ -105,7 +105,12 @@ fn action_button<'a>(ui: &'a Ui, label: &'a str, message: Message) -> Element<'a
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background =
|
||||
matches!(status, button::Status::Hovered | button::Status::Pressed).then(|| {
|
||||
Background::Color(ui.state_over(StateLayer::Hover, Role::OnSurface, Role::Surface))
|
||||
// M237:这一行住在设置卡里(`frame::card`),hover 状态层叠卡底。
|
||||
Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
frame::BASE.role(),
|
||||
))
|
||||
});
|
||||
button::Style {
|
||||
background,
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! 「Java」卡片里的**禁用控件叠的是卡底**——页面级**像素**判据(M237)。
|
||||
//!
|
||||
//! ## 为什么非要从页面量
|
||||
//!
|
||||
//! `interaction::disabled::tests` 能证明「`Base` 传对了,合成就是对的」,`buttons::tests::base`
|
||||
//! 能证明「`Button::base` 接到了样式闭包」——两条都**不**证明页面那天真的把 `frame::BASE` 传下去了
|
||||
//! (M229 的教训:参数在、语义在,8 个调用点全传死 `Role::Surface`)。只有页面自己的像素能证明
|
||||
//! 「这一屏里的禁用按钮是叠在卡底上画的」。
|
||||
//!
|
||||
//! ## 量什么
|
||||
//!
|
||||
//! `ULL_JAVA_STATE=scanning` 那一格里有一颗**禁用**的 Filled 按钮(「立即扫描」;「保存」同样是
|
||||
//! 禁用态,但在这张卡的下半段、1180×760 的一帧里落在折叠线以下,量不到),
|
||||
//! 它住在 `frame::card` 里(卡底 = `MaterialLayer::Card` = `SurfaceContainerLow`)。
|
||||
//! 禁用容器 = `OnSurface` @10% 叠在底上,于是「叠卡底」与「叠画布」是**两个不同的逐位值**
|
||||
//! (实测的 8 位 RGB,十进制写在这里——色值十六进制字面量在源码里是禁的,见
|
||||
//! `tests/no_hardcoded_tokens.rs`):
|
||||
//!
|
||||
//! | 板 | 叠画布(`Base::CANVAS`) | 叠卡底(`frame::BASE`) | 逐位差 |
|
||||
//! |---|---|---|---|
|
||||
//! | Silver(浅) | (226, 226, 227) | (221, 221, 222) | 5/255 |
|
||||
//! | Ink(深) | (37, 43, 36) | (40, 45, 40) | 3/255 |
|
||||
//!
|
||||
//! 差得这么小正是**必须逐位匹配**的理由:肉眼(以及 `±10` 容差的扫描器)分不出这两档,
|
||||
//! 而「叠错层」这件事本身是确定的。两个值都在测试里**现算**
|
||||
//! (`interaction::disabled_button_colors`),不写死——换板 / 改令牌时判据跟着走;
|
||||
//! 上表只是给人读的量测记录。
|
||||
//!
|
||||
//! ## 跑法(1180×760 @1.0,`--software-renderer`)
|
||||
//!
|
||||
//! ```text
|
||||
//! EXE=${CARGO_TARGET_DIR:-target}/debug/unlucky-launcher-iced
|
||||
//! OUT=${CARGO_TARGET_DIR:-target}/m237 && mkdir -p "$OUT"
|
||||
//!
|
||||
//! ULL_START_ROUTE=settings ULL_SETTINGS_GROUP=2 ULL_JAVA_STATE=scanning \
|
||||
//! $EXE --software-renderer --scale-factor=1 \
|
||||
//! --screenshot "$OUT/java-light.png" --screenshot-theme=light
|
||||
//! ULL_M237_JAVA_SHOT="$OUT/java-light.png" cargo test -p unlucky-app-iced java_shot
|
||||
//! ```
|
||||
//!
|
||||
//! `ULL_SETTINGS_GROUP=2` 是「Java」组(`settings::java::JAVA_GROUP`);不给就画出 General 组,
|
||||
//! 那张图上没有禁用填充按钮,判据会拿不到样本并**大声报出来**(不是静默通过)。
|
||||
//!
|
||||
//! ## 抓得住 / 抓不住
|
||||
//!
|
||||
//! - 抓得住:① 页面调用点漏了 `.base(..)`(回落到 `Base::CANVAS`)——卡区里会出现「叠画布」档、
|
||||
//! 「叠卡底」档消失;② `frame::BASE` 被改成别的角色(判据跟着 `BASE` 走,于是与像素对不上);
|
||||
//! ③ 出图通道换了组 / 换了主题(判据先用卡底色**认出**板,认不出就红)。
|
||||
//! - 抓不住:① 面板 α < 1 的 GPU 档(本判据读的是 tiny-skia 的图,α 恒 1);② 卡外的控件
|
||||
//! (判据只看卡区);③ 具体是哪一颗按钮变成了卡底(只数颜色总量,不定位控件)。
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::app::components::interaction::{self, Base};
|
||||
use crate::pages::frame;
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
/// 出图路径(判据的输入)。
|
||||
const SHOT_ENV: &str = "ULL_M237_JAVA_SHOT";
|
||||
/// 卡区里「叠卡底」的禁用容器像素下限(两颗 40 高的药丸,扣掉字形与圆角后实测 ≈3100)。
|
||||
const MIN_HITS: usize = 1000;
|
||||
/// 「正确档 : 错误档」的最小倍数(抗锯齿会在边缘撞上另一档的精确值,实测个位到十几像素)。
|
||||
const STRAY_RATIO: usize = 10;
|
||||
/// 卡区的下限(一张 400+ 宽的卡,实测 ≈32 万像素)——低于它说明这一屏根本没有卡。
|
||||
const MIN_CARD_PX: usize = 50_000;
|
||||
|
||||
fn ui(flavor: SchemeFlavor) -> Ui {
|
||||
Ui::with_platform(flavor, Platform::Windows, FAMILIES)
|
||||
}
|
||||
|
||||
/// iced 颜色 → 8 位三元组(与 `to_iced` 同一条口径:`f32` 0..1 四舍五入回 0..255)。
|
||||
fn rgb(color: iced::Color) -> [u8; 3] {
|
||||
let byte = |channel: f32| (channel * 255.0).round().clamp(0.0, 255.0) as u8;
|
||||
[byte(color.r), byte(color.g), byte(color.b)]
|
||||
}
|
||||
|
||||
/// 一张 RGB 图(只留本判据要的两件事:尺寸 + 取点)。
|
||||
struct Shot {
|
||||
width: usize,
|
||||
height: usize,
|
||||
pixels: Vec<[u8; 3]>,
|
||||
}
|
||||
|
||||
impl Shot {
|
||||
fn load(path: &std::path::Path) -> Self {
|
||||
let bytes = std::fs::read(path).expect("读截图");
|
||||
let image = image::load_from_memory(&bytes).expect("解 PNG").to_rgb8();
|
||||
let (width, height) = (image.width() as usize, image.height() as usize);
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
pixels: image.pixels().map(|pixel| pixel.0).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn at(&self, x: usize, y: usize) -> [u8; 3] {
|
||||
self.pixels[y * self.width + x]
|
||||
}
|
||||
|
||||
/// 逐位相等的像素数(`±10` 容差在这里没用:两档底只差 5 个 RGB 单位)。
|
||||
fn count(&self, color: [u8; 3]) -> usize {
|
||||
self.pixels.iter().filter(|pixel| **pixel == color).count()
|
||||
}
|
||||
|
||||
/// 某颜色像素的外接矩形 `(x0, y0, x1, y1)`(闭区间),一个都没有时 `None`。
|
||||
fn bbox(&self, color: [u8; 3]) -> Option<(usize, usize, usize, usize)> {
|
||||
let mut found = None;
|
||||
for y in 0..self.height {
|
||||
for x in 0..self.width {
|
||||
if self.at(x, y) == color {
|
||||
let (x0, y0, x1, y1) = found.unwrap_or((x, y, x, y));
|
||||
found = Some((x0.min(x), y0.min(y), x1.max(x), y1.max(y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// 矩形内的逐位相等像素数。
|
||||
fn count_in(&self, color: [u8; 3], rect: (usize, usize, usize, usize)) -> usize {
|
||||
let (x0, y0, x1, y1) = rect;
|
||||
(y0..=y1.min(self.height - 1))
|
||||
.map(|y| {
|
||||
(x0..=x1.min(self.width - 1))
|
||||
.filter(|x| self.at(*x, y) == color)
|
||||
.count()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// 按**卡底色**认出这张图是哪块板(认不出 = 这一屏没有卡 / 主题对不上,直接红)。
|
||||
fn flavor_of(shot: &Shot) -> SchemeFlavor {
|
||||
let mut candidates = Vec::new();
|
||||
for flavor in [SchemeFlavor::Light, SchemeFlavor::Dark] {
|
||||
let card = rgb(ui(flavor).color(frame::BASE.role()));
|
||||
let hits = shot.count(card);
|
||||
if hits >= MIN_CARD_PX {
|
||||
candidates.push((flavor, card, hits));
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
candidates.len(),
|
||||
1,
|
||||
"这一屏里「卡底」色的像素必须恰好落在一种板上,实测 {candidates:?}:\
|
||||
检查出图命令是否带了 `--screenshot-theme` 与 `ULL_SETTINGS_GROUP=2`"
|
||||
);
|
||||
candidates[0].0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_java_card_composites_disabled_controls_over_its_own_base() {
|
||||
let Some(path) = std::env::var_os(SHOT_ENV) else {
|
||||
// 本机允许不喂图(会大声说一句);CI 里没图就是失败——判据静默空转等于没有判据。
|
||||
assert!(
|
||||
std::env::var_os("CI").is_none(),
|
||||
"守门未喂图:{SHOT_ENV} 没设(跑法见 `java_shot.rs` 的模块文档)"
|
||||
);
|
||||
eprintln!("[skip] {SHOT_ENV} 没设:页面级像素判据未执行(本机档)");
|
||||
return;
|
||||
};
|
||||
|
||||
let shot = Shot::load(std::path::Path::new(&path));
|
||||
let flavor = flavor_of(&shot);
|
||||
let ui = ui(flavor);
|
||||
|
||||
let over_card = rgb(interaction::disabled_button_colors(&ui, frame::BASE).0);
|
||||
let over_canvas = rgb(interaction::disabled_button_colors(&ui, Base::CANVAS).0);
|
||||
assert_ne!(
|
||||
over_card, over_canvas,
|
||||
"{flavor:?}: 叠卡底与叠画布的禁用容器算出来一样 —— 判据自己立不住"
|
||||
);
|
||||
|
||||
// 卡区 = 卡底色像素的外接矩形(卡是这一屏最大的同色块)
|
||||
let card_color = rgb(ui.color(frame::BASE.role()));
|
||||
let rect = shot
|
||||
.bbox(card_color)
|
||||
.unwrap_or_else(|| panic!("{flavor:?}: 图里找不到卡底色 {card_color:?}"));
|
||||
|
||||
let hits = shot.count_in(over_card, rect);
|
||||
let stray = shot.count_in(over_canvas, rect);
|
||||
|
||||
eprintln!(
|
||||
"[pages] OK flavor={flavor:?} 卡区 {rect:?}:叠卡底 {over_card:?} {hits} px · \
|
||||
叠画布 {over_canvas:?} {stray} px"
|
||||
);
|
||||
|
||||
assert!(
|
||||
hits >= MIN_HITS,
|
||||
"{flavor:?}: 卡区里「叠 {:?}」的禁用容器只有 {hits} px(应 ≥{MIN_HITS})—— \
|
||||
卡片里的禁用控件没把 `frame::BASE` 传下去(还在按画布合成)",
|
||||
frame::BASE.role()
|
||||
);
|
||||
assert!(
|
||||
hits > stray * STRAY_RATIO,
|
||||
"{flavor:?}: 卡区里「叠卡底」{hits} px,而「叠画布」{stray} px(要求前者 ≥ 后者 ×{STRAY_RATIO})—— \
|
||||
卡区里仍有一部分禁用控件按 `Role::Surface` 合成"
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,7 @@ mod appearance_section;
|
||||
mod download_section;
|
||||
mod general_section;
|
||||
mod java_section;
|
||||
mod java_shot;
|
||||
mod scroll_fit;
|
||||
mod update_section;
|
||||
|
||||
|
||||
Reference in new issue
Block a user