fix(app-iced): M75 r5 —— 禁用内容角色按变体(A1)+ 删 probe 重复块 + 逐行护栏
1) A1 逐变体(AAR javap):Filled/Elevated/Outlined/Text = OnSurfaceVariant@0.38; FilledTonal = OnSurface@0.38(FilledTonalButtonTokens.DisabledLabelTextColor); Standard/Filled IconButton = OnSurface@0.38(*IconButtonTokens.DisabledColor)。 新增 Variant::disabled_content() / IconVariant::disabled_content(), interaction::disabled_content_over_role(ui, role, on_container) 成唯一入口。 2) probe.rs 删掉被两个 reviewer 点名的重复块(disabled_colors 调用两次、 disabled_container 绑定两次),并补 grep 证据。 3) 护栏改成**逐行**:矩阵 7 个有内容的变体行各自断言 disabled 列的 38% 墨迹 > 40 px (浅/深同一套阈值,单行回退就会红),取代原来按浅色标定的全局 500 阈值。 6) 删掉 interaction/mod.rs 里挂在 mod tests 头上的悬挂文档。
This commit is contained in:
1 parent
b31a758cda
commit
eed1c9d3f7
5 files changed
+99
-51
No files matched your search
@@ -68,7 +68,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
// 早先这里恒取 `ui.color(content)`,再被 `text(...).color(..)` 钉死,`Style.text_color`
|
||||
// 里的 38% 永远下发不出去 —— 表现为「text / standard icon 的 enabled 格与 disabled 格
|
||||
// 逐像素相同」(视觉 reviewer 实测 changed_px = 0;filled 禁用标签 1.23:1)。
|
||||
let ink = content_color(ui, forced, content, fill);
|
||||
let ink = content_color(ui, forced, content, fill, variant.disabled_content());
|
||||
|
||||
let label = text(spec.label)
|
||||
.size(ui.style(TypeRole::LabelLarge).size_px)
|
||||
@@ -132,19 +132,25 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
|
||||
///
|
||||
/// 单独成函数是为了让「内容色跟态走」这件事只有一处实现——`assemble`(文字按钮)与 `icon`
|
||||
/// (图标按钮)都调它,两条路径不会再各自钉死满档色。
|
||||
/// 内容色:**禁用时的角色由调用方按变体给**([`super::Variant::disabled_content`] /
|
||||
/// [`super::IconVariant::disabled_content`])——A1 里 FilledTonal 与 IconButton 是 `OnSurface`,
|
||||
/// 其余是 `OnSurfaceVariant`,不能在这里统一写死。
|
||||
pub(super) fn content_color(
|
||||
ui: &Ui,
|
||||
forced: Option<Interaction>,
|
||||
content: Role,
|
||||
fill: Option<Role>,
|
||||
disabled_content: Role,
|
||||
) -> iced::Color {
|
||||
match forced {
|
||||
// 有容器:38% 叠在**12% 容器**上(Filled / Tonal / Elevated)
|
||||
Some(Interaction::Disabled) if fill.is_some() => {
|
||||
interaction::disabled_colors(ui, Role::Surface).1
|
||||
interaction::disabled_content_over_role(ui, disabled_content, true)
|
||||
}
|
||||
// 无容器:38% 直接叠在 **surface** 上(Text / Outlined / standard IconButton)
|
||||
Some(Interaction::Disabled) => interaction::disabled_content_over(ui, Role::Surface),
|
||||
Some(Interaction::Disabled) => {
|
||||
interaction::disabled_content_over_role(ui, disabled_content, false)
|
||||
}
|
||||
_ => ui.color(content),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,22 @@ impl Variant {
|
||||
self.component().spec().shape
|
||||
}
|
||||
|
||||
/// **禁用态的内容角色**(A1,逐变体不同——别在这里统一猜)。
|
||||
///
|
||||
/// | 变体 | 禁用内容 |
|
||||
/// |---|---|
|
||||
/// | Filled / Elevated / Outlined / Text | `OnSurfaceVariant` @0.38 |
|
||||
/// | **FilledTonal** | **`OnSurface` @0.38** ← A1 的显式例外(`FilledTonalButtonTokens`) |
|
||||
///
|
||||
/// 出处:AAR `material3-android:1.4.0` 的 `javap -c`,`FilledButtonTokens.
|
||||
/// DisabledLabelTextColor = OnSurfaceVariant`、`FilledTonalButtonTokens` 同一字段 = `OnSurface`。
|
||||
pub(crate) const fn disabled_content(self) -> Role {
|
||||
match self {
|
||||
Variant::Tonal => Role::OnSurface,
|
||||
_ => Role::OnSurfaceVariant,
|
||||
}
|
||||
}
|
||||
|
||||
/// 高程:只有 Elevated 是 L1(`ElevatedButtonTokens.ContainerElevation`),其余 L0。
|
||||
///
|
||||
/// **不画阴影**:rubric §7「L0/L1 不应有可见阴影(用 tone 差)」,所以 L1 只体现为
|
||||
@@ -172,6 +188,12 @@ impl IconVariant {
|
||||
}
|
||||
}
|
||||
|
||||
/// **禁用态图标色**:三式都是 `OnSurface` @0.38 —— A1
|
||||
/// (`StandardIconButtonTokens.DisabledColor` / `FilledIconButtonTokens.DisabledColor`)。
|
||||
pub(crate) const fn disabled_content(self) -> Role {
|
||||
Role::OnSurface
|
||||
}
|
||||
|
||||
/// 色彩角色三元组(同 [`Variant::roles`] 的口径)。
|
||||
pub(crate) const fn roles(self) -> (Option<Role>, Role, Option<Role>) {
|
||||
match self {
|
||||
@@ -295,7 +317,7 @@ pub fn icon<'a, M: Clone + 'a>(
|
||||
// 这里把盒子做成整个 40×40(而不是 24×24):`button::layout` 把内容放左上角、不居中,
|
||||
// 所以「居中」这件事必须由容器自己写死,别指望 button。
|
||||
// 内容色跟态走(禁用 = on-surface@38%),见 `assemble::content_color` 的说明
|
||||
let glyph = assemble::content_color(ui, forced, content, fill);
|
||||
let glyph = assemble::content_color(ui, forced, content, fill, variant.disabled_content());
|
||||
let icon_box =
|
||||
layout::centered_fixed(geometry.side, geometry.side, icons::view::<M>(kind, glyph));
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
use crate::app::components::interaction;
|
||||
|
||||
use super::scan::{matches, nearer, rgb, Shot};
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
@@ -312,9 +314,12 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
"单条分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
|
||||
);
|
||||
|
||||
// ---- 12b 1.8:禁用格必须与静息格不同 ------------------------------------
|
||||
let disabled_container =
|
||||
crate::app::components::interaction::disabled_colors(&ui, Role::Surface).0;
|
||||
// ---- 12b 1.8 禁用态:容器 + **每一行**的内容色(A1 逐变体不同)----------------
|
||||
//
|
||||
// 上一版把两个 reviewer 都指出的重复块留在这里(`disabled_colors` 被调两次、
|
||||
// `disabled_container` 被绑定两次),且只有一条「全图数墨迹」的护栏 —— 深色下阈值失效、
|
||||
// 单行回退也抓不住。现在按矩阵的行逐条断言。
|
||||
let (disabled_container, _) = interaction::disabled_colors(&ui, Role::Surface);
|
||||
assert!(
|
||||
shot.bbox_of(rgb(disabled_container))
|
||||
.iter()
|
||||
@@ -322,37 +327,36 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
"没找到禁用态实心按钮(12b 1.8:容器 = on-surface@12%)"
|
||||
);
|
||||
|
||||
// ---- 12b 1.8(视觉 reviewer 的 P1-A):禁用态的内容色必须真的下发 ----
|
||||
let (disabled_container, disabled_ink) =
|
||||
crate::app::components::interaction::disabled_colors(&ui, Role::Surface);
|
||||
let ink_px = count_pixels(&shot, rgb(disabled_ink));
|
||||
assert!(
|
||||
ink_px > 200,
|
||||
"**有容器**四式的禁用内容(OnSurfaceVariant@38% 叠 12% 容器)只出现 {ink_px} 像素: 说明 Filled/Tonal/Elevated 的字形没走 38%"
|
||||
);
|
||||
// 矩阵行序(gallery/buttons.rs 的 Variant::ALL + IconVariant::ALL),行距 48 = 40 + 8
|
||||
let row_step = 48usize;
|
||||
let rows: [(usize, Role, bool, &str); 7] = [
|
||||
(0, Role::OnSurfaceVariant, true, "filled"),
|
||||
(1, Role::OnSurface, true, "tonal"), // A1:FilledTonal 是例外
|
||||
(2, Role::OnSurfaceVariant, true, "elevated"),
|
||||
(3, Role::OnSurfaceVariant, false, "outlined"),
|
||||
(4, Role::OnSurfaceVariant, false, "text"),
|
||||
(5, Role::OnSurface, false, "icon·standard"), // A1:IconButton 全是 OnSurface
|
||||
(6, Role::OnSurface, false, "icon·filled"),
|
||||
];
|
||||
let column_from = shot.width * 2 / 3; // 矩阵最右列 = disabled 列
|
||||
for (index, role, on_container, name) in rows {
|
||||
let y0 = label_button.top + index * row_step;
|
||||
let y1 = y0 + 44;
|
||||
let ink = interaction::disabled_content_over_role(&ui, role, on_container);
|
||||
let count = shot.count_in(rgb(ink), column_from, shot.width, y0, y1);
|
||||
assert!(
|
||||
count > 40,
|
||||
"{name} 行的禁用内容({role:?}@38%)在 disabled 列里只有 {count} px —— 说明这一行没走 A1 的禁用角色"
|
||||
);
|
||||
}
|
||||
|
||||
// **无容器**三式(Text / Outlined / standard IconButton)的禁用内容叠在 surface 上,
|
||||
// 是另一个色值;单查上面那个只会覆盖到有容器的四式(reviewer code-r2 p2-1:护栏漏一半)。
|
||||
let bare_ink = crate::app::components::interaction::disabled_content_over(&ui, Role::Surface);
|
||||
// **只数 disabled 列**(矩阵最右那一列,x ≥ 全宽 2/3):整图计数会被别处的抗锯齿像素
|
||||
// 抬到 1000+,把该列涂回满档色也降不到阈值以下 —— 那正是「护栏拦不住」的成因。
|
||||
let bare_px = shot.count_pixels_in(rgb(bare_ink), shot.width * 2 / 3, shot.width);
|
||||
// 阈值 500 的来历(实测,矩阵 8 行 × 5 列):
|
||||
// 浅色正常 800 px、深色正常 1966 px;把 disabled 列的 38% 墨迹涂回满档色后只剩 348 px
|
||||
// (剩下的都是别处抗锯齿像素恰好落进容差)——所以 500 是「真墨迹在 / 不在」的分界。
|
||||
assert!(
|
||||
bare_px > 500,
|
||||
"**无容器**三式在 disabled 列里的 38% 墨迹只出现 {bare_px} 像素: 说明 Text/Outlined/standard 的字形没走 38%"
|
||||
);
|
||||
|
||||
// filled 禁用容器里不许出现满档 OnPrimary(旧实现是 #FFFFFF 压在 12% 容器上 = 1.23:1)
|
||||
// filled 禁用容器里不许出现满档 OnPrimary(旧实现是白字压在 12% 容器上 = 1.23:1)
|
||||
let full_content = rgb(ui.color(Role::OnPrimary));
|
||||
for bbox in shot.bbox_of(rgb(disabled_container)) {
|
||||
if bbox.width() < 60 {
|
||||
continue;
|
||||
}
|
||||
// 只扫**内带**:圆角外的页面底色 (246,250,255) 与 OnPrimary(255,255,255) 在容差内,
|
||||
// 贴边扫描会把圆角透出的底色当成「满档内容」(首版就在 (866,162) 误报)。
|
||||
// 只扫**内带**:圆角外的页面底色与 OnPrimary 在容差内,贴边扫描会误报。
|
||||
for y in (bbox.top + 12)..=(bbox.bottom - 12) {
|
||||
for x in (bbox.left + 12)..=(bbox.right - 12) {
|
||||
assert!(
|
||||
@@ -369,7 +373,7 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0)
|
||||
实心块 {} 个(含 40×40 IconButton)
|
||||
分隔线长行 {} 行(每条 1px)
|
||||
禁用容器 (on-surface@12%) 命中 · 禁用内容 38% 墨迹 {} px(有容器)/ {} px(无容器)
|
||||
禁用容器 (on-surface@12%) 命中 · 逐行禁用内容断言 {} 行通过
|
||||
分隔线墨量覆盖率 {:.2}(1dp 应 ≈1.0)
|
||||
字形墨迹 {}px(24 盒应 16~26)",
|
||||
flavor,
|
||||
@@ -378,8 +382,7 @@ fn gallery_geometry_matches_the_tokens() {
|
||||
top_inset,
|
||||
primary.len(),
|
||||
runs,
|
||||
ink_px,
|
||||
bare_px,
|
||||
rows.len(),
|
||||
coverage,
|
||||
glyph_width
|
||||
);
|
||||
|
||||
@@ -93,11 +93,18 @@ impl Shot {
|
||||
boxes
|
||||
}
|
||||
|
||||
/// 只在 `x` 区间 `[from, to)` 内数某颜色的像素(矩阵按列取证时用)。
|
||||
pub(super) fn count_pixels_in(&self, color: [u8; 3], from: usize, to: usize) -> usize {
|
||||
(0..self.height)
|
||||
/// 在矩形 `[x0,x1) × [y0,y1)` 内数某颜色的像素(按行/列取证用)。
|
||||
pub(super) fn count_in(
|
||||
&self,
|
||||
color: [u8; 3],
|
||||
x0: usize,
|
||||
x1: usize,
|
||||
y0: usize,
|
||||
y1: usize,
|
||||
) -> usize {
|
||||
(y0..y1.min(self.height))
|
||||
.map(|y| {
|
||||
(from..to.min(self.width))
|
||||
(x0..x1.min(self.width))
|
||||
.filter(|x| matches(self.at(*x, y), color))
|
||||
.count()
|
||||
})
|
||||
|
||||
@@ -125,20 +125,31 @@ pub fn overlay_color(ui: &Ui, state: Interaction, content: Role, container: Role
|
||||
.map(|layer| ui.state_over(layer, content, container))
|
||||
}
|
||||
|
||||
/// 禁用态内容色:`OnSurfaceVariant` @ 38% 叠在 **`base` 角色**上(不经过 12% 容器)。
|
||||
/// 禁用态内容色(**按角色、按有没有容器**求值)——A1 口径的唯一入口。
|
||||
///
|
||||
/// 给**不画容器**的三式(Text / Outlined / standard IconButton)用:错用 [`disabled_colors`]
|
||||
/// 的内容值(叠在 12% 容器上)会比令牌口径深 21/255。
|
||||
pub fn disabled_content_over(ui: &Ui, base: Role) -> Color {
|
||||
/// - `role`:由变体决定(Filled/Outlined/Text/Elevated = `OnSurfaceVariant`;
|
||||
/// FilledTonal 与三式 IconButton = `OnSurface`,见 `Variant::disabled_content`);
|
||||
/// - `on_container = true`:叠在 **12% 容器**上(有容器的三式);
|
||||
/// `false`:直接叠在 **surface** 上(Text / Outlined / standard IconButton)。
|
||||
///
|
||||
/// 之前这里写死 `OnSurfaceVariant` 且只有一个「底」,导致 FilledTonal 与 IconButton 两族偏浅
|
||||
/// 15/255(视觉 r3 用像素量的)。
|
||||
pub fn disabled_content_over_role(ui: &Ui, role: Role, on_container: bool) -> Color {
|
||||
let scheme = Scheme::baseline(ui.flavor());
|
||||
// A1(AAR `FilledButtonTokens` javap):DisabledLabelTextColor/DisabledIconColor 都是
|
||||
// `OnSurfaceVariant`、Opacity 0.38f。设计层的 `disabled_content()` 用 OnSurface(与 A1 不符),
|
||||
// 令牌线修好前这里按 A1 用角色 + 令牌 α 直接求值(不引入字面量)。
|
||||
let base = if on_container {
|
||||
scheme
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_CONTAINER_ALPHA)
|
||||
.over(scheme.get(Role::Surface))
|
||||
} else {
|
||||
scheme.get(Role::Surface)
|
||||
};
|
||||
|
||||
to_iced(
|
||||
scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.get(role)
|
||||
.with_alpha(DISABLED_CONTENT_ALPHA)
|
||||
.over(scheme.get(base)),
|
||||
.over(base),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,7 +170,8 @@ pub fn disabled_colors(ui: &Ui, surface: Role) -> (Color, Color) {
|
||||
.get(Role::OnSurface)
|
||||
.with_alpha(DISABLED_CONTAINER_ALPHA)
|
||||
.over(base);
|
||||
// 内容角色按 A1 用 `OnSurfaceVariant`(见 `disabled_content_over` 的说明)
|
||||
// 这里是**默认口径**(OnSurfaceVariant);按变体取值请走 [`disabled_content_over_role`]
|
||||
// ——FilledTonal 与三式 IconButton 的禁用内容是 `OnSurface`(A1)。
|
||||
let content = scheme
|
||||
.get(Role::OnSurfaceVariant)
|
||||
.with_alpha(DISABLED_CONTENT_ALPHA)
|
||||
@@ -168,8 +180,6 @@ pub fn disabled_colors(ui: &Ui, surface: Role) -> (Color, Color) {
|
||||
(to_iced(container), to_iced(content))
|
||||
}
|
||||
|
||||
/// 自定义 `Widget` 的状态机:把 iced 事件折成 [`Interaction`]。
|
||||
///
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user