- P2-1 判据恒绿:`gallery/probe/bases.rs` 的「反向检查」把字段档计数(`:172` 遮蔽后的
`hits`)拿去比按钮档 stray ⇒ 恒绿。改成 `button_hits` / `field_hits` 两个计数,各比各的
stray,失败信息的数值与单位对齐量测对象。变异自证:卡列里留一颗忘了 `.base(..)` 的按钮
⇒ 该断言单独红(2137 px vs 3178 px,退出码 101)。
- P2-2 FAB 的底错了:禁用态背景仍是满档 `PrimaryContainer`(`Disabled.layer() == None`),
38% 内容却合在画布上。改成 `Base::of(PrimaryContainer)` + `on_container = false`
(那一层 10% 在 FAB 身上不存在),两处调用收进一个 `content_color()`;新增值判据
`disabled_fab_ink_composites_over_its_own_container`(修前 #8CA0AA / 修后 #548FBC 量级)。
- P2-3 漏改:`selection::paint::state_layer()`(hover/focus/pressed)与 `list_item` 的
状态层叠底仍写死 `Role::Surface`,改成跟 `base`;同类两处(`assemble::transparent_state_color`
的 Outlined/Text hover、drawer 项药丸)一并跟上。
- P2-4 文档失真:`feedback/{action,mod}.rs` 删掉「本地 α 合成 / 规则只认两档」的旧描述
与失效链接;`list_item` 的旧签名;三处 `probe/base.rs` → `probe/bases.rs`;
新增 `surfaces::elevated::tests::the_card_base_constant_is_the_role_the_card_paints`
真断言(原来只在文档里声称那条测试守 `BASE`),并把这句描述改准。
- nit:`disabledbase.rs` 字符串里塌成一行的续行空格、probe 汇总行把列左缘横坐标说成
「像素计数」的措辞。
门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-234):fmt / check --all-targets /
test -p unlucky-app-iced(896 + 6 通过,0 失败)/ clippy --workspace --all-targets -D warnings /
文件 ≤400 全绿;probe 浅色与深色都过。
168 lines
6.0 KiB
Rust
168 lines
6.0 KiB
Rust
//! Radio button(`research/15` §19 + 规格卡):外圈 **18** / 环线 **2** / 内点 **10**(不是 12)/
|
||
//! 选中环+点 `Primary` / 未选中环 `OnSurfaceVariant`(hover 提到 `OnSurface`)/ Ø40 状态层。
|
||
//!
|
||
//! ## 为什么它不是「基于 Checkbox 画的」
|
||
//!
|
||
//! §19.6 明令:Radio **只能**是「2px 环 + 实心内点」,Checkbox **只能**是「2px 方框 + 勾/横」;
|
||
//! 两者尺寸相同(**18**)但形状语言不同,禁止混用。所以本文件是独立实现,**没有** import
|
||
//! `super::checkbox`(gallery 的 probe 另有一条断言:radio 的墨迹里不得出现方角像素)。
|
||
//!
|
||
//! ## 几何怎么落到像素
|
||
//!
|
||
//! 环:**18×18** 容器 + `Border { width: 2, radius: 9 }`。iced 的 quad 渲染器把描边路径向内缩
|
||
//! 半个线宽(半径 9−1=8、路径外接 16),再以 2 描边 → 外缘正好 18、内缘 14。这与 Compose
|
||
//! `RadioButton.kt` 的「环半径 = side/2 − 1 且描边居中 → 外缘正好 side」逐像素同源,
|
||
//! 只看 side 取 18(规格卡 `r=9`)而不是官方的 20。
|
||
//! 内点:独立 10×10 圆容器居中在环里(绘制半径 12/2 − 2/2 = 5,即**直径 10**,§19.2)。
|
||
|
||
use iced::widget::{container, row, text};
|
||
use iced::{Alignment, Element, Length, Padding};
|
||
|
||
use unlucky_design::typography::TypeRole;
|
||
|
||
use crate::theme::Ui;
|
||
|
||
use super::super::interaction::{Base, Interaction};
|
||
use super::geometry::ControlGeometry;
|
||
use super::paint::{self, Frame};
|
||
use super::shell::{self, StateSource};
|
||
|
||
/// 一个单选框(环 + 可选标签;`on_select` 为 `None` = 禁用)。
|
||
#[derive(Debug, Clone)]
|
||
pub struct Radio<'a, M> {
|
||
label: &'a str,
|
||
selected: bool,
|
||
on_select: Option<M>,
|
||
state: Option<Interaction>,
|
||
/// 脚下那一层的底(M234,禁用态合成用)。
|
||
base: Base,
|
||
}
|
||
|
||
impl<'a, M> Radio<'a, M> {
|
||
/// 一个可选的单选框。
|
||
pub fn new(label: &'a str, selected: bool, on_select: M) -> Self {
|
||
Self {
|
||
label,
|
||
selected,
|
||
on_select: Some(on_select),
|
||
state: None,
|
||
base: Base::CANVAS,
|
||
}
|
||
}
|
||
|
||
/// 禁用的单选框(无消息)。
|
||
pub fn disabled(label: &'a str, selected: bool) -> Self {
|
||
Self {
|
||
label,
|
||
selected,
|
||
on_select: None,
|
||
state: None,
|
||
base: Base::CANVAS,
|
||
}
|
||
}
|
||
|
||
/// 强制视觉态(gallery 取证用;运行期不要调)。
|
||
pub fn state(mut self, state: Interaction) -> Self {
|
||
self.state = Some(state);
|
||
self
|
||
}
|
||
|
||
/// **告诉单选框它踩在什么底上**(M234):禁用态的 38% 环/内点叠在这个底上。
|
||
pub fn base(mut self, base: impl Into<Base>) -> Self {
|
||
self.base = base.into();
|
||
self
|
||
}
|
||
}
|
||
|
||
/// 画一个单选框(环 + 内点 + 标签)。
|
||
pub fn radio<'a, M: Clone + 'a>(ui: &'a Ui, spec: Radio<'a, M>) -> Element<'a, M> {
|
||
let geometry = ControlGeometry::radio(ui);
|
||
let selected = spec.selected;
|
||
let radius = geometry.radius;
|
||
let stroke = geometry.stroke;
|
||
let dot_side = geometry.inner_dot;
|
||
let base = spec.base;
|
||
let source = StateSource::new(spec.state, spec.on_select.is_some());
|
||
|
||
// 内点与环的颜色与 hover/pressed 无关(A1:选中态四态都是 `Primary`),
|
||
// 只随「选中 + 禁用」变 —— 所以点的颜色在这里静态求值即可。
|
||
let at_rest = paint::radio(ui, base, selected, source.get(), radius, stroke);
|
||
|
||
// Ø10 内点(半径 = 直径/2)
|
||
let dot: Element<'a, M> = match at_rest.dot {
|
||
Some(ink) => container(iced::widget::Space::new())
|
||
.width(Length::Fixed(dot_side))
|
||
.height(Length::Fixed(dot_side))
|
||
.style(move |_theme| {
|
||
Frame {
|
||
fill: Some(ink),
|
||
outline: None,
|
||
stroke: 0.0,
|
||
radius: dot_side / 2.0,
|
||
}
|
||
.container_style()
|
||
})
|
||
.into(),
|
||
None => iced::widget::Space::new().into(),
|
||
};
|
||
|
||
// 20 外圈:环(未选中时环就是唯一的墨迹;选中时环 + 点同色)
|
||
let ring = container(dot)
|
||
.width(Length::Fixed(geometry.side))
|
||
.height(Length::Fixed(geometry.side))
|
||
.align_x(Alignment::Center)
|
||
.align_y(Alignment::Center)
|
||
.style(source.container_style(move |state| {
|
||
paint::radio(ui, base, selected, state, radius, stroke)
|
||
.ring
|
||
.container_style()
|
||
}));
|
||
|
||
// Ø40 状态层
|
||
let layer_side = geometry.state_layer;
|
||
let layer = container(ring)
|
||
.width(Length::Fixed(layer_side))
|
||
.height(Length::Fixed(layer_side))
|
||
.align_x(Alignment::Center)
|
||
.align_y(Alignment::Center)
|
||
.style(source.container_style(move |state| {
|
||
Frame {
|
||
fill: paint::state_layer(ui, base, state, paint::radio_layer_role(selected)),
|
||
outline: None,
|
||
stroke: 0.0,
|
||
radius: layer_side / 2.0,
|
||
}
|
||
.container_style()
|
||
}));
|
||
|
||
let mut content = row![layer].align_y(Alignment::Center).spacing(geometry.gap);
|
||
if !spec.label.is_empty() {
|
||
let style = ui.style(TypeRole::BodyLarge);
|
||
content = content.push(
|
||
text(spec.label)
|
||
.size(style.size_px)
|
||
.font(ui.font_of(style))
|
||
.color(paint::label(ui, base, source.get())),
|
||
);
|
||
}
|
||
|
||
// 命中区 48×48:左右各留 `hit_padding` 透明内边距(无标签时行宽才不是 40;复审 P2-2)
|
||
let pad = geometry.hit_padding();
|
||
let body = container(content)
|
||
.height(Length::Fill)
|
||
.align_y(Alignment::Center)
|
||
.padding(Padding {
|
||
left: pad,
|
||
right: pad,
|
||
..Padding::default()
|
||
});
|
||
|
||
shell::shell(
|
||
&source,
|
||
Length::Shrink,
|
||
Length::Fixed(geometry.hit),
|
||
spec.on_select,
|
||
body.into(),
|
||
)
|
||
}
|