//! 页面骨架:**页头**(标题 + 副标题 + 可选操作区)+ **内容区**(列宽由 [`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, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH, }; 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>, } 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> { 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> { centered(metrics.column, content) } /// 两列版面:左列 `secondary`(独立滚动区由调用方自己加 `scrollable`)+ 右列 `main`。 /// /// 两列都**显式**取布局系统给的宽度,不吃 Fill:列表列必须固定 336, /// 详情列必须窄于可用宽度,否则又回到「全宽单卡」。 pub fn panes<'a>( metrics: FrameMetrics, secondary: impl Into>, main: impl Into>, ) -> 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> { 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> { container(container(content).width(Length::Fixed(width))) .width(Length::Fill) .height(Length::Fill) .align_x(Alignment::Start) .align_y(Alignment::Start) .into() }