Merge branch 'feat/navpill-rail-hover-pressed-state-layer-o'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
bc2ad85c63
3 files changed
+236
-58
No files matched your search
@@ -13,9 +13,11 @@
|
|||||||
//! | 项间距 | 5.7 **4** | `LayoutMetrics::nav_item_gap` |
|
//! | 项间距 | 5.7 **4** | `LayoutMetrics::nav_item_gap` |
|
||||||
|
|
||||||
use iced::widget::{button, column, container};
|
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::elevation::MaterialLayer;
|
||||||
use unlucky_design::icons::IconKind;
|
use unlucky_design::icons::IconKind;
|
||||||
use unlucky_design::shape::ShapeScale;
|
use unlucky_design::shape::ShapeScale;
|
||||||
@@ -29,6 +31,7 @@ pub use account::RailAccount;
|
|||||||
|
|
||||||
use account::account_item;
|
use account::account_item;
|
||||||
|
|
||||||
|
use crate::app::components::interaction::{self, Interaction};
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::cli::ForcedState;
|
use crate::cli::ForcedState;
|
||||||
use crate::icons;
|
use crate::icons;
|
||||||
@@ -112,7 +115,7 @@ pub(super) fn rail<'a>(
|
|||||||
.iter()
|
.iter()
|
||||||
// `ALL` 本来就不含 `Account`(它固定在轨底),filter 只是把这条意图写明。
|
// `ALL` 本来就不含 `Account`(它固定在轨底),filter 只是把这条意图写明。
|
||||||
.filter(|item| **item != NavItem::Account)
|
.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<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// 轨顶留白走 container padding,而不是「先塞一个 spacer 元素」:后者会被 column 的
|
// 轨顶留白走 container padding,而不是「先塞一个 spacer 元素」:后者会被 column 的
|
||||||
@@ -141,12 +144,15 @@ fn rail_item<'a>(
|
|||||||
ui: &'a Ui,
|
ui: &'a Ui,
|
||||||
item: NavItem,
|
item: NavItem,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
indicator: (f32, f32),
|
|
||||||
compact: bool,
|
compact: bool,
|
||||||
forced: Option<ForcedState>,
|
forced: Option<ForcedState>,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
) -> Element<'a, Message> {
|
) -> 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 {
|
let icon_color = if selected {
|
||||||
ui.color(Role::OnSecondaryContainer)
|
ui.color(Role::OnSecondaryContainer)
|
||||||
@@ -164,25 +170,23 @@ fn rail_item<'a>(
|
|||||||
// 于是 56 被父级(button 宽 88)覆盖成 88、32 靠布局凑巧(视觉复审 P2-1)。
|
// 于是 56 被父级(button 宽 88)覆盖成 88、32 靠布局凑巧(视觉复审 P2-1)。
|
||||||
// 尺寸只认令牌推导值,居中交给父 column 的 `align_x`。
|
// 尺寸只认令牌推导值,居中交给父 column 的 `align_x`。
|
||||||
let pill = container(icons::view::<Message>(item.icon(), icon_color))
|
let pill = container(icons::view::<Message>(item.icon(), icon_color))
|
||||||
.width(indicator_width)
|
.width(pill_width)
|
||||||
.height(indicator_height)
|
.height(pill_height)
|
||||||
.align_x(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.style(move |_theme| container::Style {
|
.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 {
|
border: Border {
|
||||||
radius: ui.radius(ShapeScale::Full, indicator_height),
|
radius: ui.radius(ShapeScale::Full, pill_height),
|
||||||
..Border::default()
|
..Border::default()
|
||||||
},
|
},
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let item_height = nav_item_height(ui, indicator_height);
|
let item_height = nav_item_height(ui, pill_height);
|
||||||
let height = if compact {
|
let height = if compact { pill_height } else { item_height };
|
||||||
indicator_height
|
|
||||||
} else {
|
|
||||||
item_height
|
|
||||||
};
|
|
||||||
// 列必须显式占满轨宽,否则它会 shrink 到 56,`align_x(Center)` 就变成空操作,
|
// 列必须显式占满轨宽,否则它会 shrink 到 56,`align_x(Center)` 就变成空操作,
|
||||||
// 药丸会贴左缘(x=0..55 而不是 16..71)——r1 实测踩到的第二个坑。
|
// 药丸会贴左缘(x=0..55 而不是 16..71)——r1 实测踩到的第二个坑。
|
||||||
let mut stack = column![pill]
|
let mut stack = column![pill]
|
||||||
@@ -201,35 +205,26 @@ fn rail_item<'a>(
|
|||||||
}
|
}
|
||||||
stack = stack.spacing(ui.space(Space::X1));
|
stack = stack.spacing(ui.space(Space::X1));
|
||||||
|
|
||||||
button(stack)
|
let clickable: Element<'a, Message> = button(stack)
|
||||||
.width(nav_rail_width(ui.metrics()))
|
.width(nav_rail_width(ui.metrics()))
|
||||||
.height(height)
|
.height(height)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.style(move |_theme: &IcedTheme, status| {
|
.style(move |_theme: &IcedTheme, _status| button::Style {
|
||||||
let background = match overridden(forced, status) {
|
// 底色**恒为 `None`**:状态层由药丸给(M283,同一个元素不许并存两种形状)。
|
||||||
button::Status::Hovered if !selected => Some(Background::Color(ui.state_over(
|
background: None,
|
||||||
StateLayer::Hover,
|
text_color: label_color,
|
||||||
Role::OnSurface,
|
border: Border::default(),
|
||||||
Role::Surface,
|
shadow: Shadow::default(),
|
||||||
))),
|
snap: true,
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.on_press(Message::Select(item))
|
.on_press(Message::Select(item))
|
||||||
.into()
|
.into();
|
||||||
|
// A8:运行期态套状态取样控件(强制态 = 取证,**不套**:取证图不许有过渡中间帧)。
|
||||||
|
if forced.is_none() {
|
||||||
|
interaction::tracked(clickable, true)
|
||||||
|
} else {
|
||||||
|
clickable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导航轨宽度(M69 起是令牌层的 `nav_rail_width`)。
|
/// 导航轨宽度(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` 是列表行节奏,
|
/// 导航项高度 = 指示器 + 4 + 标签行高 = 32 + 4 + 16 = 52(`LM.nav_item_height` 是列表行节奏,
|
||||||
/// 药丸 + 标签的轨项放不进 44,偏差登记在交接文档里)。
|
/// 药丸 + 标签的轨项放不进 44,偏差登记在交接文档里)。
|
||||||
pub fn nav_item_height(ui: &Ui, indicator_height: f32) -> f32 {
|
pub fn nav_item_height(ui: &Ui, indicator_height: f32) -> f32 {
|
||||||
|
|||||||
@@ -4,14 +4,16 @@
|
|||||||
//! (未登录/已登录两态、窄高收成图标条的判据、点击跳账户页)。
|
//! (未登录/已登录两态、窄高收成图标条的判据、点击跳账户页)。
|
||||||
|
|
||||||
use iced::widget::{button, column, container, text};
|
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::icons::IconKind;
|
||||||
use unlucky_design::shape::ShapeScale;
|
use unlucky_design::shape::ShapeScale;
|
||||||
use unlucky_design::spacing::Space;
|
use unlucky_design::spacing::Space;
|
||||||
use unlucky_design::typography::TypeRole;
|
use unlucky_design::typography::TypeRole;
|
||||||
|
|
||||||
|
use crate::app::components::interaction::{self, Interaction};
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::cli::ForcedState;
|
use crate::cli::ForcedState;
|
||||||
use crate::icons;
|
use crate::icons;
|
||||||
@@ -75,6 +77,9 @@ pub(super) fn account_item<'a>(
|
|||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let (indicator_width, indicator_height) = indicator;
|
let (indicator_width, indicator_height) = indicator;
|
||||||
let active = selected == NavItem::Account;
|
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 {
|
let label_color = if active {
|
||||||
ui.color(Role::Secondary)
|
ui.color(Role::Secondary)
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +102,8 @@ pub(super) fn account_item<'a>(
|
|||||||
.align_x(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.style(move |_theme| container::Style {
|
.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 {
|
border: Border {
|
||||||
radius: ui.radius(ShapeScale::Full, avatar_size(ui, indicator_width)),
|
radius: ui.radius(ShapeScale::Full, avatar_size(ui, indicator_width)),
|
||||||
..Border::default()
|
..Border::default()
|
||||||
@@ -115,6 +121,8 @@ pub(super) fn account_item<'a>(
|
|||||||
.align_x(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.style(move |_theme| container::Style {
|
.style(move |_theme| container::Style {
|
||||||
|
// 未登录的头像静息只有描边:状态层照样落在**这个圆**里(M283)。
|
||||||
|
background: avatar_color(ui, state, false).map(Background::Color),
|
||||||
border: Border {
|
border: Border {
|
||||||
color: ui.color(Role::Outline),
|
color: ui.color(Role::Outline),
|
||||||
width: 1.0,
|
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()))
|
.width(nav_rail_width(ui.metrics()))
|
||||||
.height(height)
|
.height(height)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.style(move |_theme: &IcedTheme, status| button::Style {
|
.style(move |_theme: &IcedTheme, _status| button::Style {
|
||||||
background: match overridden(forced, status) {
|
// 底色**恒为 `None`**:状态层由头像圆给(M283,同一个元素不许并存两种形状)。
|
||||||
button::Status::Hovered if !active => Some(Background::Color(ui.state_over(
|
background: None,
|
||||||
StateLayer::Hover,
|
|
||||||
Role::OnSurface,
|
|
||||||
Role::Surface,
|
|
||||||
))),
|
|
||||||
button::Status::Pressed if !active => Some(Background::Color(ui.state_over(
|
|
||||||
StateLayer::Pressed,
|
|
||||||
Role::OnSurface,
|
|
||||||
Role::Surface,
|
|
||||||
))),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
text_color: label_color,
|
text_color: label_color,
|
||||||
border: Border::default(),
|
border: Border::default(),
|
||||||
shadow: Shadow::default(),
|
shadow: Shadow::default(),
|
||||||
snap: true,
|
snap: true,
|
||||||
})
|
})
|
||||||
.on_press(Message::Select(NavItem::Account))
|
.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 里没有这个控件,按令牌栅格取整)。
|
/// 头像位边长 = 指示器宽 − 16,但不超过 48(12b 里没有这个控件,按令牌栅格取整)。
|
||||||
|
|||||||
@@ -129,3 +129,123 @@ fn caption_icon_shrinks_without_touching_the_container() {
|
|||||||
"caption 图标必须小于官方 24(缩小是用户点名的)"
|
"caption 图标必须小于官方 24(缩小是用户点名的)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **M283 几何判据**:状态层(hover / pressed)画的矩形 == 药丸 [`nav::indicator_size`](56×32),
|
||||||
|
/// **不是**按钮的轨宽 × 项高(88×52)。
|
||||||
|
///
|
||||||
|
/// 抓得住:谁把状态层改回「铺满整条轨宽」(旧的 `button(stack).style(..)` 铺底写法)——那条路
|
||||||
|
/// 下这块矩形就是 88×52,两个 `assert_ne` 立刻红。
|
||||||
|
/// 抓不住:真画出来的像素(那要出图,见 `DevDocs/artifacts/navpill/`);也抓不住颜色对不对
|
||||||
|
/// (那归 `interaction` 的 `layer_color` 单测)。
|
||||||
|
#[test]
|
||||||
|
fn the_state_layer_is_drawn_on_the_pill_not_across_the_rail() {
|
||||||
|
use unlucky_design::shape::ShapeScale;
|
||||||
|
|
||||||
|
use super::nav::{indicator_size, nav_item_height, state_layer_box};
|
||||||
|
|
||||||
|
let ui = ui(SchemeFlavor::Light);
|
||||||
|
let metrics = ui.metrics();
|
||||||
|
let pill = state_layer_box(metrics);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pill,
|
||||||
|
indicator_size(metrics),
|
||||||
|
"状态层与选中指示器必须是同一块矩形"
|
||||||
|
);
|
||||||
|
assert_eq!(pill, (56.0, 32.0), "12b 5.4 的指示器尺寸");
|
||||||
|
assert_ne!(
|
||||||
|
pill.0, metrics.nav_rail_width,
|
||||||
|
"状态层不许铺满轨宽({})——那正是用户点名的方形加深",
|
||||||
|
metrics.nav_rail_width
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
pill,
|
||||||
|
(metrics.nav_rail_width, nav_item_height(&ui, pill.1)),
|
||||||
|
"状态层也不许铺满整个按钮(轨宽 × 项高 = 88×52)"
|
||||||
|
);
|
||||||
|
|
||||||
|
let radius = ui.radius(ShapeScale::Full, pill.1);
|
||||||
|
assert_eq!(
|
||||||
|
(radius.top_left, radius.bottom_right),
|
||||||
|
(pill.1 / 2.0, pill.1 / 2.0),
|
||||||
|
"Full ⇒ r=16:药丸四角必须是空的,不能有 hover 色"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **M283 结构判据**:状态层不许回到按钮上(扫源码,不渲染)。
|
||||||
|
///
|
||||||
|
/// `button(stack).style(..)` 的闭包里出现 `Background::Color` 就是「同一个元素并存两种形状」
|
||||||
|
/// 回来了——选中画 56×32 圆角药丸、hover 却铺 88×52 直角底(用户原话:父容器整个方形区域加深)。
|
||||||
|
///
|
||||||
|
/// 抓得住:①有人把 `button::Status` 分支接回按钮的 `style`;②`interaction::tracked` 被套到
|
||||||
|
/// 强制态上(取证图会出现过渡中间帧);③药丸尺寸改回按钮尺寸(`state_layer_box` 的调用点消失)。
|
||||||
|
/// 抓不住:颜色对不对、真画出来是圆的还是方的——前者归 `layer_color` 的单测,后者归出图。
|
||||||
|
#[test]
|
||||||
|
fn the_rail_never_paints_its_state_layer_on_the_button() {
|
||||||
|
let sources = [
|
||||||
|
("shell/nav.rs", include_str!("nav.rs")),
|
||||||
|
("shell/nav/account.rs", include_str!("nav/account.rs")),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, source) in sources {
|
||||||
|
let mut buttons = 0;
|
||||||
|
for (start, _) in source.match_indices("button(stack)") {
|
||||||
|
let rest = &source[start..];
|
||||||
|
let end = rest
|
||||||
|
.find(".on_press(")
|
||||||
|
.unwrap_or_else(|| panic!("{name}:`button(stack)` 之后找不到 `.on_press(`"));
|
||||||
|
let builder = &rest[..end];
|
||||||
|
assert!(
|
||||||
|
!builder.contains("Background::Color"),
|
||||||
|
"{name}:按钮的 style 闭包里又铺底了 —— 状态层必须落在药丸/头像上"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
builder.contains("background: None"),
|
||||||
|
"{name}:按钮的 `background` 必须恒为 `None`"
|
||||||
|
);
|
||||||
|
buttons += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(buttons, 1, "{name}:期望正好一处 `button(stack)`");
|
||||||
|
|
||||||
|
// `tracked` 只许在 `forced.is_none()` 时套(强制态 = 取证,不许有过渡中间帧)
|
||||||
|
let lines = source.lines().collect::<Vec<_>>();
|
||||||
|
let mut tracked = 0;
|
||||||
|
for (index, line) in lines.iter().enumerate() {
|
||||||
|
if !line.contains("interaction::tracked(") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracked += 1;
|
||||||
|
let guard = index.saturating_sub(2);
|
||||||
|
assert!(
|
||||||
|
lines[guard..index]
|
||||||
|
.iter()
|
||||||
|
.any(|line| line.contains("forced.is_none()")),
|
||||||
|
"{name}:{}:`tracked` 必须只在 `forced.is_none()` 时套(取证图不许有过渡)",
|
||||||
|
index + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(tracked, 1, "{name}:期望正好一处 `interaction::tracked(`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 另一半:药丸/头像那一侧必须真的经 `layer_color` 求值,且药丸尺寸取自几何定义点
|
||||||
|
let nav = include_str!("nav.rs");
|
||||||
|
let account = include_str!("nav/account.rs");
|
||||||
|
assert!(
|
||||||
|
nav.contains("pill_color(ui, state, selected)"),
|
||||||
|
"nav.rs:药丸底色必须经 pill_color 求值"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
account.contains("avatar_color(ui, state,"),
|
||||||
|
"account.rs:头像底色必须经 avatar_color 求值"
|
||||||
|
);
|
||||||
|
for (name, source) in [("nav.rs", nav), ("account.rs", account)] {
|
||||||
|
assert!(
|
||||||
|
source.contains("interaction::layer_color"),
|
||||||
|
"{name}:状态层必须经 layer_color 求值(没有取样控件时逐位退回 overlay_color)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
nav.contains("state_layer_box(ui.metrics())"),
|
||||||
|
"nav.rs:药丸尺寸必须取自 `state_layer_box`(几何判据钉的就是它)"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user