(i) 上轮新引入的错误声明 rescope:错误行「恒为 None / 滚动区只有 head+chips」只在**三栏档 +
带版本列表**这一支成立(has_loader_versions() 要求 problem.is_none())。**chips 档**(可用宽
< 600,TailShare::of 对 Options/Install 无条件 Shared)里 problem 可以是 Some(静态不兼容 /
列表取不到 / 空表三种),错误行会进滚动区且可能被滚走;show_versions() 为假时钉死区只剩
实例名。install.rs 与 options/view.rs 都按这两支分别写清,并注明该行为域由 M227 重设计。
(ii) install.rs 的「显式给对半分档标志」「对半分:面板吃满自己那一半」改成预算优先口径
(与同文件十行上方「不是早期的对半分」不再矛盾)。
(iii) rows.rs 三处:三栏档「照旧钉死」限定为「**不带版本列表**的选项区」;`content` 的 share
说明改成引用 TailShare::of 的真实两支,不再复述「三栏钉死 / chips 各吃一半」;
「只有对半分那一档才按整行取高」改成「只有 Shared(面板先保预算)那一档」。
全量扫附带修正:tail.rs 的 ROW_PITCH / PANEL_BUDGET 账(760 例改成 176/437、900 改成 569;
预算拆成滚动区 76 + 钉死区 153);options/model.rs 的 has_loader_versions 文档改成预算口径。
零代码零测试变更。
324 lines
15 KiB
Rust
324 lines
15 KiB
Rust
//! 选项区的控件树(M105)。
|
||
//!
|
||
//! ## 版面(424 正文列里的四段)
|
||
//!
|
||
//! ```text
|
||
//! ── 滚动区(Shared 档)────────────────────────────
|
||
//! 1.12.2 [安装]
|
||
//! [原版][Forge][NeoForge][Fabric][Quilt][LiteLoader] ← 六枚一行
|
||
//! ── 钉死区 ──────────────────────────────────────
|
||
//! Vanilla parent only — Forge goes through the CLI ← 说明行;没有说明时是「加载器版本」标签
|
||
//! ┌ 1.12.2-SNAPSHOT ┐ ← 官方那份列表(menu + 滚动,按条目数占高:1 条 64 / 2 条 112)
|
||
//! └───────────────────┘
|
||
//! [实例名 ]
|
||
//! ```
|
||
//!
|
||
//! - **钉死档**(原版等不带版本列表的面板)走另一条路:标签、说明、菜单、实例名都在同一列里
|
||
//! 顺序铺开(`04` 基线图就是这一档,不能改);
|
||
//! - 不兼容时的原因行(Error 色)在滚动区末尾(**chips 档**可达;三栏档 + 带版本列表时
|
||
//! `problem` 恒为 `None`,所以那支看不到它);
|
||
//! - 说明只有 LiteLoader 有(`Key::DownloadOptionsLiteLoaderVanillaOnly`)。
|
||
//!
|
||
//! ## 为什么加载器是**六枚一行**(M222 D30 r1 实测订正)
|
||
//!
|
||
//! 这里原先写「五家一起排会溢出 ⇒ 折成两行」,那是 M105 的推算。D30 的实测图(1180×760,
|
||
//! 正文列 424)是**六枚一行、末枚右缘约 404px**,没有溢出——所以示意与说明都改成了实际形态。
|
||
//! chip 的几何仍是组件自己的真源(`ChipGeometry`),页面不许压它;再来第七家时先看图再决定
|
||
//! 是否换行。
|
||
//!
|
||
//! ## 为什么「不兼容」不禁用 chip、而是禁用确认键
|
||
//!
|
||
//! 禁用的 chip 点不动,用户就**看不到原因**(那正是「选完才失败」的另一种形态)。
|
||
//! 现在的做法:五家永远都能点,选中不兼容的那家时下面出现一行具名原因,同时「安装」变成禁用态。
|
||
//! 于是「为什么不能装」和「怎么才能装」永远同屏。
|
||
|
||
use iced::widget::{column, container, row, scrollable};
|
||
use iced::{Alignment, Element, Length};
|
||
|
||
use unlucky_design::color::Role;
|
||
use unlucky_design::icons::IconKind;
|
||
use unlucky_design::spacing::Space;
|
||
use unlucky_design::typography::TypeRole;
|
||
|
||
use unlucky_i18n::{Catalog, Key};
|
||
|
||
use crate::app::components::buttons::{self, Button};
|
||
use crate::app::components::inputs::{menu, Menu, MenuItem};
|
||
use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||
use crate::app::components::selection::{chip, Chip};
|
||
use crate::theme::Ui;
|
||
|
||
use super::super::frame;
|
||
use super::super::Message;
|
||
use super::model::{Loader, LoaderVersionsView, OptionsView};
|
||
|
||
// 加载器 chip 的顺序与集合**委派**给 `Loader::ALL`(原版 / Forge / NeoForge / Fabric /
|
||
// Quilt / LiteLoader)。这里原先是手写的五枚常量——同一份清单的第三处副本
|
||
// (`dto::wizard_choices`、`Loader::ALL`、这里),M222 加 LiteLoader 时三处都得改,索性委派。
|
||
// D30 r1 实测(1180×760 与 720×760 两种宽度、浅/深两套主题):六枚 chip 排在一行,
|
||
// 末枚(LiteLoader)右缘约 404px,正文列 424 仍有余量;窄窗 720 下同一行也没有溢出。
|
||
|
||
/// 加载器版本菜单的**可见高度**(像素):最多两行项 + 上下留白 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
|
||
}
|
||
|
||
/// 选项区(面板的下半块):**可滚的上半块 + 钉死的下半块**。
|
||
///
|
||
/// `scroll` = 面板此刻是不是「自己可滚」的那一档(`rows::TailShare::Shared`)。是的话:
|
||
///
|
||
/// - **滚动区**:head(版本 id + 安装按钮)+ 六枚加载器 chips(**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 四角闭合)完整在首屏。
|
||
pub(crate) fn section<'a>(
|
||
ui: &'a Ui,
|
||
catalog: Catalog,
|
||
options: &'a OptionsView,
|
||
scroll: bool,
|
||
) -> Element<'a, Message> {
|
||
let head = row![
|
||
container(frame::text_role(
|
||
ui,
|
||
TypeRole::BodyMedium,
|
||
false,
|
||
options.version.as_str(),
|
||
ui.color(Role::OnSurface),
|
||
))
|
||
.width(Length::Fill),
|
||
confirm(ui, catalog, options),
|
||
]
|
||
.width(Length::Fill)
|
||
.align_y(Alignment::Center);
|
||
|
||
// 钉死档(原版 / 没有加载器版本列表)**保持旧结构**:标签与说明都在列内。
|
||
// 为什么不一并改:`04` 是别处基线图的对照,它的面板矮、列表 6 行——把标签撤出会
|
||
// 让面板矮 26px,列表涨到 302px(6.86 行)反而切出半行。方案 B 只对 Shared 档生效。
|
||
if !scroll {
|
||
let mut plain = column![
|
||
head,
|
||
frame::text_role(
|
||
ui,
|
||
TypeRole::LabelMedium,
|
||
false,
|
||
catalog.text(Key::DownloadOptionsTitle),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
),
|
||
loader_row(ui, catalog, options),
|
||
]
|
||
.spacing(ui.space(Space::X2))
|
||
.width(Length::Fill);
|
||
if let Some(note) = &options.note {
|
||
plain = plain.push(frame::text_role(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
note.as_str(),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
));
|
||
}
|
||
if show_versions(options) {
|
||
plain = plain.push(frame::text_role(
|
||
ui,
|
||
TypeRole::LabelMedium,
|
||
false,
|
||
catalog.text(Key::DownloadOptionsLoaderVersion),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
));
|
||
plain = plain.push(version_list(ui, catalog, options));
|
||
}
|
||
if let Some(problem) = &options.problem {
|
||
plain = plain.push(frame::text_role(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
problem.as_str(),
|
||
ui.color(Role::Error),
|
||
));
|
||
}
|
||
plain = plain.push(text_field(
|
||
ui,
|
||
Field::new(options.instance_name.as_str())
|
||
.variant(FieldVariant::Outlined)
|
||
.label(catalog.text(Key::DownloadOptionsInstanceName))
|
||
.placeholder(catalog.text(Key::DownloadOptionsInstanceNameHint)),
|
||
Message::SetInstanceName,
|
||
));
|
||
return plain.into();
|
||
}
|
||
|
||
// 滚动区只剩 head + 六枚 chips(方案 B,D30 六审裁决;chips 档还可能有错误行,见 `section` 文档)。
|
||
//
|
||
// 为什么把「安装选项」标签与中性说明都撤出滚动区:
|
||
// 五审 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 自解释,
|
||
// 且正文列只有 424px、六枚 chip 已占 404px,加标签必然横向裁掉最后一枚。
|
||
// 原版等 Pinned 档仍保留该标签(见上面 `!scroll` 那条路)。
|
||
let mut body = column![head, loader_row(ui, catalog, options)]
|
||
.spacing(ui.space(Space::X1))
|
||
.width(Length::Fill);
|
||
|
||
// 中性说明**只在钉死区画一份**(八审:这里原先也画了一份,两处像素级重影)。
|
||
// 「加载器版本」这一段只在**真该有列表**时出现:原版没有、静态不兼容没有(原因在下面那行)、
|
||
// 取不到也没有(同样在原因行)。留一个空标题只会让人以为界面缺了一块。
|
||
if let Some(problem) = &options.problem {
|
||
body = body.push(frame::text_role(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
problem.as_str(),
|
||
ui.color(Role::Error),
|
||
));
|
||
}
|
||
|
||
// 钉死区:加载器版本菜单 + 实例名(都不参与滚动)。见 `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);
|
||
pinned = pinned.push(version_list(ui, catalog, options));
|
||
}
|
||
pinned = pinned.push(text_field(
|
||
ui,
|
||
Field::new(options.instance_name.as_str())
|
||
.variant(FieldVariant::Outlined)
|
||
.label(catalog.text(Key::DownloadOptionsInstanceName))
|
||
.placeholder(catalog.text(Key::DownloadOptionsInstanceNameHint)),
|
||
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()
|
||
}
|
||
|
||
/// 加载器版本这一段要不要画(见 `section` 的注释)。
|
||
fn show_versions(options: &OptionsView) -> bool {
|
||
if options.loader == Loader::Vanilla || options.problem.is_some() {
|
||
return false;
|
||
}
|
||
match &options.versions {
|
||
LoaderVersionsView::Loading => true,
|
||
LoaderVersionsView::Ready(items) => !items.is_empty(),
|
||
LoaderVersionsView::Failed(_) => false,
|
||
}
|
||
}
|
||
|
||
/// 一行加载器 chip(Filter 档:选中 = `SecondaryContainer`)。
|
||
fn loader_row<'a>(ui: &'a Ui, catalog: Catalog, options: &'a OptionsView) -> Element<'a, Message> {
|
||
let mut line = row![]
|
||
.spacing(ui.space(Space::X1))
|
||
.align_y(Alignment::Center);
|
||
for loader in Loader::ALL.iter() {
|
||
line = line.push(chip(
|
||
ui,
|
||
Chip::filter(
|
||
loader.label(catalog),
|
||
options.loader == *loader,
|
||
Message::SetLoader(*loader),
|
||
),
|
||
));
|
||
}
|
||
line.into()
|
||
}
|
||
|
||
/// 官方那份加载器版本列表(`menu` + 滚动:Fabric 有上百条,不做下拉也不铺满屏)。
|
||
fn version_list<'a>(
|
||
ui: &'a Ui,
|
||
catalog: Catalog,
|
||
options: &'a OptionsView,
|
||
) -> Element<'a, Message> {
|
||
match &options.versions {
|
||
LoaderVersionsView::Loading => frame::text_role(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
catalog.text(Key::DownloadOptionsLoaderVersionsLoading),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
),
|
||
// 取不到 / 一条都没有:原因已经在上面那行 `problem` 里说清,这里不再重复。
|
||
LoaderVersionsView::Failed(_) => frame::gap(0.0),
|
||
LoaderVersionsView::Ready(items) if items.is_empty() => frame::gap(0.0),
|
||
LoaderVersionsView::Ready(items) => {
|
||
let mut list = Menu::new().width(Length::Fill);
|
||
for item in items {
|
||
let mut entry =
|
||
MenuItem::new(item.as_str(), Message::SetLoaderVersion(item.clone()));
|
||
if *item == options.loader_version {
|
||
entry = entry.selected();
|
||
}
|
||
list = list.item(entry);
|
||
}
|
||
scrollable(menu(ui, list))
|
||
.height(Length::Fixed(version_menu_height(items.len())))
|
||
.style(frame::scroll_style(ui))
|
||
.into()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 「安装」:不兼容 / 列表没到位 / 已装 都按下不去(禁用态保留几何,只清消息)。
|
||
fn confirm<'a>(ui: &'a Ui, catalog: Catalog, options: &'a OptionsView) -> Element<'a, Message> {
|
||
if options.installed {
|
||
return buttons::outlined(ui, Button::disabled(catalog.text(Key::DownloadInstalled)));
|
||
}
|
||
if !options.can_confirm {
|
||
return buttons::outlined(ui, Button::disabled(catalog.text(Key::DownloadInstall)));
|
||
}
|
||
buttons::filled(
|
||
ui,
|
||
Button::new(
|
||
catalog.text(Key::DownloadInstall),
|
||
Message::InstallVersion(options.version.clone()),
|
||
)
|
||
.leading(IconKind::Download),
|
||
)
|
||
}
|