fix(app-iced): M102 页面几何 —— 正文块贴左吃满页宽、列表末行不再被裁半行、日志按词断行

① 正文列与页头不对齐(下载 / 日志):根因是 `frame::single_column` 把**行宽上限 424**
当成**整页块宽度**并水平居中(1180 下 1044 里只摆 424,左缘 422 对页头标题 112)。
现在两个量分开:`layout` 的 `column` 只管成段散文(≤424),页面块宽走新的
`frame::body_width`(= available / main_pane)。新增 `frame::body`(贴左吃满)与
`frame::prose`(散文锁 424)。1180×760 下下载 / 日志的块左缘 = 112 = 页头标题左缘。

② 实例列表末行被裁半行:页级行距 `Space::X1`(4) 让行节奏变成 64、与可视高度除不尽,
且滚动区直贴卡片 1px 圆角描边。行距归零(下载页同改)+ 滚动区底部内缩 `Space::X2`(8)
⇒ 可视区 580−32−8 = 540 = 9×60,Redstone Lab 及其副标题完整可见,第 10 行整体在裁切线外。

③ 日志断行把单词劈开(`graphics.gl_o` / `peration_failure`):新增
`layout::wrap::breakable`,在 `.` `_` `:` `/` `\` 后插零宽空格(U+200B 软断点),
日志行正文与一行摘要都过它;悬挂缩进不变(前缀仍是独立文本控件),
`Line::text` 与 `Logs::summary()`(剪贴板)仍是原文。工具函数放 `pages/layout/`:
断点是「列宽之后」的另一半排版判据,纯函数、可单测、不碰控件树。

护栏:`body_block_starts_at_the_header_title_and_fills_the_page`、
`list_viewport_fits_whole_rows_and_still_overflow`(把 580/32/8/540/60 五个数钉住)、
`wrap::tests`(断点位置 / 不重复插 / 可还原 / 幂等)、`log_text_carries_soft_breaks_but_keeps_the_original`。
`download/tests.rs` 的「宽窗留白」口径按新分工改成「宽度归块、不归散文列」。
This commit is contained in:
AskaEth committed 2026-11-04 19:53:38 +08:00
1 parent 8574d6ba2d
commit 89d98184c1
15 files changed
+466 -53

No files matched your search

+25 -6
View File
@@ -4,8 +4,8 @@
//!
//! | 版面 | 结构 | 退化行为 |
//! |---|---|---|
//! | `ListDetail`(窗口 ≥840) | 左列 336 固定、**独立滚动**;右列详情,正文列 ≤424 | — |
//! | `SingleColumn`(<840) | 列表与详情**整页切换**:本页只渲染详情列(≤424,居中) | 顶部给「Back to instances」,列表退成上一屏 |
//! | `ListDetail`(窗口 ≥840) | 左列 336 固定、**独立滚动**;右列详情,散文 ≤424 | — |
//! | `SingleColumn`(<840) | 列表与详情**整页切换**:本页只渲染详情列(块贴左吃满页宽,散文 ≤424) | 顶部给「Back to instances」,列表退成上一屏 |
//!
//! 为什么窄窗不是「上下堆」:官方 compact 下 list-detail 的口径是一次只显示一个 pane(列表 → 详情,
//! 整页切换)。上下堆会把详情挤到折叠线以下,等于详情不存在。
@@ -27,6 +27,21 @@
//! M84 的行高是「`row_height` 44 + `Space::X3` 12 = 56」的手工值;真 `ListItem` 的两行档是
//! **60**(令牌 `row_height` 44 + 每多一行 16,A1 是 72)。差异来自组件自己的几何真源,
//! 页面不许覆盖——这正是「接真组件」的应有结果。列表仍高于可视区,独立滚动条照样可见。
//!
//! ## 行距归零 + 底部内缩(M102,视觉复审 ②)
//!
//! 视觉复审在 1180×760 的截图里抓到「末行 Redstone Lab 被卡片底边切掉半行(副标题消失)」。
//! 根因是**两道页级附加量**:`Space::X1` 的行距让行节奏变成 64,与可视高度除不尽;
//! 列表滚动区贴到卡片的 1px 圆角描边上,切点正好落在描边线上。修法两件:
//!
//! | 改什么 | 值 | 结果 |
//! |---|---|---|
//! | 页级行距 | 4 → **0**(见 `download/rows.rs` 同一条) | 行节奏回到组件的 60 |
//! | 滚动区底部内缩 | 0 → **`Space::X2` 8** | 切点落在卡片的内边距带里,不压描边 |
//!
//! 于是可视区 = 580 − 列头 32 − 内缩 8 = **540 = 9 × 60**,正好 9 整行:末行完整可见,
//! 第 10 行整体落在裁切线之外(`instances/tests.rs::list_viewport_fits_whole_rows_and_still_overflow`
//! 把这四个数钉住)。列表内容 12×60 = 720 仍高过可视区,独立滚动条照样可见。
mod data;
mod demo;
@@ -60,8 +75,11 @@ use super::layout::{FrameMetrics, PageLayout};
use super::model::{Instances, Model, Row};
use super::{Action, Message};
/// 列表行的间距(`Space::X1`;与 M84 同值,行高差异全部来自 `ListItem` 自己的几何)。
const ROW_GAP: Space = Space::X1;
/// 列表滚动区的**底部内缩**(`Space::X2`):让裁切线落在卡片的内边距带里,而不是压在 1px 描边上。
///
/// 行距归零(`list()` 不写 `.spacing(…)`)与这条内缩是一对,理由见模块文档「行距归零(M102)」
/// 与本页 `instances.rs` 的同名条目。
const LIST_BOTTOM_INSET: Space = Space::X2;
/// 整页(`block.detail = None` 时详情区显示空态)。
pub fn view<'a>(ui: &'a Ui, metrics: FrameMetrics, model: &'a Model) -> Element<'a, Message> {
@@ -95,7 +113,7 @@ pub fn view<'a>(ui: &'a Ui, metrics: FrameMetrics, model: &'a Model) -> Element<
),
_ => match &block.detail {
Some(detail) => detail::narrow_detail(ui, metrics, detail, forced),
None => frame::single_column(metrics, detail::detail_empty(ui)),
None => frame::body(metrics, detail::detail_empty(ui)),
},
};
@@ -146,7 +164,6 @@ fn list_column<'a>(
.map(|entry| list_row(ui, entry, is_selected(block, entry), forced))
.collect::<Vec<_>>(),
)
.spacing(ui.space(ROW_GAP))
.padding(Padding::from([0.0, ui.space(Space::X2)]))
.width(Length::Fill);
@@ -161,9 +178,11 @@ fn list_column<'a>(
.height(Length::Fill)
.style(frame::scroll_style(ui));
// 底部内缩:裁切线落在卡片的内边距带里,而不是压在圆角描边上(M102 / 视觉复审 ②)。
container(column![head, scroller].height(Length::Fill))
.width(Length::Fill)
.height(Length::Fill)
.padding(Padding::default().bottom(ui.space(LIST_BOTTOM_INSET)))
.style(move |_theme| container::Style {
background: Some(Background::Color(ui.layer(MaterialLayer::Card))),
border: Border {