fix(app-iced): M75 复审 P1-A/P1-B/P2 —— 内容色跟态走 + 断言收紧 + 矩阵排版

P1-A 禁用内容色没下发(视觉 reviewer:text/standard 的 enabled 与 disabled 逐像素相同):
  assemble 里内容色恒取 ui.color(content) 再被 text().color() 钉死,Style.text_color 的 38%
  永远下发不出去。新增 assemble::content_color(ui, forced, content),文字按钮与 IconButton
  共用;probe 补两条像素断言(禁用 38% 墨迹必须存在、禁用容器内不得有满档 OnPrimary)。

P1-B 渲染层断言抓不住 2px 级回归(reviewer 变异验证:高度 42 仍绿):
  高度改成精确 == 40;补「带图标左内边距 16(墨迹 ≈21)」断言;分隔线从「行数」改成
  「墨量覆盖率 ≈1.0」(1.5dp 会到 ≈1.5)。

P2-1 probe 给了图却读不到时硬失败(只有完全没给 ULL_GALLERY_SHOT 才跳过)。
P2-2 Geometry::stroke 真正接到 Border.width;IconGeometry::icon 改由 side−2×padding 推导。
P2-3 矩阵 cell 宽 192→160(总宽 1040<1180,disabled 列不再被裁);禁用格保留前后图标。
P2-4 补注:snap 仅 wgpu 实现,CPU 验收图不受它影响。
This commit is contained in:
AskaEth committed 2026-11-02 04:50:51 +08:00
1 parent 8072f1cc7e
commit e905fa3922
8 files changed
+195 -68

No files matched your search

@@ -13,7 +13,7 @@ use unlucky_design::shape::ShapeScale;
use unlucky_design::typography::TypeRole;
use crate::icons;
use crate::theme::{Ui, STROKE_PX};
use crate::theme::Ui;
use super::super::interaction::{self, Interaction};
use super::super::layout;
@@ -30,6 +30,8 @@ pub(super) struct StyleSpec {
pub border: Option<Role>,
/// 圆角档位(来自组件表,五式统一 `Full`)。
pub shape: ShapeScale,
/// 描边宽度(`Geometry::stroke`:有描边的变体 = `stroke::HAIRLINE`,其余 0)。
pub stroke: f32,
/// 圆角折算用的边长(药丸 = 可见高)。
pub radius_side: f32,
/// 强制态(gallery 取证);`None` = 跟随运行期的 `button::Status`。
@@ -48,6 +50,11 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
) -> Element<'a, M> {
let geometry = Geometry::of(ui, variant, spec.leading.is_some());
let (fill, content, border) = variant.roles();
let forced = spec.state.or(if spec.on_press.is_none() {
Some(Interaction::Disabled)
} else {
None
});
// 按钮家族没有浮层级高程:L1(Elevated)只用容器色调差表达(rubric §7 不画阴影)
debug_assert!(
@@ -57,7 +64,11 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
),
"按钮家族不许有浮层高程"
);
let content_color = ui.color(content);
// **内容色必须跟着态走**:禁用时文字/图标是 on-surface@38%,不是满档内容色。
// 早先这里恒取 `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);
let label = text(spec.label)
.size(ui.style(TypeRole::LabelLarge).size_px)
@@ -65,7 +76,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
ui.style(TypeRole::LabelLarge).line_height_px,
)))
.font(ui.font_of(ui.emphasized(TypeRole::LabelLarge)))
.color(content_color);
.color(ink);
let mut row = Row::new().align_y(Alignment::Center);
if let Some(kind) = spec.leading {
@@ -73,7 +84,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
row = row.push(layout::fixed(
geometry.icon,
geometry.icon,
icons::view::<M>(kind, content_color),
icons::view::<M>(kind, ink),
));
}
row = row.push(label);
@@ -81,7 +92,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
row = row.push(layout::fixed(
geometry.icon,
geometry.icon,
icons::view::<M>(kind, content_color),
icons::view::<M>(kind, ink),
));
}
let row = row.spacing(geometry.gap);
@@ -93,12 +104,6 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
.height(Length::Fill)
.align_y(iced::Alignment::Center);
let forced = spec.state.or(if spec.on_press.is_none() {
Some(Interaction::Disabled)
} else {
None
});
let style = style_closure(
ui,
StyleSpec {
@@ -106,6 +111,7 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
content,
border,
shape: variant.shape(),
stroke: geometry.stroke,
radius_side: geometry.height,
forced,
},
@@ -122,6 +128,17 @@ pub(super) fn assemble<'a, M: Clone + 'a>(
}
}
/// 内容色(文字与图标共用):禁用态走令牌的 38%,其余态就是角色色。
///
/// 单独成函数是为了让「内容色跟态走」这件事只有一处实现——`assemble`(文字按钮)与 `icon`
/// (图标按钮)都调它,两条路径不会再各自钉死满档色。
pub(super) fn content_color(ui: &Ui, forced: Option<Interaction>, content: Role) -> iced::Color {
match forced {
Some(Interaction::Disabled) => interaction::disabled_colors(ui, Role::Surface).1,
_ => ui.color(content),
}
}
/// 生成 `button::style` 闭包:状态层 / 禁用 / 描边全在这里落地。
pub(super) fn style_closure(
ui: &Ui,
@@ -166,15 +183,16 @@ pub(super) fn style_closure(
} else {
iced::Color::TRANSPARENT
},
width: if spec.border.is_some() {
STROKE_PX
} else {
0.0
},
// 线宽来自 `Geometry::stroke`(有描边的变体 = HAIRLINE,其余 0);
// `spec.border.is_none()` 时颜色是透明的,宽度也就无所谓了。
width: spec.stroke,
radius: ui.radius(spec.shape, spec.radius_side),
},
// rubric §7:L0/L1 不用阴影(Elevated 的 L1 由容器色调差表达)
shadow: Shadow::default(),
// ⚠️ `snap` 目前**只有 wgpu 后端实现**;我们的验收图走 tiny-skia(CPU),
// 所以这个 `true` 在验收图上不起作用——分隔线的「1dp 摊成 2 行」正是这个原因,
// 不是「iced 没有 snap」(reviewer P2-4 纠正的正是我上一版报告里的说法)。
snap: true,
}
}