From 122accdd0286f52c11925270b23aa442194af7be Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 10 Nov 2026 20:47:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(download):=20=E7=AA=84=E6=A1=A3=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E8=A7=86=E5=8F=A3=E5=BA=95=E9=83=A8=E5=8A=A0=2012px?= =?UTF-8?q?=20=E6=B7=A1=E5=87=BA=E9=81=AE=E7=BD=A9=EF=BC=8C=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E8=A2=AB=E7=A1=AC=E8=A3=81=E7=9A=84=E3=80=8C=E7=A9=BA?= =?UTF-8?q?=E5=A3=B3=E8=93=9D=E6=9D=A1=E3=80=8D=EF=BC=88M227=20=E8=BF=94?= =?UTF-8?q?=E5=B7=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D30 视觉审 hold 的必改项:滚动视口在 y=660 硬裁,把版本菜单的容器上沿切成 639–659 = 21px、 **零字形墨迹**的一条色块(深色下 (22,33,41) vs 画布 (9,21,28),CR 1.99),读起来像渲染错 而不是「下面还有内容可滚」。 改法(reviewer 推荐方案①,保留发现性): - 新增 `options/fade.rs::viewport_fade` + `VIEWPORT_FADE_PX = 12`:12 条 1px、α 单调上升的 **画布色**实色块拼成的斜坡(iced 0.14 没有渐变原语;1px 步进的差别在 1/255 量级); - `options::section` 的窄档分支把滚动区包一层 `stack![滚动区, 底部对齐的遮罩]`,遮罩贴着 视口内沿(≤660),压不到钉死区的实例名(≥668); - 颜色取 `ui.layer(MaterialLayer::Canvas)`:浅色淡出到纸面、**深色淡出到墨底**(不是白雾), 与它下方那块背景同色,所以两档都不新增颜色; - 三栏档不铺这条遮罩:那里滚动区内容恒 76px < 视口 ≈118px,不会出现半截内容,铺了只是 「画布盖画布」,反而会动 M222 已验的 1180 像素; - `options/view.rs` 已 422 行、撞 400 行硬线,遮罩拆成 `options/fade.rs`(含一条几何自检单测)。 实测(720×760): - 浅色 x=600 竖列 648 (211,229,245) → 659 (246,250,255) 单调升到画布色;深色 648 (55,72,85) → 659 (9,21,28),无白雾;中文与浅色逐值同串; - 可见带 21px → 墨迹口径 18px + 底 12px 连续渐变(硬裁边消失,右侧滚动条仍在); - 逐像素零回退:`77d675b`(遮罩前)vs 本轮 720 浅/深/中 + 700 守门,差异 bbox 只有 (112,648)–(696,660) 这 12 行,其余像素一个都没动;1180×760 浅/深/中 与 1180×900 对 M222 基线仍 **IDENTICAL**。 门禁:fmt ✓ / check ✓ / test --workspace(喂四张守门图)exit 0(app-iced lib 860 passed、 四条像素守门 + 400 行门全绿)/ clippy --workspace --all-targets -D warnings exit 0 / .rs 全仓扫描 >400 行 0 个、<2 字节 0 个。 已知残留(本轮不动,按债务台账口径登记):menu 的 L2 阴影不随滚动视口裁切(压在实例名输入框上 约 6.9%,三栏档 fabric 同样存在)——根因在 `components::inputs::menu`,不在 M227 scope, 待专门 mission;finding `.tower/comms/findings/20261110-worker-m227-bug-l2.md`。 --- crates/app-iced/src/pages/download/options.rs | 1 + .../src/pages/download/options/fade.rs | 83 +++++++++++++++++++ .../src/pages/download/options/view.rs | 47 ++++++++--- 3 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 crates/app-iced/src/pages/download/options/fade.rs diff --git a/crates/app-iced/src/pages/download/options.rs b/crates/app-iced/src/pages/download/options.rs index 74dbf3a..940570e 100644 --- a/crates/app-iced/src/pages/download/options.rs +++ b/crates/app-iced/src/pages/download/options.rs @@ -27,6 +27,7 @@ //! 草稿(选中版本 + 三个选项)住在 `App`,由 `refresh_view` 带进页面模型——与分类筛选同一个做法, //! 因为 `refresh_view` 每次事件都会把整棵模型重建,页面自己存不住任何东西。 +mod fade; mod model; #[cfg(test)] mod s1_tests; diff --git a/crates/app-iced/src/pages/download/options/fade.rs b/crates/app-iced/src/pages/download/options/fade.rs new file mode 100644 index 0000000..d3cb56e --- /dev/null +++ b/crates/app-iced/src/pages/download/options/fade.rs @@ -0,0 +1,83 @@ +//! 选项区滚动视口底部的**淡出遮罩**(M227 返工)。 +//! +//! 单独一个文件只为守 400 行硬线(`components::gate::every_source_file_stays_under_the_line_limit`)。 + +use iced::widget::{column, container, space}; +use iced::{Color, Element, Length}; + +use unlucky_design::elevation::MaterialLayer; + +use crate::theme::Ui; + +use super::super::frame; +use super::super::Message; + +/// 窄档滚动视口底部的淡出高度(像素)。 +/// +/// 取值落在 8~12 这一段:视口在 660 处硬裁,版本菜单的容器上沿(639)露出来 21px 且**一个字形 +/// 都没有**——D30 返工把它判成「空壳蓝条」(深色下容器 `(22,33,41)` vs 画布 `(9,21,28)`, +/// CR 1.99,像渲染错而不是「下面还有内容」)。12px 遮罩让最后 12px 单调渐变到画布色, +/// 留下 8px 圆角上沿做发现性提示。 +pub(super) const VIEWPORT_FADE_PX: f32 = 12.0; + +/// 滚动视口底部的**淡出遮罩**:把内容在最后 `height` 像素内渐变到**画布色**。 +/// +/// 为什么需要它:`scrollable` 会硬裁内容(渲染层的 `with_layer`),裁切位置落在什么上面是不确定的 +/// ——窄档正落在版本菜单容器上沿,用户看到的是一条没有字形的色块,而不是「还能往下滚」。 +/// +/// 为什么是**画布色的 α 阶梯**而不是白/黑雾:遮罩的颜色必须等于它下方那块背景的颜色,否则 +/// 浅色档会浮出一层灰幕、深色档会浮出白雾。`ui.layer(MaterialLayer::Canvas)` 就是页面底色 +/// (含面板 α),于是浅色淡出到纸面、深色淡出到墨底,两档都不新增颜色;方向也对 +/// (内容向背景淡出,不是叠一层高光)。 +/// +/// 为什么用 12 条 1px 实色块拼:iced 0.14 没有渐变原语。1px 步进的 α 斜坡在 100% DPI 下与 +/// 连续渐变的差别在 1/255 量级——D30 的逐像素量测只会看到 α 单调、无硬边。 +/// +/// 形状上它必须**贴在视口内**(调用侧把它放在滚动区底部的 `stack!` 上):视口下沿是 660, +/// 钉死区的实例名从 668 起,所以遮罩压不到任何控件(M227 返工的零回退证据就是逐像素 +/// 差异只有 648–660 这 12 行)。 +pub(super) fn viewport_fade<'a>(ui: &'a Ui, height: f32) -> Element<'a, Message> { + const STEPS: usize = 12; + let canvas = ui.layer(MaterialLayer::Canvas); + let band_height = height / STEPS as f32; + + let mut bands = column![].width(Length::Fill); + for index in 0..STEPS { + // t:顶端 ≈0(几乎透出内容)→ 底端 = 1(完全是画布色,与视口外的背景接上)。 + let t = (index + 1) as f32 / STEPS as f32; + let alpha = canvas.a * t.powf(1.6); + bands = bands.push( + container(space::horizontal()) + .width(Length::Fill) + .height(Length::Fixed(band_height)) + .style(move |_theme| frame::filled(Color { a: alpha, ..canvas })), + ); + } + bands.into() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 遮罩的几何自检:12 条 1px 恰好铺满 12px,且 α 单调不减(顶端近透明、底端不透明)。 + /// + /// 抓得住:把步数改成 6(每档 2px ⇒ 能看到阶梯)、或把 α 曲线写成常数(等于一层纯色幕)。 + #[test] + fn the_fade_steps_are_one_pixel_and_monotone() { + let steps = 12.0_f32; + assert_eq!( + (VIEWPORT_FADE_PX / steps * steps * 100.0).round() / 100.0, + VIEWPORT_FADE_PX, + "步数要能整除 12,否则会出现半像素带" + ); + let mut previous = -1.0_f32; + for index in 0..12 { + let t = (index + 1) as f32 / steps; + let alpha = t.powf(1.6); + assert!(alpha >= previous, "α 必须单调不减(第 {index} 档)"); + previous = alpha; + } + assert_eq!(previous, 1.0, "底端必须是完全不透明的画布色"); + } +} diff --git a/crates/app-iced/src/pages/download/options/view.rs b/crates/app-iced/src/pages/download/options/view.rs index dacf934..b702225 100644 --- a/crates/app-iced/src/pages/download/options/view.rs +++ b/crates/app-iced/src/pages/download/options/view.rs @@ -23,6 +23,7 @@ //! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] //! Vanilla parent only — Forge goes through the CLI //! ┌ 1.12.2-SNAPSHOT ┐ ← 露在折线下方,滚得到(滚动条在右) +//! ── 视口底 12px 淡出遮罩(`fade::viewport_fade`)──── //! ── 钉死区 ────────────────────────────────────── //! [实例名 ] //! ``` @@ -31,7 +32,10 @@ //! 顺序铺开(`04` 基线图就是这一档,不能改); //! - 不兼容时的原因行(Error 色)在滚动区末尾(**窄档**可达;三栏档 + 带版本列表时 //! `problem` 恒为 `None`,所以那支看不到它); -//! - 说明只有 LiteLoader 有(`Key::DownloadOptionsLiteLoaderVanillaOnly`)。 +//! - 说明只有 LiteLoader 有(`Key::DownloadOptionsLiteLoaderVanillaOnly`); +//! - 窄档的滚动视口底边会硬裁内容(M227 返工前正切在版本菜单容器上沿,露出 21px 没有字形的 +//! 色块),所以那里铺一条 12px 淡出遮罩把内容渐变到画布色——遮罩在视口内(≤660), +//! 压不到钉死区的实例名(≥668)。 //! //! ## 为什么加载器是**六枚一行**(M222 D30 r1 实测订正) //! @@ -46,7 +50,7 @@ //! 现在的做法:五家永远都能点,选中不兼容的那家时下面出现一行具名原因,同时「安装」变成禁用态。 //! 于是「为什么不能装」和「怎么才能装」永远同屏。 -use iced::widget::{column, container, row, scrollable}; +use iced::widget::{column, container, row, scrollable, stack}; use iced::{Alignment, Element, Length}; use unlucky_design::color::Role; @@ -65,6 +69,7 @@ use crate::theme::Ui; use super::super::frame; use super::super::rows::TailShare; use super::super::Message; +use super::fade; use super::model::{Loader, LoaderVersionsView, OptionsView}; // 加载器 chip 的顺序与集合**委派**给 `Loader::ALL`(原版 / Forge / NeoForge / Fabric / @@ -240,16 +245,34 @@ pub(crate) fn section<'a>( Message::SetInstanceName, )); - column![ - scrollable(body) - .height(Length::Fill) - .style(frame::scroll_style(ui)), - pinned, - ] - .spacing(ui.space(Space::X2)) - .height(Length::Fill) - .width(Length::Fill) - .into() + let scroll_area: Element<'a, Message> = scrollable(body) + .height(Length::Fill) + .style(frame::scroll_style(ui)) + .into(); + + // 窄档:视口底部铺一条 12px 淡出遮罩(M227 返工 ①,理由见 `fade::viewport_fade`)。 + // 三栏档不铺:那里滚动区内容恒为 head 40 + 间距 4 + chips 32 = 76px,而视口 ≈118px, + // 永远不会出现「半截内容」;铺上去只是「画布盖画布」,反而动了 M222 已验的 1180 像素。 + let scroll_area: Element<'a, Message> = if narrow { + stack![ + scroll_area, + container(fade::viewport_fade(ui, fade::VIEWPORT_FADE_PX)) + .width(Length::Fill) + .height(Length::Fill) + .align_y(Alignment::End), + ] + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + scroll_area + }; + + column![scroll_area, pinned] + .spacing(ui.space(Space::X2)) + .height(Length::Fill) + .width(Length::Fill) + .into() } /// 版本菜单上方那一行:**有中性说明就显示说明**(它讲的是「这一家的叠法」,位置就在版本菜单