fix(app-iced): M68 r1 —— M69 迁移 + 守门补洞 + 药丸 56×32 + caption 图标 24 + 东侧热区回 1180
代码复审 p2-2items / 视觉复审 p2-2items 的修复轮(配色重拍等 M70 定案)。
M69 迁移(2 行)
- nav 轨宽:nav_rail_width(metrics) 函数体改用 metrics.nav_rail_width
- theme::STROKE_PX 改为 pub use unlucky_design::stroke::HAIRLINE as STROKE_PX
P2-1 零硬编码守门补洞(reviewer 造样 5 个漏网样本全部补上,并进自检正向断言)
- COLOR 调用表补 from_rgb8 / rgb8 / rgba(`from_rgb(` 子串匹配不到 `from_rgb8(` 是根因)
- 新增 `Color { r: .., g: .. }` 结构体字面量检查
- 尺寸规则遇到 Length::Fixed( / Padding::from( / Size::new( 往里剥一层再判数字
- 新增渲染层回归守门 pill_never_overrides_its_size_with_fill(禁 nav.rs 出现 center_*( ):
视觉复审 P2-1 的病根就是 center_*(Length::Fill) 把容器尺寸覆盖成 Fill
视觉 P2-1 药丸 88×32 → 56×32(实测 x=16..71)
- 去掉 center_x/center_y(Fill);居中改由父 column 的 align_x + 显式 width(轨宽) 完成
(只去掉 center_* 还不够:column 会 shrink 到 56,药丸贴左缘 x=0..55,是同一个坑的第二层)
视觉 P2-2 / 用户点名 caption 图标 46 → 24(按钮容器保持 46×48)
- button 内包 container(...).width(24).height(24).center(Length::Fill):
只设 width/height 会让容器缩到 24 并贴 button 内容左上角;center(Fill) 才既撑满又居中
视觉 P3-1 东侧/南侧缩放热区改成叠层
- 原来是 row 的兄弟列,吃掉 4px 布局宽(右留白 28、关闭键离窗角 4px);
现在用 stack! + 对齐容器叠在工作区上,布局宽回到 1180(卡片右缘 x=1155 = 右留白 24)
P3 清单
1. --data-dir 接受并忽略(翻牌前 CI 兼容;现役 smoke 命令行可原样跑,exit 0)+ 单测
2. --diff 内建 ±N px 平移扫描(--diff-scan),CI 不再依赖 numpy
3. 截图模式等 SystemTheme 到达才出图(否则暗色系统上会拍到浅色)
4. (Auto, 未编入 wgpu) 返回 tiny-skia,不再把 wgpu 写进候选列表
5. --renderer=gpu 也跳过适配器枚举(决策与探测无关,省 ~0.6~1.2s)
6. icons::handle 加 debug_assert_eq!(IconKind::ALL[kind as usize], kind)
7. 轨顶留白改 container padding-top,实测 24(原来 spacer + column spacing = 28)
顺带:shot.rs 拆成 shot/mod.rs + shot/diff.rs(≤400 行门禁)
门禁:fmt OK / clippy -D warnings 干净 / 46+5 测试全过 / cargo check --workspace --all-targets OK
体积:双后端 6,769,664 B(6.46 MiB)、仅 CPU 3,955,712 B(3.77 MiB)
截图:r1 代码验证图(r1-verify / hover / pressed / compare-nav-rail)已出;4 张交付主图待 M70 定案后重拍
This commit is contained in:
1 parent
14da7b3dec
commit
a1adf024ba
11 files changed
+599
-200
No files matched your search
@@ -20,7 +20,7 @@ mod nav;
|
||||
pub use nav::NavItem;
|
||||
|
||||
use iced::widget::text::LineHeight;
|
||||
use iced::widget::{button, column, container, mouse_area, row, space, text};
|
||||
use iced::widget::{button, column, container, mouse_area, row, space, stack, text};
|
||||
use iced::{mouse, Alignment, Background, Border, Color, Element, Length, Padding, Pixels, Shadow};
|
||||
use iced::{window::Direction, Theme as IcedTheme};
|
||||
|
||||
@@ -32,6 +32,7 @@ use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::cli::ForcedState;
|
||||
use crate::icons;
|
||||
use crate::theme::Ui;
|
||||
|
||||
@@ -42,39 +43,52 @@ pub fn view<'a>(
|
||||
app_title: &'a str,
|
||||
page_title: &'a str,
|
||||
body: &'a str,
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
let work_area = column![
|
||||
titlebar(ui, app_title),
|
||||
row![nav::rail(ui, selected), content(ui, page_title, body)]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
// 无边框窗口没有系统缩放边框 → 自己给两条热区(东/南)。西/北边见模块文档的已知缺口。
|
||||
let thickness = Length::Fixed(ui.space(Space::X1));
|
||||
|
||||
let shell = column![
|
||||
titlebar(ui, app_title, forced),
|
||||
row![
|
||||
work_area,
|
||||
resize_edge(
|
||||
Direction::East,
|
||||
(thickness, Length::Fill),
|
||||
mouse::Interaction::ResizingHorizontally,
|
||||
),
|
||||
nav::rail(ui, selected, forced),
|
||||
content(ui, page_title, body)
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
resize_edge(
|
||||
Direction::South,
|
||||
(Length::Fill, thickness),
|
||||
mouse::Interaction::ResizingVertically,
|
||||
),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
// 无边框窗口没有系统缩放边框 → 自己给两条热区(东/南)。
|
||||
// **必须叠层而不是兄弟列**:当兄弟列会吃掉 4px 布局宽(右留白变 28、caption 按钮离窗角 4px,
|
||||
// 视觉复审 P3-1)。叠层容器自己铺满,再用 align 把 4px 热区贴到边上,布局宽保持 1180。
|
||||
// 东侧热区从标题栏下方开始,避免吃掉三个 caption 按钮最右 4px 的点击区。
|
||||
let thickness = ui.space(Space::X1);
|
||||
|
||||
let east = container(
|
||||
column![
|
||||
space::vertical().height(ui.metrics().titlebar_height),
|
||||
resize_edge(
|
||||
Direction::East,
|
||||
(Length::Fixed(thickness), Length::Fill),
|
||||
mouse::Interaction::ResizingHorizontally,
|
||||
),
|
||||
]
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::End);
|
||||
|
||||
let south = container(resize_edge(
|
||||
Direction::South,
|
||||
(Length::Fill, Length::Fixed(thickness)),
|
||||
mouse::Interaction::ResizingVertically,
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_y(Alignment::End);
|
||||
|
||||
let shell = stack![work_area, east, south];
|
||||
|
||||
container(shell)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -101,7 +115,7 @@ fn resize_edge<'a>(
|
||||
|
||||
// ---------------------------------------------------------------- 标题栏
|
||||
|
||||
fn titlebar<'a>(ui: &'a Ui, title: &'a str) -> Element<'a, Message> {
|
||||
fn titlebar<'a>(ui: &'a Ui, title: &'a str, forced: Option<ForcedState>) -> Element<'a, Message> {
|
||||
let metrics = ui.metrics();
|
||||
|
||||
// 拖动热区:从窗口左缘到第一个按钮(按钮自己不能被拖区吞掉,所以只包左侧一段)
|
||||
@@ -122,9 +136,9 @@ fn titlebar<'a>(ui: &'a Ui, title: &'a str) -> Element<'a, Message> {
|
||||
|
||||
let bar = row![
|
||||
drag_area,
|
||||
caption(ui, IconKind::Minimize, Message::Minimize),
|
||||
caption(ui, IconKind::Maximize, Message::ToggleMaximize),
|
||||
caption(ui, IconKind::Close, Message::Close),
|
||||
caption(ui, IconKind::Minimize, Message::Minimize, forced),
|
||||
caption(ui, IconKind::Maximize, Message::ToggleMaximize, forced),
|
||||
caption(ui, IconKind::Close, Message::Close, forced),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(metrics.titlebar_height)
|
||||
@@ -138,41 +152,57 @@ fn titlebar<'a>(ui: &'a Ui, title: &'a str) -> Element<'a, Message> {
|
||||
}
|
||||
|
||||
/// 标题栏按钮(自绘最小化/最大化/关闭)。
|
||||
fn caption<'a>(ui: &'a Ui, kind: IconKind, message: Message) -> Element<'a, Message> {
|
||||
///
|
||||
/// 图标必须**再包一层固定 24×24 的容器**:iced 的 `button` 在 layout 里给内容 Fill 约束,
|
||||
/// 直接把 `svg(...).width(24)` 塞进 button,usvg 会按 Contain 撑满内区(实测渲染盒 46 → 视觉线宽
|
||||
/// 从 1.5 变 2.9,视觉复审 P2-2)。Container 尊重子元素的 Length,所以这层是「锁尺寸」用的。
|
||||
fn caption<'a>(
|
||||
ui: &'a Ui,
|
||||
kind: IconKind,
|
||||
message: Message,
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
let metrics = ui.metrics();
|
||||
|
||||
button(icons::view::<Message>(
|
||||
// 用 `center(Length::Fill)` 而不是只设 `width(24).height(24)`:后者会让容器缩到 24×24 并贴到
|
||||
// button 内容的左上角(button 不居中「收缩后」的内容,r1 实测图标跑到 cell 左上 (left+11.5, 11.5));
|
||||
// `center(Fill)` = 撑满按钮内容区 + 内容居中,字形仍是 24 盒、命中区仍是 46×48。
|
||||
let icon = container(icons::view::<Message>(
|
||||
kind,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
))
|
||||
.width(metrics.titlebar_button_width)
|
||||
.height(metrics.titlebar_height)
|
||||
.padding(0)
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background = match status {
|
||||
button::Status::Hovered => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
))),
|
||||
button::Status::Pressed => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Pressed,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
))),
|
||||
button::Status::Active | button::Status::Disabled => None,
|
||||
};
|
||||
.width(icons::RENDER_PX)
|
||||
.height(icons::RENDER_PX)
|
||||
.center(Length::Fill);
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: ui.color(Role::OnSurfaceVariant),
|
||||
border: Border::default(),
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
})
|
||||
.on_press(message)
|
||||
.into()
|
||||
button(icon)
|
||||
.width(metrics.titlebar_button_width)
|
||||
.height(metrics.titlebar_height)
|
||||
.padding(0)
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background = match overridden(forced, status) {
|
||||
button::Status::Hovered => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
))),
|
||||
button::Status::Pressed => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Pressed,
|
||||
Role::OnSurface,
|
||||
Role::Surface,
|
||||
))),
|
||||
button::Status::Active | button::Status::Disabled => None,
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background,
|
||||
text_color: ui.color(Role::OnSurfaceVariant),
|
||||
border: Border::default(),
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
})
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 内容区
|
||||
@@ -252,6 +282,15 @@ pub(super) fn label<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 取证用的状态覆盖(`--force-state`):只改画出来的状态,不动任何状态机。
|
||||
pub(super) fn overridden(forced: Option<ForcedState>, status: button::Status) -> button::Status {
|
||||
match forced {
|
||||
Some(ForcedState::Hover) => button::Status::Hovered,
|
||||
Some(ForcedState::Pressed) => button::Status::Pressed,
|
||||
None => status,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn filled(color: Color) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
//! | 颜色 | 5.6 选中图标 `OnSecondaryContainer` / 选中标签 `Secondary` / 未选中 `OnSurfaceVariant` | `Ui::color` |
|
||||
//! | 项间距 | 5.7 **4** | `LayoutMetrics::nav_item_gap` |
|
||||
|
||||
use iced::widget::{button, column, container, space};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Shadow, Theme as IcedTheme};
|
||||
use iced::widget::{button, column, container};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
|
||||
|
||||
use unlucky_design::color::{Role, StateLayer};
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
@@ -23,8 +23,9 @@ use unlucky_design::spacing::{LayoutMetrics, Space};
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::cli::ForcedState;
|
||||
use crate::icons;
|
||||
use crate::shell::{filled, label};
|
||||
use crate::shell::{filled, label, overridden};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 导航占位项(任务要求 4~6 个;真正的页面由后续 mission 接管)。
|
||||
@@ -76,20 +77,25 @@ impl NavItem {
|
||||
}
|
||||
|
||||
/// 整条导航轨。
|
||||
pub(super) fn rail<'a>(ui: &'a Ui, selected: NavItem) -> Element<'a, Message> {
|
||||
pub(super) fn rail<'a>(
|
||||
ui: &'a Ui,
|
||||
selected: NavItem,
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
let metrics = ui.metrics();
|
||||
let indicator = indicator_size(metrics);
|
||||
|
||||
let mut items: Vec<Element<'a, Message>> =
|
||||
vec![space::vertical().height(ui.space(Space::X6)).into()];
|
||||
|
||||
for item in NavItem::ALL {
|
||||
items.push(rail_item(ui, item, item == selected, indicator));
|
||||
}
|
||||
let items = NavItem::ALL
|
||||
.iter()
|
||||
.map(|item| rail_item(ui, *item, *item == selected, indicator, forced))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 轨顶留白走 container padding,而不是「先塞一个 spacer 元素」:后者会被 column 的
|
||||
// spacing(4)再加一档,实测变 28(视觉复审 P3-5)。
|
||||
container(column(items).spacing(metrics.nav_item_gap))
|
||||
.width(nav_rail_width(metrics))
|
||||
.height(Length::Fill)
|
||||
.padding(Padding::from([ui.space(Space::X6), 0.0]))
|
||||
.style(|_theme| filled(ui.layer(MaterialLayer::Canvas)))
|
||||
.into()
|
||||
}
|
||||
@@ -99,6 +105,7 @@ fn rail_item<'a>(
|
||||
item: NavItem,
|
||||
selected: bool,
|
||||
indicator: (f32, f32),
|
||||
forced: Option<ForcedState>,
|
||||
) -> Element<'a, Message> {
|
||||
let (indicator_width, indicator_height) = indicator;
|
||||
|
||||
@@ -114,11 +121,14 @@ fn rail_item<'a>(
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
|
||||
// **不要用 `center_x/center_y(Fill)`**:那会把容器自己的 width/height 改成 Fill,
|
||||
// 于是 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)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.style(move |_theme| container::Style {
|
||||
background: selected.then(|| Background::Color(ui.color(Role::SecondaryContainer))),
|
||||
border: Border {
|
||||
@@ -129,11 +139,14 @@ fn rail_item<'a>(
|
||||
});
|
||||
|
||||
let item_height = nav_item_height(ui, indicator_height);
|
||||
// 列必须显式占满轨宽,否则它会 shrink 到 56,`align_x(Center)` 就变成空操作,
|
||||
// 药丸会贴左缘(x=0..55 而不是 16..71)——r1 实测踩到的第二个坑。
|
||||
let stack = column![
|
||||
pill,
|
||||
label(ui, TypeRole::LabelMedium, false, item.label(), label_color),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(nav_rail_width(ui.metrics()))
|
||||
.align_x(Alignment::Center)
|
||||
.height(item_height);
|
||||
|
||||
@@ -142,7 +155,7 @@ fn rail_item<'a>(
|
||||
.height(item_height)
|
||||
.padding(0)
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background = match status {
|
||||
let background = match overridden(forced, status) {
|
||||
button::Status::Hovered if !selected => Some(Background::Color(ui.state_over(
|
||||
StateLayer::Hover,
|
||||
Role::OnSurface,
|
||||
@@ -168,21 +181,19 @@ fn rail_item<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 导航轨宽度(**M69 迁移点**)。
|
||||
/// 导航轨宽度(M69 起是令牌层的 `nav_rail_width`)。
|
||||
///
|
||||
/// M69(`141e9b1`)把 `LayoutMetrics` 的 nav 字段做了语义纠正:
|
||||
///
|
||||
/// | 旧 | 新 |
|
||||
/// |---|---|
|
||||
/// | `nav_expanded_width = 88` | **`nav_rail_width = 88`**(值不变,只是名字从「展开」改成「轨」) |
|
||||
/// | `nav_expanded_width = 88` | **`nav_rail_width = 88`**(值不变,名字从「展开」改成「轨」) |
|
||||
/// | `nav_collapsed_width = 48` | 删除(48 连 56 宽的选中指示器都放不下) |
|
||||
/// | —(缺) | 新增 `nav_drawer_width = 240`(D33 的展开态抽屉) |
|
||||
/// | —(缺) | `nav_drawer_width = 240`(D33 的展开态抽屉,本 crate 未用) |
|
||||
///
|
||||
/// 本 crate 的分支基线(`0aac307`)还没有 M69,所以这里只能写旧字段名;**等 M69 合入后
|
||||
/// 把函数体那一行改成 `metrics.nav_rail_width` 即可,其余代码与视觉零变化**。
|
||||
/// 把三处调用点收敛到这个函数,就是为了让迁移永远只有一行。
|
||||
/// 三处调用点都走这里:将来令牌再改名,仍然只改这一行。
|
||||
fn nav_rail_width(metrics: LayoutMetrics) -> f32 {
|
||||
metrics.nav_expanded_width
|
||||
metrics.nav_rail_width
|
||||
}
|
||||
|
||||
/// 选中指示器尺寸(12b 5.4 参考 56×32),全部由令牌推导、不写死:
|
||||
|
||||
Reference in new issue
Block a user