feat(app-iced): M84 页面骨架与布局系统(断点 / 行宽上限 / 三种版面)
视觉复审在 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;`。
This commit is contained in:
1 parent
352dd7937e
commit
2c442585b1
15 files changed
+2979
No files matched your search
@@ -0,0 +1,189 @@
|
||||
//! 页面骨架:**页头**(标题 + 副标题 + 可选操作区)+ **内容区**(列宽由 [`super::layout`] 决定)
|
||||
//! + **空态位**。
|
||||
//!
|
||||
//! ## 边界
|
||||
//!
|
||||
//! 这里只放「页面级」的东西:页头、页边距、内容列、两列骨架、空态位的摆放。
|
||||
//! 具体控件(卡片 / 分隔线 / 文本按钮 / 图标)在 [`pieces`],它们是**占位实现**
|
||||
//! (同一份令牌,没有完整状态机),等 M75 的 `components/` 家族合入后由另一条 mission 换掉。
|
||||
//!
|
||||
//! ## 判据落点(`research/12b`)
|
||||
//!
|
||||
//! | 部件 | 参考 | 落点 |
|
||||
//! |---|---|---|
|
||||
//! | 页头 | 16.1 AppBar 高 56(登记偏离官方 64)/ 两行 84 | `LayoutMetrics::header_height` |
|
||||
//! | 页头标题 | 16.4 `TitleLarge 22/28` | `Component::TopAppBar.spec().text` |
|
||||
//! | 页头副标题 | 16.4 `LabelMedium 12/16` `OnSurfaceVariant` | 同上 `.support` |
|
||||
//! | 空态 | 15.1 图标块 96×96 / `Large(16)`;15.2 标题 `HeadlineSmall 24`;15.3 副文案 `BodySmall 12` | `Component::EmptyState` |
|
||||
|
||||
mod pieces;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use pieces::{
|
||||
card, divider, empty_state, filled, gap, glyph, glyph_box, scroll_style, text_action,
|
||||
text_role, text_role_centered, ICON_PX, ICON_SMALL_PX,
|
||||
};
|
||||
|
||||
use iced::widget::{column, container, row};
|
||||
use iced::{Alignment, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::layout::{self, FrameMetrics};
|
||||
use super::Message;
|
||||
|
||||
/// 页头操作区之间的间隔(`Space::X2`)。
|
||||
const HEADER_ACTION_GAP: f32 = Space::X2.dp();
|
||||
|
||||
/// 页头内容。
|
||||
pub struct Header<'a> {
|
||||
/// 标题(`TitleLarge`)。
|
||||
pub title: &'a str,
|
||||
/// 副标题(`LabelMedium`,给了页头就是两行高)。
|
||||
pub subtitle: Option<&'a str>,
|
||||
/// 右侧操作区(text button 占位,数量 ≤3)。
|
||||
pub actions: Vec<Element<'a, Message>>,
|
||||
}
|
||||
|
||||
impl<'a> Header<'a> {
|
||||
/// 单行页头。
|
||||
pub fn new(title: &'a str) -> Self {
|
||||
Self {
|
||||
title,
|
||||
subtitle: None,
|
||||
actions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 加副标题(页头自动变两行高)。
|
||||
#[must_use]
|
||||
pub fn subtitle(mut self, subtitle: &'a str) -> Self {
|
||||
self.subtitle = Some(subtitle);
|
||||
self
|
||||
}
|
||||
|
||||
/// 追加一个右侧操作。
|
||||
#[must_use]
|
||||
pub fn action(mut self, action: Element<'a, Message>) -> Self {
|
||||
self.actions.push(action);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 一整页:页头 + 内容区(页边距已由布局系统决定)。
|
||||
pub fn page<'a>(
|
||||
ui: &'a Ui,
|
||||
metrics: FrameMetrics,
|
||||
header: Header<'a>,
|
||||
body: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
let bar = header_bar(ui, metrics, header);
|
||||
let content = container(body)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(metrics.gutter);
|
||||
|
||||
column![bar, content]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn header_bar<'a>(ui: &'a Ui, metrics: FrameMetrics, header: Header<'a>) -> Element<'a, Message> {
|
||||
let spec = Component::TopAppBar.spec();
|
||||
// 16.4:标题 TitleLarge(emphasized w500),副标题 LabelMedium + OnSurfaceVariant
|
||||
let mut titles = column![text_role(
|
||||
ui,
|
||||
spec.text,
|
||||
true,
|
||||
header.title,
|
||||
ui.color(Role::OnSurface)
|
||||
)]
|
||||
.spacing(ui.space(Space::X1));
|
||||
if let Some(subtitle) = header.subtitle {
|
||||
titles = titles.push(text_role(
|
||||
ui,
|
||||
spec.support.unwrap_or(TypeRole::LabelMedium),
|
||||
false,
|
||||
subtitle,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
}
|
||||
|
||||
let actions = row(header.actions)
|
||||
.spacing(HEADER_ACTION_GAP)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
container(
|
||||
row![titles.width(Length::Fill), actions]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(ui.metrics().header_height(header.subtitle.is_some()))
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding::from([0.0, metrics.gutter]))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 单列版面的正文列:宽 = 行宽上限内的实际列宽,**水平居中**。
|
||||
pub fn single_column<'a>(
|
||||
metrics: FrameMetrics,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
centered(metrics.column, content)
|
||||
}
|
||||
|
||||
/// 两列版面:左列 `secondary`(独立滚动区由调用方自己加 `scrollable`)+ 右列 `main`。
|
||||
///
|
||||
/// 两列都**显式**取布局系统给的宽度,不吃 Fill:列表列必须固定 336,
|
||||
/// 详情列必须窄于可用宽度,否则又回到「全宽单卡」。
|
||||
pub fn panes<'a>(
|
||||
metrics: FrameMetrics,
|
||||
secondary: impl Into<Element<'a, Message>>,
|
||||
main: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
let width = metrics.secondary.unwrap_or(0.0);
|
||||
row![
|
||||
container(secondary)
|
||||
.width(Length::Fixed(width))
|
||||
.height(Length::Fill),
|
||||
container(main)
|
||||
.width(Length::Fixed(metrics.main_pane))
|
||||
.height(Length::Fill),
|
||||
]
|
||||
.spacing(layout::PANES_GAP)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 把内容塞进固定宽度并水平居中(`align_x` 不会覆盖子元素的宽度,见 Q8)。
|
||||
pub fn centered<'a>(width: f32, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
container(container(content).width(Length::Fixed(width)))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Start)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 与 [`centered`] 同宽,但**贴左**:列表右侧的详情列要贴着自己的 pane 左边,不再二次居中。
|
||||
pub fn left_aligned<'a>(
|
||||
width: f32,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
container(container(content).width(Length::Fixed(width)))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Start)
|
||||
.align_y(Alignment::Start)
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user