fix(app-iced): 选项区变高时与版本列表对半分(S1)+ 中英出图注入 + 注释同步

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 已就位。
This commit is contained in:
AskaEth committed 2026-11-10 19:34:13 +08:00
1 parent 841c17152a
commit cd7813f8eb
7 files changed
+162 -18

No files matched your search

@@ -29,6 +29,8 @@
mod model;
#[cfg(test)]
mod s1_tests;
#[cfg(test)]
mod tests;
mod view;
@@ -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())
@@ -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(), "不兼容时没有版本列表");
}
@@ -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<Vec<&str>, &str>) -> AppState {
pub(super) fn state_with(kind: LoaderKind, mc: &str, items: Result<Vec<&str>, &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<Vec<&str>, &str>) -> App
state
}
fn draft(loader: Loader) -> InstallDraft {
pub(super) fn draft(loader: Loader) -> InstallDraft {
InstallDraft {
selected: Some("1.20.1".to_string()),
loader,
@@ -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()
}
+13 -1
View File
@@ -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,
}
}
+12 -1
View File
@@ -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:设置页「更新」组的出图夹具(一个可装的新版本 + 一次成功检查)。