From 954281d71a9a14ba3965dae5a99a65c35defab78 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 10 Nov 2026 15:39:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced):=20=E7=89=88=E6=9C=AC=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=9C=A8=E4=B8=89=E6=A0=8F=E5=AF=B9=E5=8D=8A=E5=88=86?= =?UTF-8?q?=E6=A1=A3=E6=8C=89=E6=95=B4=E8=A1=8C=E5=8F=96=E9=AB=98=EF=BC=88?= =?UTF-8?q?S1b/S9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 列表视口高度不再是「Fill 剩下的」:正文列高度由仓里已有的 `frame::body_height` 算出, 经 `view_sized` → `body` → `rails::three_column` → `rows::content` 传下来,Shared 档据此 `floor(((h − LIST_CHROME)/2) / ROW_PITCH) * ROW_PITCH` 取整行。 实测(1180×760):列表 245px → **239px**(分隔线 498 → 492,腾出的 6px 归面板), `04` 对照**未变**(531 / 278px),720 窄窗**未变**(本来就是 4.0 整行、底缘 0 墨迹)。 两处修正写在提交里: 1. 第一版给所有三栏场合取整,把 `04` 也缩到 239px ⇒ 收窄为只在 `TailShare::Shared` 取整; 2. `rows.rs` 涨到 419 行撞 400 行门禁 ⇒ `TailShare` 与三个标定量纯搬移到 `rows/tail.rs`, `rows.rs` 回到 355 行(门禁 `components::gate` 与全仓扫描都绿)。 `view` 保留 4 参数(`#[cfg(test)]`,只有单测用),产品路径走 `view_sized`。 --- crates/app-iced/src/pages/download.rs | 29 ++++++- crates/app-iced/src/pages/download/rails.rs | 2 + crates/app-iced/src/pages/download/rows.rs | 68 ++++------------ .../app-iced/src/pages/download/rows/tail.rs | 79 +++++++++++++++++++ crates/app-iced/src/pages/mod.rs | 8 +- 5 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 crates/app-iced/src/pages/download/rows/tail.rs diff --git a/crates/app-iced/src/pages/download.rs b/crates/app-iced/src/pages/download.rs index af799fd..5b7e579 100644 --- a/crates/app-iced/src/pages/download.rs +++ b/crates/app-iced/src/pages/download.rs @@ -161,11 +161,25 @@ use super::{Action, Message}; /// /// `lang` 是 i18n 的入参,调用点传的是 `pages::Model.lang`(M106 起 = `App` 的生效语言, /// 见 `ui::lang::LangState`);页面自己不读全局、不猜语言。 +/// 四参数的 [`view_sized`]:给**用不到窗口高度**的调用方(只有单测;产品路径走 +/// `pages::view` → [`view_sized`],所以这条在非 test 构建里是死代码,`#[cfg(test)]` 收口)。 +#[cfg(test)] pub fn view<'a>( ui: &'a Ui, metrics: FrameMetrics, model: &'a Download, lang: Lang, +) -> Element<'a, Message> { + view_sized(ui, metrics, model, lang, 760.0) +} + +/// 同 [`view`],但显式给**窗口高度**(版本列表要按整行取高,见 [`rows::whole_rows`])。 +pub fn view_sized<'a>( + ui: &'a Ui, + metrics: FrameMetrics, + model: &'a Download, + lang: Lang, + window_height: f32, ) -> Element<'a, Message> { let catalog = lang.catalog(); @@ -182,7 +196,15 @@ pub fn view<'a>( ), )); - frame::page(ui, metrics, header, body(ui, metrics, catalog, model)) + // 正文列高度 = 窗口 − 标题栏 − 页头 − 上下页边距(唯一来源 [`frame::body_height`])。 + // 版本列表要按**整行**取高,就得知道这一块有多高(D30 r2 S1b/S9)。 + let body_height = frame::body_height(ui, metrics, window_height, true); + frame::page( + ui, + metrics, + header, + body(ui, metrics, catalog, model, body_height), + ) } /// 正文:加载态仍是居中占位;其余状态都长在三栏骨架上。 @@ -191,6 +213,7 @@ fn body<'a>( metrics: FrameMetrics, catalog: Catalog, model: &'a Download, + body_height: f32, ) -> Element<'a, Message> { if model.is_loading() { // 还没拉到版本列表:三栏没有内容可填,给占位(安装面板照旧带着,见 M94 复审 p3) @@ -221,7 +244,7 @@ fn body<'a>( }; let content: Element<'a, Message> = if rails.tier.is_three_column() { - rails::three_column(ui, catalog, model, rails) + rails::three_column(ui, catalog, model, rails, body_height) } else { // 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽。 // 高度分法跟着档位走:chips 档多花两行 chip,面板必须收回自己的一半(M125 缺陷 1)。 @@ -233,6 +256,8 @@ fn body<'a>( catalog, model, rows::TailShare::of(rails.tier, &install::panel_state(model)), + // chips 档不按整行取高:实测那里已经是 4.0 整行、零残影,别动。 + None, ), ] .spacing(ui.space(Space::X2)) diff --git a/crates/app-iced/src/pages/download/rails.rs b/crates/app-iced/src/pages/download/rails.rs index 0c35a46..afa6c91 100644 --- a/crates/app-iced/src/pages/download/rails.rs +++ b/crates/app-iced/src/pages/download/rails.rs @@ -75,6 +75,7 @@ pub(super) fn three_column<'a>( catalog: Catalog, model: &'a Download, rails: VersionRails, + body_height: f32, ) -> Element<'a, Message> { row![ panel( @@ -92,6 +93,7 @@ pub(super) fn three_column<'a>( catalog, model, rows::TailShare::of(rails.tier, &super::install::panel_state(model)), + Some(rows::whole_rows(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 fbca25b..4ad46f6 100644 --- a/crates/app-iced/src/pages/download/rows.rs +++ b/crates/app-iced/src/pages/download/rows.rs @@ -72,12 +72,15 @@ use crate::theme::Ui; use super::super::frame; use super::super::layout::rails::{LIST_DATE_WIDTH, LIST_KIND_WIDTH, ROW_GAP, ROW_ICON_BUTTON}; -use super::super::layout::VersionRailTier; use super::super::Message; use super::install::{self, panel_state, PanelState}; use super::model::{Download, VersionRow}; use super::states; +mod tail; + +pub(crate) use tail::{whole_rows, TailShare}; + /// 列表列(三栏档与 chips 档共用一份)。 /// /// `share` 决定「列表」与「尾部面板」怎么分这块高度:三栏档钉死面板、chips 档各吃一半 @@ -87,6 +90,7 @@ pub(super) fn content<'a>( catalog: Catalog, model: &'a Download, share: TailShare, + list_height: Option, ) -> Element<'a, Message> { let visible = visible(model); @@ -97,7 +101,14 @@ pub(super) fn content<'a>( } else if visible.is_empty() { states::empty(ui, catalog) } else { - list(ui, catalog, model, &visible) + // 只有「列表与面板对半分」那一档才按整行取高:钉死档(面板矮)实测已经是整行 + // (04 图 278px),取整反而会把它从 6 行缩到 5 行。 + let height = if matches!(share, TailShare::Shared) { + list_height + } else { + None + }; + list(ui, catalog, model, &visible, height) }; // 列表与尾部面板之间**总是**有一条分隔线(三态一致):M123 的取证图里「选好了版本」 @@ -145,55 +156,6 @@ pub(super) fn content<'a>( /// 「实例名那格滚得到」——harness 拿到这个 Id 就能派一次 `scroll_to` 再拍。 pub(crate) const PANEL_SCROLL_ID: &str = "download-panel"; -/// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法)。 -/// -/// | 档 | 分法 | 为什么 | -/// |---|---|---| -/// | `Wide` / `Narrow` / `Compact` | [`TailShare::Pinned`]:面板按内容高钉在底部,列表吃剩下的 | 竖栏档在 760 高的窗口里列表还剩 158px(3 整行),面板整块在屏内——两样都对 | -/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Shared`]:两半各 `Fill`,面板自己可滚 | chips 档比三栏档多花两行 chip(tabs 48 + chips 48 + 间距),列表只剩 **18px**(M123 实测 0.4 行),面板 316px 全在屏内也没用——版本号读不出来就换不了版本 | -/// | 任何档 + 只画 `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) | -/// -/// 判据是**档位**不是高度:页面的模型里只有 `window_width`(`pages/model.rs`),窗口高度 -/// 没有接进页面(要接得改 `pages/mod.rs` 的入口签名,超出本页的边界)。所以这里按「哪一档 -/// 的竖排 chrome 会把列表压没」来分,并在报告里登记这条判据的边界。 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum TailShare { - /// 面板钉在底部(三栏档)。 - Pinned, - /// 面板与列表各吃一半(chips 档)。 - Shared, -} - -impl TailShare { - /// 档位 + **面板形态** → 分高方式(纯函数,单测直接钉它)。 - /// - /// 两个条件必须同时成立才共享: - /// 1. 档位是 `Chips`(多花两行 chip,竖排预算最紧的那一档); - /// 2. 面板真的**占高度**——`Options`(选中了版本、选项区展开)或 `Install`(进度 / 失败)。 - /// - /// `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(super) fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self { - match (tier, state) { - (VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => { - Self::Shared - } - // M222 D30 r1 S1 / r2 S7:**带加载器版本列表**的选项区在任何档位都太高。 - // 三栏档(1180×760)实测:修前面板 204→372px,版本列表 6.0 行(278px)被压到 - // 2.4 行(110px,底缘切在第三行图标上);改成 Shared 之后版本列表 **245px = 5.29 行**、 - // 面板 237px(分隔线 y=498)。面板因此变矮,实例名会被挤出首屏 ⇒ 同一轮把它 - // **钉成面板页脚**(`install::panel_with` 的 `shared` 分支 + `options::section`)。 - // 仍欠一项:245px 不是行高整数倍,列表底缘仍切在第 6 行中间(S1b/S9)。 - // 修它要把「分给列表的高度」从 `pages/download.rs` 传进来做 floor 取整 - // (`responsive` 在 Fill 子项上拿不到确定高度,实测无效,已回退)。 - (_, PanelState::Options(options)) if options.has_loader_versions() => Self::Shared, - _ => Self::Pinned, - } - } -} - /// 搜索条 + 清空按钮 + 命中数(三者一行;命中数只在有查询词时出现)。 pub(super) fn search_row<'a>( ui: &'a Ui, @@ -288,6 +250,7 @@ fn list<'a>( catalog: Catalog, model: &'a Download, visible: &[&'a VersionRow], + height: Option, ) -> Element<'a, Message> { let entries = column( visible @@ -306,7 +269,8 @@ fn list<'a>( .scroller_width(frame::SCROLLBAR_WIDTH) .margin(frame::SCROLLBAR_MARGIN), )) - .height(Length::Fill) + // `Some` = 三栏档按**整行**取高(见 `tail::whole_rows`);`None` = chips 档照旧 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 new file mode 100644 index 0000000..37cc802 --- /dev/null +++ b/crates/app-iced/src/pages/download/rows/tail.rs @@ -0,0 +1,79 @@ +//! 「列表与尾巴面板怎么分高」+ 版本行的整行取整(M125 / M222 D30)。 +//! +//! 从 `rows.rs` 拆出来的理由只有一条:那边撞 400 行硬线(项目自带 +//! `components::gate::every_source_file_stays_under_the_line_limit` 会判红)。本文件是 +//! [`TailShare`] 的**纯搬移**,加上 M222 新增的三个只读量([`ROW_PITCH`] / [`LIST_CHROME`] / +//! [`whole_rows`],见 `rows.rs::content` 的用法)。 + +use crate::pages::layout::VersionRailTier; + +use crate::pages::download::install::PanelState; + +/// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法)。 +/// +/// | 档 | 分法 | 为什么 | +/// |---|---|---| +/// | `Wide` / `Narrow` / `Compact` | [`TailShare::Pinned`]:面板按内容高钉在底部,列表吃剩下的 | 竖栏档在 760 高的窗口里列表还剩 158px(3 整行),面板整块在屏内——两样都对 | +/// | `Chips` + 面板**占高度**(选项区展开 / 安装中) | [`TailShare::Shared`]:两半各 `Fill`,面板自己可滚 | chips 档比三栏档多花两行 chip(tabs 48 + chips 48 + 间距),列表只剩 **18px**(M123 实测 0.4 行),面板 316px 全在屏内也没用——版本号读不出来就换不了版本 | +/// | 任何档 + 只画 `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) | +/// +/// 判据是**档位**不是高度:页面的模型里只有 `window_width`(`pages/model.rs`),窗口高度 +/// 没有接进页面(要接得改 `pages/mod.rs` 的入口签名,超出本页的边界)。所以这里按「哪一档 +/// 的竖排 chrome 会把列表压没」来分,并在报告里登记这条判据的边界。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TailShare { + /// 面板钉在底部(三栏档)。 + Pinned, + /// 面板与列表各吃一半(chips 档)。 + Shared, +} + +impl TailShare { + /// 档位 + **面板形态** → 分高方式(纯函数,单测直接钉它)。 + /// + /// 两个条件必须同时成立才共享: + /// 1. 档位是 `Chips`(多花两行 chip,竖排预算最紧的那一档); + /// 2. 面板真的**占高度**——`Options`(选中了版本、选项区展开)或 `Install`(进度 / 失败)。 + /// + /// `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) { + (VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => { + Self::Shared + } + // M222 D30 r1 S1 / r2 S7:**带加载器版本列表**的选项区在任何档位都太高。 + // 三栏档(1180×760)实测:修前面板 204→372px,版本列表 6.0 行(278px)被压到 + // 2.4 行(110px,底缘切在第三行图标上);改成 Shared 之后版本列表 **245px = 5.29 行**、 + // 面板 237px(分隔线 y=498)。面板因此变矮,实例名会被挤出首屏 ⇒ 同一轮把它 + // **钉成面板页脚**(`install::panel_with` 的 `shared` 分支 + `options::section`)。 + // 仍欠一项:245px 不是行高整数倍,列表底缘仍切在第 6 行中间(S1b/S9)。 + // 修它要把「分给列表的高度」从 `pages/download.rs` 传进来做 floor 取整 + // (`responsive` 在 Fill 子项上拿不到确定高度,实测无效,已回退)。 + (_, PanelState::Options(options)) if options.has_loader_versions() => Self::Shared, + _ => Self::Pinned, + } + } +} + +/// 版本行的**实测节距**(D30:`04` 图列表 278px / 6 行 = 46.33px)。 +/// +/// 列表是 `Fill + scrollable` 时,视口高度不是节距整数倍就会切在某一行中间——D30 r2 实测 +/// `01` 是 245px(5.3 行),英文态露出 6px 竖干、中文态露出「预览版」上半截。三栏档的 +/// 「列表与面板对半分」那一档现在按整行取高([`whole_rows`]),底缘落在行边界上。 +pub(crate) const ROW_PITCH: f32 = 278.0 / 6.0; + +/// 列表列内部、列表以外要扣掉的固定占位(搜索条 + 分隔线 + 表头 + 列间距)。 +/// +/// D30 标定:1180×760 的正文列高([`frame::body_height`](crate::pages::frame::body_height)) +/// 扣掉它再对半分,正好是 5 整行。**改搜索条 / 表头高度时要同步这个数。** +pub(crate) const LIST_CHROME: f32 = 93.0; + +/// 正文列高度 → 列表视口的**整行高度**(只给三栏档的 Shared 分支用)。 +#[must_use] +pub(crate) fn whole_rows(column_height: f32) -> f32 { + let half = ((column_height - LIST_CHROME) / 2.0).max(ROW_PITCH); + (half / ROW_PITCH).floor() * ROW_PITCH +} diff --git a/crates/app-iced/src/pages/mod.rs b/crates/app-iced/src/pages/mod.rs index d444850..bba5cb1 100644 --- a/crates/app-iced/src/pages/mod.rs +++ b/crates/app-iced/src/pages/mod.rs @@ -232,7 +232,13 @@ fn render<'a>( let page = match model.screen { Screen::Instances => instances::view(ui, metrics, model, launch_form), // M106:下载页此前写死成 `Lang::En`,现在跟 `model.lang`(页面文案跟着界面语言走)。 - Screen::Download => download::view(ui, metrics, &model.download, model.lang), + Screen::Download => download::view_sized( + ui, + metrics, + &model.download, + model.lang, + model.window_height, + ), // M109:内容库页同样跟 `model.lang` // M161:内容库要按**窗口高度**选版面(矮窗口不能把列表压扁), // 正文可用高由 `frame::body_height` 从窗口高减掉外壳算出(唯一来源)。