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:
1 parent
26ebf9a944
commit
813ec88d89
3 files changed
+236
-58
No files matched your search
@@ -13,9 +13,11 @@
|
||||
//! | 项间距 | 5.7 **4** | `LayoutMetrics::nav_item_gap` |
|
||||
|
||||
use iced::widget::{button, column, container};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
|
||||
use iced::{
|
||||
Alignment, Background, Border, Color, Element, Length, Padding, 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;
|
||||
@@ -29,6 +31,7 @@ pub use account::RailAccount;
|
||||
|
||||
use account::account_item;
|
||||
|
||||
use crate::app::components::interaction::{self, Interaction};
|
||||
use crate::app::Message;
|
||||
use crate::cli::ForcedState;
|
||||
use crate::icons;
|
||||
@@ -112,7 +115,7 @@ pub(super) fn rail<'a>(
|
||||
.iter()
|
||||
// `ALL` 本来就不含 `Account`(它固定在轨底),filter 只是把这条意图写明。
|
||||
.filter(|item| **item != NavItem::Account)
|
||||
.map(|item| rail_item(ui, *item, *item == selected, indicator, compact, forced, lang))
|
||||
.map(|item| rail_item(ui, *item, *item == selected, compact, forced, lang))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 轨顶留白走 container padding,而不是「先塞一个 spacer 元素」:后者会被 column 的
|
||||
@@ -141,12 +144,15 @@ fn rail_item<'a>(
|
||||
ui: &'a Ui,
|
||||
item: NavItem,
|
||||
selected: bool,
|
||||
indicator: (f32, f32),
|
||||
compact: bool,
|
||||
forced: Option<ForcedState>,
|
||||
lang: Lang,
|
||||
) -> Element<'a, Message> {
|
||||
let (indicator_width, indicator_height) = indicator;
|
||||
// 状态层(hover / pressed / 过渡帧)落在**药丸**上,与选中指示器同一块几何(M283)。
|
||||
let (pill_width, pill_height) = state_layer_box(ui.metrics());
|
||||
// 取证态与运行期态是**同一个** `Interaction`:强制态由 `overridden` 直接推(`None` → `Active`
|
||||
// → `Enabled`),运行期的真态由 `interaction::tracked` 的取样层在 draw 里给。
|
||||
let state = Interaction::from_status(overridden(forced, button::Status::Active));
|
||||
|
||||
let icon_color = if selected {
|
||||
ui.color(Role::OnSecondaryContainer)
|
||||
@@ -164,25 +170,23 @@ fn rail_item<'a>(
|
||||
// 于是 56 被父级(button 宽 88)覆盖成 88、32 靠布局凑巧(视觉复审 P2-1)。
|
||||
// 尺寸只认令牌推导值,居中交给父 column 的 `align_x`。
|
||||
let pill = container(icons::view::<Message>(item.icon(), icon_color))
|
||||
.width(indicator_width)
|
||||
.height(indicator_height)
|
||||
.width(pill_width)
|
||||
.height(pill_height)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.style(move |_theme| container::Style {
|
||||
background: selected.then(|| Background::Color(ui.color(Role::SecondaryContainer))),
|
||||
// 状态层画在**药丸**上(M283):hover / pressed / 过渡帧都只这一块 56×32。
|
||||
// 之前它摊在 88×52 的按钮上(`Border::default()` ⇒ r=0),同一个元素并存两种形状。
|
||||
background: pill_color(ui, state, selected).map(Background::Color),
|
||||
border: Border {
|
||||
radius: ui.radius(ShapeScale::Full, indicator_height),
|
||||
radius: ui.radius(ShapeScale::Full, pill_height),
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
});
|
||||
|
||||
let item_height = nav_item_height(ui, indicator_height);
|
||||
let height = if compact {
|
||||
indicator_height
|
||||
} else {
|
||||
item_height
|
||||
};
|
||||
let item_height = nav_item_height(ui, pill_height);
|
||||
let height = if compact { pill_height } else { item_height };
|
||||
// 列必须显式占满轨宽,否则它会 shrink 到 56,`align_x(Center)` 就变成空操作,
|
||||
// 药丸会贴左缘(x=0..55 而不是 16..71)——r1 实测踩到的第二个坑。
|
||||
let mut stack = column![pill]
|
||||
@@ -201,35 +205,26 @@ fn rail_item<'a>(
|
||||
}
|
||||
stack = stack.spacing(ui.space(Space::X1));
|
||||
|
||||
button(stack)
|
||||
let clickable: Element<'a, Message> = button(stack)
|
||||
.width(nav_rail_width(ui.metrics()))
|
||||
.height(height)
|
||||
.padding(0)
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background = match overridden(forced, status) {
|
||||
button::Status::Hovered if !selected => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
))),
|
||||
button::Status::Pressed if !selected => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Pressed,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
))),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: label_color,
|
||||
border: Border::default(),
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
.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(item))
|
||||
.into()
|
||||
.into();
|
||||
// A8:运行期态套状态取样控件(强制态 = 取证,**不套**:取证图不许有过渡中间帧)。
|
||||
if forced.is_none() {
|
||||
interaction::tracked(clickable, true)
|
||||
} else {
|
||||
clickable
|
||||
}
|
||||
}
|
||||
|
||||
/// 导航轨宽度(M69 起是令牌层的 `nav_rail_width`)。
|
||||
@@ -258,6 +253,41 @@ pub fn indicator_size(metrics: LayoutMetrics) -> (f32, f32) {
|
||||
)
|
||||
}
|
||||
|
||||
/// 状态层(hover / pressed / 过渡帧)画在**哪块矩形**上:药丸,与选中指示器同一块几何(M283)。
|
||||
///
|
||||
/// 这是那块矩形的唯一定义点——`rail_item` 用它定尺寸,`shell::tests` 的几何判据钉住它。
|
||||
/// 它**不是**按钮的 `nav_rail_width × 项高`(88×52):那正是用户点名的「父容器整个方形区域加深」。
|
||||
pub(super) fn state_layer_box(metrics: LayoutMetrics) -> (f32, f32) {
|
||||
indicator_size(metrics)
|
||||
}
|
||||
|
||||
/// 药丸底色(**唯一**实现点):hover / pressed / 过渡帧都画在这块 56×32 上。
|
||||
///
|
||||
/// 选中/未选中**两条分支不许合并**(照 `components/navigation/drawer/item.rs` 的 `pill_color`):
|
||||
/// 选中项静息是实色 `SecondaryContainer`,缺层时兜底到那个角色;未选中项静息不画药丸,
|
||||
/// 兜底就必须是 `None`。经 [`interaction::layer_color`] 求值 ⇒ 没有取样控件时**逐位**等于原来的
|
||||
/// `state_over(..)`(`--force-state=rest` 的取证图因此一字不变)。
|
||||
fn pill_color(ui: &Ui, state: Interaction, selected: bool) -> Option<Color> {
|
||||
if selected {
|
||||
interaction::layer_color(
|
||||
ui,
|
||||
state,
|
||||
Role::OnSecondaryContainer,
|
||||
Role::SecondaryContainer,
|
||||
)
|
||||
.or_else(|| Some(ui.color(Role::SecondaryContainer)))
|
||||
} else {
|
||||
// 导航轨自己画的就是 `MaterialLayer::Canvas`(见 [`rail`] 的 `.style(..)`):状态层要叠在
|
||||
// **同一个**角色上,否则 hover 色与轨底色对不上。
|
||||
interaction::layer_color(
|
||||
ui,
|
||||
state,
|
||||
Role::OnSurface,
|
||||
MaterialLayer::Canvas.surface_role(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 导航项高度 = 指示器 + 4 + 标签行高 = 32 + 4 + 16 = 52(`LM.nav_item_height` 是列表行节奏,
|
||||
/// 药丸 + 标签的轨项放不进 44,偏差登记在交接文档里)。
|
||||
pub fn nav_item_height(ui: &Ui, indicator_height: f32) -> f32 {
|
||||
|
||||
Reference in new issue
Block a user