视觉复审在 M68 的判词是不能把「全宽单卡」当页面模板(1180 宽下正文单行 712px ≈ 118 字符)。 本 mission 把它落成一套**布局系统**,四个页面只是它的用例: - `pages/layout.rs`:官方 600 / 840 两个档位 → 三种版面(单列 / supporting-pane / list-detail), 阈值由列宽和算出(设置 240+24+320 → 720;实例 336+24+320 → 816 ≤ 840),行宽硬上限 14 × 0.5em × 72 = **504px**,全部是纯函数,`pages/layout/tests.rs` 用绝对值和边界表钉死。 - `pages/frame.rs`:页头(56 / 两行 84)+ 内容列(≤504)+ 两列骨架 + 空态位; `pages/frame/pieces.rs` 是占位控件(卡片 / 分隔线 / 文本按钮 / 图标),令牌驱动、无完整状态机, 待 M75 的 components 家族合入后替换。 - 四页:实例(list-detail,列表列 336 独立滚动,窄窗退化成整页切换)、 设置(supporting-pane,240 分组导航 + ≤504 表单列)、下载 / 日志(单列,两行页头)。 - `pages/harness.rs` + `examples/pages_demo.rs`:自带 `--screen/--width/--screenshot-theme`, 不碰 `cli.rs` / `app.rs`(M75 在改)。 偏离与说明: - 布局用「页边距 16/24」而非 16/24/32,官方 expanded 也是 24(少一条偏离登记); - 实例空详情态一屏会有 5 档字号(22/16/24/12 + 14 的操作按钮),未截图的状态,已知并登记; - `lib.rs` 只加一行 `pub mod pages;`。
107 lines
3.8 KiB
Rust
107 lines
3.8 KiB
Rust
//! 下载页:**单列**版面的用例(列宽 ≤504,水平居中)。
|
||
//!
|
||
//! 这一页永远单列,理由不是「简单」,而是官方对「一次只做一件事」的入口(下载队列)的口径:
|
||
//! 内容是一串线性进度项,分栏没有第二个阅读对象。窗口变宽时**正文列不变宽**(撞行宽上限),
|
||
//! 多出来的宽度留白——这就是 M68「全宽单卡」的反面。
|
||
//!
|
||
//! 页头 1 个操作 + 空态 3 个快捷入口(12b §15.5:≤3)。
|
||
|
||
use iced::widget::{column, container, row};
|
||
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 crate::theme::Ui;
|
||
|
||
use super::frame::{self, Header};
|
||
use super::layout::FrameMetrics;
|
||
use super::{Action, Message};
|
||
|
||
/// 整页。
|
||
pub fn view<'a>(ui: &'a Ui, metrics: FrameMetrics) -> Element<'a, Message> {
|
||
let header = Header::new("Download")
|
||
.subtitle("Versions, mod loaders and modpacks")
|
||
.action(frame::text_action(
|
||
ui,
|
||
"Refresh sources",
|
||
Action::RefreshSources,
|
||
));
|
||
|
||
let empty = frame::empty_state(
|
||
ui,
|
||
IconKind::Download,
|
||
"No downloads in progress",
|
||
"Pick a version or drop a modpack file here. Sources are probed per region and can be pinned \
|
||
in Settings → Downloads.",
|
||
vec![
|
||
frame::text_action(ui, "Browse versions", Action::BrowseVersions),
|
||
frame::text_action(ui, "Import modpack", Action::ImportModpack),
|
||
frame::text_action(ui, "Check sources", Action::CheckSources),
|
||
],
|
||
);
|
||
|
||
let body = frame::single_column(
|
||
metrics,
|
||
column![source_status(ui), empty]
|
||
.spacing(ui.space(Space::X4))
|
||
.width(Length::Fill)
|
||
.height(Length::Fill),
|
||
);
|
||
|
||
frame::page(ui, metrics, header, body)
|
||
}
|
||
|
||
/// 来源状态行(12pt,`OnSurfaceVariant`):说明这一页的数据从哪来,不是业务数据。
|
||
fn source_status<'a>(ui: &'a Ui) -> Element<'a, Message> {
|
||
container(
|
||
row![
|
||
frame::glyph(
|
||
IconKind::Check,
|
||
ui.color(Role::Secondary),
|
||
frame::ICON_SMALL_PX
|
||
),
|
||
frame::text_role(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
"3 sources healthy · BMCLAPI pinned by region · 2 min ago",
|
||
ui.color(Role::OnSurfaceVariant),
|
||
),
|
||
]
|
||
.spacing(ui.space(Space::X2))
|
||
.align_y(Alignment::Center),
|
||
)
|
||
.width(Length::Fill)
|
||
.into()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::super::layout::{content_column_width, frame_metrics, measure_cap_px, PaneKit};
|
||
|
||
/// 单列页:任何窗口宽度都不许出现第二列,正文列不许超过行宽上限。
|
||
#[test]
|
||
fn download_is_single_column_at_every_width() {
|
||
for width in [600.0, 840.0, 900.0, 1180.0, 2400.0] {
|
||
let metrics = frame_metrics(PaneKit::Single, width);
|
||
assert!(metrics.secondary.is_none(), "{width} 宽下载页出现了第二列");
|
||
assert!(metrics.column <= measure_cap_px() + f32::EPSILON);
|
||
assert_eq!(metrics.column, content_column_width(width));
|
||
}
|
||
}
|
||
|
||
/// 宽窗下多出来的宽度必须留白(不是把卡片拉宽)——1180 的正文列 < 可用宽度。
|
||
#[test]
|
||
fn extra_width_goes_to_margin_not_to_the_column() {
|
||
let metrics = frame_metrics(PaneKit::Single, 1180.0);
|
||
assert_eq!(metrics.column, measure_cap_px());
|
||
assert!(
|
||
metrics.available - metrics.column >= 400.0,
|
||
"1180 宽下应留出大片留白(M68 的全宽卡就是没有这段留白)"
|
||
);
|
||
}
|
||
}
|