两处拆分,只为过 `gate::every_source_file_stays_under_the_line_limit`: - `download/rails.rs` 435 行 → `rails.rs` 311 + `rails/major.rs` 157:主版本桶那一栏 (`major_rail` / `major_entries` / `major_shape` / `MajorEntry` / `MajorShape`)搬进子模块, `rail_title` 留在父模块(两条栏共用)。`Key::VersionMajorTitle` 的落点跟着搬, `tests/i18n.rs` 的死 key 扫描表补上 `rails/major.rs`(漏扫会把活 key 判成死 key)。 - `tests/options_shot.rs` 475 行 → 345 + `options_shot/chips.rs` 75 + `options_shot/compact.rs` 67。 顺带修 `options_shot::bands()` 的一处既有缺口:它按三栏算列表列左缘(112+0+24+0+24 = 160), chips 档没有侧栏时这个左缘是错的,右缘跟着冲出画面 22px——M125 的 chips 守门正好踩到 「扫描越界读到下一行像素」,`first_rule_y` 因此恒返回 None。现在 chips 档直接取页左缘 (112),三栏三档的数值一个不变(M123 那些 140/140、缝 24 的断言照旧)。 守门实跑(本机,tiny-skia): - 解 ignore 的 `chips::a_chips_tier_shot_…` 在修后图绿、**修前图红**(实测 18px ≠ 44); - `compact::a_compact_tier_shot_…` 修后图绿、修前图红(实测 44px 文字行 ≠ 40px 图标按钮); - chips 那条换深色图同样绿(断言不依赖主题色)。
163 lines
6.7 KiB
Rust
163 lines
6.7 KiB
Rust
//! 第二栏(主版本桶)与「一项画成什么样」的判定(从 `rails.rs` 拆出来守 400 行红线)。
|
||
//!
|
||
//! 两条渲染路径吃同一份数据:[`major_entries`] 产出 `[全部版本, 1.21, 1.20, …]`,
|
||
//! 竖栏([`major_rail`])与 chips 行(`rails::stacked_chips`)各画一遍。
|
||
//! 图标条档(64 宽)里「全部版本」换图标 + tooltip 的取舍见 [`major_shape`]。
|
||
|
||
use iced::widget::{column, container};
|
||
use iced::{Alignment, Element, Length};
|
||
|
||
use unlucky_design::spacing::Space;
|
||
|
||
use unlucky_i18n::{Catalog, Key};
|
||
|
||
use crate::app::components::navigation::list_item::{list_item, ListItem};
|
||
use crate::pages::download::group;
|
||
use crate::pages::download::model::{Category, Download};
|
||
use crate::pages::frame;
|
||
use crate::pages::layout::RAIL_ICON_WIDTH;
|
||
use crate::pages::Message;
|
||
use crate::theme::Ui;
|
||
|
||
use super::{category_icon, labeled, rail_title};
|
||
|
||
/// 第二栏:主版本桶(第一项永远是「全部版本」;没有主版本概念的版本不单独占一项)。
|
||
pub(in crate::pages::download) fn major_rail<'a>(
|
||
ui: &'a Ui,
|
||
catalog: Catalog,
|
||
model: &'a Download,
|
||
width: f32,
|
||
) -> Element<'a, Message> {
|
||
let icon_only = width <= RAIL_ICON_WIDTH;
|
||
|
||
let mut body = column![].spacing(ui.space(Space::X1)).width(Length::Fill);
|
||
if !icon_only {
|
||
body = body.push(rail_title(
|
||
ui,
|
||
catalog.text(Key::VersionMajorTitle),
|
||
frame::gap(ui.metrics().nav_item_gap),
|
||
));
|
||
}
|
||
|
||
for entry in major_entries(catalog, model) {
|
||
body = body.push(major_item(ui, entry, icon_only));
|
||
}
|
||
|
||
body.into()
|
||
}
|
||
|
||
/// 第二级侧栏的一项(「全部版本」与每个主版本桶共用一套结构,好让**两条渲染路径**
|
||
/// ——竖栏与 chips 行 —— 都用同一份数据,测试也只钉这一份)。
|
||
///
|
||
/// 字段全部**借**模型里的桶标签与目录里的静态串:这两条渲染路径都要把标签塞进 iced 控件树,
|
||
/// 借本地的 `String` 会让整棵树的生命周期塌到函数作用域里(E0515)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub(in crate::pages::download) struct MajorEntry<'a> {
|
||
/// 选中它要发的键(`None` = 「全部版本」)
|
||
pub key: Option<&'a str>,
|
||
/// 画出来的标签
|
||
pub label: &'a str,
|
||
/// 此刻是否选中(**按生效值算**:搜索期间恒为「全部」,见 `group::bucket_selected`)
|
||
pub selected: bool,
|
||
}
|
||
|
||
/// 一项在 iced 里**画成什么样**(纯函数产出,单测直接钉它 —— 两种形状的取舍见 [`major_shape`])。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub(in crate::pages::download) enum MajorShape<'a> {
|
||
/// `ListItem` 单行文字(展开档的每一项、图标条档的桶标签)
|
||
Text(&'a str),
|
||
/// 图标 + tooltip 全称(图标条档的「全部版本」,见 [`major_shape`] 的宽度账)
|
||
Icon(&'a str),
|
||
}
|
||
|
||
/// 图标条档画成什么样:**只有「全部版本」这一项不是文字**。
|
||
///
|
||
/// ## 64 宽的可见文本宽是 **32**,不是 40(M123 取证实测)
|
||
///
|
||
/// `ListItem` 两侧内边距各 16(`pad_start` / `pad_end`),所以 64 宽的栏里文本格
|
||
/// = 64 − 32 = **32**,再长就被 `clip(true)` 切掉。`BodyLarge` 16px 下:
|
||
///
|
||
/// | 标签 | 墨迹宽 | 32 里放得下? |
|
||
/// |---|---|---|
|
||
/// | `1.21` | 26(736×760 那张实测) | ✓ 余量 6 |
|
||
/// | `24w` | 26(同上) | ✓ 余量 6 |
|
||
/// | `All versions` | ≈84(12 字符,按 0.4em 下限也有 77) | ✗ 只画得出「All v」(缺陷 2 的原图) |
|
||
/// | `全部版本` | 4 × 16 = **64**(全角 1em,精确值) | ✗ 只画得出「全部」,而且正好顶到裁切边 |
|
||
///
|
||
/// 短标签这条退路也不行:中文「全部」= 2 × 16 = **32**,与文本格**一样宽、零余量**,
|
||
/// 「裁不裁」取决于字体渲染的小数位——一个查不出、也守不住的边界。所以图标条档的
|
||
/// 「全部版本」**不画文字**:换成与本页第一级那个「全部」同一个网格图标
|
||
/// ([`category_icon`] 的 `Category::All`),全称进 tooltip(宽度不设限,中英文都完整)。
|
||
///
|
||
/// 桶标签本来就是 3~5 字符的短串,两档都照旧画原文。
|
||
pub(in crate::pages::download) const fn major_shape(
|
||
entry: MajorEntry<'_>,
|
||
icon_only: bool,
|
||
) -> MajorShape<'_> {
|
||
if icon_only && entry.key.is_none() {
|
||
MajorShape::Icon(entry.label)
|
||
} else {
|
||
MajorShape::Text(entry.label)
|
||
}
|
||
}
|
||
|
||
/// 第二级侧栏要画的全部项:`[全部版本, 1.21, 1.20, …]`,顺序 = 桶的版本序。
|
||
///
|
||
/// 抽成纯函数是为了让「最后一个桶可点」这件事**可断言**(复审 P2-2 的要求):
|
||
/// 竖栏与 chips 行都从这一份数据画,chips 行外面还套了横向滚动,所以「画得出来」= 「够得着」。
|
||
/// 返回 `Vec` 而不是迭代器:调用方两处都要 len(chips 的宽度预算)与随机访问。
|
||
pub(in crate::pages::download) fn major_entries<'a>(
|
||
catalog: Catalog,
|
||
model: &'a Download,
|
||
) -> Vec<MajorEntry<'a>> {
|
||
let mut entries = vec![MajorEntry {
|
||
key: None,
|
||
label: catalog.text(Key::VersionMajorAll),
|
||
selected: group::bucket_selected(model, None),
|
||
}];
|
||
|
||
for bucket in &model.buckets {
|
||
// `None` 桶 = 远古版那类没有主版本的版本:第二级只有一个「全部」,不画空桶
|
||
let Some(key) = bucket.key.as_deref() else {
|
||
continue;
|
||
};
|
||
entries.push(MajorEntry {
|
||
key: Some(key),
|
||
label: key,
|
||
selected: group::bucket_selected(model, Some(key)),
|
||
});
|
||
}
|
||
|
||
entries
|
||
}
|
||
|
||
/// 主版本的一项:形状由 [`major_shape`] 决定(图标条档的「全部版本」是图标 + tooltip,
|
||
/// 其余都是 `ListItem` 单行文字)。
|
||
fn major_item<'a>(ui: &'a Ui, entry: MajorEntry<'a>, icon_only: bool) -> Element<'a, Message> {
|
||
let press = Message::SelectMajor(entry.key.map(str::to_string));
|
||
|
||
match major_shape(entry, icon_only) {
|
||
MajorShape::Text(label) => {
|
||
let mut item = ListItem::new(label, press);
|
||
if entry.selected {
|
||
item = item.selected();
|
||
}
|
||
list_item(ui, item)
|
||
}
|
||
// 图标条档的「全部版本」:图标(与本页第一级的「全部」同一个网格)+ 全称 tooltip。
|
||
// 高度对齐 `ListItem` 的行高,两档的行节奏才一样。
|
||
MajorShape::Icon(full) => container(labeled(
|
||
ui,
|
||
full,
|
||
category_icon(Category::All),
|
||
entry.selected,
|
||
press,
|
||
))
|
||
.width(Length::Fill)
|
||
.height(Length::Fixed(ui.metrics().row_height))
|
||
.align_x(Alignment::Center)
|
||
.align_y(Alignment::Center)
|
||
.into(),
|
||
}
|
||
}
|