diff --git a/crates/app-iced/src/components/feedback/action.rs b/crates/app-iced/src/components/feedback/action.rs index e2716ef..843ce2d 100644 --- a/crates/app-iced/src/components/feedback/action.rs +++ b/crates/app-iced/src/components/feedback/action.rs @@ -135,10 +135,12 @@ pub(super) fn icon_action<'a, M: Clone + 'a>( let ink = content_color(ui, content, container_role, disabled); + // 走 `view_at(24)` 而不是 `view(..)`:`icons.rs` 的守门测试按**逐文件调用点计数** + // 要求「不在白名单里的文件 `icons::view` 必须为 0」;本文件按盒尺寸显式烘一份(同值 24)。 let element = button(layout::centered_fixed( glyph_side, glyph_side, - icons::view::(kind, ink), + icons::view_at::(kind, ink, glyph_side), )) .width(Length::Fixed(side)) .height(Length::Fixed(side)) diff --git a/crates/app-iced/src/components/feedback/geometry.rs b/crates/app-iced/src/components/feedback/geometry.rs index 433119d..5ac3206 100644 --- a/crates/app-iced/src/components/feedback/geometry.rs +++ b/crates/app-iced/src/components/feedback/geometry.rs @@ -118,6 +118,10 @@ line!(SEG_TAIL_2, 900, 1750); /// /// 值来自:第二线 `[0.003, 0.076]`(tail 0.003 / head 0.079 → 合并到 0.076 区间的实测), /// 第一线 `[0.355, 0.860]`。 +/// +/// `#[cfg(test)]`:只有单测与渲染层探针(都在 `cfg(test)` 里)读它,产品路径不需要—— +/// 不留成产品常量是为了让 `-D warnings` 下的 `dead_code` 保持沉默。 +#[cfg(test)] pub const PHASE_055: [(f32, f32); 2] = [(0.003, 0.076), (0.355, 0.860)]; /// 线性进度的几何(厚度与圆角;轨道宽由宿主给,所以没有 width)。 diff --git a/crates/app-iced/src/components/feedback/probe.rs b/crates/app-iced/src/components/feedback/probe.rs index d8a1ada..bf7e4bd 100644 --- a/crates/app-iced/src/components/feedback/probe.rs +++ b/crates/app-iced/src/components/feedback/probe.rs @@ -275,10 +275,22 @@ fn check_snackbar(shot: &Shot, ui: &Ui) { if !(near(width, 300) || near(width, 344)) { continue; } + // 1px 描边常落在半像素上(两行各 ~50% 覆盖),精确色匹配抓不住; + // 用「更靠近框色而不是画布」判(这一带除了框没有别的灰线)。 + let distance = |pixel: [u8; 3], target: [u8; 3]| -> u32 { + pixel + .iter() + .zip(target) + .map(|(a, b)| u32::from(a.abs_diff(b))) + .sum() + }; let frame_row = ((bottom + 1)..shot.height) .find(|y| { (0..shot.width) - .filter(|x| matches(shot.at(*x, *y), frame)) + .filter(|x| { + let pixel = shot.at(*x, *y); + distance(pixel, frame) < distance(pixel, rgb(surface)) + }) .count() >= 200 }) diff --git a/crates/app-iced/src/components/feedback/probe/panels.rs b/crates/app-iced/src/components/feedback/probe/panels.rs index e6e996a..76a3df9 100644 --- a/crates/app-iced/src/components/feedback/probe/panels.rs +++ b/crates/app-iced/src/components/feedback/probe/panels.rs @@ -44,37 +44,85 @@ pub(super) fn check_dialog(shot: &Shot, ui: &Ui) { // ---- 7.4 宽度:先分「卡带」(同排两张卡的 y 区间会重叠,所以只能按行分组), // 再在每条带里按**列剖面**分出每一张卡(复审 A/G:早先只量最宽的那张)。 - let card_bands = bands(shot, card, surface, 200); + // 卡带用**精确色**数行(不用斜坡):深色档里正文/描边的抗锯齿像素都落在 + // 「surface → card」那条很短的斜坡上,用斜坡会把整屏都算成卡带(实测出现 1180 宽的假卡)。 + let card_rgb = rgb(card); + let scrim_rgb = rgb(scrim(ui)); + let card_rows: Vec = (0..shot.height) + .filter(|y| { + (0..shot.width) + .filter(|x| { + let pixel = shot.at(*x, *y); + matches(pixel, card_rgb) && !matches(pixel, scrim_rgb) + }) + .count() + >= 200 + }) + .collect(); + let mut card_bands: Vec<(usize, usize)> = Vec::new(); + let mut start: Option = None; + let mut previous: usize = 0; + for y in card_rows { + match start { + None => { + start = Some(y); + previous = y; + } + Some(_) if y == previous + 1 => previous = y, + Some(top) => { + card_bands.push((top, previous)); + start = Some(y); + previous = y; + } + } + } + if let Some(top) = start { + card_bands.push((top, previous)); + } assert!( card_bands.len() >= 3, "至少要找到 3 条卡带(第一排两张 + 双动作 + scrim 里那张),实测 {card_bands:?}" ); - let mut widths: Vec = Vec::new(); + // 每张卡记 (左缘, 宽):宽度给「逐卡」断言,左缘给内距断言用(不能按单行找左缘—— + // 圆角 28 会让 band_top+4 那一行的卡片像素内缩 ~10px,实测内距被量成 12 而不是 24)。 + // + // **深色档要把遮罩色排掉**:暗色下 `SurfaceContainerHigh` 与预合成 scrim 只差 ~35/通道, + // 不排掉时 scrim 那片也被算成「卡」(实测出现 1180 宽的假卡)。 + let mut cards: Vec<(usize, usize)> = Vec::new(); for band in &card_bands { let height = band.1 - band.0 + 1; let profile: Vec = (0..shot.width) .map(|x| { (band.0..=band.1) - .filter(|y| matches(shot.at(x, *y), rgb(card))) + .filter(|y| { + let pixel = shot.at(x, *y); + matches(pixel, card_rgb) && !matches(pixel, scrim_rgb) + }) .count() }) .collect(); let mut start: Option = None; - for x in 0..=shot.width { - let column = x < shot.width && profile[x] >= height / 3; + for (x, ink) in profile.iter().enumerate() { + let column = *ink >= height / 3; match (column, start) { (true, None) => start = Some(x), (false, Some(from)) => { if x - from >= 280 { - widths.push(x - from); + cards.push((from, x - from)); } start = None; } _ => {} } } + if let Some(from) = start { + if shot.width - from >= 280 { + cards.push((from, shot.width - from)); + } + } } + let mut widths: Vec = cards.iter().map(|(_, width)| *width).collect(); widths.sort_unstable(); assert!( widths.iter().any(|w| near(*w, DIALOG_W)), @@ -93,14 +141,7 @@ pub(super) fn check_dialog(shot: &Shot, ui: &Ui) { // 内距与 scrim 只在一张卡上量(第一张,最靠左上)。 let first = card_bands[0]; - let width = band_width(shot, first, card, surface); - let mut card_left = 0; - for x in 0..shot.width { - if matches(shot.at(x, first.0 + 4), rgb(card)) { - card_left = x; - break; - } - } + let (card_left, width) = cards[0]; let top = first.0; let bottom = first.1; let bbox = BBox { diff --git a/crates/app-iced/src/components/gallery/feedback.rs b/crates/app-iced/src/components/gallery/feedback.rs index 7a6163f..7da2d75 100644 --- a/crates/app-iced/src/components/gallery/feedback.rs +++ b/crates/app-iced/src/components/gallery/feedback.rs @@ -107,6 +107,7 @@ fn caption<'a>(ui: &'a Ui, value: &'a str) -> Element<'a, DemoMessage> { } /// 带 1px 取景框的宿主:`Snackbar` 的贴底/贴顶需要一条**可见的宿主边界**才量得出来 +/// (描边画在盒内但**不参与布局**:内容区仍是整盒宽,实测条宽不受影响)。 /// (透明宿主的底边在图上不存在,`Placement::Bottom` 就不可证)。 fn framed_host<'a>( ui: &'a Ui,