WIP(app-iced): M75 组件基座 checkpoint —— 组件骨架/按钮五式/分隔线/gallery harness

已落地:
- components/{mod,interaction,layout,divider,gate}.rs + buttons/{mod,geometry,assemble,tests}.rs
- gallery/{mod,buttons,probe,scan}.rs + 四个家族桩(selection/inputs/feedback/navigation)
- cli.rs: --screen=shell|gallery + capture_flavor(截图默认 Light,删掉等系统主题的闸门)
- app.rs: gallery 路由 + DemoMessage 接线(400 行)

待办(下一个 commit):编译/测试全绿、浅深矩阵出图、1:1 并排与数值表。
This commit is contained in:
AskaEth committed 2026-11-02 04:50:44 +08:00
1 parent 22ddc74784
commit 4774e7b788
23 files changed
+3136 -133

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
//! 布局陷阱的两个固定解法(D39 §4):**要精确尺寸就调这里,别手写**。
//!
//! ## 陷阱 Q8:`center_x(Length::Fill)` 会把已设的 `.width()` 覆盖成 Fill
//!
//! iced 的 `container::center_x/center_y/center` 不只是「对齐」——它们**同时**把容器该轴的
//! `Length` 改成 `Fill`(源码里 `self.width = Length::Fill`)。于是
//! `.width(56).center_x(Length::Fill)` 最终是 `Fill`,56 被父级宽度吃掉:
//! M68 的导航药丸因此从 **56×32 变成 88×32**(父级是 88 宽的轨),图上看着「横贯全轨」,
//! 而单元测试测的 `indicator_size()` 返回值一直是 56×32 —— **函数对、图错**。
//!
//! 正确写法是二选一:
//!
//! - 尺寸留在**自己**身上,居中交给**父级**(`row/column` 的 `align_x`)——`shell/nav.rs` 的做法;
//! - 或者用 [`centered_fixed`]:容器写死宽高,**用 `align_x/align_y` 居中**(它们只改对齐,
//! 不改 `Length`)。
//!
//! ## 陷阱 Q9:`Button` 给内容 Fill 约束,`svg().width(24)` 会被忽略
//!
//! `button` 在 layout 里把「内容可用区」作为约束交给子元素,`svg` 拿到的是 Fill,
//! usvg 于是按 `Contain` 撑满内区:**字形渲染成 46px、线宽从 1.5 变 2.9**(M68 视觉复审 P2-2),
//! 比导航图标重一倍。反过来「把 `.width(24)` 设在 button 上」也不行——那会缩掉点击区。
//!
//! 正确写法:**在中间插一层固定尺寸容器**——[`fixed`]:
//!
//! ```text
//! let icon = layout::fixed(24.0, 24.0, icons::view(kind, color)); // 字形 24×24
//! button(icon).width(46).height(48) // 命中区仍是 46×48
//! ```
//!
//! ## 为什么还要 [`inset`]
//!
//! 「左右缩进 16 的一条线」「内容四周留 24」这类**只改内边距、不改尺寸**的语义,
//! 写 `.padding(Padding::from([...]))` 时数组顺序(上右下左)极易写反;封装一次,
//! 全组件线只写一次顺序。
//!
//! ## 自检
//!
//! `components::gate` 会扫 `src/components/**`:出现 `center_x(` / `center_y(` / `.center(`
//! 即判红(这条门禁就是 Q8 的回归测试);图标一律经 [`fixed`] / [`centered_fixed`] 包一层。
use iced::widget::container;
use iced::{Alignment, Element, Length, Padding};
/// 固定尺寸的盒子:宽高**只由参数决定**,子元素贴左上(对齐交给子元素自己或父级)。
///
/// Q9 的解法:`svg` 放进 24×24 的盒子,它拿到的约束就是 24,`Contain` 无从撑满。
pub fn fixed<'a, M: 'a>(
width: f32,
height: f32,
content: impl Into<Element<'a, M>>,
) -> Element<'a, M> {
container(content)
.width(Length::Fixed(width))
.height(Length::Fixed(height))
.into()
}
/// 固定尺寸 + 内容居中(Q8 的解法)。
///
/// 用 `align_x/align_y` 而不是 `center_x/center_y`:前者只改对齐,后者会把本容器的
/// `Length` 改成 `Fill`,把参数里的宽高**静默丢掉**。
///
/// 典型用法:导航轨的选中指示器 [`centered_fixed(56.0, 32.0, icon)`](centered_fixed)。
pub fn centered_fixed<'a, M: 'a>(
width: f32,
height: f32,
content: impl Into<Element<'a, M>>,
) -> Element<'a, M> {
container(content)
.width(Length::Fixed(width))
.height(Length::Fixed(height))
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}
/// 只加内边距、不碰尺寸(`horizontal` = 左右,`vertical` = 上下)。
///
/// 语义等价于 `.padding(Padding { top: vertical, right: horizontal, bottom: vertical,
/// left: horizontal })`,但只暴露两个参数——避免把「上右下左」写反。
pub fn inset<'a, M: 'a>(
horizontal: f32,
vertical: f32,
content: impl Into<Element<'a, M>>,
) -> Element<'a, M> {
container(content)
.padding(Padding {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
})
.into()
}
/// 单侧缩进(只缩左侧;分隔线的「列表内左缩进 16」用这个)。
pub fn inset_start<'a, M: 'a>(start: f32, content: impl Into<Element<'a, M>>) -> Element<'a, M> {
container(content)
.width(Length::Fill)
.padding(Padding {
left: start,
..Padding::default()
})
.into()
}
#[cfg(test)]
mod tests {
use super::*;
/// Q8 的**形态级**钉法:这三个 API 一旦内部改用 `center_*`,这条测试之外的门禁
/// (`components::gate`)也会红;这里再钉一次「参数就是参数」,防止签名被悄悄改成
/// `impl Into<Length>` 之类。
#[test]
fn helpers_take_pixel_sizes_not_lengths() {
let _: fn(f32, f32, Element<'static, ()>) -> Element<'static, ()> = fixed;
let _: fn(f32, f32, Element<'static, ()>) -> Element<'static, ()> = centered_fixed;
let _: fn(f32, f32, Element<'static, ()>) -> Element<'static, ()> = inset;
let _: fn(f32, Element<'static, ()>) -> Element<'static, ()> = inset_start;
}
#[test]
fn helpers_build_a_tree_without_panicking() {
let _: Element<'_, ()> = fixed(24.0, 24.0, iced::widget::text("x"));
let _: Element<'_, ()> = centered_fixed(56.0, 32.0, iced::widget::text("x"));
let _: Element<'_, ()> = inset(16.0, 8.0, iced::widget::text("x"));
let _: Element<'_, ()> = inset_start(16.0, iced::widget::text("x"));
}
}