fix(app-iced): 导航轨 hover/pressed 状态层落回药丸(M283)

`rail_item` 里同一个元素并存两种形状:选中画 56×32 `radius(Full)` 的药丸,
而 hover/pressed 走 `button(stack).style(..)`,底色铺在整个按钮上
(88×52,`Border::default()` ⇒ r=0)。用户原话就是这一处:「侧边栏按钮
hover/click 是父容器整个方形区域颜色加深,而不是 MD3E 应有的胶囊 UI」。

改法照 `components/navigation/drawer/item.rs`(全仓唯一正确的导航实现):

- 状态层改由药丸自己的 `container.style` 求值,经 `interaction::layer_color`;
  选中/未选中两条分支不合并(选中静息兜底 `SecondaryContainer`,未选中兜底 `None`);
- `base` 取 `MaterialLayer::Canvas.surface_role()`(导航轨画的就是 Canvas),不写字面量角色;
- `button.style` 的 `background` 恒为 `None` —— 按钮不再有任何形状上的主张;
- `interaction::tracked` 只在 `forced.is_none()` 时套(强制态是取证,不许有过渡中间帧);
- 轨底账户位同型:状态层落在头像那个 Ø40 圆上,不是 88×52 的按钮;
- 药丸几何收敛到 `state_layer_box()` 一个定义点(`rail_item` 与单测都读它)。

判据:
- `shell::tests::the_state_layer_is_drawn_on_the_pill_not_across_the_rail`(几何)
- `shell::tests::the_rail_never_paints_its_state_layer_on_the_button`(结构:扫源码,
  `button(stack)..on_press` 闭包里不许出现 `Background::Color`、`tracked` 必须有
  `forced.is_none()` 守卫)。两条都做过反向验证:把写法改回去,两条同时红。

取证(`DevDocs/artifacts/navpill/`):`--force-state=rest` 与改前**逐字节相同**
(md5 1ebb611b3edc156925ca802d3f4fa76d);hover/pressed 的高亮区从 88×52 矩形
变成 56×32 药丸(四角为空),账户位从 88×52 变成 Ø40 圆。
This commit is contained in:
AskaEth committed 2026-11-12 21:42:30 +08:00
1 parent 26ebf9a944
commit 813ec88d89
3 files changed
+236 -58

No files matched your search

+47 -19
View File
@@ -4,14 +4,16 @@
//! (未登录/已登录两态、窄高收成图标条的判据、点击跳账户页)。
use iced::widget::{button, column, container, text};
use iced::{Alignment, Background, Border, Element, Length, Shadow, Theme as IcedTheme};
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme as IcedTheme};
use unlucky_design::color::{Role, StateLayer};
use unlucky_design::color::Role;
use unlucky_design::elevation::MaterialLayer;
use unlucky_design::icons::IconKind;
use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::app::components::interaction::{self, Interaction};
use crate::app::Message;
use crate::cli::ForcedState;
use crate::icons;
@@ -75,6 +77,9 @@ pub(super) fn account_item<'a>(
) -> Element<'a, Message> {
let (indicator_width, indicator_height) = indicator;
let active = selected == NavItem::Account;
// 取证态与运行期态是**同一个** `Interaction`:强制态由 `overridden` 直接推(`None` → `Active`
// → `Enabled`),运行期的真态由 `interaction::tracked` 的取样层在 draw 里给。
let state = Interaction::from_status(overridden(forced, button::Status::Active));
let label_color = if active {
ui.color(Role::Secondary)
} else {
@@ -97,7 +102,8 @@ pub(super) fn account_item<'a>(
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.color(Role::SecondaryContainer))),
// 状态层画在**头像这个圆**上(M283),不再摊到 88×52 的按钮上。
background: avatar_color(ui, state, true).map(Background::Color),
border: Border {
radius: ui.radius(ShapeScale::Full, avatar_size(ui, indicator_width)),
..Border::default()
@@ -115,6 +121,8 @@ pub(super) fn account_item<'a>(
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.style(move |_theme| container::Style {
// 未登录的头像静息只有描边:状态层照样落在**这个圆**里(M283)。
background: avatar_color(ui, state, false).map(Background::Color),
border: Border {
color: ui.color(Role::Outline),
width: 1.0,
@@ -145,31 +153,51 @@ pub(super) fn account_item<'a>(
);
}
button(stack)
let clickable: Element<'a, Message> = button(stack)
.width(nav_rail_width(ui.metrics()))
.height(height)
.padding(0)
.style(move |_theme: &IcedTheme, status| button::Style {
background: match overridden(forced, status) {
button::Status::Hovered if !active => Some(Background::Color(ui.state_over(
StateLayer::Hover,
Role::OnSurface,
Role::Surface,
))),
button::Status::Pressed if !active => Some(Background::Color(ui.state_over(
StateLayer::Pressed,
Role::OnSurface,
Role::Surface,
))),
_ => None,
},
.style(move |_theme: &IcedTheme, _status| button::Style {
// 底色**恒为 `None`**:状态层由头像圆给(M283,同一个元素不许并存两种形状)。
background: None,
text_color: label_color,
border: Border::default(),
shadow: Shadow::default(),
snap: true,
})
.on_press(Message::Select(NavItem::Account))
.into()
.into();
// A8:运行期态套状态取样控件(强制态 = 取证,**不套**:取证图不许有过渡中间帧)。
if forced.is_none() {
interaction::tracked(clickable, true)
} else {
clickable
}
}
/// 头像圆的底色(**唯一**实现点):hover / pressed / 过渡帧都画在这个 Ø40 圆上。
///
/// 两条分支不许合并(照 `drawer/item.rs` 的 `pill_color`):已登录的头像静息是实色
/// `SecondaryContainer`,缺层时兜底到那个角色;未登录的头像静息只有描边、不填底,
/// 兜底就必须是 `None`。
fn avatar_color(ui: &Ui, state: Interaction, signed_in: bool) -> Option<Color> {
if signed_in {
interaction::layer_color(
ui,
state,
Role::OnSecondaryContainer,
Role::SecondaryContainer,
)
.or_else(|| Some(ui.color(Role::SecondaryContainer)))
} else {
// 头像直接摆在导航轨上(轨画的是 `MaterialLayer::Canvas`):状态层要叠在同一个角色上。
interaction::layer_color(
ui,
state,
Role::OnSurface,
MaterialLayer::Canvas.surface_role(),
)
}
}
/// 头像位边长 = 指示器宽 − 16,但不超过 48(12b 里没有这个控件,按令牌栅格取整)。