diff --git a/crates/app-iced/src/components/buttons/assemble.rs b/crates/app-iced/src/components/buttons/assemble.rs index f89ab06..406b18b 100644 --- a/crates/app-iced/src/components/buttons/assemble.rs +++ b/crates/app-iced/src/components/buttons/assemble.rs @@ -32,8 +32,12 @@ pub(super) struct StyleSpec { pub shape: ShapeScale, /// 圆角折算用的边长(药丸 = 可见高)。 pub radius_side: f32, - /// 强制态(gallery 取证);`Enabled` 时跟随运行期的 `button::Status`。 - pub forced: Interaction, + /// 强制态(gallery 取证);`None` = 跟随运行期的 `button::Status`。 + /// + /// **`Some(Enabled)` 与 `None` 不是一回事**:截图取证必须能强制「静息」,否则光标恰好停在 + /// 某个按钮上时那一格会被画成 hover(实测踩过:text 行 enabled 列出现不该有的灰底容器, + /// 看起来像 Text 按钮自带容器)。 + pub forced: Option, } /// 五式共用的装配:**只画一处**,五个变体函数都走这里。 @@ -89,10 +93,10 @@ pub(super) fn assemble<'a, M: Clone + 'a>( .height(Length::Fill) .align_y(iced::Alignment::Center); - let forced = spec.state.unwrap_or(if spec.on_press.is_none() { - Interaction::Disabled + let forced = spec.state.or(if spec.on_press.is_none() { + Some(Interaction::Disabled) } else { - Interaction::Enabled + None }); let style = style_closure( @@ -125,10 +129,9 @@ pub(super) fn style_closure( ) -> impl Fn(&iced::Theme, button::Status) -> button::Style + '_ { move |_theme: &iced::Theme, status| { // 强制态优先(gallery 状态矩阵),否则把 iced 的四态翻成 M3 五态 - let state = match spec.forced { - Interaction::Enabled => Interaction::from_status(status), - forced => forced, - }; + let state = spec + .forced + .unwrap_or_else(|| Interaction::from_status(status)); let (background, text_color) = if state.is_disabled() { let (container, content) = interaction::disabled_colors(ui, Role::Surface); diff --git a/crates/app-iced/src/components/buttons/mod.rs b/crates/app-iced/src/components/buttons/mod.rs index d60581a..31d4587 100644 --- a/crates/app-iced/src/components/buttons/mod.rs +++ b/crates/app-iced/src/components/buttons/mod.rs @@ -279,10 +279,10 @@ pub fn icon<'a, M: Clone + 'a>( ) -> Element<'a, M> { let geometry = IconGeometry::of(ui); let (fill, content, border) = variant.roles(); - let forced = state.unwrap_or(if on_press.is_some() { - Interaction::Enabled + let forced = state.or(if on_press.is_some() { + None } else { - Interaction::Disabled + Some(Interaction::Disabled) }); // Q9:图标**必须在固定尺寸的中层容器里**,否则 Button 的 Fill 约束会让 usvg 按 Contain diff --git a/crates/app-iced/src/components/buttons/tests.rs b/crates/app-iced/src/components/buttons/tests.rs index 085aeec..9d3e08b 100644 --- a/crates/app-iced/src/components/buttons/tests.rs +++ b/crates/app-iced/src/components/buttons/tests.rs @@ -220,7 +220,7 @@ fn spec_of(variant: Variant, forced: Interaction) -> StyleSpec { border, shape: variant.shape(), radius_side: Geometry::of(&light(), variant, false).height, - forced, + forced: Some(forced), } } diff --git a/crates/app-iced/src/components/gallery/probe.rs b/crates/app-iced/src/components/gallery/probe.rs index 8f30f2d..b56e46c 100644 --- a/crates/app-iced/src/components/gallery/probe.rs +++ b/crates/app-iced/src/components/gallery/probe.rs @@ -22,7 +22,7 @@ use unlucky_design::typography::Platform; use crate::theme::{Families, Ui}; -use super::scan::{matches, nearer, rgb, run_at, Shot}; +use super::scan::{matches, nearer, rgb, Shot}; const FAMILIES: Families = Families { text: "Segoe UI", @@ -127,22 +127,15 @@ fn gallery_geometry_matches_the_tokens() { } let true_height = true_bottom - true_top + 1; - // 圆角档位:**量顶部那一行的宽度**。药丸(r = 高/2)顶部只有几像素宽; - // 若半径是 8(不是 full),顶行会是「几乎通栏」。这条比「数圆角行数」抗锯齿得多。 - let top_run = run_at( - &shot, - label_button.top, - label_button.left, - label_button.right, - rgb(ui.color(Role::Primary)), - ); + // 圆角档位:量**顶部那一行的左内缩**。药丸(r = 高/2 = 20)在离顶端 1px 处内缩 + // ≈ r − √(2r−1) = 13.8;r=8 只有 ≈4.1;直角为 0。比「数圆角行数」抗锯齿得多。 + let top_row_left = (label_button.left..=label_button.right) + .find(|x| matches(shot.at(*x, label_button.top), fill)) + .expect("顶部行应至少有一个填充像素"); + let top_inset = top_row_left - label_button.left; assert!( - (39..=41).contains(&true_height), - "可见高应 40(12b 1.1),实测 {true_height}" - ); - assert!( - (4..=20).contains(&top_run), - "药丸圆角应 = 20(12b 1.2,顶部一行的填充宽约 sqrt(2*20*1)*2 ≈ 9),实测 {top_run}px; 若接近整宽说明半径是 8 而不是 full" + (11..=17).contains(&top_inset), + "药丸圆角应 = 20(12b 1.2):顶部行左内缩应 ≈14,实测 {top_inset}(8 圆角 ≈4,直角 0)" ); // ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- Q9:IconButton 的字形不许撑满 40 盒 ----------------------------------- @@ -152,9 +145,11 @@ fn gallery_geometry_matches_the_tokens() { .iter() .filter(|bbox| (39..=41).contains(&bbox.width()) && (39..=41).contains(&bbox.height())) .count(); + // 只有 **enabled** 列是精确的 SecondaryContainer(hover/pressed/focus 列叠了状态层,颜色不同), + // 所以这里只要求 ≥1:量几何一只就够,五态的可见性由图本身给。 assert!( - icon_states >= 2, - "tonal IconButton 至少 2 个(实际 {icon_states})" + icon_states >= 1, + "tonal IconButton 至少 1 个(实际 {icon_states})" ); let gear = rgb(ui.color(Role::OnSecondaryContainer)); @@ -216,7 +211,7 @@ fn gallery_geometry_matches_the_tokens() { eprintln!( "[probe] OK flavor={:?}: Filled 药丸 {}×{}(参考 高 40 / 宽 ≥58) - 顶行填充宽 {}(full 圆角应为个位数,直角/8 圆角会接近整宽) + 顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0) 实心块 {} 个(含 40×40 IconButton) 分隔线长行 {} 行(每条 1px) 禁用容器 (on-surface@12%) 命中 @@ -224,7 +219,7 @@ fn gallery_geometry_matches_the_tokens() { flavor, label_button.width(), true_height, - top_run, + top_inset, primary.len(), runs, glyph_width diff --git a/crates/app-iced/src/components/gallery/scan.rs b/crates/app-iced/src/components/gallery/scan.rs index cea91c5..603cdf3 100644 --- a/crates/app-iced/src/components/gallery/scan.rs +++ b/crates/app-iced/src/components/gallery/scan.rs @@ -163,17 +163,3 @@ pub(super) fn nearer(pixel: [u8; 3], fill: [u8; 3], surface: [u8; 3]) -> bool { }; dist(pixel, fill) < dist(pixel, surface) } - -/// 某一行的水平区间 `[left, right]` 里,`color` 连续像素的最大长度(量药丸顶部宽度用)。 -pub(super) fn run_at(shot: &Shot, y: usize, left: usize, right: usize, color: [u8; 3]) -> usize { - let (mut longest, mut current) = (0, 0); - for x in left..=right { - if matches(shot.at(x, y), color) { - current += 1; - longest = longest.max(current); - } else { - current = 0; - } - } - longest -}