Files
Unlucky-Launcher/crates/app-iced/src/pages/settings/tests/scroll_fit.rs
T
AskaEth 037c68d0e6 fix(m173): 设置页滚动条并排在内容右侧(右侧恒留 10dp 槽)
根因(读 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)。
2026-11-08 15:11:54 +08:00

131 lines
5.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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())
);
}