fix(iced/feedback): 圆角/描边带来的 probe 阈值订正 + Dialog 宽度逃生口接线
上一轮被 2 小时上限打断前的工作,逐条落地(都已在本地编译 + 单测 17 passed + 8 组 probe 中 6 组通过、empty-light 差 ±1 阈值): - `keyframes::sample` 的 debug_assert 收窄成「首点不在 0ms **且** 首值不是 0」才炸: 原先 `debug_assert_eq!(first.ms, 0)` 会把 `SEG_TAIL_1/2`(首点 250/900ms,值为 0)也判红, 而那一类的「保持首值」与 Compose 的「动画初值 0」等价,是合法的。真正危险的是 ROTATION 那种 「首值 ≠ 0」(会画出不存在的静止段),现在只拦这一种。 - `empty_state::block_layer()` 改成 `pub(super) const BLOCK_LAYER`:函数版在非 test 构建里 是 dead_code(`-D warnings` 会红),改成常量后组件与 probe 共用一个来源。 - `probe/panels.rs`:图标块尺寸容差放到 ±2(块上补的 1px `OutlineVariant` 描边各占 1 行, 浅色实测块色行 94、深色 97);顺带去掉没用到的 `matches` import。 - `tests/progress.rs`:三处期望值按实现对齐 —— 旋转每周期 **4 圈**(不是 1 圈)、 右段宽 **0.505**(0.355..0.860,手算 0.516 偏了 0.01)、单调扫描只扫 `[0,1)`(phase==1.0 就是下一周期的 0,回绕正常)。 - `dialog.rs`:`Fill + max_width(480)` **实测不生效**(Row 给 Fill 子项的 limits 是 min=max=分配额,`Limits::max_width()` 的 `.max(self.min.width)` 会把 480 顶回去, 两列并排实测渲染成 548)⇒ 改回 `Fixed(min(可用宽, 480))`,并加 `Dialog::width(available)` 让页面递可用宽;gallery 里接线用 `HOST_WIDE`(=800 ⇒ 仍取 480)。能力缺口登记在注释里 (与 G17-d 同一堵墙,报告里有 548 的实测)。 未完成(下一轮/合并前补):`cmp/dialog-*` 并排图要按 REF_H≈420 重出(动作行被裁掉)、 empty-light 的 ±2 阈值要复跑确认、clippy/test 门禁要贴真实 `test result:` 行。
This commit is contained in:
1 parent
38b1e7a4dd
commit
10d4f86fdf
7 files changed
+63
-29
No files matched your search
@@ -82,6 +82,7 @@ pub struct Dialog<'a, M> {
|
||||
title: &'a str,
|
||||
body: &'a str,
|
||||
actions: DialogActions<'a, M>,
|
||||
max_width: f32,
|
||||
state: Option<Interaction>,
|
||||
}
|
||||
|
||||
@@ -92,10 +93,21 @@ impl<'a, M> Dialog<'a, M> {
|
||||
title,
|
||||
body,
|
||||
actions: DialogActions::None,
|
||||
// 项目取值 480(官方区间 280..560 内);窄宿主由页面传更小的可用宽。
|
||||
max_width: 480.0,
|
||||
state: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 可用宽(默认 480,组件取 `min(available, 480)`)。
|
||||
///
|
||||
/// 为什么不让组件自己从宿主推:iced 的 `Length` 没有 min-width,而 `Fill + max_width`
|
||||
/// 会被父级的精确分配顶掉(实测渲染成 548 而不是 480,见 `dialog()` 里的说明)。
|
||||
pub fn width(mut self, available: f32) -> Self {
|
||||
self.max_width = available;
|
||||
self
|
||||
}
|
||||
|
||||
/// 单动作。
|
||||
pub fn confirm(mut self, label: &'a str, on_press: M) -> Self {
|
||||
self.actions = DialogActions::Single { label, on_press };
|
||||
@@ -168,12 +180,16 @@ pub fn dialog<'a, M: Clone + 'a>(ui: &'a Ui, spec: Dialog<'a, M>) -> Element<'a,
|
||||
));
|
||||
}
|
||||
|
||||
// 宽度 = `min(宿主, 480)`:**不能**写 `Length::Fixed(480)`(复审 P2-1)——
|
||||
// 窗口窄于 528 时对话框会直接溢出宿主,而 `.max_width(480)` 对 `Fixed` 是空操作。
|
||||
// `Fill` + `max_width` 才是 iced 里表达「至多 480」的唯一办法(无 min-width,见 G17-d)。
|
||||
// 宽度 = `min(spec.max_width, 480)`,**可用宽由调用方递**(`Dialog::width`)。
|
||||
//
|
||||
// 复审 P2-1 建议「Fill + max_width」:实测**不行**。`Row` 给 `Fill` 子项的 limits 是
|
||||
// `with_compression(min = max = 分配额)`,而 `Limits::max_width()` 的实现是
|
||||
// `self.max.width.min(max_width).max(self.min.width)` —— 父级那个 **min 会把 480 顶回去**,
|
||||
// 两列并排时每张卡渲染成 **548**(探针实测,见报告)。iced 没有 min-width,也没有在
|
||||
// 「父级精确分配」下可用的 max-width ⇒ 与 Snackbar 的最小宽同一堵墙(G17-d 之二)。
|
||||
// 所以:默认 480,窄宿主由页面传 `Dialog::width(可用宽)`。
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.max_width(geometry.width)
|
||||
.width(Length::Fixed(spec.max_width.min(geometry.width)))
|
||||
.padding(Padding::from(geometry.padding))
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(surface)),
|
||||
|
||||
@@ -136,12 +136,7 @@ pub fn empty_state<'a, M: Clone + 'a>(ui: &'a Ui, spec: EmptyState<'a, M>) -> El
|
||||
/// 图标块的材质层(`[估]`:空态块是「抬起来的卡片」层级,不是画布也不是浮层)。
|
||||
///
|
||||
/// probe 与测试都从这里取,避免「组件一个色、断言另一个色」。
|
||||
const BLOCK_LAYER: MaterialLayer = MaterialLayer::Overlay;
|
||||
|
||||
/// 图标块的实际材质层(probe / 测试用)。
|
||||
pub fn block_layer() -> MaterialLayer {
|
||||
BLOCK_LAYER
|
||||
}
|
||||
pub(super) const BLOCK_LAYER: MaterialLayer = MaterialLayer::Overlay;
|
||||
|
||||
/// 固定高度的透明留白(列间距的唯一来源:`Column::spacing` 必须保持 0,否则双算)。
|
||||
fn gap<'a, M: 'a>(height: f32) -> Element<'a, M> {
|
||||
|
||||
@@ -61,9 +61,15 @@ impl Keyframes {
|
||||
}
|
||||
|
||||
let cycle = self.cycle_ms as f32;
|
||||
debug_assert_eq!(
|
||||
first.ms, 0,
|
||||
"首点必须在 0ms:Compose 会补一个 t=0 的初值点,我们不猜,请显式写出来"
|
||||
// 首点不在 0ms 时,`[0, first.ms)` 按「保持首值」算;Compose 那里放的是动画**初值**。
|
||||
// 两者只在「首值 = 初值 = 0」时等价 —— 不等价就会画出一段不存在的静止(ROTATION 曾因此
|
||||
// 每周期末尾倒跳 270°)。所以这里只拦「首点不在 0 且首值不是 0」。
|
||||
debug_assert!(
|
||||
first.ms == 0 || first.value == 0.0,
|
||||
"首点 {}ms 的值是 {}:Compose 在 [0, {}ms) 用的是动画初值 0,不是首值",
|
||||
first.ms,
|
||||
first.value,
|
||||
first.ms
|
||||
);
|
||||
let ms = phase.rem_euclid(1.0) * cycle;
|
||||
if ms <= first.ms as f32 {
|
||||
|
||||
@@ -14,10 +14,8 @@ use unlucky_design::color::Role;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
|
||||
use super::super::dialog::scrim;
|
||||
use super::super::empty_state::block_layer;
|
||||
use super::super::pixel::{
|
||||
band_width, bands, ink_inside, matches, near, rgb, row_ink, runs, BBox, Shot,
|
||||
};
|
||||
use super::super::empty_state::BLOCK_LAYER;
|
||||
use super::super::pixel::{band_width, bands, ink_inside, near, rgb, row_ink, runs, BBox, Shot};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// Dialog 宽(本项目取值,官方区间 280..560 内)。
|
||||
@@ -131,23 +129,26 @@ pub(super) fn check_dialog(shot: &Shot, ui: &Ui) {
|
||||
/// §15:图标块 96(`[估]`)+ 间距 16 / 8 / 24 + CTA 40。
|
||||
pub(super) fn check_empty(shot: &Shot, ui: &Ui) {
|
||||
let surface = ui.color(Role::Surface);
|
||||
let block = ui.layer(block_layer());
|
||||
let block = ui.layer(BLOCK_LAYER);
|
||||
|
||||
// 下限 40 而不是 88:块只有 96 宽,中间的图标(32)会把那几行的块色墨量压到 ≈64。
|
||||
let found = bands(shot, block, surface, 40);
|
||||
// 容差 ±2:块上补了 1px `OutlineVariant` 描边,块色行/列各少 1(浅色实测 94,深色 97)。
|
||||
let (top, bottom) = found
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by_key(|(top, bottom)| bottom - top)
|
||||
.filter(|(top, bottom)| near(bottom - top + 1, BLOCK))
|
||||
.filter(|(top, bottom)| (BLOCK - 2..=BLOCK + 2).contains(&(bottom - top + 1)))
|
||||
.unwrap_or_else(|| {
|
||||
panic!("15.1:没找到高 {BLOCK} 的图标块(`[估]` 项目自定值),实测量到的带:{found:?}")
|
||||
panic!(
|
||||
"15.1:没找到高 {BLOCK}(±2) 的图标块(`[估]` 项目自定值),实测量到的带:{found:?}"
|
||||
)
|
||||
});
|
||||
|
||||
let width = band_width(shot, (top, bottom), block, surface);
|
||||
assert!(
|
||||
near(width, BLOCK),
|
||||
"15.1:图标块必须 {BLOCK}×{BLOCK},实测宽 {width}"
|
||||
(BLOCK - 2..=BLOCK + 2).contains(&width),
|
||||
"15.1:图标块必须 {BLOCK}(±2,1px 描边)宽,实测 {width}"
|
||||
);
|
||||
|
||||
// ---- P2-1 回归:块↔标题的**实际**间距(16 而不是 32)-------------------------
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::theme::{to_iced, Families, Ui};
|
||||
use super::super::interaction::{self, Interaction};
|
||||
use super::dialog::scrim;
|
||||
use super::panel::{DialogGeometry, EmptyStateGeometry, SnackbarGeometry};
|
||||
use super::progress::Progress;
|
||||
use super::snackbar::{Placement, Snackbar};
|
||||
use super::{circular, dialog, empty_state, linear, snackbar, Dialog, EmptyState};
|
||||
|
||||
@@ -116,7 +117,11 @@ fn empty_state_geometry_is_the_project_defined_set() {
|
||||
assert_eq!(geometry.gap_body, 8.0, "标题↔副文案 = Space::X2");
|
||||
assert_eq!(geometry.gap_action, 24.0, "副文案↔CTA = Space::X6");
|
||||
assert_eq!(geometry.block, 96.0);
|
||||
assert_eq!(super::empty_state::block_layer(), MaterialLayer::Overlay);
|
||||
assert_eq!(
|
||||
super::empty_state::BLOCK_LAYER,
|
||||
MaterialLayer::Overlay,
|
||||
"块底色是 SurfaceContainerHigh(材质层 Overlay)"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 结构 / 角色 ------------------------------------------------------------
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//! (代码 r1 P1-B:旧实现只有 1 圈,且末尾倒跳 270°)。
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
|
||||
use super::super::geometry::{phase_segments, CircularGeometry, LinearGeometry};
|
||||
use super::super::keyframes::{KeyPoint, Keyframes};
|
||||
@@ -103,7 +104,10 @@ fn raw_segments_are_unordered_and_layout_sorts_them() {
|
||||
assert!((left_start - 0.003).abs() < 0.01, "左段起点 {left_start}");
|
||||
assert!((left - 0.076).abs() < 0.01, "左段宽 {left}");
|
||||
assert!((right_start - 0.355).abs() < 0.01, "右段起点 {right_start}");
|
||||
assert!((right - 0.516).abs() < 0.01, "右段宽 {right}");
|
||||
assert!(
|
||||
(right - 0.505).abs() < 0.01,
|
||||
"右段宽 {right}(0.355..0.860)"
|
||||
);
|
||||
|
||||
let total: f32 = laid.iter().map(|(_, fraction)| fraction).sum();
|
||||
assert!(
|
||||
@@ -156,9 +160,10 @@ fn circular_indeterminate_sweeps_the_official_range() {
|
||||
"弧长下限 0.1(CircularIndeterminateMinProgress),实测 {min}"
|
||||
);
|
||||
assert!((max - 0.87).abs() < 1.0e-3, "弧长上限 0.87,实测 {max}");
|
||||
// P1-B:每周期 4 圈(1080° 全局 + 360° 关键帧),不是 1 圈
|
||||
assert!(
|
||||
(rotation - 1.0).abs() < 1.0e-3,
|
||||
"旋转应走满一整圈,实测 {rotation}"
|
||||
(rotation - 4.0).abs() < 0.01,
|
||||
"旋转应走满 4 圈,实测 {rotation}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,8 +197,9 @@ fn circular_indeterminate_rotation_is_four_turns_and_continuous() {
|
||||
);
|
||||
|
||||
// 旋转必须单调(相位 0 → 1 逐帧不回退,按圈算)
|
||||
// 只扫 [0, 1) 的相位:phase == 1.0 就是下一周期的 0,回绕是**正常的**(mod 1 等价)。
|
||||
let mut previous = geometry.indeterminate(0.0).1;
|
||||
for step in 1..=600 {
|
||||
for step in 1..600 {
|
||||
let phase = step as f32 / 600.0;
|
||||
let current = geometry.indeterminate(phase).1;
|
||||
assert!(
|
||||
|
||||
@@ -278,7 +278,12 @@ fn empty_block<'a>(ui: &'a Ui) -> Element<'a, DemoMessage> {
|
||||
fn dialog_block<'a>(ui: &'a Ui, full: bool) -> Element<'a, DemoMessage> {
|
||||
let press = |id| DemoMessage::Pressed(id);
|
||||
|
||||
let plain = feedback::dialog(ui, Dialog::new("已是最新版本", "当前没有可用更新。"));
|
||||
// 页面把自己的**可用宽**递给对话框(组件取 `min(可用宽, 480)`)——窄窗口时靠这一手,
|
||||
// 组件自己无法从宿主推(iced 的 Length 没有 min-width,见 `Dialog::width` 的说明)。
|
||||
let plain = feedback::dialog(
|
||||
ui,
|
||||
Dialog::new("已是最新版本", "当前没有可用更新。").width(HOST_WIDE),
|
||||
);
|
||||
let single = feedback::dialog(
|
||||
ui,
|
||||
Dialog::new("已导入整合包", "共 42 个模组,缺失 0 个。")
|
||||
|
||||
Reference in new issue
Block a user