fix(app-iced): M79 r2 —— 四条 review 修复(对齐/单行/可比对象/整行分隔线)

代码 r1 P2-1 = 视觉 r1 P2-1(同一处,必修)
- `app_bar::action`:容器只给了 `align_y` → 24px 图标贴 40×40 命中区左缘(实测前导墨迹
  `38..58`,应 `44..68`;hover 的圆形状态层也露在图标一侧)。补 `align_x(Center)`;
  同时页头内容行补 `align_y(Center)`(`Container` 默认 Top,整行原本贴顶)。
- 把 Q21 升格成规则并全目录复查(结论写进 `navigation/mod.rs` 的表):
  app_bar 命中区/页头行、tabs tab 内容、drawer 药丸 = 两轴显式居中;
  list_item 文本块与 drawer 分组标题 = 左上起排(`Start` 是对的,不需要居中)。
  回归钉子:`navigation::probe` 的 appbar 断言量「左内缩 == 右内缩 ±2」。

代码 r1 P2-2(必修):四件文本都没有单行约束,容器高度却是固定值
- `app_bar`(标题/副标题)、`list_item::line`、`drawer::line`、`tabs` 标签统一
  `.wrapping(Wrapping::None)` + 外层 `clip(true)`;页头/ListiItem/drawer 的文本块改成
  `Fill + clip`,长文本切在文本区里,不折行、不顶穿、不压 trailing 动作。
- iced 没有 ellipsis → 按 `inputs/menu.rs` 的 D19 先例登记 **D-M79-5**
  (官方要单行 ellipsis,我们单行 + 超长直接切割)。
- 取证:gallery 新增 `ULL_NAV_PART=long`(长实例名整块)+ `navigation/probe/long.rs`
  量「行高仍 44/60/76 / 页头仍 56 / 标题墨迹右缘 < trailing 左缘」。取证块按
  `selection::matrix` 的先例收进 `navigation/matrix.rs`(gallery 单文件贴 400 红线)。

视觉 r1 P2-2(必修):ListItem 三列内容不同 → 「选中 trailing = Primary」没有可比对象
- 三列都挂 `trailing(settings)`,只有 selected 列走 `Primary`(其余 `OnSurface` / 38%)。

视觉 r1 P2-2(可选,已做):Secondary 分隔线逐 tab 分段 + 0.5 圆角留了 1px 亮缝
- 改成**整行一条**:`Stack` 把一条 `Fill` 的 1px `SurfaceVariant` 压在 tab 行底下
  (底座是 tab 行本身 → 线长 = tab 行总宽,横向滚动跟着走)。
- `TabsGeometry::bar_radius(bool)` 收成 `indicator_radius()`(只有 3px 的 Primary 条要圆角),
  单测同步;probe 新增「Secondary 分隔线必须 1 段」的断言。

门禁:`cargo fmt --all --check` ✓ · `cargo build -p unlucky-app-iced --lib` 零 warning ✓
(截图/探针/测试的实时输出见完成报告;本轮宿主机被多条并行线的 rustc 打满,
bin 链接排队严重,如未取到会如实标注未完成项。)
This commit is contained in:
AskaEth committed 2026-11-02 11:56:02 +08:00
1 parent 81e34d77da
commit a36f542f28
13 files changed
+471 -88

No files matched your search

@@ -38,7 +38,7 @@
//! tab 宽度因此是**内容宽**而不是「行宽 ÷ n」——官方 `ScrollableTabRow` 的口径。
//! `TabMinWidth = 90` **本轮不施加**(iced 无 min-width,登记在 G23)。
use iced::widget::text::LineHeight;
use iced::widget::text::{LineHeight, Wrapping};
use iced::widget::{button, container, scrollable, space, text, Row, Stack};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow};
@@ -181,25 +181,26 @@ fn content_color(ui: &Ui, primary: bool, state: Interaction, selected: bool) ->
}
}
/// 指示条 / 分隔线的颜色(Primary 只给选中项;Secondary 每项都有)。
fn bar_color(ui: &Ui, primary: bool, state: Interaction, selected: bool) -> Option<Color> {
if primary {
// 禁用项的指示条也降档(否则「点不动却亮着」)
if !selected {
return None;
}
if state.is_disabled() {
return Some(interaction::disabled_content_over_role(
ui,
Role::OnSurface,
false,
));
}
Some(ui.color(Role::Primary))
} else {
// 6.6:Secondary 的分隔线 `SurfaceVariant`(不是 OutlineVariant——那是 §22 的分隔线)
Some(ui.color(Role::SurfaceVariant))
/// Primary 指示条的颜色(只有选中项有;禁用项降档,否则「点不动却亮着」)。
fn bar_color(ui: &Ui, state: Interaction, selected: bool) -> Option<Color> {
if !selected {
return None;
}
if state.is_disabled() {
return Some(interaction::disabled_content_over_role(
ui,
Role::OnSurface,
false,
));
}
Some(ui.color(Role::Primary))
}
/// 6.6:Secondary 的分隔线色 = **`SurfaceVariant`**(不是 `OutlineVariant`——那是 §22 的分隔线)。
///
/// 色值只在这里出现一次:整行那条线与逐 tab 的实现都从这里取。
fn secondary_line_color(ui: &Ui) -> Color {
ui.color(Role::SurfaceVariant)
}
/// 一行标签页(自带横向滚动)。
@@ -220,7 +221,31 @@ pub fn tabs<'a, M: Clone + 'a>(ui: &'a Ui, tabs: Tabs<'a, M>) -> Element<'a, M>
row = row.push(tab_cell(ui, tab, geometry, primary, height));
}
scrollable(row)
let body: Element<'a, M> = if primary {
row.into()
} else {
// A1 的 Secondary 分隔线是**整行一条直线**,不是每个 tab 一段:
// 逐段画会在相邻段之间留缝(视觉 r1 P2-2)。这里用 `Stack` 把一条 Fill 线压在整行底下,
// 底座是 tab 行本身(Shrink → 内容宽),所以线长 = tab 行总宽,横向滚动时跟着走。
let line = container(space::vertical())
.width(Length::Fill)
.height(Length::Fixed(geometry.secondary_bar))
.style(move |_theme| container::Style {
background: Some(Background::Color(secondary_line_color(ui))),
..container::Style::default()
});
Stack::new()
.push(container(row))
.push(
container(line)
.width(Length::Fill)
.height(Length::Fill)
.align_y(Alignment::End),
)
.into()
};
scrollable(body)
.direction(scrollable::Direction::Horizontal(
scrollable::Scrollbar::hidden(),
))
@@ -249,6 +274,8 @@ fn tab_cell<'a, M: Clone + 'a>(
let label = text(tab.label)
.size(style.size_px)
.line_height(LineHeight::Absolute(Pixels(style.line_height_px)))
// 单行(容器高固定 48/64):长标签让 tab 变宽 + 横向滚动,不折行(偏离 D-M79-5)
.wrapping(Wrapping::None)
.font(ui.font_of(style))
.color(color);
@@ -266,6 +293,8 @@ fn tab_cell<'a, M: Clone + 'a>(
// 底层:内容自己决定宽度(Shrink),底部留出条的高度 —— 未选中的 Primary tab 也占同样的高
let base = container(content)
.height(Length::Fixed(height))
// Q21:要给居中就两轴都显式给(`Container` 默认 Left/Top)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.padding(Padding {
top: 0.0,
@@ -274,8 +303,13 @@ fn tab_cell<'a, M: Clone + 'a>(
left: geometry.pad,
});
let bar = bar_color(ui, primary, state, selected);
let radius = geometry.bar_radius(primary);
// Primary:逐 tab 画条(要跟随该 tab 的宽度);Secondary:整行一条,在 `tabs()` 里画
let bar = if primary {
bar_color(ui, state, selected)
} else {
None
};
let radius = geometry.indicator_radius();
let mut stack = Stack::new().push(base);
if let Some(color) = bar {
// 上层:Fill —— `Stack` 先按底层定尺寸,所以这里的 Fill = 这个 tab 的宽(不是整行)