diff --git a/crates/app-iced/src/pages/download.rs b/crates/app-iced/src/pages/download.rs index 5b7e579..107c456 100644 --- a/crates/app-iced/src/pages/download.rs +++ b/crates/app-iced/src/pages/download.rs @@ -173,7 +173,7 @@ pub fn view<'a>( view_sized(ui, metrics, model, lang, 760.0) } -/// 同 [`view`],但显式给**窗口高度**(版本列表要按整行取高,见 [`rows::whole_rows`])。 +/// 同 [`view`],但显式给**窗口高度**(版本列表要按整行取高,见 [`rows::TailShare::list_height`])。 pub fn view_sized<'a>( ui: &'a Ui, metrics: FrameMetrics, @@ -247,18 +247,14 @@ fn body<'a>( rails::three_column(ui, catalog, model, rails, body_height) } else { // 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽。 - // 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的一半(M125 缺陷 1)。 + // 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的预算(M125 缺陷 1)。 + // M227:这一档也改成「面板先保预算、列表按整行吃剩下的」——预算比三栏档紧得多 + // (`rows::CHIPS_PANEL_BUDGET` 192 vs `PANEL_BUDGET` 300),清单见 `rows/tail.rs`。 + let share = rows::TailShare::of(rails.tier, &install::panel_state(model)); column![ rails::stacked_chips(ui, catalog, model), divider::full(ui), - rows::content( - ui, - catalog, - model, - rows::TailShare::of(rails.tier, &install::panel_state(model)), - // chips 档不按整行取高:实测那里已经是 4.0 整行、零残影,别动。 - None, - ), + rows::content(ui, catalog, model, share, share.list_height(body_height)), ] .spacing(ui.space(Space::X2)) .width(Length::Fill) diff --git a/crates/app-iced/src/pages/download/install.rs b/crates/app-iced/src/pages/download/install.rs index 5464aa2..d3c29ae 100644 --- a/crates/app-iced/src/pages/download/install.rs +++ b/crates/app-iced/src/pages/download/install.rs @@ -10,15 +10,16 @@ //! 所以列表独立滚动(`rows::list`),面板固定占尾部一块高度。 //! //! **M125 的例外**:chips 档(窗口 < 736,竖栏退回顶部 chip 行)里这块面板太贵——它按内容高 -//! 钉死会把版本列表压成 18px 的缝。那一档改成「列表与面板各吃一半、面板自己可滚」 -//! (`rows::TailShare::Shared`)——但只在**面板真的占高度**时(`Options` / `Install`); -//! 只画一行提示的 `Hint` 态仍钉死,否则 8 个版本的列表会被砍到 4 行(r1 复审 P2-1)。 +//! 钉死会把版本列表压成 18px 的缝。那一档改成「面板先保预算、列表按整行吃剩下的」 +//! (`rows::TailShare::Narrow`,预算见 `rows/tail.rs::CHIPS_PANEL_BUDGET`)——而且只在 +//! **面板真的占高度**时(`Options` / `Install`);只画一行提示的 `Hint` 态仍钉死, +//! 否则 8 个版本的列表会被砍到 4 行(r1 复审 P2-1)。 //! -//! ## 面板的滚动分层(M222 D30 六轮打磨后的实际形态) +//! ## 面板的滚动分层(M227 之后的实际形态) //! //! ```text //! 版本行列表 scrollable ← 第 1 层(任何档位都在) -//! └ 选项区 scrollable(Shared 档才有) ← 第 2 层(`options/view.rs::section`,只装 head + chips) +//! └ 选项区 scrollable(Shared / Narrow) ← 第 2 层(`options/view.rs::section`) //! └ 加载器版本 menu 的 scrollable ← 第 3 层(`options/view.rs::version_menu_height`) //! ``` //! @@ -28,12 +29,15 @@ //! **鼠标落在内层上滚轮会不会冒泡到外层,本仓仍没有实测通道**(静态图拍不到滚动嵌套)。 //! iced 在指针悬停于内层时由内层消费滚动,所以「滚到底之后继续滚会带动外层」属**未验证行为**; //! 两个方向的坏结果分别是「面板卡住滚不动」与「一滚就跳到外层」。 +//! **M227 加了新的嵌套**:窄档里版本菜单在选项区的滚动区**之内**(三栏档它在钉死区、不嵌套), +//! 于是窄档是「菜单 → 选项区 → (列表外层没有)」两级嵌套,这条未验项在窄档变得更相关。 //! -//! ## 钉死页脚**已经做了**(M222 `6f87c8b` 起) +//! ## 钉死页脚(M222 `6f87c8b` 起,M227 收窄) //! -//! `options::section` 现在是「可滚的上半块 + 钉死的下半块」:钉死区内是**加载器版本菜单 + -//! 实例名输入框**(D30 四审:菜单也钉住,否则它正好落在裁切线上)。所以「实例名被滚走」 -//! 这个风险已经消除,只剩上面那条滚轮冒泡未验。 +//! `options::section` 是「可滚的上半块 + 钉死的下半块」:三栏档钉死区是**说明行 + 版本菜单 + +//! 实例名**(D30 四审:菜单也钉住,否则它正好落在裁切线上);chips 档(`Narrow`)钉死区 +//! **只剩实例名**——窄档的 459px 装不下「钉住菜单」+「3 整行列表」两个都要(算术见 +//! `rows/tail.rs::CHIPS_PANEL_BUDGET`),于是菜单进滚动区(滚得到、滚动条可见)。 //! //! ## 只有意图,没有乐观回显 //! @@ -78,6 +82,7 @@ use super::super::frame; use super::super::Message; use super::model::{Download, InstallView}; use super::options::OptionsView; +use super::rows::TailShare; /// 面板要画哪一种形态(**决策与画法分开**:决策是纯函数,单测直接钉它)。 /// @@ -110,50 +115,55 @@ pub(super) fn panel_state(model: &Download) -> PanelState<'_> { /// 底部的安装面板。 /// -/// `shared` = 选项区走「面板先保预算、列表按整行吃剩下的」那一档(`rows::TailShare::Shared` -/// 的带版本列表分支;分高规则见 `rows/tail.rs::whole_rows`,**不是**早期的「对半分」)。 -/// 此时选项区自己分成两段(`options::section`): +/// `share` = 分高方式(`rows::TailShare`,面板内部形态跟着它走): /// -/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips; -/// - **钉死区**:说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框。 +/// - [`TailShare::Pinned`]:内容照原样铺开(面板按内容高钉在底部); +/// - [`TailShare::Shared`](三栏档 + 带版本列表的选项区): +/// **滚动区** = head(版本 id + 安装按钮)+ 六枚加载器 chips; +/// **钉死区** = 说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框; +/// - [`TailShare::Narrow`](chips 档 + 面板占高度): +/// **滚动区** = head + 六枚 chips + 说明行 + 版本菜单; +/// **钉死区**只剩实例名输入框(理由与预算见 `rows/tail.rs::CHIPS_PANEL_BUDGET`)。 /// -/// 菜单与说明分别是 D30 四审 / 八审搬进钉死区的(裁切位置由钉死区高度决定,与窗宽无关)。 +/// 菜单在 `Shared` 档是钉住的(D30 四审:裁切位置由钉死区高度决定,与窗宽无关,760 下它会正好 +/// 落在切线上),在 `Narrow` 档则**在滚动区里**——窄档没有多余高度同时钉住「菜单 + 3 整行列表」, +/// 而列表那 3 整行是 M125 像素守门的下限。两支的取舍都写在 `rows/tail.rs`。 /// /// **两个 Shared 分支的现实不同,别一概而论**(`rows/tail.rs::TailShare::of`): /// /// - **三栏档 + 带加载器版本列表**:`has_loader_versions()` 要求 `problem.is_none()`,所以这一支 /// 滚动区里**只会有 head + chips**,错误行与它不可达; -/// - **chips 档(可用宽 < 600)**:`TailShare::of` 对 `Options`/`Install` 无条件 `Shared`, -/// `problem` **可以是** `Some`(静态不兼容 / 版本列表取不到 / 空表三种),那时错误行**会在 -/// 滚动区里出现**、可能被滚走;`show_versions()` 为假时钉死区只剩实例名(没有菜单、也没有 -/// 说明那一行)。这个行为域正好是 M227 要重新设计的地方,这里只如实记录现状。 +/// - **chips 档**:`TailShare::of` 对 `Options`/`Install` 无条件 `Narrow`,`problem` **可以是** +/// `Some`(静态不兼容 / 版本列表取不到 / 空表三种),那时错误行**在滚动区里**、可能被滚走; +/// `show_versions()` 为假时滚动区只剩 head + chips + 错误行,钉死区只有实例名。 pub(super) fn panel<'a>(ui: &'a Ui, catalog: Catalog, model: &'a Download) -> Element<'a, Message> { - panel_with(ui, catalog, model, false) + panel_with(ui, catalog, model, TailShare::Pinned) } -/// 同 [`panel`],但显式给「面板先保预算」这一档的标志(只有 `rows::content` 需要)。 +/// 同 [`panel`],但显式给分高方式(只有 `rows::content` 需要)。 pub(super) fn panel_with<'a>( ui: &'a Ui, catalog: Catalog, model: &'a Download, - shared: bool, + share: TailShare, ) -> Element<'a, Message> { let content = match panel_state(model) { PanelState::Install(install) => installing(ui, catalog, install), // 选中版本之后**先给选项,再点「安装」**(M105:以前是选中就直接开工,等于没有加载器维度) - PanelState::Options(options) => super::options::section(ui, catalog, options, shared), + PanelState::Options(options) => super::options::section(ui, catalog, options, share), PanelState::Hint => hint(ui, catalog), }; + // 上下各 12 的面板留白(`[X3, 0]` 的两元组是「垂直 / 水平」)。窄档预算把它算进去了。 let panel = container(content) .width(Length::Fill) .padding(Padding::from([ui.space(Space::X3), 0.0])); - // 面板先保预算档:面板吃满自己那份(`rows/tail.rs::whole_rows` 之后剩下的高度), + // 面板先保预算那两支:面板吃满自己那份(列表按整行取高之后剩下的高度), // 选项区的 `scrollable` 才有确定高度可吃。 - if shared { - panel.height(Length::Fill).into() - } else { + if matches!(share, TailShare::Pinned) { panel.into() + } else { + panel.height(Length::Fill).into() } } diff --git a/crates/app-iced/src/pages/download/options/s1_tests.rs b/crates/app-iced/src/pages/download/options/s1_tests.rs index 9159da6..eff1819 100644 --- a/crates/app-iced/src/pages/download/options/s1_tests.rs +++ b/crates/app-iced/src/pages/download/options/s1_tests.rs @@ -28,14 +28,16 @@ fn version_menu_height_counts_whole_items_only() { ); } -/// S1(D30 r1)的装配判据:**带加载器版本列表**的选项区在任何档位都与列表对半分。 +/// S1(D30 r1)的装配判据:**带加载器版本列表**的选项区在三栏档与列表共享高度。 /// /// 实测背景(1180×760):选项区从 204px 长到 372px 时,版本列表被压成 2.4 行(110px), -/// 底缘切在第三行图标上;对半分之后列表回到 4.6 行、面板自己可滚。 +/// 底缘切在第三行图标上;改成分高之后列表回到 4.6 行、面板自己可滚。 /// -/// 抓得住:有人把这条规则删掉(`TailShare::of` 只判 chips 档)——列表又会掉回 2 行; -/// 或者把它放宽成「任何选项区都对半分」(原版没有版本列表时不该对半分,那会把 6 行列表 -/// 砍成 4 行)。 +/// M227 起 chips 档单独一支(`Narrow`):同样的「先保预算」思路,但钉死区只留实例名, +/// 因为窄档 459px 装不下「钉住菜单 + 3 整行列表」(算术见 `rows/tail.rs::CHIPS_PANEL_BUDGET`)。 +/// +/// 抓得住:有人把 arm 2 删掉(`TailShare::of` 只判 chips 档)——三栏档列表又会掉回 2 行; +/// 或者把它放宽成「任何选项区都共享」(原版没有版本列表时不该共享,那会把 6 行列表砍成 4 行)。 #[test] fn tall_options_panel_shares_height_at_every_tier() { use crate::pages::download::install::PanelState; @@ -61,14 +63,19 @@ fn tall_options_panel_shares_height_at_every_tier() { VersionRailTier::Compact, VersionRailTier::Chips, ] { + let expected = if tier == VersionRailTier::Chips { + TailShare::Narrow + } else { + TailShare::Shared + }; assert_eq!( TailShare::of(tier, &PanelState::Options(&tall)), - TailShare::Shared, - "{tier:?}:带版本列表的选项区必须与列表对半分" + expected, + "{tier:?}:带版本列表的选项区必须与列表共享高度" ); } - // 原版没有版本列表 ⇒ 仍然钉死(三栏档不该无谓对半分)。 + // 原版没有版本列表 ⇒ 仍然钉死(三栏档不该无谓共享)。 let plain_state = AppState::new(); let plain = OptionsView::of( &version_row, @@ -85,7 +92,7 @@ fn tall_options_panel_shares_height_at_every_tier() { ); } - // 不兼容(有 problem)时列表不画 ⇒ 也不该对半分。 + // 不兼容(有 problem)时列表不画 ⇒ 也不该共享。 let blocked_state = state_with(LoaderKind::LiteLoader, "1.13", Ok(Vec::new())); let blocked = OptionsView::of( &row("1.13", VersionKind::Release), diff --git a/crates/app-iced/src/pages/download/options/view.rs b/crates/app-iced/src/pages/download/options/view.rs index 41b8c64..dacf934 100644 --- a/crates/app-iced/src/pages/download/options/view.rs +++ b/crates/app-iced/src/pages/download/options/view.rs @@ -1,9 +1,9 @@ -//! 选项区的控件树(M105)。 +//! 选项区的控件树(M105;窄档形态 M227)。 //! -//! ## 版面(424 正文列里的四段) +//! ## 版面(424 正文列里的四段,三栏档 `Shared`) //! //! ```text -//! ── 滚动区(Shared 档)──────────────────────────── +//! ── 滚动区 ────────────────────────────────────── //! 1.12.2 [安装] //! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] ← 六枚一行 //! ── 钉死区 ────────────────────────────────────── @@ -13,9 +13,23 @@ //! [实例名 ] //! ``` //! -//! - **钉死档**(原版等不带版本列表的面板)走另一条路:标签、说明、菜单、实例名都在同一列里 +//! 窄档(chips 档,可用宽 < 600,`rows::TailShare::Narrow`)**钉死区只剩实例名**,说明行与 +//! 菜单都进滚动区——那里的高度只够「钉住实例名 + 3 整行版本列表」,算术见 +//! `rows/tail.rs::CHIPS_PANEL_BUDGET`: +//! +//! ```text +//! ── 滚动区(首屏 104px = head 40 + 间距 4 + chips 32 + 间距 8 + 说明 20)── +//! 1.12.2 [安装] +//! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] +//! Vanilla parent only — Forge goes through the CLI +//! ┌ 1.12.2-SNAPSHOT ┐ ← 露在折线下方,滚得到(滚动条在右) +//! ── 钉死区 ────────────────────────────────────── +//! [实例名 ] +//! ``` +//! +//! - **钉死档**(原版等不带版本列表的面板)走第三条路:标签、说明、菜单、实例名都在同一列里 //! 顺序铺开(`04` 基线图就是这一档,不能改); -//! - 不兼容时的原因行(Error 色)在滚动区末尾(**chips 档**可达;三栏档 + 带版本列表时 +//! - 不兼容时的原因行(Error 色)在滚动区末尾(**窄档**可达;三栏档 + 带版本列表时 //! `problem` 恒为 `None`,所以那支看不到它); //! - 说明只有 LiteLoader 有(`Key::DownloadOptionsLiteLoaderVanillaOnly`)。 //! @@ -49,6 +63,7 @@ use crate::app::components::selection::{chip, Chip}; use crate::theme::Ui; use super::super::frame; +use super::super::rows::TailShare; use super::super::Message; use super::model::{Loader, LoaderVersionsView, OptionsView}; @@ -72,26 +87,33 @@ pub(super) fn version_menu_height(items: usize) -> f32 { /// 选项区(面板的下半块):**可滚的上半块 + 钉死的下半块**。 /// -/// `scroll` = 面板此刻是不是「自己可滚」的那一档(`rows::TailShare::Shared`)。是的话: +/// `share` = 分高方式(`rows::TailShare`,三个形态见 `install::panel_with` 的文档): /// -/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips(**chips 档**还可能有错误行,见下); -/// - **钉死区**:说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框。 +/// - `Pinned`(原版等不带版本列表的面板)走另一条路:标签、说明、菜单、实例名都在同一列里 +/// 顺序铺开(`04` 基线图就是这一档,不能改); +/// - `Shared`(三栏档 + 带版本列表): +/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips(**chips 档**还可能有错误行,见下); +/// - **钉死区**:说明行(有说明时;否则是「加载器版本」标签)+ 版本菜单 + 实例名输入框。 +/// - `Narrow`(chips 档 + 面板占高度,M227): +/// - **滚动区**:head + 六枚 chips + 说明行 + 错误行 + **版本菜单**; +/// - **钉死区**:只有实例名输入框。 /// -/// 为什么菜单也要钉(D30 四审):裁切位置由**钉死区高度**决定,与窗宽无关(1180 与 720 都切在 -/// 同一个 y)。之前只钉实例名,菜单就正好落在切线上——760 下菜单容器 642–706 只有 18px 可见、 -/// 条目文字一格都没画出来。把菜单一起钉住之后,它不再依赖首屏剩余高度分配,可达性由结构保证; -/// **两个 Shared 分支不一样**(`rows/tail.rs::TailShare::of`): -/// - 三栏档 + 带版本列表:`has_loader_versions()` 要求 `problem.is_none()` ⇒ 滚动区只有 -/// head + chips,错误行不可达; -/// - chips 档(可用宽 < 600,`of` 对 `Options`/`Install` 无条件 Shared):`problem` 可以是 -/// `Some`,错误行**会进滚动区**、可能被滚走;`show_versions()` 为假时钉死区只剩实例名。 -/// D30 八审实测:滚动区可见高 ≈96–104px,内容(head 40 + chips 32 + 间距 4)≈76px ⇒ -/// 1180×760 下 head(墨迹 462–501)与六枚 chips(506–537,32px 四角闭合)完整在首屏。 +/// 为什么菜单在三栏档钉、在窄档滚:裁切位置由**钉死区高度**决定(D30 四审), +/// 760 下菜单曾正好落在切线上(容器 642–706 只有 18px 可见、条目文字一格没画出来)。 +/// 三栏档把菜单一起钉住之后不再依赖首屏剩余高度分配;窄档没有那么多高度—— +/// 钉住菜单 + 说明 + 实例名要 180~228px,再加首屏 head+chips 76 就把列表挤到 ≤1 整行, +/// 而 M125 的像素守门要求 ≥3 整行。所以窄档只钉实例名,其余进滚动区(预算算术见 +/// `rows/tail.rs::CHIPS_PANEL_BUDGET`)。 +/// +/// D30 八审实测(1180×760,`Shared`):滚动区可见高 ≈96–120px,内容(head 40 + chips 32 + +/// 间距 4)≈76px ⇒ head(墨迹 462–501)与六枚 chips(506–537,32px 四角闭合)完整在首屏。 +/// M227 实测(720×760,`Narrow`):滚动区 120px ≥ 首屏 104(head 40 + 4 + chips 32 + 8 + +/// 说明行 20)⇒ head、六枚 chips、说明行三者完整在首屏,菜单露出容器上沿(有滚动条)。 pub(crate) fn section<'a>( ui: &'a Ui, catalog: Catalog, options: &'a OptionsView, - scroll: bool, + share: TailShare, ) -> Element<'a, Message> { let head = row![ container(frame::text_role( @@ -109,8 +131,8 @@ pub(crate) fn section<'a>( // 钉死档(原版 / 没有加载器版本列表)**保持旧结构**:标签与说明都在列内。 // 为什么不一并改:`04` 是别处基线图的对照,它的面板矮、列表 6 行——把标签撤出会 - // 让面板矮 26px,列表涨到 302px(6.86 行)反而切出半行。方案 B 只对 Shared 档生效。 - if !scroll { + // 让面板矮 26px,列表涨到 302px(6.86 行)反而切出半行。方案 B 只对共享档生效。 + if matches!(share, TailShare::Pinned) { let mut plain = column![ head, frame::text_role( @@ -163,25 +185,32 @@ pub(crate) fn section<'a>( return plain.into(); } - // 滚动区只剩 head + 六枚 chips(方案 B,D30 六审裁决;chips 档还可能有错误行,见 `section` 文档)。 + // 共享档的滚动区以 head + 六枚 chips 起头(方案 B,D30 六审裁决;错误行见下)。 // - // 为什么把「安装选项」标签与中性说明都撤出滚动区: - // 五审 82px → 六审 103px 的视口里,head(40) + 标签(17) + chips(32) + 两处间距(16) = 105px, - // 差 2px 就把 chip 的底边框裁掉;再塞说明(20px)就差 27~29px,说明整块 0 像素。 - // 并排标签(方案 C)在这里不可行:六枚 chip 实测已占 404px,正文列只有 424px, + // 为什么把「安装选项」标签撤出滚动区:五审 82px → 六审 103px 的视口里, + // head(40) + 标签(17) + chips(32) + 两处间距(16) = 105px,差 2px 就把 chip 的底边框裁掉; + // 再塞说明(20px)就差 27~29px,说明整块 0 像素。 + // 并排标签(方案 C)在这里不可行:六枚 chip 实测已占 404px 正文列只有 424px, // 加上「安装选项」必然横向溢出、把第六枚裁掉——那是拿一个已完成项换另一个。 // 所以按塔授权的 B 落地:**说明**搬到钉死区(见下面的 `pinned`);「安装选项」标签在 - // Shared 档**删掉**(不是搬——八审裁决,见下三行),Pinned 档保留。滚动区只装控件。 - // Shared 档**没有**「安装选项」标签:这是设计决定(D30 八审裁决)——六枚 chip 自解释, + // 共享档**删掉**(不是搬——八审裁决,见下三行),Pinned 档保留。滚动区只装控件。 + // 共享档**没有**「安装选项」标签:这是设计决定(D30 八审裁决)——六枚 chip 自解释, // 且正文列只有 424px、六枚 chip 已占 404px,加标签必然横向裁掉最后一枚。 - // 原版等 Pinned 档仍保留该标签(见上面 `!scroll` 那条路)。 + // 原版等 Pinned 档仍保留该标签(见上面 `Pinned` 那条路)。 + let narrow = matches!(share, TailShare::Narrow); let mut body = column![head, loader_row(ui, catalog, options)] .spacing(ui.space(Space::X1)) .width(Length::Fill); - // 中性说明**只在钉死区画一份**(八审:这里原先也画了一份,两处像素级重影)。 - // 「加载器版本」这一段只在**真该有列表**时出现:原版没有、静态不兼容没有(原因在下面那行)、 - // 取不到也没有(同样在原因行)。留一个空标题只会让人以为界面缺了一块。 + // 窄档:说明行(或「加载器版本」标签)与版本菜单**都进滚动区**——钉死区只剩实例名。 + // 顺序必须是 head → chips → 说明 → 菜单:预算(`CHIPS_PANEL_BUDGET`)保证前 104px 完整, + // 菜单露在折线下方(滚动条可见,滚得到)。 + if narrow && show_versions(options) { + body = body.push(versions_label(ui, catalog, options)); + } + + // 中性说明在三栏档**只在钉死区画一份**(八审:这里原先也画了一份,两处像素级重影)。 + // 窄档没有钉死的那一行(见上),所以它在这里画、也只画一次。 if let Some(problem) = &options.problem { body = body.push(frame::text_role( ui, @@ -192,30 +221,14 @@ pub(crate) fn section<'a>( )); } - // 钉死区:加载器版本菜单 + 实例名(都不参与滚动)。见 `section` 的文档。 + if narrow && show_versions(options) { + body = body.push(version_list(ui, catalog, options)); + } + + // 钉死区:三栏档是「说明/标签 + 菜单 + 实例名」,窄档只剩实例名。见 `section` 的文档。 let mut pinned = column![].spacing(ui.space(Space::X2)).width(Length::Fill); - if show_versions(options) { - // 菜单上方那一行:**有中性说明就显示说明**(它讲的是「这一家的叠法」,位置就在 - // 版本菜单正上方,语义对得上);没有说明时才退回通用标签。 - // 注意标签不是「搬」下来的:Shared 档的「安装选项」标签是**删掉**的(八审裁决,见上)。 - // 高度相同(BodySmall 与 LabelMedium 都是一行),所以钉死区不涨。 - let line = match &options.note { - Some(note) => frame::text_role( - ui, - TypeRole::BodySmall, - false, - note.as_str(), - ui.color(Role::OnSurfaceVariant), - ), - None => frame::text_role( - ui, - TypeRole::LabelMedium, - false, - catalog.text(Key::DownloadOptionsLoaderVersion), - ui.color(Role::OnSurfaceVariant), - ), - }; - pinned = pinned.push(line); + if !narrow && show_versions(options) { + pinned = pinned.push(versions_label(ui, catalog, options)); pinned = pinned.push(version_list(ui, catalog, options)); } pinned = pinned.push(text_field( @@ -239,6 +252,34 @@ pub(crate) fn section<'a>( .into() } +/// 版本菜单上方那一行:**有中性说明就显示说明**(它讲的是「这一家的叠法」,位置就在版本菜单 +/// 正上方,语义对得上);没有说明时才退回通用标签。 +/// +/// 高度相同(`BodySmall` 与 `LabelMedium` 都是一行),所以无论放滚动区还是钉死区都不改预算。 +/// 注意标签不是「搬」下来的:共享档的「安装选项」标签是**删掉**的(八审裁决,见 `section`)。 +fn versions_label<'a>( + ui: &'a Ui, + catalog: Catalog, + options: &'a OptionsView, +) -> Element<'a, Message> { + match &options.note { + Some(note) => frame::text_role( + ui, + TypeRole::BodySmall, + false, + note.as_str(), + ui.color(Role::OnSurfaceVariant), + ), + None => frame::text_role( + ui, + TypeRole::LabelMedium, + false, + catalog.text(Key::DownloadOptionsLoaderVersion), + ui.color(Role::OnSurfaceVariant), + ), + } +} + /// 加载器版本这一段要不要画(见 `section` 的注释)。 fn show_versions(options: &OptionsView) -> bool { if options.loader == Loader::Vanilla || options.problem.is_some() { diff --git a/crates/app-iced/src/pages/download/rails.rs b/crates/app-iced/src/pages/download/rails.rs index afa6c91..7b664e1 100644 --- a/crates/app-iced/src/pages/download/rails.rs +++ b/crates/app-iced/src/pages/download/rails.rs @@ -77,6 +77,8 @@ pub(super) fn three_column<'a>( rails: VersionRails, body_height: f32, ) -> Element<'a, Message> { + // 分高方式与列表高度都从 `TailShare` 一处取(`Pinned` 档不给整行高度)。 + let share = rows::TailShare::of(rails.tier, &super::install::panel_state(model)); row![ panel( ui, @@ -92,8 +94,8 @@ pub(super) fn three_column<'a>( ui, catalog, model, - rows::TailShare::of(rails.tier, &super::install::panel_state(model)), - Some(rows::whole_rows(body_height)), + share, + share.list_height(body_height), )) .width(Length::Fixed(rails.list)) .height(Length::Fill), diff --git a/crates/app-iced/src/pages/download/rows.rs b/crates/app-iced/src/pages/download/rows.rs index 7019961..13eeb94 100644 --- a/crates/app-iced/src/pages/download/rows.rs +++ b/crates/app-iced/src/pages/download/rows.rs @@ -29,11 +29,14 @@ //! 都在面板钉死区,见 `options/view.rs::section`)。 //! 同样的 760 高窗口里:三栏档列表还剩 158px(3 整行),chips 档只剩 18px。 //! -//! 修法是 [`TailShare`] 两档:**不带加载器版本列表**的选项区在三栏档照旧钉死(面板整块在屏内), +//! 修法是 [`TailShare`] 三档:**不带加载器版本列表**的选项区在三栏档照旧钉死(面板整块在屏内), //! chips 档**且面板真的占高度** //! (选项区展开 / 安装中)时改成「**面板先保预算、剩下按整行给列表**」 -//! (`tail::whole_rows` + `PANEL_BUDGET = 300`):1180×760 下列表 **176px = 4 整行**, -//! 面板拿走其余(≈307px)。实例名那格**已经钉死**在面板底部、不需要滚(M222 `6f87c8b`)。 +//! (`tail::narrow_rows` + `CHIPS_PANEL_BUDGET = 192`):700/720×760 下列表 **132px = 3 整行**、 +//! 面板拿走其余(208px)。M227 之前这一档是「Fill 与 Fill 五五分」——面板只分到 164px, +//! 钉死区(说明 + 菜单 + 实例名 ≈180px)把它吃穿,滚动区剩 0、head / 六枚 chip 全不在屏。 +//! 三栏档 + 带版本列表的选项区走另一支(`tail::whole_rows` + `PANEL_BUDGET = 300`): +//! 1180×760 列表 **176px = 4 整行**、面板 ≈294px,那里菜单与说明都钉在面板底部。 //! 「刚进页面还没选版本」(`Hint`,面板只有一行提示)**不共享**:那一态 main 本来就有 //! **7 整行**在屏(不是 8:第 8 条墨迹其实是 `Pick a version, then install` 那句 hint, //! 横向范围对不上版本号;700×760 上放 8 行要 352px,实测只剩 309~322px),五五分会砍成 @@ -82,13 +85,14 @@ use super::states; mod tail; -pub(crate) use tail::{whole_rows, TailShare}; +pub(crate) use tail::TailShare; /// 列表列(三栏档与 chips 档共用一份)。 /// /// `share` 决定「列表」与「尾部面板」怎么分这块高度(真实分支见 `tail::TailShare::of`): -/// `Pinned` = 面板按内容高钉在底部、列表吃剩下的;`Shared` = 面板先吃 `tail::PANEL_BUDGET`、 -/// 列表按**整行**吃剩下的(`tail::whole_rows`)。哪些组合走哪一支由那一处决定,这里不重复判。 +/// `Pinned` = 面板按内容高钉在底部、列表吃剩下的;`Shared` / `Narrow` = 面板先吃预算、 +/// 列表按**整行**吃剩下的(`tail::whole_rows` / `tail::narrow_rows`,两者差别在面板内部 +/// 钉什么,见 `tail::CHIPS_PANEL_BUDGET`)。哪些组合走哪一支由那一处决定,这里不重复判。 pub(super) fn content<'a>( ui: &'a Ui, catalog: Catalog, @@ -105,40 +109,38 @@ pub(super) fn content<'a>( } else if visible.is_empty() { states::empty(ui, catalog) } else { - // 只有 `Shared`(面板先保预算)那一档才按整行取高:`Pinned` 档(面板矮)实测已经 - // 是整行(`04` 图),取整反而会把它从 6 行缩到 5 行。 - let height = if matches!(share, TailShare::Shared) { - list_height - } else { + // 只有「面板先保预算」那两支(`Shared` / `Narrow`)才按整行取高:`Pinned` 档(面板矮) + // 实测已经是整行(`04` 图),取整反而会把它从 6 行缩到 5 行。 + let height = if matches!(share, TailShare::Pinned) { None + } else { + list_height }; list(ui, catalog, model, &visible, height) }; // 列表与尾部面板之间**总是**有一条分隔线(三态一致):M123 的取证图里「选好了版本」 // 那一态原先没有它,版本行与选项区第一行(版本号 + 安装按钮)读起来像同一块内容。 - let shared = matches!(share, TailShare::Shared); let tail: Element<'a, Message> = column![ divider::full(ui), - install::panel_with(ui, catalog, model, shared) + install::panel_with(ui, catalog, model, share) ] .spacing(ui.space(Space::X3)) .width(Length::Fill) .into(); - // chips 档 / 带版本列表的选项区:面板收回自己的一半。 + // chips 档 / 带版本列表的选项区:面板吃满自己那份。 // // `PanelState::Options` 走**面板内部**的滚动(上半块可滚 + 实例名钉页脚,见 `install::panel`), // 所以这里不再整块套一层 scrollable——那一层曾让窄窗出现两条并列滚动条,也因为把实例名 // 卷进滚动区而让它掉到首屏外(D30 r2 S7)。其它面板(安装中/失败)内容短,保留外层滚动。 let tail: Element<'a, Message> = match (share, panel_state(model)) { - (TailShare::Shared, PanelState::Options(_)) => tail, - (TailShare::Shared, _) => scrollable(tail) + (TailShare::Pinned, _) | (_, PanelState::Options(_)) => tail, + (TailShare::Shared | TailShare::Narrow, _) => scrollable(tail) .id(iced::widget::Id::new(PANEL_SCROLL_ID)) .height(Length::Fill) .style(frame::scroll_style(ui)) .into(), - (TailShare::Pinned, _) => tail, }; column![ @@ -273,7 +275,8 @@ fn list<'a>( .scroller_width(frame::SCROLLBAR_WIDTH) .margin(frame::SCROLLBAR_MARGIN), )) - // `Some` = 三栏档的 `Shared` 分支按**整行**取高(见 `tail::whole_rows`);`None` = 其它(含 chips 档)用 Fill。 + // `Some` = 面板先保预算那一档按**整行**取高(三栏档 `tail::whole_rows` / + // chips 档 `tail::narrow_rows`);`None` = `Pinned`(面板矮)用 Fill。 .height(height.map_or(Length::Fill, Length::Fixed)) .style(frame::scroll_style(ui)) .into() diff --git a/crates/app-iced/src/pages/download/rows/tail.rs b/crates/app-iced/src/pages/download/rows/tail.rs index b5ad9a3..859e8a6 100644 --- a/crates/app-iced/src/pages/download/rows/tail.rs +++ b/crates/app-iced/src/pages/download/rows/tail.rs @@ -1,85 +1,111 @@ -//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30)。 +//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30 / M227)。 //! //! 从 `rows.rs` 拆出来的理由只有一条:那边撞 400 行硬线(项目自带 //! `components::gate::every_source_file_stays_under_the_line_limit` 会判红)。本文件是 //! [`TailShare`] 的**纯搬移**,加上 M222 新增的三个只读量([`ROW_PITCH`] / [`LIST_CHROME`] / -//! [`whole_rows`],见 `rows.rs::content` 的用法)。 +//! [`whole_rows`])与 M227 的 chips 档预算([`CHIPS_STACK`] / [`CHIPS_LIST_CHROME`] / +//! [`CHIPS_PANEL_BUDGET`] / [`narrow_rows`])。 use crate::pages::layout::VersionRailTier; use crate::pages::download::install::PanelState; -/// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法)。 +/// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法;M227 拆出 chips 档一支)。 /// -/// | 档 | 分法 | 为什么 | +/// | 档位 × 面板形态 | 分法 | 为什么 | /// |---|---|---| -/// | `Wide` / `Narrow` / `Compact` + 面板**不带**加载器版本列表 | [`TailShare::Pinned`]:面板按内容高钉在底部,列表吃剩下的 | 原版面板矮(`04` 实测列表 6 行),整块在屏内 | -/// | 任何档 + 选项区**带加载器版本列表** | [`TailShare::Shared`]:面板先吃 [`PANEL_BUDGET`],剩下按整行给列表([`whole_rows`]) | 带版本列表的选项区实高 ≈307px,对半分(旧规则)把列表压到 110px;现在 1180×760 是 **176px = 4 整行** | -/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Shared`] | chips 档比三栏档多花两行 chip,列表只剩 **18px**(M123 实测 0.4 行)——版本号读不出来就换不了版本 | -/// | 任何档 + 只画 `Hint`(没选版本,面板只有一行) | [`TailShare::Pinned`] | 一行提示不值得对半分:700×760 下 main 是 **7 整行**在屏(不是 8——第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,横向范围对不上版本号;要放 8 行得 352px,实测只剩 309~322px),对半分只剩 **4 整行**、下面留 160px 空白(r1 复审 P2-1,r2 修回 7) | +/// | `Wide` / `Narrow` / `Compact` + 面板**不带**加载器版本列表 | [`TailShare::Pinned`] | 原版面板矮(`04` 实测列表 6 行),整块在屏内 | +/// | 任何档 + 选项区**带加载器版本列表** | [`TailShare::Shared`]:面板先吃 [`PANEL_BUDGET`],剩下按整行给列表([`whole_rows`]) | 带版本列表的选项区实高 ≈294~307px;旧规则(对半分)把列表压到 110px,现在 1180×760 是 **176px = 4 整行** | +/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Narrow`]:面板先吃 [`CHIPS_PANEL_BUDGET`],剩下按整行给列表([`narrow_rows`]) | 窄档可用高比三栏档少两行 chip(459 vs 580)⇒ 只钉实例名,head/chips/说明/菜单进滚动区;700/720×760 列表 **132px = 3 整行** | +/// | 任何档 + 只画 `Hint`(没选版本,面板只有一行) | [`TailShare::Pinned`] | 一行提示不值得让面板吃预算:700×760 下 main 是 **7 整行**在屏(不是 8——第 8 条墨迹其实是 `Pick a version, then install` 那句 hint,横向范围对不上版本号;要放 8 行得 352px,实测只剩 309~322px),对半分只剩 **4 整行**、下面留 160px 空白(r1 复审 P2-1,r2 修回 7) | /// /// 分高方式按「档位 + 面板形态」判;**具体像素**由窗口高度算(`pages/download.rs::view_sized` -/// 把 `model.window_height` 经 `frame::body_height` 传下来,见 [`whole_rows`])。 +/// 把 `model.window_height` 经 `frame::body_height` 传下来,见 [`whole_rows`] / [`narrow_rows`])。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TailShare { - /// 面板钉在底部(三栏档)。 + /// 面板钉在底部(面板矮的档位)。 Pinned, - /// 面板先吃预算、列表按整行吃剩下的(带版本列表的选项区 / chips 档占高度时)。 + /// 三栏档 + 带版本列表的选项区:面板先吃预算、列表按整行吃剩下的;钉死区含**说明/标签 + /// + 版本菜单 + 实例名**(D30 四审/八审判定:1180 档高度够,菜单钉住最省事)。 Shared, + /// chips 档(可用宽 < 600)+ 面板占高度:同一套「先保预算、列表按整行」, + /// 但面板钉死区**只留实例名**,head / 六枚 chip / 说明 / 版本菜单全在滚动区 + /// (理由见 [`CHIPS_PANEL_BUDGET`]:459 里装不下「钉住菜单」+「3 整行列表」)。 + Narrow, } impl TailShare { /// 档位 + **面板形态** → 分高方式(纯函数,单测直接钉它)。 /// - /// 两个条件必须同时成立才共享: - /// 1. 档位是 `Chips`(多花两行 chip,竖排预算最紧的那一档); - /// 2. 面板真的**占高度**——`Options`(选中了版本、选项区展开)或 `Install`(进度 / 失败)。 + /// 三条 arm,**不是**「两个条件同时成立」才共享(M227 重写:旧文档只写 M125 那一条, + /// 把 arm 2 的「任何档位」漏了 —— r8 复审 improve finding): + /// 1. `Chips`(可用宽 < 600,多花两行 chip、竖排预算最紧的那一档)**且**面板真占高度 + /// (`Options` 选中了版本 / `Install` 进度或失败)⇒ [`TailShare::Narrow`]; + /// 2. **任何档位**的 `Options` 只要 [`has_loader_versions`](super::super::options::OptionsView::has_loader_versions) + /// (选项区带加载器版本列表 ⇒ 实高 ≈294~307px)⇒ [`TailShare::Shared`]。三栏档只走这条, + /// chips 档被 arm 1 先接走; + /// 3. 其余 ⇒ [`TailShare::Pinned`]:不带版本列表的 `Options`(原版 / 不兼容 / 空表)与 + /// `Hint`。 /// - /// `Hint`(刚进页面还没选版本)只是**一行**提示(≈30px),对半分会让列表从 **7 整行** - /// 掉到 **4 整行**、下半块留 ≈160px 空画布(r1 复审 P2-1 实测:main 在 700×760 的 + /// `Hint`(刚进页面还没选版本)只是**一行**提示(≈30px),让它吃预算会把列表从 **7 整行** + /// 砍到 **4 整行**、下半块留 ≈160px 空画布(r1 复审 P2-1 实测:main 在 700×760 的 /// `Hint` 态是 7 整行——不是 8,第 8 条墨迹其实是 `Pick a version, then install` 那句 /// hint,横向范围对不上版本号;r2 修回 7 整行)。所以 `Hint` **在任何档位都钉死**。 pub(crate) fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self { match (tier, state) { + // M227:chips 档(窄档)单独一支 —— 同一份「先保预算」思路,但钉死区只留实例名。 (VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => { - Self::Shared + Self::Narrow } // M222 D30 r1/r2:**带加载器版本列表**的选项区在任何档位都太高。三栏档修前 // 面板 204→372px,版本列表被压到 2.4 行(110px)且底缘切在第三行图标上。 // 现在走 Shared:面板先吃 `PANEL_BUDGET`,列表按**整行**取(`whole_rows`, - // 标定 44/97)⇒ 1180×760 列表 **176px = 4 整行**、面板 ≈307px; + // 标定 44/97)⇒ 1180×760 列表 **176px = 4 整行**、面板 ≈294px; // 900 档 308px = 7 整行。实例名与版本菜单都在面板钉死区(`options::section`)。 (_, PanelState::Options(options)) if options.has_loader_versions() => Self::Shared, _ => Self::Pinned, } } + + /// 列表视口该拿多少高(`None` = 面板按内容高钉死,列表吃剩下的 `Fill`)。 + /// + /// `column_height` 两个分支的入参口径一致:都传 + /// [`frame::body_height`](crate::pages::frame::body_height) 的返回值 + /// (chips 档上方那两行 chip 由 [`narrow_rows`] 自己扣,调用点不用管)。 + pub(crate) fn list_height(self, column_height: f32) -> Option { + match self { + Self::Pinned => None, + Self::Shared => Some(whole_rows(column_height)), + Self::Narrow => Some(narrow_rows(column_height)), + } + } } /// 版本行的**实测节距**(D30 三审:`04` 图行内下载图标的行首 265/309/353/397/441/485, /// 节距恰 **44.0**;首行选中高亮矩形 253–296 也是 44px)。 /// /// 列表是 `Fill + scrollable` 时,视口高度不是节距整数倍就会切在某一行中间——D30 r2 实测 -/// `01` 是 245px(5.3 行),英文态露出 6px 竖干、中文态露出「预览版」上半截。三栏档的 -/// `Shared`(面板先保预算)那一支现在按整行取高([`whole_rows`]),底缘落在行边界上。 +/// `01` 是 245px(5.3 行),英文态露出 6px 竖干、中文态露出「预览版」上半截。`Shared` 与 +/// `Narrow` 两支都按整行取高,底缘落在行边界上。 pub(crate) const ROW_PITCH: f32 = 44.0; -/// 列表以外、列内要扣掉的固定占位(**列顶 156 → 列表顶 253**,D30 三审实测 = 97)。 +/// 三栏档「列表以外、列内要扣掉的固定占位」(**列顶 156 → 列表顶 253**,D30 三审实测 = 97)。 /// /// 1180×760:正文列 580 ⇒ floor((580−97−300) / 44) × 44 = **176px = 4 整行**,分隔线落在 437。 /// 1180×900:floor((720−97−300) / 44) × 44 = **308px = 7 整行**(八审实测分隔线 569)。 /// **改搜索条 / 表头高度时要同步这个数。** pub(crate) const LIST_CHROME: f32 = 97.0; -/// 面板(滚动区 + 钉死区)的**最小预算**。 +/// 三栏档面板(滚动区 + 钉死区)的**最小预算**。 /// /// 滚动区(八审后只剩 head 40 + 六枚 chips 32 + 间距 4 = **76**); -/// 钉死区 = 说明/标签行 17 + 间距 8 + 菜单 64 + 间距 8 + 实例名 56 ≈ **153**。 -/// 取 300 覆盖两者(76 + 153 = 229)再留余量给 chips 底边框与滚动条。 +/// 钉死区 = 说明/标签行 20 + 间距 8 + 菜单 64 + 间距 8 + 实例名 56 = **156**。 +/// 取 300 覆盖两者(76 + 156 = 232)再留余量给 chips 底边框与滚动条。 /// D30 五审实测:上一版「对半分」把滚动区压到 82px——宽档切「安装选项」字身、六枚 chip -/// 0 像素;窄档 24px 全被留白吃掉。 +/// 0 像素;窄档 24px 全被留白吃掉。窄档另有一套更紧的预算,见 [`CHIPS_PANEL_BUDGET`]。 pub(crate) const PANEL_BUDGET: f32 = 300.0; -/// 正文列高度 → 列表视口的**整行高度**(只给三栏档的 Shared 分支用)。 +/// 正文列高度 → 列表视口的**整行高度**(给三栏档的 `Shared` 分支用)。 /// /// 分高规则从「对半分」改成「**面板先保预算,剩下的按整行给列表**」:760 档 /// (正文列 580)⇒ 580 − 97 − 300 = 183 ⇒ floor(183/44)×44 = **176px = 4 整行**; @@ -90,3 +116,85 @@ pub(crate) fn whole_rows(column_height: f32) -> f32 { let room = (column_height - LIST_CHROME - PANEL_BUDGET).max(ROW_PITCH); (room / ROW_PITCH).floor() * ROW_PITCH } + +/// chips 档(可用宽 < 600)里 `rows::content` **上方**那一坨的实测高(M227)。 +/// +/// 分类 tabs 48 + 间距 8 + 主版本 chip 条 48 + 间距 8 + 分隔线 1 + 间距 8 = **121** +/// (720×760 实测:正文列顶 156 → `rows::content` 顶 277)。 +/// **改 `rails::stacked_chips` 的高度时同步这个数**(`Element` 没有几何内省,只能实测 + 常量)。 +pub(crate) const CHIPS_STACK: f32 = 121.0; + +/// chips 档里 `rows::content` 自身的固定占位(M227 实测)。 +/// +/// 列表之前:搜索条 56 + 间距 8 + 分隔线 1 + 间距 8 + 计数头 17 + 间距 8 = 98 +/// (实测内容顶 277 → 列表顶 375);列表之后还要给尾部:间距 8 + 分隔线 1 + 间距 12 = 21 +/// (实测列表底 551 → 分隔线 559 → 面板顶 572)⇒ **119**。 +pub(crate) const CHIPS_LIST_CHROME: f32 = 119.0; + +/// chips 档里选项区**至少要拿到**的高度(M227 的窄档预算)。 +/// +/// 窄档的取舍:钉死区只留实例名,其余全进滚动区(`options::section` 的 `Narrow` 一支)。 +/// 预算 = 上下内边距 12×2 + 首屏内容 104 + 滚动区↔钉死区间距 8 + 实例名 56 = **192**; +/// 其中首屏 104 = head 40 + 间距 4 + 六枚 chip 32 + 间距 8 + 说明/标签行 20。 +/// +/// 为什么菜单在窄档**不钉**(三栏档是钉的):700/720×760 的 `rows::content` 只有 **459**, +/// 若钉死区还要含菜单(说明 20 + 8 + 菜单 64~112 + 8 + 实例名 56 + 内边距 24 = 180~228), +/// 再加首屏 head+chips 76 就是 256~304,列表只剩 ≤1 整行 —— 而 M125 的像素守门 +/// (`tests/options_shot/chips.rs` ②)要求 **≥3 整行 = 132**。两者在 459 里互斥, +/// 所以窄档把菜单放进滚动区(滚得到、滚动条可见),钉死区只留实例名。 +/// +/// 五审实测的坏形态(本预算要消灭的):滚动区视口被钉死区吃成 **0**(五审记的「24px」其实是 +/// 面板内边距 12 + 间距 8 + 说明行顶端),head / 六枚 chip / 说明一个像素都不在屏; +/// 实例名输入框被挤成 **35px**(五审记 36),值墨迹的降部被自己的底边框裁掉。 +#[doc(alias = "窄档预算")] +pub(crate) const CHIPS_PANEL_BUDGET: f32 = 192.0; + +/// 正文列高度 → chips 档列表视口的**整行高度**(M227)。 +/// +/// 720×760:580 − 121([`CHIPS_STACK`])− 119([`CHIPS_LIST_CHROME`])− 192([`CHIPS_PANEL_BUDGET`]) +/// = 148 ⇒ floor(148/44)×44 = **132px = 3 整行**;面板拿走剩下的 208(滚动区 120 ≥ 首屏 104)。 +/// 与 [`whole_rows`] 同一条公式,只是分子换成窄档的三段固定占位。 +#[must_use] +pub(crate) fn narrow_rows(column_height: f32) -> f32 { + let room = + (column_height - CHIPS_STACK - CHIPS_LIST_CHROME - CHIPS_PANEL_BUDGET).max(ROW_PITCH); + (room / ROW_PITCH).floor() * ROW_PITCH +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 窄档预算的算术(M227):760 档恰好 3 整行、面板剩下的高度够首屏 + 实例名。 + /// + /// 抓得住:把预算改小(列表涨到 4 整行 ⇒ 滚动区 < 首屏 104,说明行被切)或改大 + /// (列表掉到 2 整行 ⇒ M125 的像素守门 ② 红)。 + #[test] + fn the_chips_budget_keeps_three_whole_rows_at_760() { + let body = 580.0; // 760 − 标题栏 48 − 页头 84 − 上下页边距 48 + assert_eq!(narrow_rows(body), 132.0, "720×760 的 chips 档列表 = 3 整行"); + let panel = body - CHIPS_STACK - CHIPS_LIST_CHROME - narrow_rows(body); + assert_eq!(panel, 208.0, "面板拿剩下的那块"); + // 滚动区 = 面板 − 上下内边距 24 − 实例名 56 − 滚动区↔钉死区间距 8 + assert!( + panel - 24.0 - 56.0 - 8.0 >= 104.0, + "滚动区 {} < 首屏 104(head 40 + 4 + chips 32 + 8 + 说明 20)", + panel - 24.0 - 56.0 - 8.0 + ); + } + + /// 三栏档那支不动(M227 回归钉子):1180×760 = 176(4 整行)、900 高 = 308(7 整行)。 + #[test] + fn the_three_column_budget_is_unchanged() { + assert_eq!(whole_rows(580.0), 176.0); + assert_eq!(whole_rows(720.0), 308.0); + } + + /// `list_height` 把三档的「列表吃多少」收敛到一处(`Pinned` = 不给定高)。 + #[test] + fn list_height_follows_the_share() { + assert_eq!(TailShare::Pinned.list_height(580.0), None); + assert_eq!(TailShare::Shared.list_height(580.0), Some(176.0)); + assert_eq!(TailShare::Narrow.list_height(580.0), Some(132.0)); + } +} diff --git a/crates/app-iced/src/pages/download/tests/options_shot/chips.rs b/crates/app-iced/src/pages/download/tests/options_shot/chips.rs index 20380e5..2d1f51f 100644 --- a/crates/app-iced/src/pages/download/tests/options_shot/chips.rs +++ b/crates/app-iced/src/pages/download/tests/options_shot/chips.rs @@ -7,7 +7,8 @@ use super::{bands, feed, ui, Shot, CHIPS_SHOT}; /// 修前(M123 取证,2026-11-05,tiny-skia,700×760,`ULL_DL_DEMO=options-fabric`): /// chips 档多出两行 chip,选项区又整块钉在列表下面(316px),于是列表列只剩一条 **18px** 的缝, /// 选中行的底色带实测 19px(行高 44)——版本号一个字读不出来,用户换不了版本。 -/// 修法见 `rows::TailShare`:chips 档改成「列表与面板各吃一半,面板自己可滚」。 +/// 修法见 `rows::TailShare`:chips 档改成「面板先保预算、列表按整行吃剩下的」 +/// (M227 起是 `TailShare::Narrow` + `rows/tail.rs::CHIPS_PANEL_BUDGET`,700/720×760 = 3 整行)。 /// /// ```text /// ULL_DL_CHIPS_SHOT=02-options-fabric-en-light-chips700.png \ @@ -15,13 +16,15 @@ use super::{bands, feed, ui, Shot, CHIPS_SHOT}; /// ``` /// /// 抓得住:①首行版本行的底色带 = 44(整行在屏);②**列表区 ≥ 3 整行**(面板上沿那条通栏 -/// 分隔线就是下界);③分隔线下方 96px 内有一条 ≥ 80px 的实色带(面板头那条实心「安装」按钮)—— -/// 「能换版本」与「能看见选项面板」是这条断言的成对验收。 +/// 分隔线就是下界)——这是窄档预算的硬下限:面板钉死区若重新钉住版本菜单(M227 之前的样子), +/// 它在 459px 里只能再留 1 整行给列表;③分隔线下方 96px 内有一条 ≥ 80px 的实色带(面板头那条 +/// 实心「安装」按钮 / 菜单容器的满宽底边)——「能换版本」与「能看见选项面板」是这条断言的成对验收。 /// /// 抓不住:文本有没有被**横向**裁掉(那要看字形宽度,见 `tests/rails.rs` 的标签断言与 /// 报告里的量测表)、hover/tooltip 层(静态图拍不到)、面板内部滚动到底后长什么样 /// (静态图只有一帧,只能拍到 scroll offset 0)——所以这里**不**断言「面板没被窗口底边切」: -/// chips 档的面板本来就是可滚的一半,实例名那一格确实落在折线以下。 +/// chips 档的面板本来自己可滚,实例名钉在面板底部。**M227 起**滚动区首屏装 +/// head + 六枚 chip + 说明行(104px),版本菜单露在折线下方、滚得到。 #[test] fn a_chips_tier_shot_keeps_a_whole_version_row_readable() { let Some(path) = feed(CHIPS_SHOT) else { diff --git a/crates/app-iced/src/pages/download/tests/view.rs b/crates/app-iced/src/pages/download/tests/view.rs index 6c0ffcd..c3a3a7f 100644 --- a/crates/app-iced/src/pages/download/tests/view.rs +++ b/crates/app-iced/src/pages/download/tests/view.rs @@ -146,7 +146,7 @@ fn the_demo_data_is_sane() { assert!(demo.rows.iter().all(|row| !row.supporting.contains('版'))); } -/// 列表与尾部面板怎么分高:**档位 × 面板形态**两个条件同时成立才对半分。 +/// 列表与尾部面板怎么分高:**档位 + 面板形态** → 三档之一(`Pinned` / `Shared` / `Narrow`)。 /// /// 修前(M123):面板一律按内容高钉在底部,700×760 的 chips 档里列表只剩 18px(0.4 行)。 /// M125 r1 修法只判档位,又踩出另一条回归:chips 档「刚进页面还没选版本」(`Hint`,面板只有 @@ -154,12 +154,14 @@ fn the_demo_data_is_sane() { /// (复审 P2-1 实测;main 那一态是 **7 整行**——不是 8:第 8 条墨迹其实是 /// `Pick a version, then install` 那句 hint,横向范围对不上版本号。r2 修回 7 整行)。 /// -/// 现在的判据:`Chips` **且**面板真占高度(`Options` / `Install`)才 `Shared`; -/// `Hint` 在任何档位都 `Pinned`。 +/// 现在的判据(M227 重写,旧文档只写了 M125 那一条条件 —— r8 复审 improve finding): +/// ① `Chips` **且**面板真占高度(`Options` / `Install`)⇒ `Narrow`; +/// ② **任何档位**的 `Options` 只要带加载器版本列表 ⇒ `Shared`(三栏档只走这条); +/// ③ 其余(含 `Hint`)⇒ `Pinned`。 /// -/// 抓得住:①chips 档改回「只判档位」(`Hint` 跟着对半分 ⇒ 列表掉到 4 行,像素守门 -/// `ready::a_ready_shot_keeps_the_whole_visible_list` 立刻红);②三栏档也改成 `Shared` -/// (1180 下选项面板被切成一半,`an_options_shot_…` 会红)。 +/// 抓得住:①chips 档改回「Pinned」(面板把列表压成缝 ⇒ 像素守门 +/// `ready::a_ready_shot_keeps_the_whole_visible_list` / `chips::a_chips_tier_shot_…` 立刻红); +/// ②三栏档也改成 `Narrow`(1180 下钉死区少了菜单与说明,`an_options_shot_…` 的量测会红)。 /// 抓不住:**实际分到多少像素**(`iced` 的 `Element` 没有几何内省,那只能出图量)。 #[test] fn the_chips_tier_shares_the_height_only_when_the_panel_takes_room() { @@ -207,8 +209,8 @@ fn the_chips_tier_shares_the_height_only_when_the_panel_takes_room() { assert_eq!( TailShare::of(VersionRailTier::Chips, &panel_state(&options)), - TailShare::Shared, - "chips 档 + 选项区展开:对半分(列表 4 整行、面板自己可滚)" + TailShare::Narrow, + "chips 档 + 选项区展开:窄档预算(面板只钉实例名,列表 3 整行)" ); assert_eq!( TailShare::of(VersionRailTier::Chips, &panel_state(&hint)),