feat(app-iced): 强调色接线落地(M250 ACCENTWIRE)
appearance.accent_color 此前是死线:配置键、presenter 往返、预设色板都在,但造 tokens 的 唯一入口恒走 DesignTokens::brand(board),用户填了色界面不变。 - theme.rs:Ui::with_accent + accent_scheme(唯一调用 DesignTokens::with_accent 的地方) - app/state/theme.rs:rebuild_ui 三选一(显式强调色 > D38 取色方案 > 出厂板),diag 一行说清是谁生效 - pages/appearance:新增 accent 状态(原文/解析结果/方案/结构化错误)+ 设置页输入行与预设色块; 非法值保持上一次合法值并给出 Short/Long/Digit 三档原因,不静默回退出厂色 - presenter::prefs:AccentChoice::parse 成为唯一解析点,set_accent_checked 交回结构化原因 - app/appearance.rs:persist_appearance 带上 accent_color(此前写不进去),只写上一次合法值 - pages/harness/app.rs:gallery 也能吃 ULL_APPEARANCE_ACCENT,供出图矩阵 - core keys.rs:订正「空 = 出厂 #0078D4;accent 令牌全域生效」这句半真半假的说明 - i18n:强调色 7 条文案(链尾新域,金表只追加) 判据:同一组件(进度条 Primary)在空配置与 #006A5C 下色值不同(App 级 + 令牌级两条); 非法值拿到结构化错误、生效色不回退、不写盘、tokens 仍可构造。
This commit is contained in:
1 parent
4cdb46d2e0
commit
e6b2200360
18 files changed
+787
-38
No files matched your search
@@ -0,0 +1,165 @@
|
||||
//! 外观分组第 ⓪ 段(视觉上排在最前):**自定义强调色**(M250 ACCENTWIRE)。
|
||||
//!
|
||||
//! 为什么这一段值得单开:`appearance.accent_color` 从 M41 起就有键、有 presenter 往返、
|
||||
//! 有 `ACCENT_PRESETS` 色板,却**没有任何 UI 写者**——造 tokens 的唯一入口恒走
|
||||
//! `DesignTokens::brand(board)`,用户即使改了配置文件,界面也不会变。接线之后这一段就是那条
|
||||
//! 链路的手动入口,也是**非法值唯一能被看见的地方**(错行紧贴输入框,不折叠、不写进日志了事)。
|
||||
//!
|
||||
//! ## 三件控件对应的键与消息
|
||||
//!
|
||||
//! | 控件 | 键 | 消息 |
|
||||
//! |---|---|---|
|
||||
//! | 强调色文本框(受控,输入即生效) | `appearance.accent_color` | [`Change::Accent`] |
|
||||
//! | 「出厂」+ 8 个预设色块 | 同上(写入色板里的 hex) | [`Change::Accent`] |
|
||||
//! | 非法值错行 | —(不进配置) | 无 |
|
||||
//!
|
||||
//! ## 色块为什么画成圆点而不是药丸
|
||||
//!
|
||||
//! 9 颗写着 `#0078D4` 的药丸会变成一排噪声,而且色相要靠读字获取——色板控件的可见内容**就是**
|
||||
//! 颜色本身。所以色块 = `ShapeScale::Full` 的实心圆 + 描边;选中态加粗描边(不靠色相区分:
|
||||
//! 色块底下垫的是它自己的颜色,色相对比不可靠)。
|
||||
|
||||
use iced::widget::{button, column, container, row, Space as Gap};
|
||||
use iced::{Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::prefs::{AccentError, ACCENT_PRESETS};
|
||||
|
||||
use crate::app::components::inputs::{text_field, Field};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::{to_iced, Ui, STROKE_PX};
|
||||
|
||||
use super::super::accent::Accent;
|
||||
use super::super::state::State;
|
||||
use super::super::Change;
|
||||
use super::{choice, label, note_line, t, tr};
|
||||
|
||||
/// 把 ⓪ 段推进 `rows`。
|
||||
pub(super) fn push<'a>(
|
||||
ui: &'a Ui,
|
||||
state: &'a State,
|
||||
lang: Lang,
|
||||
rows: &mut Vec<Element<'a, Message>>,
|
||||
) {
|
||||
let accent = &state.accent;
|
||||
|
||||
// 受控输入框 + 失败态:`Field::error()` 让框本身变成错误色,错行再补「错在哪」。
|
||||
let mut field = Field::new(accent.text())
|
||||
.label(t(Key::SettingsAppearanceAccent, lang))
|
||||
.placeholder("#RRGGBB");
|
||||
if accent.error().is_some() {
|
||||
field = field.error();
|
||||
}
|
||||
let input: Element<'a, Message> = text_field(ui, field, |value| {
|
||||
Message::Appearance(Change::Accent(value))
|
||||
});
|
||||
rows.push(
|
||||
column![label(ui, t(Key::SettingsAppearanceAccent, lang)), input]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
|
||||
rows.push(presets(ui, accent, lang));
|
||||
|
||||
// 非法值:**结构化的原因**,不是「格式不对」这种没法行动的一句话。
|
||||
if let Some(error) = accent.error() {
|
||||
let text = reason(lang, error);
|
||||
rows.push(note_line(ui, &text, ui.color(Role::Error)));
|
||||
}
|
||||
|
||||
// 说明:空 = 出厂板;自定义色只换三族(中性表面与 error 保持基线是 design 既定语义)。
|
||||
rows.push(note_line(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceAccentHint, lang),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
}
|
||||
|
||||
/// 「预设」一行:`出厂` + 8 颗色块。
|
||||
fn presets<'a>(ui: &'a Ui, accent: &'a Accent, lang: Lang) -> Element<'a, Message> {
|
||||
let current = accent.persist_value();
|
||||
let mut items: Vec<Element<'a, Message>> = vec![
|
||||
container(label(ui, t(Key::SettingsAppearanceAccentPresets, lang)))
|
||||
.align_y(iced::Alignment::Center)
|
||||
.into(),
|
||||
choice(
|
||||
ui,
|
||||
t(Key::SettingsAppearanceAccentFactory, lang),
|
||||
current.is_empty(),
|
||||
Change::Accent(String::new()),
|
||||
),
|
||||
];
|
||||
for hex in ACCENT_PRESETS {
|
||||
items.push(swatch(ui, hex, current == hex));
|
||||
}
|
||||
|
||||
row(items)
|
||||
.spacing(ui.space(Space::X2))
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.align_y(iced::Alignment::Center)
|
||||
.wrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一颗色块:实心圆 + 描边(选中态加粗)。点击写入该预设。
|
||||
fn swatch<'a>(ui: &'a Ui, hex: &'static str, selected: bool) -> Element<'a, Message> {
|
||||
let fill = unlucky_design::color::Color::from_hex(hex).map(to_iced);
|
||||
// 直径取控件高度的六成(40 → 24dp):与药丸按钮同一行时不抢节奏。
|
||||
let diameter = ui.metrics().control_height * 0.6;
|
||||
let ring = if selected {
|
||||
ui.color(Role::OnSurface)
|
||||
} else {
|
||||
ui.color(Role::OutlineVariant)
|
||||
};
|
||||
let width = if selected { STROKE_PX * 2.0 } else { STROKE_PX };
|
||||
let radius = ui.radius(ShapeScale::Full, diameter);
|
||||
|
||||
button(
|
||||
Gap::new()
|
||||
.width(Length::Fixed(diameter))
|
||||
.height(Length::Fixed(diameter)),
|
||||
)
|
||||
.padding(Padding::from(0.0))
|
||||
.style(move |_theme: &IcedTheme, status| button::Style {
|
||||
background: fill.map(Background::Color),
|
||||
text_color: ring,
|
||||
border: Border {
|
||||
color: match status {
|
||||
button::Status::Hovered | button::Status::Pressed => ui.color(Role::OnSurface),
|
||||
_ => ring,
|
||||
},
|
||||
width,
|
||||
radius,
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
})
|
||||
.on_press(Message::Appearance(Change::Accent(hex.to_owned())))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 结构化原因 → 一句话(参数由 Rust 侧格式化,词表只认 `{name}`)。
|
||||
fn reason(lang: Lang, error: AccentError) -> String {
|
||||
match error {
|
||||
AccentError::Short { digits } => tr(
|
||||
lang,
|
||||
Key::SettingsAppearanceAccentShort,
|
||||
&[("digits", &digits.to_string())],
|
||||
),
|
||||
AccentError::Long { digits } => tr(
|
||||
lang,
|
||||
Key::SettingsAppearanceAccentLong,
|
||||
&[("digits", &digits.to_string())],
|
||||
),
|
||||
AccentError::Digit { ch } => tr(
|
||||
lang,
|
||||
Key::SettingsAppearanceAccentDigit,
|
||||
&[("ch", &ch.to_string())],
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user