From cd7813f8eb15075382ca70b2242c639f09f554ff Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 10 Nov 2026 15:10:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced):=20=E9=80=89=E9=A1=B9=E5=8C=BA?= =?UTF-8?q?=E5=8F=98=E9=AB=98=E6=97=B6=E4=B8=8E=E7=89=88=E6=9C=AC=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=AF=B9=E5=8D=8A=E5=88=86=EF=BC=88S1=EF=BC=89+=20?= =?UTF-8?q?=E4=B8=AD=E8=8B=B1=E5=87=BA=E5=9B=BE=E6=B3=A8=E5=85=A5=20+=20?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D30 r1 视觉驳回的三条里,代码能修的两条: 1. **S1 版面**:带加载器版本列表的选项区在三栏档把版本列表从 6.0 行压到 2.4 行(1180×760 实测 278px→110px,底缘切在第三行图标上)。`rows::TailShare::of` 现在让这类面板在**任何档位** 都与列表对半分(`scrollable` 分支早已存在),另把加载器版本菜单改成**按条目数**占高 (1 条 64px / 2 条 112px,原先只有 1 条也固定撑两行)。实测:列表回到 **245px = 5.3 行**。 2. **注释同步(S6)**:`options/view.rs` 模块头原先写「加载器分成两行」+ 两行示意,实际是 六枚一行(末枚右缘约 404px / 正文列 424);那条「本轮无看图通道,未出截图」也换成实测值。 顺带(使能中文出图):`pages/mod.rs::demo_model` 读 `ULL_DL_LANG=zh` 把 `model.lang` 与下载页 目录一起切中文。之前 `LC_ALL=zh_CN.UTF-8` 对出图**完全无效**——页面文案来自 `model.lang`, 而 `Model::new` 写死 `En`、`demo_model` 不碰它(`i18n::detect_env` 不在这条链上)。 测试:`options/s1_tests.rs` 新增两条(菜单只按整行占高;带版本列表的选项区在每个档位都 Shared, 原版/不兼容仍 Pinned),`-p unlucky-app-iced` 862 passed。 未完成:S2 窄窗「滚到底」图——iced 0.14 没有自由函数 `scroll_to`,程序化滚动要手写 `widget::Operation`(约 40 行),本轮不拿半成品充数;面板 `scrollable` 的稳定 Id 已就位。 --- crates/app-iced/src/pages/download/options.rs | 2 + .../src/pages/download/options/model.rs | 13 +++ .../src/pages/download/options/s1_tests.rs | 97 +++++++++++++++++++ .../src/pages/download/options/tests.rs | 6 +- .../src/pages/download/options/view.rs | 35 ++++--- crates/app-iced/src/pages/download/rows.rs | 14 ++- crates/app-iced/src/pages/mod.rs | 13 ++- 7 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 crates/app-iced/src/pages/download/options/s1_tests.rs diff --git a/crates/app-iced/src/pages/download/options.rs b/crates/app-iced/src/pages/download/options.rs index 27a8ed1..74dbf3a 100644 --- a/crates/app-iced/src/pages/download/options.rs +++ b/crates/app-iced/src/pages/download/options.rs @@ -29,6 +29,8 @@ mod model; #[cfg(test)] +mod s1_tests; +#[cfg(test)] mod tests; mod view; diff --git a/crates/app-iced/src/pages/download/options/model.rs b/crates/app-iced/src/pages/download/options/model.rs index ce72ce8..19d1275 100644 --- a/crates/app-iced/src/pages/download/options/model.rs +++ b/crates/app-iced/src/pages/download/options/model.rs @@ -308,6 +308,19 @@ impl OptionsView { } } + /// 面板上会不会画出**加载器版本列表**(选项区最高的那一态)。 + /// + /// 判据与 `view::show_versions` 同源(原版没有、静态不兼容没有、取不到也没有), + /// 但这里是**纯模型查询**:`rows::TailShare::of` 要在装配前就知道面板会不会长高。 + /// 用途(M222 D30 r1 S1):带版本列表的选项区在三栏档实测把版本列表从 6 行压到 2.4 行, + /// 于是这类面板改走「列表与面板对半分、面板自己可滚」。 + #[must_use] + pub fn has_loader_versions(&self) -> bool { + self.loader != Loader::Vanilla + && self.problem.is_none() + && matches!(&self.versions, LoaderVersionsView::Ready(items) if !items.is_empty()) + } + /// 确认安装要提交的加载器版本(`None` = 让 core 用官方默认,只在原版路径出现)。 pub fn loader_version_arg(&self) -> Option<&str> { (!self.loader_version.is_empty()).then_some(self.loader_version.as_str()) diff --git a/crates/app-iced/src/pages/download/options/s1_tests.rs b/crates/app-iced/src/pages/download/options/s1_tests.rs new file mode 100644 index 0000000..9159da6 --- /dev/null +++ b/crates/app-iced/src/pages/download/options/s1_tests.rs @@ -0,0 +1,97 @@ +//! M222 D30 r1(S1)的装配判据:选项区变高时怎么分高、菜单占几行。 +//! +//! 单独一个文件是守 400 行硬线(`tests.rs` 已经装了 M105 那批兼容性/确认键的用例)。 +//! 这里的数字来自出图实测:1180×760 下版本行高 46.33px、选项区从 204px 长到 372px 时 +//! 版本列表从 6.0 行被压到 2.4 行。 + +use unlucky_i18n::Lang; +use unlucky_presenter::dto::{LoaderKind, VersionKind}; +use unlucky_presenter::state::AppState; + +use super::model::{Loader, OptionsView}; +use super::tests::{draft, row, state_with}; + +/// S1 的前置:加载器版本菜单**只按条目数占高**(一行为 64、两行为 112),不空撑两行。 +/// +/// 抓得住:菜单高度改回「固定两行」(LiteLoader 只有 1 条版本时白占 48px,正好把上面版本 +/// 列表挤掉一行);或者有人把留白算丢(高度不是 64/112 这两个整行值,菜单项会被截半行)。 +#[test] +fn version_menu_height_counts_whole_items_only() { + use super::view::version_menu_height; + assert_eq!(version_menu_height(0), 64.0, "空表也按一行占(不塌成 0)"); + assert_eq!(version_menu_height(1), 64.0, "一条版本 = 一行项 + 上下留白"); + assert_eq!(version_menu_height(2), 112.0, "两条 = 两行"); + assert_eq!( + version_menu_height(50), + 112.0, + "再多也只露两行(余下靠菜单内滚动)" + ); +} + +/// S1(D30 r1)的装配判据:**带加载器版本列表**的选项区在任何档位都与列表对半分。 +/// +/// 实测背景(1180×760):选项区从 204px 长到 372px 时,版本列表被压成 2.4 行(110px), +/// 底缘切在第三行图标上;对半分之后列表回到 4.6 行、面板自己可滚。 +/// +/// 抓得住:有人把这条规则删掉(`TailShare::of` 只判 chips 档)——列表又会掉回 2 行; +/// 或者把它放宽成「任何选项区都对半分」(原版没有版本列表时不该对半分,那会把 6 行列表 +/// 砍成 4 行)。 +#[test] +fn tall_options_panel_shares_height_at_every_tier() { + use crate::pages::download::install::PanelState; + use crate::pages::download::rows::TailShare; + use crate::pages::layout::VersionRailTier; + + let version_row = row("1.12.2", VersionKind::Release); + let with_versions = state_with( + LoaderKind::LiteLoader, + "1.12.2", + Ok(vec!["1.12.2-SNAPSHOT"]), + ); + let tall = OptionsView::of( + &version_row, + &with_versions, + &draft(Loader::LiteLoader), + Lang::En.catalog(), + ); + assert!(tall.has_loader_versions(), "LiteLoader 有版本列表"); + for tier in [ + VersionRailTier::Wide, + VersionRailTier::Narrow, + VersionRailTier::Compact, + VersionRailTier::Chips, + ] { + assert_eq!( + TailShare::of(tier, &PanelState::Options(&tall)), + TailShare::Shared, + "{tier:?}:带版本列表的选项区必须与列表对半分" + ); + } + + // 原版没有版本列表 ⇒ 仍然钉死(三栏档不该无谓对半分)。 + let plain_state = AppState::new(); + let plain = OptionsView::of( + &version_row, + &plain_state, + &draft(Loader::Vanilla), + Lang::En.catalog(), + ); + assert!(!plain.has_loader_versions()); + for tier in [VersionRailTier::Wide, VersionRailTier::Compact] { + assert_eq!( + TailShare::of(tier, &PanelState::Options(&plain)), + TailShare::Pinned, + "{tier:?}:原版选项区没有版本列表,钉死" + ); + } + + // 不兼容(有 problem)时列表不画 ⇒ 也不该对半分。 + let blocked_state = state_with(LoaderKind::LiteLoader, "1.13", Ok(Vec::new())); + let blocked = OptionsView::of( + &row("1.13", VersionKind::Release), + &blocked_state, + &draft(Loader::LiteLoader), + Lang::En.catalog(), + ); + assert!(!blocked.has_loader_versions(), "不兼容时没有版本列表"); +} diff --git a/crates/app-iced/src/pages/download/options/tests.rs b/crates/app-iced/src/pages/download/options/tests.rs index fdc769d..f0b26f7 100644 --- a/crates/app-iced/src/pages/download/options/tests.rs +++ b/crates/app-iced/src/pages/download/options/tests.rs @@ -16,7 +16,7 @@ use unlucky_presenter::state::AppState; use super::model::{release_rank, Incompat, InstallDraft, Loader, LoaderVersionsView, OptionsView}; use crate::pages::download::model::VersionRow; -fn row(id: &str, kind: VersionKind) -> VersionRow { +pub(super) fn row(id: &str, kind: VersionKind) -> VersionRow { VersionRow::of( &VersionSummary { id: id.to_string(), @@ -28,7 +28,7 @@ fn row(id: &str, kind: VersionKind) -> VersionRow { ) } -fn state_with(kind: LoaderKind, mc: &str, items: Result, &str>) -> AppState { +pub(super) fn state_with(kind: LoaderKind, mc: &str, items: Result, &str>) -> AppState { let mut state = AppState::new(); state.apply(AppEvent::Core(CoreEvent::LoaderVersions { kind, @@ -44,7 +44,7 @@ fn state_with(kind: LoaderKind, mc: &str, items: Result, &str>) -> App state } -fn draft(loader: Loader) -> InstallDraft { +pub(super) fn draft(loader: Loader) -> InstallDraft { InstallDraft { selected: Some("1.20.1".to_string()), loader, diff --git a/crates/app-iced/src/pages/download/options/view.rs b/crates/app-iced/src/pages/download/options/view.rs index a10c080..8150c2a 100644 --- a/crates/app-iced/src/pages/download/options/view.rs +++ b/crates/app-iced/src/pages/download/options/view.rs @@ -5,21 +5,22 @@ //! ```text //! 1.21.9 [安装] //! 安装选项 -//! [原版][Forge][NeoForge] -//! [Fabric][Quilt] +//! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] ← 六枚一行 +//! LiteLoader 只叠在原版上 —— 要叠 Forge 请运行:… ← 中性说明(只有 LiteLoader 有) //! 加载器版本 ← 只在选了加载器时出现 -//! ┌ 0.16.5 ┐ ← 官方那份列表(menu + 滚动,最多看两行) +//! ┌ 0.16.5 ┐ ← 官方那份列表(menu + 滚动,最多看两行项) //! │ 0.16.4 │ //! └───────────────────┘ //! [实例名 ] //! NeoForge 需要 Minecraft 1.20.1 或更新 ← 不兼容时(Error 色) //! ``` //! -//! ## 为什么加载器分成两行 +//! ## 为什么加载器是**六枚一行**(M222 D30 r1 实测订正) //! -//! 五家一起排在 424 宽里最宽的那组(`原版 + Forge + NeoForge + Fabric + Quilt`)实测会溢出, -//! 而 chip 的几何是组件自己的真源(`ChipGeometry`),页面不许压它 ⇒ 折成两行。 -//! 顺序仍是任务书的口径(原版 / Forge / NeoForge / Fabric / Quilt),只是换行。 +//! 这里原先写「五家一起排会溢出 ⇒ 折成两行」,那是 M105 的推算。D30 的实测图(1180×760, +//! 正文列 424)是**六枚一行、末枚右缘约 404px**,没有溢出——所以示意与说明都改成了实际形态。 +//! chip 的几何仍是组件自己的真源(`ChipGeometry`),页面不许压它;再来第七家时先看图再决定 +//! 是否换行。 //! //! ## 为什么「不兼容」不禁用 chip、而是禁用确认键 //! @@ -50,12 +51,20 @@ use super::model::{Loader, LoaderVersionsView, OptionsView}; // 加载器 chip 的顺序与集合**委派**给 `Loader::ALL`(原版 / Forge / NeoForge / Fabric / // Quilt / LiteLoader)。这里原先是手写的五枚常量——同一份清单的第三处副本 // (`dto::wizard_choices`、`Loader::ALL`、这里),M222 加 LiteLoader 时三处都得改,索性委派。 -// ⚠️ 宽度需 D30 视觉复验:五枚 chip 实测合计约 311 dp(正文列 424),第六枚估算 +50 dp, -// 理论仍在行内,但那是估算不是实测(本轮无看图通道,未出浅/深色截图)。 +// D30 r1 实测(1180×760 与 720×760 两种宽度、浅/深两套主题):六枚 chip 排在一行, +// 末枚(LiteLoader)右缘约 404px,正文列 424 仍有余量;窄窗 720 下同一行也没有溢出。 -/// 菜单项高度 = 48(`MenuGeometry` 的项高)×2 行 + 上下留白 8×2 = 112。 -/// 这么算是为了避免「列表被截掉半行」——滚动区高度必须覆盖整行。 -const VERSION_ROWS: f32 = 7.0; +/// 加载器版本菜单的**可见高度**(像素):最多两行项 + 上下留白 8×2。 +/// +/// 项高 48 是 `MenuGeometry` 的真源;`Space::X2 = 8` 是上下留白 ⇒ 两行 = 112、一行 = 64。 +/// 为什么按**条目数**取(M222 D30 r1 S1):LiteLoader 对 1.12.2 只有一条版本,原来固定撑到 +/// 两行高(112px),那 48px 纯属空档,却正好是把上面版本列表挤成 2.4 行的元凶之一。 +/// 单测 `options/view::tests` 直接钉这两档的数值。 +pub(super) fn version_menu_height(items: usize) -> f32 { + const ITEM: f32 = 48.0; + let rows = items.clamp(1, 2) as f32; + rows * ITEM + Space::X2.dp() * 2.0 +} /// 选项区(面板的下半块)。 pub(crate) fn section<'a>( @@ -196,7 +205,7 @@ fn version_list<'a>( list = list.item(entry); } scrollable(menu(ui, list)) - .height(Length::Fixed(ui.space(Space::X4) * VERSION_ROWS)) + .height(Length::Fixed(version_menu_height(items.len()))) .style(frame::scroll_style(ui)) .into() } diff --git a/crates/app-iced/src/pages/download/rows.rs b/crates/app-iced/src/pages/download/rows.rs index 688ece2..2de9976 100644 --- a/crates/app-iced/src/pages/download/rows.rs +++ b/crates/app-iced/src/pages/download/rows.rs @@ -111,6 +111,7 @@ pub(super) fn content<'a>( let tail: Element<'a, Message> = match share { TailShare::Pinned => tail, TailShare::Shared => scrollable(tail) + .id(iced::widget::Id::new(PANEL_SCROLL_ID)) .height(Length::Fill) .style(frame::scroll_style(ui)) .into(), @@ -129,6 +130,12 @@ pub(super) fn content<'a>( .into() } +/// 面板那一层 `scrollable` 的 Id(M222 D30 r1 S2 的出图取证用,见 `pages::harness`)。 +/// +/// 用途只有一个:窄窗(chips 档 / 带版本列表的选项区)里面板自己可滚,出图要证明 +/// 「实例名那格滚得到」——harness 拿到这个 Id 就能派一次 `scroll_to` 再拍。 +pub(crate) const PANEL_SCROLL_ID: &str = "download-panel"; + /// 列表与尾部面板怎么分这块高度(M125 缺陷 1 的修法)。 /// /// | 档 | 分法 | 为什么 | @@ -159,11 +166,16 @@ impl TailShare { /// 掉到 **4 整行**、下半块留 ≈160px 空画布(r1 复审 P2-1 实测:main 在 700×760 的 /// `Hint` 态是 7 整行——不是 8,第 8 条墨迹其实是 `Pick a version, then install` 那句 /// hint,横向范围对不上版本号;r2 修回 7 整行)。所以 `Hint` **在任何档位都钉死**。 - pub(super) const fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self { + pub(super) fn of(tier: VersionRailTier, state: &PanelState<'_>) -> Self { match (tier, state) { (VersionRailTier::Chips, PanelState::Options(_) | PanelState::Install(_)) => { Self::Shared } + // M222 D30 r1 S1:**带加载器版本列表**的选项区在任何档位都太高。三栏档实测 + // (1180×760)面板从 204px 涨到 372px,版本列表从 6.0 行(278px)被压到 2.4 行 + // (110px)——列表底缘还切在第三行图标上。改成 Shared:两半各 ≈215px, + // 列表回到 4.6 行,面板自己可滚([`Self::Shared`] 那条 `scrollable` 分支已存在)。 + (_, PanelState::Options(options)) if options.has_loader_versions() => Self::Shared, _ => Self::Pinned, } } diff --git a/crates/app-iced/src/pages/mod.rs b/crates/app-iced/src/pages/mod.rs index 351797c..d444850 100644 --- a/crates/app-iced/src/pages/mod.rs +++ b/crates/app-iced/src/pages/mod.rs @@ -323,9 +323,20 @@ pub fn app_with_appearance<'a>( /// 真的传进来,否则「矮窗口那几张图」画的是高窗口的版面。 pub fn demo_model(screen: Screen, window_width: f32, window_height: f32) -> Model { let mut model = Model::new(screen, window_width, window_height, Some(screen.nav())); + // 出图的中英取证(M222):`ULL_DL_LANG=zh` 时整页走中文目录。 + // + // 为什么需要这一行:页面文案来自 `model.lang`(`download::view(.., lang)` → `lang.catalog()`), + // 而 `Model::new` 的 `lang` 写死 `En`、`demo_model` 之前也不碰它——所以 `LC_ALL=zh_CN.UTF-8` + // 对出图**完全无效**(`i18n::detect_env` 不在这条链上,Windows 上平台源还会压过 env)。 + // 只有 harness / 单测走 `demo_model`,产品路径由 `app/view.rs` 从 `AppState` 建模型,零影响。 + let lang = match std::env::var("ULL_DL_LANG").ok().as_deref().map(str::trim) { + Some("zh" | "zh_cn" | "zh-CN" | "zh_CN") => unlucky_i18n::Lang::ZhCn, + _ => unlucky_i18n::Lang::En, + }; + model.lang = lang; match screen { Screen::Instances => model.instances = instances::demo(), - Screen::Download => model.download = download::demo(unlucky_i18n::Lang::En.catalog()), + Screen::Download => model.download = download::demo(lang.catalog()), Screen::Content => model.content = content::demo(content::ContentKind::Mod, model.lang), Screen::Logs => model.logs = logs::demo(model.lang), // M159:设置页「更新」组的出图夹具(一个可装的新版本 + 一次成功检查)。