代码 r2 的复核:`435 / 6.0526 = 71.9` 是**整段平均步进**换来的数,不是每行都 ≤72; 它实测交付图里就有 **73 字符**行(`guess. Nothing on this page is wired to real data yet: the layout is, the`, ink 428px)。上一轮是系数错,这一轮是**把平均当成了逐行上界**。 选 **(a)**:不变式改成逐行可验证,cap 跟着压到能满足它的地方。依据是 FreeType 量宽 + 贪心词折行的模拟表 (同一段 361 字符正文扫列宽): | 列宽 | 最长一行 | 该行 ink | 判定 | |---|---|---|---| | 435(= 平均口径上界) | 73 字符 | 428px | ✗ | | 428 | 73 字符 | 428px | ✗ | | 426 | 71 字符 | 426px | ✓ 只剩 1 字符余量 | | **424**(4px 栅格) | **67 字符** | **402px** | ✓ 取这个 | - `measure_cap_px()` = `min(逐行安全宽 424, 平均口径上界 435)` = **424**;平均口径另立 `measure_average_bound_px()`(435)并在文档里标明「只是上界,不是取值」。 - 单测 `line_length_is_capped_at_72_characters` 改成**两侧夹逼**:`402 ≤ cap < 428` —— 放大到 428 会重新出现 73 字符行(红),压到 402 以下白丢宽度(也红);另断言 `cap < measure_average_bound_px()`,防止有人把平均口径当上限。 - §3 新增「逐行口径」小节:模拟表 + **作用域**(承诺只覆盖 14px 正文;12px 辅助文案是 1~2 行短句, 不做逐行承诺;CJK 全角 1em 另行定)。 - 四页文档与 `pages/mod.rs` 的 ≤435 同步改 424。
383 lines
17 KiB
Rust
383 lines
17 KiB
Rust
//! 页面布局系统:**窗口宽度档位 → 版面 → 列宽**。纯函数,不碰 iced 控件树。
|
||
//!
|
||
//! 这是本 mission 的核心交付物,四个页面只是它的用例。`layout.rs` 是页面级几何的
|
||
//! **唯一定义点**:页面文件里不许再出现列宽字面量。
|
||
//!
|
||
//! ## 1. 宽度档位(官方响应式断点 600 / 840)
|
||
//!
|
||
//! 档位定义照官方 M3「Window size classes」(A 级:m3.material.io 的 app-quality/window-size-classes,
|
||
//! Compose `WindowSizeClass` 的同一组硬值):**600dp 与 840dp** 是两个分界。
|
||
//!
|
||
//! | 档位 | 窗口宽度(逻辑 px @100% DPI) | 页边距(gutter) |
|
||
//! |---|---|---|
|
||
//! | `Compact` | `< 600` | 16(`Space::X4`) |
|
||
//! | `Medium` | `600 ≤ w < 840` | 24(`Space::X6`) |
|
||
//! | `Expanded` | `≥ 840` | 24(`Space::X6`) |
|
||
//!
|
||
//! 页边距取 16/24 而不是 16/24/32:官方 expanded 页边距也是 24,写 32 会白白多一条偏离登记。
|
||
//!
|
||
//! > 本 crate 的真实窗口下限是 880(`app.rs` 的 `MIN_SIZE`),所以日常只有 `Expanded`;
|
||
//! > 600 以下的档位是给「将来允许更窄窗口 / 内嵌面板」准备的,也让布局规则**可测**。
|
||
//!
|
||
//! ## 2. 三种版面与「什么宽度用哪种」
|
||
//!
|
||
//! | 版面 | 含义 | 页面上用它 | 成立条件 | 退化到 |
|
||
//! |---|---|---|---|---|
|
||
//! | `SingleColumn` | 单列,列宽 ≤ 行宽上限 | 下载 / 日志(全部档位);实例 / 设置(窄窗) | 总是成立 | — |
|
||
//! | `SupportingPane` | 主列 + 窄侧列(侧列是辅助,主列是阅读对象) | 设置(分组导航 240 + 表单列) | `w ≥ 600` 且 `w ≥ 720` | `SingleColumn` |
|
||
//! | `ListDetail` | 固定列表列(320~360,**独立滚动**)+ 详情列 | 实例 | `w ≥ 840` 且 `w ≥ 816` | `SingleColumn` |
|
||
//!
|
||
//! 两个门槛都是**算出来的**,不是拍的(见 [`panes_min_window`]):
|
||
//!
|
||
//! - 设置:`240(drawer)+ 24(间隙)+ 320(表单列下限)+ 2×24(gutter)+ 88(rail)= 720`;
|
||
//! - 实例:`336(列表列)+ 24(间隙)+ 320(详情列下限)+ 2×24(gutter)+ 88(rail)= 816`。
|
||
//!
|
||
//! 实例的几何门槛 816 低于官方 expanded 断点 840 —— 所以**档位是更严的那一条**,
|
||
//! 单测 `expanded_is_wide_enough_for_list_detail` 把这条「余量 24px」钉死。
|
||
//!
|
||
//! ## 3. 行宽约束(历史事故:M68 的全宽单卡)
|
||
//!
|
||
//! M68 骨架的内容区是**一张全宽卡**:卡片横跨 x=112..1156(**1044px**),卡片内文本区 **1004px**
|
||
//! (边距 24 + 卡片内边距 20)。那张卡里的正文是 `BodySmall` 12px:步进 = 14px 实测值
|
||
//! 6.0526 × 12/14 = **5.19px/字符**,所以 137 字符 × 5.19 ≈ **711px** —— 与
|
||
//! `DevDocs/ops/screens-m68/shell-light-cpu.png` 里那一行**实测 ink 712px**(x=133..845)吻合。
|
||
//! 视觉复审的判词因此是「**不能把全宽单卡当页面模板**,官方桌面口径是 supporting-pane / list-detail」。
|
||
//!
|
||
//! > **两个订正记录**(都是 P2 复审抓出来的):
|
||
//! > ① **712 是那一行的墨迹宽度,不是列宽**。早期文档写成「1180 − 88 − 2×24 = 712」,
|
||
//! > 那个减法得 1044,712 是 ink——列宽是 1004,ink 只占其中的 71%。
|
||
//! > ② **字符数按实测算**:那一行是 **137 字符**(12px × 0.4323em = 5.19px/字符)。
|
||
//! > 早期口径拿 `0.5em` 估算的「14px 正文步进」换算,既高估步进、也对错了字号,把事故说轻了。
|
||
//! > 现在事故描述一律用**实测步进 × 实际字号**;这也是
|
||
//! > `line_length_is_capped_at_72_characters` 里那条「旧口径 504px 能放 83 字符」护栏的由来。
|
||
//!
|
||
//! ### 逐行口径:72 字符是**最长一行**的上界,不是平均(r2 复审的教训)
|
||
//!
|
||
//! 「平均步进 × 72」只能保证**平均**一行 72 字符:r2 复审在交付图里抓到一行 **73 字符**
|
||
//! (`guess. Nothing on this page is wired to real data yet: the layout is, the`,ink 428px)。
|
||
//! 所以上限**不是算术结果,而是逐行折行模拟的结果**——用 FreeType 量宽 + 贪心词折行,
|
||
//! 对实例页那段正文(361 字符)扫不同列宽:
|
||
//!
|
||
//! | 列宽 | 最长一行 | 该行 ink | 结论 |
|
||
//! |---|---|---|---|
|
||
//! | 435(= 平均口径上界) | 73 字符 | 428px | ✗ 破了 72 |
|
||
//! | 428 | 73 字符 | 428px | ✗ |
|
||
//! | 426 | 71 字符 | 426px | ✓ 但只剩 1 字符余量 |
|
||
//! | **424**(4px 栅格) | **67 字符** | **402px** | ✓ 取这个:满足上界 + 留 5 字符余量 |
|
||
//!
|
||
//! 于是 [`measure_cap_px`] = **424**(= `min(逐行安全宽 424, 平均口径上界 435)`),由
|
||
//! [`content_column_width`] 与 [`frame_metrics`] 无条件施加:任何窗口宽度、任何版面,
|
||
//! 正文列都不会超过它。1180 宽窗口下正文列是 **424**(不是 712)。
|
||
//!
|
||
//! **作用域**:这条承诺只覆盖 `BodyMedium` 14px 的正文。`BodySmall` 12px 的辅助文案
|
||
//! (空态副文案、窄窗提示)在同一列宽下每行可到 ~82 字符——它们都是 1~2 行的短句,不做逐行承诺;
|
||
//! 中文(全角 1em)另算,等 i18n mission 定。
|
||
|
||
//!
|
||
//! ### 步进是怎么量的(口径 + 可复现)
|
||
//!
|
||
//! - **测的是哪组字形**:`BodyMedium` 的字体栈(本机解析到 Segoe UI **Regular**,14px),
|
||
//! **无 letter-spacing**——令牌的 0.5px 字距 iced 0.14 不支持(已登记的令牌丢失),渲染里没有;
|
||
//! - **样本**:实例页详情那段正文(`pages/instances/data.rs` 的 `NOTE`,**361 字符**,
|
||
//! 大小写 / 标点 / 空格 / `→` 混排,是我们自己的真实正文,不是造出来的 `mmmm`);
|
||
//! - **量法**:FreeType `getlength`(PIL `ImageFont.truetype(segoeui.ttf, 14)`)求整段排版宽,
|
||
//! 再除以字符数 → **2185.00px / 361 = 6.0526 px/字符 = 0.43233em**;× 72 = 435.79(这是**平均**口径上界,逐行口径见下);
|
||
//! - **交叉验证**:同一段的首行(78 字符)渲染实测墨迹 **484px**,FreeType 对同一串给 479px
|
||
//! (差 ~1%,抗锯齿外扩),说明「步进」这个口径在 iced 渲染器与 FreeType 之间是一致的;
|
||
//! - **CJK 不适用**:全角字形是 1em(14px),中文的 72 字符是 1008px。页面文案走 i18n 之后
|
||
//! 要按语言分别定上限,这条留给 i18n mission;本模块只保证英文基线。
|
||
|
||
use unlucky_design::spacing::{LayoutMetrics, Space};
|
||
use unlucky_design::typography::TypeRole;
|
||
|
||
/// 官方 M3 窗口档的 medium 断点(600dp)。
|
||
pub const BREAKPOINT_MEDIUM: f32 = 600.0;
|
||
/// 官方 M3 窗口档的 expanded 断点(840dp)。
|
||
pub const BREAKPOINT_EXPANDED: f32 = 840.0;
|
||
|
||
/// 列表列宽(list-detail 左列)。任务区间 320~360,取 4px 栅格内的 **336**:
|
||
/// 落在区间中段,既放得下「名字 + 版本 + 尾随图标」,又给详情列多留 16px。
|
||
pub const LIST_COLUMN_WIDTH: f32 = 336.0;
|
||
|
||
/// 详情列下限:再窄就不再分栏。
|
||
///
|
||
/// 按实测步进算:45 字符 × 6.05px ≈ 272,取 4px 栅格并留余量 → **320**
|
||
/// (320 / 6.05 = 53 字符,仍在行宽下限之上)。
|
||
pub const DETAIL_MIN_WIDTH: f32 = 320.0;
|
||
|
||
/// 侧列宽(设置页分组导航)= D33 的 drawer 宽 240。
|
||
///
|
||
/// 复用 rail 的**尺寸语义**(88 ⇄ 240 同一容器的两种形态),但**不复制 rail 的实现**:
|
||
/// 分组导航是页内元素,没有 hover 药丸、没有二级标签。
|
||
pub const SIDE_NAV_WIDTH: f32 = LayoutMetrics::DESKTOP.nav_drawer_width;
|
||
|
||
/// 表单列下限(supporting-pane 主列):与详情列下限同源(320 ≈ 45 字符)。
|
||
pub const FORM_MIN_WIDTH: f32 = DETAIL_MIN_WIDTH;
|
||
|
||
/// 两列之间的间隙(`Space::X6` = 24)。
|
||
pub const PANES_GAP: f32 = Space::X6.dp();
|
||
|
||
/// 详情里元信息网格的**单卡最小宽度 = 由卡内最长内容反推**:**160**。
|
||
///
|
||
/// 推导(不是凑出来的):
|
||
/// - 卡内最长值是 demo 里的 `NeoForge 20.4.237`,Segoe UI 14px 实测 **118px**;
|
||
/// - 卡两侧内边距 `LayoutMetrics::card_padding` = 20,故最小外框 = 118 + 2×20 = **158**;
|
||
/// - 取 4px 栅格的 **160**(等价于旧值,但这次是**内容推出来的**,不是凑上限凑出来的)。
|
||
///
|
||
/// r2 复审抓到的回归:上一轮为了塞三列把这里收到 136,忘了算 padding ——
|
||
/// 卡内只剩 96px,`NeoForge 20.4.237`(118px)与 `NeoForge 21.1.72`(110px)都会折成两行,
|
||
/// 同排另两张卡仍一行 → **卡底不齐**。160 之下 3 列要 3×160 + 2×12 = 504,超出行宽上限,
|
||
/// 所以元信息网格在 1180 宽是 **2 列**(2×160 + 12 = 332,卡内可排 166px,装得下 118)。
|
||
/// 这也是有意的:mod 加载器版本串只会更长,2 列宽卡比 3 列挤卡更抗将来。
|
||
pub const META_CARD_MIN_WIDTH: f32 = 160.0;
|
||
/// 元信息网格卡片间距(`Space::X3` = 12)。
|
||
pub const META_GAP: f32 = Space::X3.dp();
|
||
|
||
/// 正文平均字符步进(em)——**实测值**,口径见模块文档 §3。
|
||
///
|
||
/// `BodyMedium` 14px / Segoe UI Regular / 361 字符英文混排正文,FreeType 实测
|
||
/// **6.0526px/字符 = 0.43233em**。
|
||
///
|
||
/// 旧的 `0.5`(`[估]`)把 72 字符的列宽算成 504px,而实测 504px 能塞 **83 字符**——
|
||
/// 「72 字符」这个名字和数字对不上的根因就在这里。换字体栈要重新量(这不是令牌)。
|
||
pub const CHAR_ADVANCE_EM: f32 = 0.43233;
|
||
/// 舒适行宽的字符数上限(任务建议 60~72,取上界 72)。
|
||
pub const MAX_MEASURE_CHARS: f32 = 72.0;
|
||
/// 行宽下限的字符数(再窄正文就碎成短行,45 是最低可读长度)。
|
||
pub const MIN_MEASURE_CHARS: f32 = 45.0;
|
||
|
||
/// 导航轨宽度(与 `shell::nav` 同一令牌值;布局系统只读它,不重定义)。
|
||
pub const fn rail_width() -> f32 {
|
||
LayoutMetrics::DESKTOP.nav_rail_width
|
||
}
|
||
|
||
/// **逐行**安全宽度(px):折行模拟下「最长一行 ≤72 字符」的最大 4px 栅格列宽 = **424**。
|
||
///
|
||
/// 见模块文档 §3 的模拟表:428 会出现 73 字符行,426 起降到 71,424 给到 67(留 5 字符余量)。
|
||
/// 这个数是**量出来的**,不是 `advance × 72` 算出来的——后者只保证平均。
|
||
pub const LINE_SAFE_WIDTH_PX: f32 = 424.0;
|
||
|
||
/// 平均口径上界(px):`floor(14px × 0.43233em × 72)` = **435**。
|
||
///
|
||
/// 只能保证**平均**一行 72 字符(实测 435 下会冒出 73 字符行),所以它是上界而不是取值。
|
||
pub fn measure_average_bound_px() -> f32 {
|
||
(TypeRole::BodyMedium.style().size_px * CHAR_ADVANCE_EM * MAX_MEASURE_CHARS).floor()
|
||
}
|
||
|
||
/// 正文列宽上限(px)= `min(逐行安全宽 424, 平均口径上界 435)` = **424**。
|
||
///
|
||
/// 硬上限,任何版面 / 任何窗口宽度都不许超;`1180` 窗宽下正文列就是它(而非 M68 的 712)。
|
||
pub fn measure_cap_px() -> f32 {
|
||
LINE_SAFE_WIDTH_PX.min(measure_average_bound_px())
|
||
}
|
||
|
||
/// 正文列宽下限(px):`floor(14 × 0.43233 × 45)` = **272**。
|
||
pub fn measure_floor_px() -> f32 {
|
||
(TypeRole::BodyMedium.style().size_px * CHAR_ADVANCE_EM * MIN_MEASURE_CHARS).floor()
|
||
}
|
||
|
||
/// 窗口宽度档位(官方 600 / 840 两个断点)。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum WidthClass {
|
||
/// `< 600`
|
||
Compact,
|
||
/// `600 ≤ w < 840`
|
||
Medium,
|
||
/// `≥ 840`
|
||
Expanded,
|
||
}
|
||
|
||
impl WidthClass {
|
||
/// 按**窗口宽度**(含导航轨,逻辑 px)判档。
|
||
pub fn of(window_width: f32) -> Self {
|
||
if window_width < BREAKPOINT_MEDIUM {
|
||
Self::Compact
|
||
} else if window_width < BREAKPOINT_EXPANDED {
|
||
Self::Medium
|
||
} else {
|
||
Self::Expanded
|
||
}
|
||
}
|
||
|
||
/// 稳定名字(日志/测试/文件名用)。
|
||
pub const fn name(self) -> &'static str {
|
||
match self {
|
||
Self::Compact => "compact",
|
||
Self::Medium => "medium",
|
||
Self::Expanded => "expanded",
|
||
}
|
||
}
|
||
|
||
/// 页边距:compact 16 / medium 24 / expanded 24(官方 expanded 也是 24)。
|
||
pub const fn gutter(self) -> f32 {
|
||
match self {
|
||
Self::Compact => Space::X4.dp(),
|
||
Self::Medium | Self::Expanded => Space::X6.dp(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 三种版面。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum PageLayout {
|
||
/// 单列(列宽 ≤ 行宽上限,水平居中)。
|
||
SingleColumn,
|
||
/// 主列 + 辅助侧列。
|
||
SupportingPane,
|
||
/// 固定列表列 + 详情列。
|
||
ListDetail,
|
||
}
|
||
|
||
impl PageLayout {
|
||
/// 稳定名字。
|
||
pub const fn name(self) -> &'static str {
|
||
match self {
|
||
Self::SingleColumn => "single-column",
|
||
Self::SupportingPane => "supporting-pane",
|
||
Self::ListDetail => "list-detail",
|
||
}
|
||
}
|
||
|
||
/// 是否有两个列(侧列 + 主列)。
|
||
pub const fn is_two_pane(self) -> bool {
|
||
!matches!(self, Self::SingleColumn)
|
||
}
|
||
}
|
||
|
||
/// 页面声明的「最宽版面」——布局系统按窗口宽度降级。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum PaneKit {
|
||
/// 单列页(下载 / 日志)。
|
||
Single,
|
||
/// 主列 + 侧列(设置)。
|
||
Supporting,
|
||
/// 列表 + 详情(实例)。
|
||
ListDetail,
|
||
}
|
||
|
||
/// 两列版面成立所需的最小窗口宽度(**公式**,不是拍值):
|
||
/// `Σ列宽 + 间隙 + 2×gutter + rail`。
|
||
pub fn panes_min_window(first: f32, second: f32) -> f32 {
|
||
first + second + PANES_GAP + 2.0 * WidthClass::Expanded.gutter() + rail_width()
|
||
}
|
||
|
||
/// `ListDetail` 的几何门槛:336 + 320 + 24 + 48 + 88 = **816**。
|
||
pub fn list_detail_min_window() -> f32 {
|
||
panes_min_window(LIST_COLUMN_WIDTH, DETAIL_MIN_WIDTH)
|
||
}
|
||
|
||
/// `SupportingPane` 的几何门槛:240 + 320 + 24 + 48 + 88 = **720**。
|
||
pub fn supporting_pane_min_window() -> f32 {
|
||
panes_min_window(SIDE_NAV_WIDTH, FORM_MIN_WIDTH)
|
||
}
|
||
|
||
/// 版面决议:档位与几何门槛**同时**满足才升到两列,否则退化单列。
|
||
pub fn resolve(kit: PaneKit, window_width: f32) -> PageLayout {
|
||
let class = WidthClass::of(window_width);
|
||
let wide_enough = match kit {
|
||
PaneKit::Single => false,
|
||
PaneKit::Supporting => {
|
||
class != WidthClass::Compact && window_width >= supporting_pane_min_window()
|
||
}
|
||
PaneKit::ListDetail => {
|
||
class == WidthClass::Expanded && window_width >= list_detail_min_window()
|
||
}
|
||
};
|
||
|
||
match (kit, wide_enough) {
|
||
(PaneKit::Single, _) | (_, false) => PageLayout::SingleColumn,
|
||
(PaneKit::Supporting, true) => PageLayout::SupportingPane,
|
||
(PaneKit::ListDetail, true) => PageLayout::ListDetail,
|
||
}
|
||
}
|
||
|
||
/// 页面可用宽度:窗口 − 导航轨 − 两侧页边距(下限 0)。
|
||
pub fn available_width(window_width: f32) -> f32 {
|
||
let class = WidthClass::of(window_width);
|
||
(window_width - rail_width() - 2.0 * class.gutter()).max(0.0)
|
||
}
|
||
|
||
/// **给定窗口宽度 → 内容(正文)列宽**。本 mission 的「行宽约束」入口。
|
||
///
|
||
/// - 单列页:就是正文列宽(水平居中摆放);
|
||
/// - 两列页:这是**主列**的正文列宽;实际值还要看 [`frame_metrics`](详情列会被列表列挤窄)。
|
||
///
|
||
/// 返回值永远 ≤ [`measure_cap_px`]:1180 窗宽下是 424(逐行 67 字符,≤72),不是 M68 的 712。
|
||
pub fn content_column_width(window_width: f32) -> f32 {
|
||
available_width(window_width).min(measure_cap_px())
|
||
}
|
||
|
||
/// 元信息网格的列数:3 列需要 504(超出行宽上限,1180 下用不到),2 列需要 332,否则 1 列。
|
||
pub fn meta_columns(width: f32) -> usize {
|
||
for columns in [3usize, 2] {
|
||
let needed = columns as f32 * META_CARD_MIN_WIDTH + (columns - 1) as f32 * META_GAP;
|
||
if width >= needed {
|
||
return columns;
|
||
}
|
||
}
|
||
1
|
||
}
|
||
|
||
/// 一屏的版面几何(页面直接消费,不再自己算列宽)。
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct FrameMetrics {
|
||
/// 窗口宽度(输入)。
|
||
pub window_width: f32,
|
||
/// 宽度档位。
|
||
pub class: WidthClass,
|
||
/// 决议出来的版面。
|
||
pub layout: PageLayout,
|
||
/// 页边距。
|
||
pub gutter: f32,
|
||
/// 页面可用宽度(已扣导航轨与页边距)。
|
||
pub available: f32,
|
||
/// 正文列宽(≤ 行宽上限)。两列版面时是**主列**的正文列宽。
|
||
pub column: f32,
|
||
/// 第二列(list-detail 的列表列 / supporting-pane 的侧列)。
|
||
pub secondary: Option<f32>,
|
||
/// 主列所在 pane 的宽度(单列 = 正文列宽)。
|
||
pub main_pane: f32,
|
||
/// 详情元信息网格列数。
|
||
pub meta_columns: usize,
|
||
}
|
||
|
||
/// 一次算齐版面几何。
|
||
pub fn frame_metrics(kit: PaneKit, window_width: f32) -> FrameMetrics {
|
||
let class = WidthClass::of(window_width);
|
||
let gutter = class.gutter();
|
||
let available = available_width(window_width);
|
||
let layout = resolve(kit, window_width);
|
||
|
||
let (column, secondary, main_pane) = match layout {
|
||
PageLayout::SingleColumn => {
|
||
let column = available.min(measure_cap_px());
|
||
(column, None, column)
|
||
}
|
||
PageLayout::ListDetail => {
|
||
let list = LIST_COLUMN_WIDTH.min(available);
|
||
let pane = (available - list - PANES_GAP).max(0.0);
|
||
(pane.min(measure_cap_px()), Some(list), pane)
|
||
}
|
||
PageLayout::SupportingPane => {
|
||
let side = SIDE_NAV_WIDTH.min(available);
|
||
let pane = (available - side - PANES_GAP).max(0.0);
|
||
(pane.min(measure_cap_px()), Some(side), pane)
|
||
}
|
||
};
|
||
|
||
FrameMetrics {
|
||
window_width,
|
||
class,
|
||
layout,
|
||
gutter,
|
||
available,
|
||
column,
|
||
secondary,
|
||
main_pane,
|
||
meta_columns: meta_columns(column),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|