From 037c68d0e68ba021b814c1ac222fad46e074ae73 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 8 Nov 2026 15:11:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(m173):=20=E8=AE=BE=E7=BD=AE=E9=A1=B5?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E6=9D=A1=E5=B9=B6=E6=8E=92=E5=9C=A8=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=8F=B3=E4=BE=A7=EF=BC=88=E5=8F=B3=E4=BE=A7=E6=81=92?= =?UTF-8?q?=E7=95=99=2010dp=20=E6=A7=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(读 iced 0.14 源码,不是猜):`scrollable` 的竖直滚动条是浮层—— `iced_widget-0.14.2/src/scrollable.rs:1970-1980` 把轨道定位在 `bounds.right - (width.max(scroller_width) + 2*margin)`,而同文件 `:437` 给内容的横向 约束是整个视口宽(`Scrollbar::spacing` 默认 None ⇒ 右内边距恒 0)。本仓 6 轨 + 2×2 内缩 = 10dp,于是这 10dp 整条压在表单列内容上(1180 与 880 宽窗口都一样)。 改法:`components::scroll::vertical()` 在内容右侧恒留一条 `轨宽 + 2×margin` 的槽,轨道画在 槽里;`pages/settings/frame.rs::scroll` 改成一行转发。不采用 iced 的 `Scrollbar::spacing`, 因为它只在滑块可见时才预留(`scrollable.rs:478-500`),长/短页面切换内容会横跳 10dp。 数值(1180×760、100% DPI、tiny-skia、实测像素): - 卡片右缘 799 → 789(内容可用宽 424 → 414 = 424 − 10); - 滑块仍是 792..797(没有移动),与内容之间隔 2px 页面底色(= SCROLLBAR_MARGIN); - 长页面(外观组)与短页面(Java 组,无滑块)左边界 376、宽 414 **逐项相同**; - 150% 缩放下 621px = 414×1.5、滑块 9px = 6×1.5、槽 15px = 10×1.5,同比例不重叠。 守卫(两条都是数值,不看图):`components/scroll.rs` 与 `pages/settings/tests/scroll_fit.rs` 用 `Renderer = ()` 真跑 iced 的 `layout()`,断言「内容宽 == 容器宽 − 槽宽」且长/短两态一致; 把 `beside` 的 padding 改回 `Padding::ZERO`,4 条断言立刻红(424 vs 414)。 --- crates/app-iced/src/components/mod.rs | 2 + crates/app-iced/src/components/scroll.rs | 224 ++++++++++++++++++ crates/app-iced/src/pages/settings/frame.rs | 49 ++-- .../app-iced/src/pages/settings/tests/mod.rs | 1 + .../src/pages/settings/tests/scroll_fit.rs | 130 ++++++++++ 5 files changed, 391 insertions(+), 15 deletions(-) create mode 100644 crates/app-iced/src/components/scroll.rs create mode 100644 crates/app-iced/src/pages/settings/tests/scroll_fit.rs diff --git a/crates/app-iced/src/components/mod.rs b/crates/app-iced/src/components/mod.rs index ac7d52c..b5289b6 100644 --- a/crates/app-iced/src/components/mod.rs +++ b/crates/app-iced/src/components/mod.rs @@ -10,6 +10,7 @@ //! layout.rs ← COMPBASE 独占:Q8/Q9 两个布局陷阱的固定解法 //! buttons/ ← COMPBASE(12b §1 按钮家族 + §2 IconButton) //! divider.rs ← COMPBASE(15 §22) +//! scroll.rs ← M173:竖直滚动区的滚动条槽(轨道不压内容) //! selection/ ← 本文件只放桩;后续 mission 在自己的目录里填(§18/§19/§10/§13) //! inputs/ ← 同上(§9/§20/§11/§8) //! feedback/ ← 同上(§12/§14/§7/§15) @@ -52,6 +53,7 @@ pub mod inputs; pub mod interaction; pub mod layout; pub mod navigation; +pub mod scroll; pub mod selection; #[cfg(test)] diff --git a/crates/app-iced/src/components/scroll.rs b/crates/app-iced/src/components/scroll.rs new file mode 100644 index 0000000..e67baa1 --- /dev/null +++ b/crates/app-iced/src/components/scroll.rs @@ -0,0 +1,224 @@ +//! 滚动条**并排**在内容右侧,不压在内容上(M173)。 +//! +//! ## 根因(读 iced 0.14 的源码,不是猜测) +//! +//! `scrollable` 的竖直滚动条是**浮层**,而且内容区不扣任何横向内边距: +//! +//! - 内容拿到的约束仍是整个视口宽——`iced_widget-0.14.2/src/scrollable.rs:437` 那条 +//! `layout::padded(limits, self.width, self.height, Padding::ZERO, …)`:`Scrollbar::spacing` +//! 默认 `None`,右内边距**恒 0**; +//! - 轨道按视口右边缘定位——同文件 `scrollable.rs:1970-1980` +//! `x = bounds.x + bounds.width - total_scrollbar_width`,`total_scrollbar_width = +//! width.max(scroller_width) + 2 * margin`。 +//! +//! 于是轨道(本仓 6 + 2×2 = **10dp**)整条压在内容最右边那 10dp 上。屏幕上的观感就是 +//! 「滚动条骑在卡片右边缘」。 +//! +//! ## 为什么不用 iced 自己的 `Scrollbar::spacing(Some(..))` +//! +//! 它**只在滑块可见时**才预留(`scrollable.rs:478-500`:`if state.is_scrollbar_visible +//! { padding } else { 0.0 }`)。长页面 ⇄ 短页面切换时内容宽度会跳 10dp,卡片会横移—— +//! 那是另一个缺陷(`M173` 的判据要求「有无滚动条时内容宽度一致」)。 +//! +//! ## 本模块的做法:恒定槽位 +//! +//! 内容外面包一层「右侧恒留 `slot`」的容器,再交给 `scrollable`: +//! +//! ```text +//! 视口 0 ───────────────────────── 视口宽 − slot │ slot │ +//! │ 内容(可滚) │ 轨道 │ +//! ``` +//! +//! `内容可用宽度 = 视口宽 − slot` 与滑块在不在**无关**(因为内边距来自本模块,不来自 iced 的 +//! 可见性判断)。`slot = rail_width + 2 * margin` 与 iced 的 `total_scrollbar_width` 同口径, +//! 轨道整个落在槽里,滑块再内缩 `margin`,所以内容与滑块之间永远隔着 `margin`。 +//! +//! 逻辑像素(dp):高 DPI 下 iced 按缩放因子整体放大,槽与轨道同比例,二者不会重新重叠。 +//! +//! ## 谁该用 +//! +//! 所有「内容宽度吃满视口」的竖直滚动区。设置页(`pages/settings/frame.rs`)是第一个用户; +//! 实例页列表列 / 详情列、日志面板、内容库正文、版本管理侧栏目前仍是 `scrollable(...)` 裸调, +//! 同一处重叠在那些页面上照样存在(M173 的 finding 里有清单)。 + +use iced::widget::{container, scrollable}; +use iced::{Element, Length, Padding, Theme}; + +/// 滚动条轨占的横向槽宽(dp)= 轨宽 + 两侧留白。 +/// +/// 与 iced 的 `total_scrollbar_width`(`width.max(scroller_width) + 2 * margin`)同一个式子: +/// 参数里的 `rail_width` 传的是 `Scrollbar::width` 与 `scroller_width` 的同一个值(本仓两者相等)。 +pub fn rail_slot(rail_width: f32, margin: f32) -> f32 { + rail_width + 2.0 * margin +} + +/// 内容可用宽度(dp)= 视口宽 − 槽宽。 +/// +/// 这条式子是本模块的**判据**:`components/scroll.rs` 的测试用 iced 的布局引擎真量一遍, +/// `pages/settings/tests/scroll_fit.rs` 在产品那条 `frame::scroll` 上再量一遍。 +/// 负数按 0 处理——视口比槽还窄时(产品窗口不会发生)宁可内容零宽,也不给 iced 送负宽。 +#[cfg(test)] +pub fn content_width(viewport: f32, slot: f32) -> f32 { + (viewport - slot).max(0.0) +} + +/// 竖直滚动区:内容右侧恒留 [`rail_slot`] 的槽,滚动条画在槽里。 +/// +/// `style` 就是 `scrollable::style` 回调(配色由调用方按令牌给),本模块只管几何。 +pub fn vertical<'a, Message, Renderer>( + rail_width: f32, + margin: f32, + style: impl Fn(&Theme, scrollable::Status) -> scrollable::Style + 'a, + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: iced::advanced::text::Renderer + 'a, +{ + scrollable(beside(rail_slot(rail_width, margin), content)) + .direction(scrollable::Direction::Vertical( + scrollable::Scrollbar::new() + .width(rail_width) + .scroller_width(rail_width) + .margin(margin), + )) + .height(Length::Fill) + .style(style) + .into() +} + +/// 内容 + 右侧槽位。容器宽度仍是 `Fill`(= 视口宽),扣掉的是**内容的**可用宽度。 +fn beside<'a, Message, Renderer>( + slot: f32, + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: iced::advanced::Renderer + 'a, +{ + container(content) + .width(Length::Fill) + .padding(Padding { + right: slot, + ..Padding::ZERO + }) + .into() +} + +#[cfg(test)] +mod tests { + //! ## 为什么这里的守卫是**真的布局**而不是源码扫描 + //! + //! iced 的控件树支持在测试里跑一遍布局:`iced_core` 在 debug 构建下给 `()` 实现了全套 + //! `Renderer` trait(`iced_core-0.14.0/src/renderer/null.rs`),所以 `Element<_, _, Theme, ()>` + //! 能被 `Widget::layout` 真正量出尺寸——**不需要开窗、不需要 GPU**。 + //! + //! 于是「内容可用宽度 = 视口宽 − 槽宽」是一条数值断言,而不是「看图觉得对」;把 + //! [`beside`] 的 `padding` 去掉,下面的 `content_width` 立刻变成 434 而断言红。 + //! + //! ⚠️ 诚实边界:`()` 的 Renderer 实现带 `#[cfg(debug_assertions)]`,所以这些测试在 + //! `cargo test --release` 下编不过(本仓 CI/本地一律 dev profile 跑测试)。 + + use iced::advanced::layout::Limits; + use iced::advanced::widget::Tree; + use iced::widget::{container, scrollable, space}; + use iced::{Rectangle, Size}; + + use super::*; + + /// 本仓的轨道参数(真源是 `pages/frame/pieces.rs` 的两个常量,这里按「用户取值」复核)。 + const RAIL: f32 = 6.0; + const MARGIN: f32 = 2.0; + /// 设置页表单列在 1180 宽窗口下的列宽(`measure_cap_px` = 424)。 + const VIEWPORT: f32 = 424.0; + /// 视口高(1180×760 减去页头/页边距后的量级;只要求「长页 > 视口」「短页 < 视口」)。 + const VIEWPORT_HEIGHT: f32 = 600.0; + /// 长页面(一定出滑块)与短页面(一定不出滑块)的内容高。 + const LONG: f32 = 4000.0; + const SHORT: f32 = 120.0; + + /// 真跑一遍布局,返回 (槽容器, 内容) 两个矩形。 + /// + /// 节点树:`scrollable → [槽容器(Fill 宽,右内边距 = slot) → [内容]]`,见 + /// `iced_core/src/layout.rs:201`(`padded` 的节点恒为「自己 + 一个子节点」)。 + fn laid_out(content_height: f32) -> (Rectangle, Rectangle) { + let filler: Element<'_, (), Theme, ()> = container(space()) + .width(Length::Fill) + .height(Length::Fixed(content_height)) + .into(); + + let mut element: Element<'_, (), Theme, ()> = + vertical::<(), ()>(RAIL, MARGIN, scrollable::default, filler); + let mut tree = Tree::new(element.as_widget()); + + let node = element.as_widget_mut().layout( + &mut tree, + &(), + &Limits::new(Size::ZERO, Size::new(VIEWPORT, VIEWPORT_HEIGHT)), + ); + + let gutter = node.children()[0].bounds(); + let content = node.children()[0].children()[0].bounds(); + + (gutter, content) + } + + /// **本 mission 的核心判据**:内容可用宽度 = 视口宽 − 滚动条槽,且与有没有滑块无关。 + #[test] + fn the_content_width_leaves_the_rail_its_own_slot() { + let slot = rail_slot(RAIL, MARGIN); + assert_eq!(slot, RAIL + 2.0 * MARGIN, "槽宽口径 = 轨宽 + 两侧留白"); + + let (_, long) = laid_out(LONG); + let (_, short) = laid_out(SHORT); + + assert_eq!( + long.width, + content_width(VIEWPORT, slot), + "长页面(出滑块)的内容宽度必须是「视口宽 − 槽宽」" + ); + assert_eq!( + short.width, + content_width(VIEWPORT, slot), + "短页面(不出滑块)的内容宽度必须**一样**:否则滑块一出现内容就横跳" + ); + assert_eq!(VIEWPORT - long.width, slot, "右侧扣掉的正好是槽宽"); + + // 滑块落在槽的中间:内容右缘 → margin → 滑块 → margin → 视口右缘。 + // 轨道按 iced 的算法定位(`scrollable.rs:1970-2005`:槽宽 = 轨宽 + 2×margin,滑块再内缩 margin), + // 所以「内容右缘 + margin」就是滑块的左缘。 + let content_right = long.x + long.width; + let pill_left = content_right + MARGIN; + let pill_right = pill_left + RAIL; + assert_eq!( + pill_right + MARGIN, + VIEWPORT, + "滑块必须整个落在槽内(两侧各留 margin),否则「并排」变成「压着」" + ); + } + + /// 槽容器自己仍是满宽(`Fill`),否则页面为它预留的 424 会被再吃掉一截。 + #[test] + fn the_slot_wrapper_keeps_the_full_viewport_width() { + let (gutter, _) = laid_out(LONG); + assert_eq!(gutter.width, VIEWPORT); + } + + /// 左边界与宽度在长/短两态下**逐项相同**(M173 明写的「两张对照图 + 数值」里的数值部分)。 + #[test] + fn long_and_short_pages_share_the_same_left_edge_and_width() { + let (_, long) = laid_out(LONG); + let (_, short) = laid_out(SHORT); + + assert_eq!(long.x, short.x, "左边界必须一致"); + assert_eq!(long.width, short.width, "宽度必须一致"); + assert_eq!(short.x, 0.0, "槽只占右侧,左侧内边距为 0"); + } + + #[test] + fn content_width_never_goes_negative() { + assert_eq!(content_width(RAIL, rail_slot(RAIL, MARGIN)), 0.0); + assert_eq!(content_width(0.0, rail_slot(RAIL, MARGIN)), 0.0); + assert_eq!(content_width(VIEWPORT, 0.0), VIEWPORT); + } +} diff --git a/crates/app-iced/src/pages/settings/frame.rs b/crates/app-iced/src/pages/settings/frame.rs index 438744d..beb8514 100644 --- a/crates/app-iced/src/pages/settings/frame.rs +++ b/crates/app-iced/src/pages/settings/frame.rs @@ -12,8 +12,8 @@ pub(super) use super::super::frame::{ text_action, text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH, }; -use iced::widget::{column, container, mouse_area, row, scrollable}; -use iced::{Alignment, Background, Border, Element, Length, Padding}; +use iced::widget::{column, container, mouse_area, row}; +use iced::{Alignment, Background, Border, Element, Length, Padding, Theme}; use unlucky_design::color::Role; use unlucky_design::component_map::Component; @@ -137,20 +137,39 @@ fn group_item<'a>( /// 表单列的滚动区:只有正文滚,页头不动(与实例页列表列同一套口径)。 /// /// 「外观」分组有 7 个控件 + 最多 3 行诊断,1180×760 下会超出可视区,所以这一层是必需的。 -pub(super) fn scroll<'a>( +/// +/// ## M173:滚动条**并排**在内容右侧 +/// +/// 原来这里是 `scrollable(content)` 裸调 + 轨宽 6 / 内缩 2。iced 的竖直滚动条是浮层:轨道按 +/// 视口右边缘定位(`iced_widget-0.14.2/src/scrollable.rs:1970`),而内容拿到的横向约束是**整个 +/// 视口宽**(同文件 `:437`,`Scrollbar::spacing` 默认 `None` ⇒ 右内边距 0)。于是滚动条占的那条 +/// 10dp 竖带(轨 6 + 两侧内缩 2×2)整条压在表单列最右边——用户看到的就是「滚动条骑在卡片上」。 +/// +/// 改法在 [`crate::app::components::scroll::vertical`]:内容右侧恒留一条 10dp 的槽,轨道画在槽里。 +/// 槽宽来自同一个式子 `轨宽 + 2 × margin`,所以: +/// +/// - 内容可用宽度 = 视口宽(424)− 槽宽(10)= **414**; +/// - 长页面(出滑块)与短页面(不出滑块)**内容宽度一致**——槽是内边距,不依赖 iced 的 +/// `is_scrollbar_visible`(那正是 `Scrollbar::spacing` 不能用在这里的理由:它只在可见时预留, +/// 滑块一出现内容就横跳 10dp)。 +/// +/// 数值由 `components/scroll.rs` 与 `tests/scroll_fit.rs` 的**布局级**断言钉住(`Renderer = ()` +/// 真跑 iced 的 `layout()`),不是看图。 +/// +/// 只泛型到 `Renderer` 是为了让上面那条断言能在 `()` 渲染器上跑;产品调用点不写泛型。 +pub(super) fn scroll<'a, Renderer>( ui: &'a Ui, - content: impl Into>, -) -> Element<'a, Message> { - scrollable(content) - .direction(scrollable::Direction::Vertical( - scrollable::Scrollbar::new() - .width(SCROLLBAR_WIDTH) - .scroller_width(SCROLLBAR_WIDTH) - .margin(SCROLLBAR_MARGIN), - )) - .height(Length::Fill) - .style(scroll_style(ui)) - .into() + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Renderer: iced::advanced::text::Renderer + 'a, +{ + crate::app::components::scroll::vertical( + SCROLLBAR_WIDTH, + SCROLLBAR_MARGIN, + scroll_style(ui), + content, + ) } /// 演示行构成的一组设置 = 一张卡,行间 1px 分隔线(M3 设置列表的常规做法)。 diff --git a/crates/app-iced/src/pages/settings/tests/mod.rs b/crates/app-iced/src/pages/settings/tests/mod.rs index 5546c52..a3ced62 100644 --- a/crates/app-iced/src/pages/settings/tests/mod.rs +++ b/crates/app-iced/src/pages/settings/tests/mod.rs @@ -40,6 +40,7 @@ mod appearance_section; mod download_section; mod general_section; mod java_section; +mod scroll_fit; mod update_section; use super::super::layout::{self, frame_metrics, measure_cap_px, PaneKit, SIDE_NAV_WIDTH}; diff --git a/crates/app-iced/src/pages/settings/tests/scroll_fit.rs b/crates/app-iced/src/pages/settings/tests/scroll_fit.rs new file mode 100644 index 0000000..2cf8ec9 --- /dev/null +++ b/crates/app-iced/src/pages/settings/tests/scroll_fit.rs @@ -0,0 +1,130 @@ +//! M173:设置页表单列的**滚动条槽**(滚动条并排,不压内容)。 +//! +//! 这里守的是整页级别的数值契约,不是「哪一组有哪些行」——所以它不属于任何 section 文件。 +//! +//! ## 怎么量 +//! +//! iced 的控件树支持在测试里真跑布局:`iced_core` 在 debug 构建下给 `()` 实现了全套 `Renderer` +//! trait(`iced_core-0.14.0/src/renderer/null.rs`),于是 `frame::scroll::<()>(…)` 返回的 +//! `Element<_, Message, Theme, ()>` 能被 `Widget::layout` 量出**真的**子节点尺寸。不需要开窗, +//! 也不需要 GPU——这是「数值断言,不是看图」的落点。 +//! +//! 节点树(`iced_core/src/layout.rs:201`:`padded` 恒为「自己 + 一个子节点」): +//! +//! ```text +//! scrollable → [槽容器(Fill,右内边距 = 槽宽) → [内容]] +//! ``` +//! +//! ## 抓什么 / 抓不住什么 +//! +//! 抓得住:内容右侧真的留出了「轨宽 + 两侧 margin」的槽;长/短页面(出/不出滑块)内容宽度与左 +//! 边界**逐项相同**;把 `app::components::scroll` 的 padding 改回 0,本文件立刻红。 +//! 抓不住:滑块画在槽里的**像素**位置(那由 `pages/settings` 的出图与 iced 自己的 `Rail::new` +//! 公式担保,见 `src/components/scroll.rs` 文档里的源码行号)。 + +use iced::advanced::layout::Limits; +use iced::advanced::widget::Tree; +use iced::widget::{container, space}; +use iced::{Element, Length, Rectangle, Size, Theme}; +use unlucky_design::color::SchemeFlavor; +use unlucky_design::typography::Platform; + +use super::super::frame; +use super::super::frame::{SCROLLBAR_MARGIN, SCROLLBAR_WIDTH}; +use crate::pages::Message; +use crate::theme::{Families, Ui}; + +/// 设置页表单列宽:`frame_metrics` 的 `column`(= `measure_cap_px()`,1180 与 880 窗口下都是它)。 +const COLUMN: f32 = 424.0; +/// 视口高(1180×760 减去标题栏 / 页头 / 上下页边距之后的量级;只要求「长页 > 它」「短页 < 它」)。 +const VIEWPORT_HEIGHT: f32 = 600.0; +/// 长页面(正文远超视口 ⇒ 一定出滑块)。 +const LONG: f32 = 4000.0; +/// 短页面(正文装得下 ⇒ 一定不出滑块)。 +const SHORT: f32 = 120.0; + +const FAMILIES: Families = Families { + text: "Segoe UI", + display: "Segoe UI", + mono: "Consolas", +}; + +fn ui() -> Ui { + Ui::with_platform(SchemeFlavor::Light, Platform::Windows, FAMILIES) +} + +/// 滚动条槽宽(dp):轨道 + 两侧留白,与 iced 的 `total_scrollbar_width` 同口径。 +fn slot() -> f32 { + SCROLLBAR_WIDTH + 2.0 * SCROLLBAR_MARGIN +} + +/// 真跑一遍布局,返回 (槽容器, 内容) 两个矩形。 +fn laid_out(content_height: f32) -> (Rectangle, Rectangle) { + let ui = ui(); + + let filler: Element<'_, Message, Theme, ()> = container(space()) + .width(Length::Fill) + .height(Length::Fixed(content_height)) + .into(); + + // 走的是**产品那条** `frame::scroll`(泛型只为换成 `()` 渲染器,几何一字不改)。 + let mut element = frame::scroll::<()>(&ui, filler); + let mut tree = Tree::new(element.as_widget()); + + let node = element.as_widget_mut().layout( + &mut tree, + &(), + &Limits::new(Size::ZERO, Size::new(COLUMN, VIEWPORT_HEIGHT)), + ); + + let gutter = node.children()[0].bounds(); + let content = node.children()[0].children()[0].bounds(); + + (gutter, content) +} + +/// **本条 mission 的核心数值断言**:内容可用宽度 = 容器宽 − 滚动条占位。 +#[test] +fn the_form_column_reserves_a_rail_slot_beside_the_content() { + let (gutter, content) = laid_out(LONG); + + assert_eq!(slot(), 10.0, "6 轨宽 + 2×2 内缩 = 10dp 的槽"); + assert_eq!(gutter.width, COLUMN, "槽容器自身仍是满宽"); + assert_eq!( + content.width, + COLUMN - slot(), + "内容可用宽度必须是「容器宽 − 滚动条槽」,否则轨道就压在内容右缘上" + ); + assert_eq!(content.width, 414.0, "424 − 10 = 414"); +} + +/// 长页面(出滑块)与短页面(不出滑块)——内容宽度与左边界必须**逐项相同**。 +/// +/// 这是 M173 要求的「两张对照图 + 数值」里的数值那一半:只给一张出滑块的图说明不了「滑块出现时 +/// 内容会不会跳」,必须两态同框比。 +#[test] +fn the_content_does_not_jump_between_a_long_and_a_short_page() { + let (long_gutter, long) = laid_out(LONG); + let (short_gutter, short) = laid_out(SHORT); + + assert_eq!(long_gutter.x, short_gutter.x, "槽容器的左边界不许变"); + assert_eq!(long_gutter.width, short_gutter.width, "槽容器的宽度不许变"); + assert_eq!(long.x, short.x, "左边界不许横跳"); + assert_eq!(long.width, short.width, "宽度不许横跳"); + assert_eq!(short.x, 0.0, "槽只占右侧:左侧内边距为 0"); + assert_eq!( + short.x + short.width, + COLUMN - slot(), + "内容右缘 = 轨道的左缘(再往右 10dp 才是轨道自己)" + ); +} + +/// 反证:`content_width` 这条纯量与实际布局量出来的是同一个数(两处不许各算一套)。 +#[test] +fn the_layout_agrees_with_the_pure_quantity() { + let (_, content) = laid_out(LONG); + assert_eq!( + content.width, + crate::app::components::scroll::content_width(COLUMN, slot()) + ); +}