Files
Unlucky-Launcher/crates/app-iced/src/components/selection/switch.rs
T
AskaEth 35dbefb08d feat(app-iced): 禁用态合成底改成容器给的 Base(M234)
`interaction::disabled` 的四个入口一直收一个「底」,但 8 个调用点全传死
`Role::Surface`:卡片(SurfaceContainer)/ 面板(SurfaceContainerLow)里的禁用控件
把 10%(按钮族)/ 12%(面状控件族)叠在画布色上 —— M223「CR 1.05、形丢失」的根因。

- 新增 `interaction::Base`(画布 / 容器底的一等参数,`From<Role>` 兼容历史调用点);
  四个入口改成 `(ui, base, …)`,内部不再出现任何写死的 `Role::Surface`。
- 容器导出自己的 `BASE`:`surfaces::elevated::BASE`(卡)、`inputs::menu::PANEL_BASE`
  (菜单面板,项的 38% 与状态层都改叠它)、`navigation::drawer::BASE`。
- 控件收 `.base(..)`:Button / icon_on / Field / SearchBar / Slider / Checkbox / Radio /
  Switch / Chip / ListItem / Tabs;Fab 与 icon() 是画布默认值。
- `feedback/action.rs` 的本地 α 合成删掉,改走 `disabled_content_over_role` 并给真实容器底
  (Snackbar `InverseSurface` / Dialog `SurfaceContainerHigh`)。
- 判据:`interaction::disabled::tests`(卡片/面板/画布三档逐值)、
  `buttons::tests::base`(Button::base → StyleSpec → style_closure 接线)、
  `gallery` 新增 `disabledbase` 段 + `gallery::probe::bases` 精确像素断言
  (±10 容差区分不了 1/255 的差,所以自己数逐位相等的像素)。
- 变异自证:assemble 的调用点改回画布底后,上面两条都红(见 artifacts/m234/README.md)。

已知未尽:`crates/app-iced/src/pages/**` 不在 scope 内,页面侧「把控件放进卡片/面板」的
调用点尚未接 `.base(..)`(行为与本 mission 之前一致);`pages/…/java/tests/marker.rs`
有一行被迫更新(入口多了一个实参,语义不变 = 画布底)。
2026-11-11 06:34:23 +08:00

191 lines
6.8 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.
//! Switch(`research/12b` §10):轨道 **52×32** / 圆角 full(16) / 未选中 2px `Outline` 描边 /
//! 滑块 **24·16**(按下 28,见偏离 D-SW1)/ 滑块内图标 16 / **Ø40 状态层**。
//!
//! ## 状态层为什么跟滑块走
//!
//! M3 的 switch 状态层是围着**滑块**的 Ø40 圆(`StateLayerSize = 40`),而轨道只有 32 高、
//! 52 宽、滑块最小 16——圆会同时超出轨道的上下与左右。所以绘制区比轨道大一圈:
//! 宽 = 52 + 2×4([`SwitchGeometry::draw_width`])、高 = 40,轨道在中间,滑块与状态层浮在上面。
//!
//! ## 偏离 D-SW1(登记在报告里)
//!
//! `PressedHandleWidth = 28` 的「按下胀开」**未做**:iced 的 `button` 只在事件时重绘,
//! 不会重建视图,而滑块尺寸是**布局量**(不是颜色)。要做它必须写自定义 `Widget`
//! (`interaction::Tracker` 那条路),本 mission 的时间盒留给四条线的像素断言与视觉证据。
//! 按下的可判定反馈仍在:状态层 α 从 8% 升到 10%(`StateLayer::Pressed`)。
use iced::widget::{container, row, text, Space as Spacer};
use iced::{Alignment, Element, Length, Padding};
use unlucky_design::icons::IconKind;
use unlucky_design::spacing::Space;
use unlucky_design::typography::TypeRole;
use crate::icons;
use crate::theme::Ui;
use super::super::interaction::{Base, Interaction};
use super::geometry::SwitchGeometry;
use super::paint::{self, Frame};
use super::shell::{self, StateSource};
/// 一个开关(轨道 + 滑块 + 可选标签;`on_toggle` 为 `None` = 禁用)。
#[derive(Debug, Clone)]
pub struct Switch<'a, M> {
label: &'a str,
value: bool,
on_toggle: Option<M>,
state: Option<Interaction>,
/// 脚下那一层的底(M234,禁用态合成用)。
base: Base,
}
impl<'a, M> Switch<'a, M> {
/// 一个可切换的开关。
pub fn new(label: &'a str, value: bool, on_toggle: M) -> Self {
Self {
label,
value,
on_toggle: Some(on_toggle),
state: None,
base: Base::CANVAS,
}
}
/// 禁用的开关(无消息)。
pub fn disabled(label: &'a str, value: bool) -> Self {
Self {
label,
value,
on_toggle: None,
state: None,
base: Base::CANVAS,
}
}
/// 强制视觉态(gallery 取证用;运行期不要调)。
pub fn state(mut self, state: Interaction) -> Self {
self.state = Some(state);
self
}
/// **告诉开关它踩在什么底上**(M234):禁用态的 12% 轨道 / 38% 滑块叠在这个底上。
pub fn base(mut self, base: impl Into<Base>) -> Self {
self.base = base.into();
self
}
}
/// 画一个开关(轨道 + 滑块 + 标签)。
pub fn switch<'a, M: Clone + 'a>(ui: &'a Ui, spec: Switch<'a, M>) -> Element<'a, M> {
let geometry = SwitchGeometry::of(ui);
let selected = spec.value;
let thumb_side = geometry.thumb(selected, false);
let base = spec.base;
let source = StateSource::new(spec.state, spec.on_toggle.is_some());
// 滑块内的勾:颜色只随选中/禁用变(A1 的 `Selected*IconColor` 都是 `OnPrimaryContainer`),
// 所以静态求值;未选中态官方图标色 = 轨道色(画了也看不见),因此不画。
let at_rest = paint::switch(ui, base, selected, source.get());
let glyph: Element<'a, M> = match at_rest.icon {
Some(ink) => super::super::layout::centered_fixed(
geometry.icon,
geometry.icon,
// 盒 16 ≠ RENDER_PX(24):必须按 16 烘焙,否则线宽被布局压细(复审 r1 P2-2)。
icons::view_at::<M>(IconKind::Check, ink, geometry.icon),
),
None => Spacer::new().into(),
};
// 滑块(颜色跟态走:hover/pressed 是 `PrimaryContainer`)
let thumb = container(glyph)
.width(Length::Fixed(thumb_side))
.height(Length::Fixed(thumb_side))
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.style(source.container_style(move |state| {
Frame {
fill: Some(paint::switch(ui, base, selected, state).thumb),
outline: None,
stroke: 0.0,
radius: thumb_side / 2.0,
}
.container_style()
}));
// Ø40 状态层(围着滑块,圆心与滑块同心)
let layer_side = geometry.state_layer;
let layer = container(thumb)
.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::switch_layer(ui, state, selected),
outline: None,
stroke: 0.0,
radius: layer_side / 2.0,
}
.container_style()
}));
// 轨道(52×32;未选中态有 2px `Outline` 描边)
let track = container(Spacer::new())
.width(Length::Fixed(geometry.track_width))
.height(Length::Fixed(geometry.track_height))
.style(source.container_style(move |state| {
paint::switch(ui, base, selected, state)
.track
.container_style()
}));
// 绘制区 = 轨道 + 状态层外扩;两层叠放(层序:轨道在下,滑块与状态层在上)
let draw_width = geometry.draw_width();
let draw_height = geometry.draw_height();
let area = iced::widget::stack![
container(track)
.width(Length::Fixed(draw_width))
.height(Length::Fixed(draw_height))
.padding(Padding {
left: geometry.bleed(),
..Padding::default()
})
.align_y(Alignment::Center),
container(layer)
.width(Length::Fixed(draw_width))
.height(Length::Fixed(draw_height))
.padding(Padding {
left: geometry.layer_offset(selected, thumb_side),
..Padding::default()
}),
]
.width(Length::Fixed(draw_width))
.height(Length::Fixed(draw_height));
let mut content = row![area]
.align_y(Alignment::Center)
.spacing(ui.space(Space::X3));
if !spec.label.is_empty() {
let style = ui.style(TypeRole::BodyMedium);
content = content.push(
text(spec.label)
.size(style.size_px)
.font(ui.font_of(style))
.color(paint::label(ui, base, source.get())),
);
}
let body = container(content)
.height(Length::Fill)
.align_y(Alignment::Center);
shell::shell(
&source,
Length::Shrink,
Length::Fixed(geometry.hit),
spec.on_toggle,
body.into(),
)
}