Files
Unlucky-Launcher/crates/app-iced/src/pages/layout/rails.rs
T
AskaEth 89a51a63b1 feat(app-iced): M107 DLVRAIL —— 版本管理页三栏侧栏 + 跨类型搜索
用户令(信息架构):这一页从「下载」改称「版本管理」,版面从「一排 tab + 平铺列表」
改成**页面内三栏**:版本类型栏 + 主版本栏 + 列表/详情,并加搜索框。

- **三栏几何**(`pages/layout/rails.rs`,纯函数 + 数值表):类型栏 140/64、主版本栏 140/64、
  间距 24、列表列下限 424(行宽红线)。四档由**可用宽**裁决:Wide ≥888 / Narrow ≥676 /
  Compact ≥600 / Chips <600,三个门槛都是 `栏宽 + 间距 + 424` 算出来的,不是拍的
- **主版本分组规则**(`download/group.rs`):正式版/预发布按 `major.minor`(`1.21.9-pre3` → `1.21`)、
  周更按年桶(`24w14a` → `24w`)、远古版**没有主版本**(`rd-132211`/`a1.2.6` → 第二级只有「全部」);
  判据用真实版本样本钉住,不按「第一个点」切
- **跨类型搜索**(`search_bar` + 清空 + 命中数):查询词写进 `AppState.download_query`(页面无状态,
  只能这么活),`hits` 是**跨分类**读数;三条口径分开:「有命中」/「没有匹配」/「命中在别的类型里」
- 两级侧栏都用 `navigation::list_item` / `tabs` / `chip` / `buttons::icon`:Tab 可达、hover/pressed/
  选中态全部来自组件,页面不自己写状态层
- 右栏展开(M104 `content_width` 852)⇒ 可用宽 716 ⇒ Narrow 档,列表 464(任务书那句 492 漏扣
  96px 的页边距与栏间距,报告里给了完整数值表)
- 文案 11 条中英进 `crates/i18n`(新域宏接在装配链末尾 ⇒ `Key::ALL` 只追加,金表只追加)
- 护栏:两层侧栏都存在 + 列表按第二级选中态过滤(变异可证)、三档门槛、424 下限扫描、
  版本行单行宽账、搜索条真的装着
- `install.rs` 只动了「已装」分支(加一句「已安装」解释禁用按钮),没碰 M105 的选项区

取证:`DevDocs/artifacts/m107/`(10 张:三档 × 浅深 + 搜索命中 + 空 + 异地 + 远古版)
2026-11-05 01:59:13 +08:00

228 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 版本管理页的**三栏几何**:版本类型栏 + 主版本栏 + 列表列。纯函数,不碰 iced 控件树。
//!
//! 这一页是 `PaneKit::Single` 之外的第二种「页内分栏」:外层仍是单列(页头铺满页宽),
//! 正文自己切成三栏。所以它不能复用 [`super::PaneKit`](那是**页面级**版面决议),
//! 而是在可用宽上再做一次**页内**裁决 —— 本文件就是那次裁决的唯一真源。
//!
//! ## 1. 两个侧栏的宽度
//!
//! | 栏 | 展开 | 收成图标条 | 依据 |
//! |---|---|---|---|
//! | 版本类型栏 | **140** | **64** | 140 = `12b §7` drawer 项「图标 24 + 间距 12 + 8 字符标签」;`Snapshot` 是最长标签(8 字符 ≈ 62px),24+12+62+2×16 内边距 = 130,取 4px 栅格 **140** |
//! | 主版本栏 | **140** | **64** | 同一档宽度:主版本是 `1.21` 这种 3~5 字符的短串,140 里放得下「主版本 + 该组版本数」两行;64 里只放得下版本号本身(40 可用宽够 4 字符 `LabelMedium`) |
//!
//! 64 = `LayoutMetrics::nav_rail_width` 的一半再对齐栅格?**不是**:64 是 4px 栅格上的
//! 「图标 24 + 两侧内边距 2×16 + 余量 8」,比导航轨 88 窄一档(导航轨要放 56 宽的选中药丸,
//! 页内侧栏只要 40 宽的选中条)。
//!
//! ## 2. 四档与各自的门槛(门槛都是**算出来的**)
//!
//! 记 `A` = **页面块宽**([`super::block::body_width`](super::block::body_width) = 可用宽
//! 封顶 1440,M102 的口径;≤1440 时它就是可用宽,所以下面这张表在 1180/1024/900/700 上不变)。
//!
//! | 档 | 门槛 | 类型栏 | 主版本栏 | 列表列 |
//! |---|---|---|---|---|
//! | `Wide` | `A ≥ 888` | 140 | 140 | `A − 328` |
//! | `Narrow` | `676 ≤ A < 888` | **64** | 140 | `A − 252` |
//! | `Compact` | `600 ≤ A < 676` | 64 | **64** | `A − 176` |
//! | `Chips` | `A < 600` | 0 | 0 | `A` |
//!
//! 三个门槛的来源(`L` = 列表列下限 [`version_list_min_width`] = 行宽上限 **424**):
//!
//! - `888` = `available_width(1024)`:任务给的产品口径「≫1024 两栏都展开」,换成可用宽就是它;
//! - `676` = `64 + 24 + 140 + 24 + 424`:**类型栏收成图标条之后**三栏仍能让列表停在 424;
//! - `600` = `64 + 24 + 64 + 24 + 424`:两条栏都收成 64 也救不回来时,三栏这件事就不成立了。
//!
//! ## 3. 列表列下限为什么是 424(这条是硬的)
//!
//! 版本行要**单行不折**地放「版本号 + 类型 + 日期 + 安装按钮」。424 是 [`super::measure_cap_px`]
//! ——正文行宽上限(逐行实测 67 字符 ≤72),比这一行真正需要的宽得多:
//! `1.21.9-pre3`(11 字符 `BodyMedium` 14px ≈ 67)+ `Snapshot`(`LabelMedium` 12px ≈ 50)
//! + `2026-09-20`(10 字符 ≈ 52)+ 安装按钮(`control_height` 40 高的 text 按钮 ≈ 76)
//! + 两侧内边距 2×16 + 三处间距 3×12 = **293**。取 424 不是「凑」:它是本仓既有的
//! 行宽红线,列表列比它还窄就等于在 424 的正文列旁边又开了一条更窄的正文列。
//!
//! ## 4. 数值表(报告里贴的是同一张)
//!
//! 页面拿到的宽度 → 可用宽 `A`(扣 rail 88 与 2×24 页边距)→ 档位 → 三列。
//! 「右栏收起 / 展开」列按 M104 的 `content_width`(窗口 − 右栗 − 拖拽缝)计算:
//!
//! | 窗口 | 右栏 | 页面拿到 | `A` | 档 | 类型 | 主版本 | 列表 |
//! |---|---|---|---|---|---|---|---|
//! | 1180 | 无(本分支) | 1180 | 1044 | Wide | 140 | 140 | **716** |
//! | 1180 | 收起 48 | 1132 | 996 | Wide | 140 | 140 | **668** |
//! | 1180 | 展开 320 | 852 | 716 | Narrow | 64 | 140 | **464** |
//! | 1024 | 无 | 1024 | 888 | Wide | 140 | 140 | **560** |
//! | 900 | 无 | 900 | 764 | Narrow | 64 | 140 | **512** |
//! | 880 | 无 | 880 | 744 | Narrow | 64 | 140 | **492** |
//! | 700 | 无 | 700 | 564 | Chips | — | — | **564** |
//!
//! **任务书那句「1180−88−320−140−140 = 492」算错了**:它漏掉 2×24 页边距与 2×24 栏间距
//! (共 96)。按真实链路,1180 展开右栏后页面拿到 **852**,可用宽 **716**,
//! 两条 140 栏 + 两处间距要 328 ⇒ 列表只剩 **388 < 424**,**那正是「一条栏必须收成 64」的
//! 触发条件**;收到 64 之后列表是 **464**(不是 492)。492 这个数在 **880 窗宽**上成立
//! (744 − 252 = 492),两者差 28 就是 1180−852−88−48 里被漏掉的那部分。
//!
//! ## 5. 为什么 Narrow 档收的是**类型栏**而不是主版本栏
//!
//! 两栏都放 3~5 个字符的短串,但可辨性不同:类型栏是**四个固定项**(有图标可认:列表 / 星 /
//! 相机 / 历史),主版本栏放的是**用户正在找的那串版本号**——同一屏里两种 64 栏,先收有图标的那个。
//! `Compact` 档才把主版本栏也收成 64(`1.21` 在 40 可用宽里仍是完整串,不会被截断)。
use unlucky_design::spacing::Space;
use super::{available_width, measure_cap_px, FrameMetrics};
/// 版本类型栏(第一栏)展开宽。
pub const TYPE_RAIL_WIDTH: f32 = 140.0;
/// 主版本栏(第二栏)展开宽。
pub const MAJOR_RAIL_WIDTH: f32 = 140.0;
/// 两条侧栏收成图标条时的宽度。
pub const RAIL_ICON_WIDTH: f32 = 64.0;
/// 栏与栏之间的间距(与两列版面的 `PANES_GAP` 同值:同一屏里不出现两种分栏节奏)。
pub const RAIL_GAP: f32 = Space::X6.dp();
/// 列表列下限 = 行宽上限 **424**(见模块文档 §3)。
pub fn version_list_min_width() -> f32 {
measure_cap_px()
}
/// `Wide` 档门槛:`available_width(1024)` = **888**(产品口径「≫1024 两栏都展开」)。
pub fn wide_min_available() -> f32 {
available_width(1024.0)
}
/// `Narrow` 档门槛:类型栏 64 + 间距 + 主版本栏 140 + 间距 + 列表下限 = **676**。
pub fn narrow_min_available() -> f32 {
RAIL_ICON_WIDTH + RAIL_GAP + MAJOR_RAIL_WIDTH + RAIL_GAP + version_list_min_width()
}
/// `Compact` 档门槛:两条 64 栏 + 两处间距 + 列表下限 = **600**。
pub fn compact_min_available() -> f32 {
RAIL_ICON_WIDTH + RAIL_GAP + RAIL_ICON_WIDTH + RAIL_GAP + version_list_min_width()
}
/// 版本行里「类型」一格的宽度:最长标签 `Snapshot`(8 字符 × `LabelMedium` 12px ≈ 50)+
/// 一格间距余量,取 4px 栅格 **84**。
pub const LIST_KIND_WIDTH: f32 = 84.0;
/// 版本行里「日期」一格的宽度:ISO 日期 `2026-09-20`(10 字符 ≈ 52)+ 余量,取 **88**。
pub const LIST_DATE_WIDTH: f32 = 88.0;
/// 版本行尾的安装图标按钮格子宽 = 图标按钮边长 **40**(`IconGeometry::side`;命中区 48 由行高 44 补)。
pub const ROW_ICON_BUTTON: f32 = 40.0;
/// 版本行内三处间距(`Space::X2` = 8):比栏间距小一档,行内节奏不与分栏节奏打架。
pub const ROW_GAP: Space = Space::X2;
/// 版本行的最小内容宽(报告里的推导用它当校验):
/// `Listitem 最小格 176 + 84 + 88 + 40 + 3×8 = 412 < 424`。
///
/// 412 略小于行宽下限 424,说明「424 下四样仍在同一行」不是估计而是有 12px 余量的结论;
/// 版本号那一格最窄拿到 `424 − 84 − 88 − 40 − 24 = 188`(比推导里取的 176 还宽 12)。
pub fn version_row_min_width() -> f32 {
176.0 + LIST_KIND_WIDTH + LIST_DATE_WIDTH + ROW_ICON_BUTTON + 3.0 * ROW_GAP.dp()
}
/// 版本管理页此刻用哪一档(四档,见模块文档 §2)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionRailTier {
/// 两栏都展开(`A ≥ 888`)。
Wide,
/// 类型栏收成图标条(`676 ≤ A < 888`)。
Narrow,
/// 两条栏都收成图标条(`600 ≤ A < 676`)。
Compact,
/// 三栏不成立:退回顶部 chips(`A < 600`)。
Chips,
}
impl VersionRailTier {
/// 稳定名字(日志 / 测试 / 出图文件名用)。
pub const fn name(self) -> &'static str {
match self {
Self::Wide => "wide",
Self::Narrow => "narrow",
Self::Compact => "compact",
Self::Chips => "chips",
}
}
/// 这一档还画不画页内侧栏。
pub const fn is_three_column(self) -> bool {
!matches!(self, Self::Chips)
}
}
/// 三栏的宽度裁决结果(页面直接消费,不再自己算)。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VersionRails {
/// 档位。
pub tier: VersionRailTier,
/// 版本类型栏宽(`Chips` 档为 0)。
pub type_rail: f32,
/// 主版本栏宽(`Chips` 档为 0)。
pub major_rail: f32,
/// 列表 / 内容列宽。
pub list: f32,
}
impl VersionRails {
/// 按可用宽裁决(门槛见模块文档 §2)。
///
/// 降级是**逐档下沉**的,不做「挤一挤」:每一档都保证 `list ≥ `[`version_list_min_width`],
/// 除非已经沉到 `Chips`(那时列表就是整条可用宽)。
pub fn of(available: f32) -> Self {
let (tier, type_rail, major_rail) = if available >= wide_min_available() {
(VersionRailTier::Wide, TYPE_RAIL_WIDTH, MAJOR_RAIL_WIDTH)
} else if available >= narrow_min_available() {
(VersionRailTier::Narrow, RAIL_ICON_WIDTH, MAJOR_RAIL_WIDTH)
} else if available >= compact_min_available() {
(VersionRailTier::Compact, RAIL_ICON_WIDTH, RAIL_ICON_WIDTH)
} else {
(VersionRailTier::Chips, 0.0, 0.0)
};
let list = if tier.is_three_column() {
(available - type_rail - RAIL_GAP - major_rail - RAIL_GAP).max(0.0)
} else {
available.max(0.0)
};
Self {
tier,
type_rail,
major_rail,
list,
}
}
/// 一屏的版本管理页几何(`metrics` 来自 [`super::frame_metrics`])。
///
/// **读的是块宽**([`super::block::body_width`] = pane 宽封顶 1440,M102 的口径),
/// 不是 `metrics.available`:三栏都长在 `frame::body` 那个块里,超宽窗(≥1576)上
/// 可用宽 2424 > 块宽 1440,按 available 切会让三列之和超出块、右侧被裁。
pub fn of_metrics(metrics: FrameMetrics) -> Self {
Self::of(super::block::body_width(metrics))
}
/// 叠上「用户手动折叠了类型栏」这条覆盖。
///
/// 只动第一栏:收窄到 [`RAIL_ICON_WIDTH`],省下来的宽**原样进列表列**(折叠只会让列表更宽,
/// 所以不需要再校验一次 424 下限)。`Chips` 档没有第一栏,返回原值。
#[must_use]
pub fn with_type_folded(self) -> Self {
if !self.tier.is_three_column() || self.type_rail <= RAIL_ICON_WIDTH {
return self;
}
Self {
type_rail: RAIL_ICON_WIDTH,
list: self.list + (self.type_rail - RAIL_ICON_WIDTH),
..self
}
}
}
#[cfg(test)]
mod tests;