Files
Unlucky-Launcher/crates/app-iced/src/components/layout.rs
T
AskaEth 4774e7b788 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 并排与数值表。
2026-11-02 04:50:44 +08:00

130 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 布局陷阱的两个固定解法(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"));
}
}