feat(settings): M299 选中项收口到 selected_fill + 分组导航改真按钮 + tabs 用 status

- pages/settings/frame.rs:分组导航从 mouse_area 换成 button(四态可辨),
  几何不变(ShapeScale::Full 胶囊),颜色走唯一的 selected_fill;forced 从
  settings::view 递下来,于是 --force-state 能拍到这一处
- language / theme / appearance choice 三处「选中档位」补状态层
- components/selection/paint/chip.rs:选中分支用上 state 参数
- shell/nav.rs / shell/nav/account.rs:两处私有拷贝收口到 selected_fill(几何没动)
- components/navigation/tabs.rs:状态层在样式闭包里跟 button::Status 求值
- 新增门禁 selection/tests/single_entry.rs:11 个站点必须调 selected_fill +
  产品代码里不许再出现 or_else(|| Some(ui.color( 这个私有拷贝形态
- 行数:frame.rs / nav.rs / appearance::section 的单测按「纯搬移」拆出去
This commit is contained in:
AskaEth committed 2026-11-13 14:41:57 +08:00
1 parent 9b6e92c7b4
commit 0d917e88da
16 files changed
+1072 -355

No files matched your search

+12 -76
View File
@@ -261,21 +261,24 @@ pub(super) fn state_layer_box(metrics: LayoutMetrics) -> (f32, f32) {
indicator_size(metrics)
}
/// 药丸底色(**唯一**实现点):hover / pressed / 过渡帧都画在这块 56×32 上。
/// 药丸底色(**几何**的唯一实现点):hover / pressed / 过渡帧都画在这块 56×32 上。
///
/// 选中/未选中**两条分支不许合并**(照 `components/navigation/drawer/item.rs` 的 `pill_color`):
/// 选中项静息是实色 `SecondaryContainer`,缺层时兜底到那个角色;未选中项静息不画药丸,
/// 兜底就必须是 `None`。经 [`interaction::layer_color`] 求值 ⇒ 没有取样控件时**逐位**等于原来的
/// `state_over(..)`(`--force-state=rest` 的取证图因此一字不变)。
/// **颜色**只有一个来源:选中支走 [`interaction::selected_fill`](全仓唯一的选中底色入口,
/// M299 收口了这里的私有拷贝),未选中支走 [`interaction::layer_color`]。选中/未选中**两条分支
/// 不许合并**:选中项静息是实色 `SecondaryContainer`(`selected_fill` 的兜底),未选中项静息
/// 不画药丸(`layer_color` 给 `None`)。两条都经状态层求值 ⇒ 没有取样控件时**逐位**等于原来的
/// `state_over(..)` / `ui.color(..)`(`--force-state=rest` 的取证图因此一字不变)。
fn pill_color(ui: &Ui, state: Interaction, selected: bool) -> Option<Color> {
if selected {
interaction::layer_color(
// 选中项走**全仓唯一的**选中底色入口(M299):静息 = 实色 `SecondaryContainer`,
// hover / pressed / 过渡帧叠在它上面。此前这里有一份私有拷贝,M296 的变异实验证明
// 它不跟助手一起动。
Some(interaction::selected_fill(
ui,
state,
Role::OnSecondaryContainer,
Role::SecondaryContainer,
)
.or_else(|| Some(ui.color(Role::SecondaryContainer)))
))
} else {
// 导航轨自己画的就是 `MaterialLayer::Canvas`(见 [`rail`] 的 `.style(..)`):状态层要叠在
// **同一个**角色上,否则 hover 色与轨底色对不上。
@@ -295,71 +298,4 @@ pub fn nav_item_height(ui: &Ui, indicator_height: f32) -> f32 {
}
#[cfg(test)]
mod tests {
use super::*;
use unlucky_design::color::SchemeFlavor;
use unlucky_design::typography::Platform;
use crate::theme::Families;
fn ui() -> Ui {
Ui::with_platform(
SchemeFlavor::Light,
Platform::Windows,
Families {
text: "Segoe UI",
display: "Segoe UI",
mono: "Consolas",
},
)
}
#[test]
fn indicator_matches_the_reference_56x32() {
let (width, height) = indicator_size(LayoutMetrics::DESKTOP);
assert_eq!((width, height), (56.0, 32.0), "12b 5.4 的参考值");
}
#[test]
fn nav_item_height_is_indicator_plus_gap_plus_label() {
let ui = ui();
let (_, indicator_height) = indicator_size(ui.metrics());
// 32 + 4 + 16
assert_eq!(nav_item_height(&ui, indicator_height), 52.0);
}
#[test]
fn every_nav_item_has_a_label_and_an_icon() {
assert!((4..=6).contains(&NavItem::ALL.len()));
for item in NavItem::ALL {
assert!(!item.label(Lang::En).is_empty(), "{item:?} 没有文案");
assert!(!item.icon().icon().path.is_empty(), "{item:?} 没有图标");
}
}
/// **M108**:导航栏上这一项叫 `Versions`(88px 轨放不下全名 `Version management`)。
///
/// 文案本身走目录(M106 的 `label_key()` + `label(lang)`),这条只守**取值**与**宽度余量**:
///
/// - **抓得住**:① 有人把 `Key::NavDownload` 的英文改回「下载」或塞回 18 字符的全名
/// (88px 轨上会截断/换行,实测全名 ≈101px);② 中文被「为了对称」砍成两个字。
/// - **抓不住**:真画出来的截断(那要出图,见 M108 报告里的产品通道与 pages_demo 图);
/// 也抓不住 240 展开抽屉(那一档在产品里还不可达)。
#[test]
fn the_download_item_is_labelled_version_management() {
// 88px 轨的余量:中文 4 字 ≈48px、英文短名 ≈45px,都该有明显的余量
assert_eq!(NavItem::Download.label(Lang::En), "Versions");
assert_eq!(NavItem::Download.label(Lang::ZhCn), "版本管理");
for item in NavItem::ALL {
let label = item.label(Lang::En);
assert!(
!label.to_ascii_lowercase().contains("download"),
"{item:?} 的英文标签还在说下载:{label}"
);
assert!(
label.chars().count() <= 12,
"{item:?} 的标签太长,88px 的轨放不下:{label}"
);
}
}
}
mod tests;