refactor(design): resolve 拆成目录模块(单文件 ≤400 行),同步文档口径
- palette/resolve/{mod.rs,provenance.rs}:编排函数与 Provenance 类型分开,路径不变
- color/mod.rs + palettes.rs:原来的「取色/色卡库不在本 crate、还没实现」改成指向 crate::palette
- 取色 gap 登记改为「单种子已实现,多 source 色仍未实现」
This commit is contained in:
1 parent
1aac62f6f0
commit
31ee69100a
4 files changed
+261
-230
No files matched your search
@@ -0,0 +1,342 @@
|
||||
//! 三级来源编排:**纯函数**,一次调用算完「用哪一级的颜色、为什么不是别的级」。
|
||||
//!
|
||||
//! D38 的优先序:① 启动器背景图 → ② 桌面壁纸 → ③ 内置色卡。
|
||||
//! 本模块不做任何 IO:像素是调用方解码好传进来的(D40 分层),壁纸路径探测是 M73 的事。
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use unlucky_design::palette::{resolve_source, CardKey, ImageInput, ImageSource, Level};
|
||||
//! use unlucky_design::color::SchemeFlavor;
|
||||
//!
|
||||
//! let pixels = [0x3C, 0x8D, 0xBC, 0xFF].repeat(16); // 4×4 天蓝图(示例)
|
||||
//! let bg = ImageInput::Pixels(ImageSource { pixels: &pixels, width: 4, height: 4 });
|
||||
//! let (scheme, provenance) = resolve_source(Some(bg), None, Some(CardKey::Forest), SchemeFlavor::Light);
|
||||
//! assert_eq!(provenance.level, Level::BackgroundImage);
|
||||
//! assert!(provenance.hint_key().is_none(), "一级成功时没有提示");
|
||||
//! ```
|
||||
//!
|
||||
//! ## 失败链:每一级都可能「有输入但不合格」
|
||||
//!
|
||||
//! | 情形 | 结果 | `Provenance` 里留下 |
|
||||
//! |---|---|---|
|
||||
//! | 没有背景图 | 跳到 ② | `Outcome::Absent` |
|
||||
//! | 背景图解码失败 | 跳到 ② | `Outcome::Failed(Skip::Undecodable)` |
|
||||
//! | 背景图太小 / 像素缓冲不对 / 全透明 | 跳到 ② | `… TooSmall / BadDimensions / NoOpaquePixels` |
|
||||
//! | 背景图彩度不足(D40 的 `< 8`) | 跳到 ② | `… LowChroma { chroma }` |
|
||||
//! | 背景图取色成功但对比度收敛失败 | 跳到 ② | `… Contrast { failures }`(理论上不会发生,仍要留) |
|
||||
//! | 三级都不成立 | 用色卡,**一定**有结果 | `Level::Preset` |
|
||||
//!
|
||||
//! 三条纪律:
|
||||
//!
|
||||
//! 1. **不许静默**。任何一次回落都在 `Provenance` 里留下原因码,[`Provenance::hint_key`]
|
||||
//! 给 UI 一个稳定的提示键(文案映射见 `DevDocs/SubAgentsTalk/palette-source-contract.md`)。
|
||||
//! 2. **不许静默变黑白**。彩度不足的图**不**会被「压成灰方案」,而是直接回落到用户选的那张色卡。
|
||||
//! 3. **不许静默换成默认卡**。回落的落点是「用户当前选中的卡」(参数 `preset`),
|
||||
//! 只有用户从没选过卡(`None`)才用 [`super::DEFAULT_CARD`]。
|
||||
//!
|
||||
//! ## 文件划分
|
||||
//!
|
||||
//! [`provenance`] 放「结果解释」的数据类型(`Level` / `Outcome` / `Skip` / `Provenance`),
|
||||
//! 本文件放编排函数与测试。拆开只是为了单文件 ≤400 行的纪律,对外路径仍是
|
||||
//! `palette::resolve::*`。
|
||||
|
||||
use super::card::{Card, CardKey, DEFAULT_CARD};
|
||||
use super::seed::{analyze_rgba, Seed};
|
||||
use super::wcag::{check, converge, Adjustment};
|
||||
use crate::color::{Scheme, SchemeFlavor};
|
||||
|
||||
mod provenance;
|
||||
|
||||
pub use provenance::{Level, Outcome, Provenance, Skip};
|
||||
|
||||
/// 已解码的图片:借用像素,不拷贝(解码在 `crates/palette-extract`)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ImageSource<'a> {
|
||||
/// RGBA8 像素,`width × height × 4` 字节。
|
||||
pub pixels: &'a [u8],
|
||||
/// 宽(像素)。
|
||||
pub width: u32,
|
||||
/// 高(像素)。
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
/// 某一级来源的输入。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImageInput<'a> {
|
||||
/// 解码好的像素。
|
||||
Pixels(ImageSource<'a>),
|
||||
/// 有这张图,但解码失败(文件损坏 / 格式不支持 / 读不到)。
|
||||
///
|
||||
/// 这个状态由适配层传进来——不传就等于「没有这张图」,UI 会说错话。
|
||||
Undecodable,
|
||||
}
|
||||
|
||||
impl<'a> ImageInput<'a> {
|
||||
/// 便捷构造:从像素缓冲包一个输入。
|
||||
pub fn pixels(pixels: &'a [u8], width: u32, height: u32) -> Self {
|
||||
ImageInput::Pixels(ImageSource {
|
||||
pixels,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
}
|
||||
/// 三级来源编排(纯函数)。
|
||||
///
|
||||
/// 参数:
|
||||
/// - `background`:用户设的启动器背景图(`None` = 没设)。解码失败传
|
||||
/// [`ImageInput::Undecodable`],**不要**传 `None`(那样 UI 分不清「没设」和「坏了」)。
|
||||
/// - `wallpaper`:桌面壁纸(`None` = 没开莫奈 / 探测不到壁纸文件)。
|
||||
/// - `preset`:用户当前选中的色卡(`None` = 从没选过 → 用 [`super::DEFAULT_CARD`])。
|
||||
/// - `flavor`:深浅模式。
|
||||
///
|
||||
/// 返回值永远有一套可用方案:三级全失败时落到色卡,并带上原因。
|
||||
pub fn resolve_source(
|
||||
background: Option<ImageInput<'_>>,
|
||||
wallpaper: Option<ImageInput<'_>>,
|
||||
preset: Option<CardKey>,
|
||||
flavor: SchemeFlavor,
|
||||
) -> (Scheme, Provenance) {
|
||||
let card = preset.map(Card::from).unwrap_or(DEFAULT_CARD);
|
||||
let mut outcomes = [Outcome::Absent; 3];
|
||||
|
||||
for (level, input) in [
|
||||
(Level::BackgroundImage, background),
|
||||
(Level::Wallpaper, wallpaper),
|
||||
] {
|
||||
match try_level(input, card, flavor) {
|
||||
Attempt::Absent => outcomes[level.index()] = Outcome::Absent,
|
||||
Attempt::Failed(skip) => outcomes[level.index()] = Outcome::Failed(skip),
|
||||
Attempt::Used {
|
||||
scheme,
|
||||
seed,
|
||||
adjustments,
|
||||
} => {
|
||||
outcomes[level.index()] = Outcome::Used;
|
||||
return (
|
||||
*scheme,
|
||||
Provenance {
|
||||
level,
|
||||
card: card_key(card),
|
||||
seed: Some(seed),
|
||||
flavor,
|
||||
adjustments,
|
||||
outcomes,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第三级:色卡。收敛仍然要跑(色卡也可能因为将来改常数而越线),但落点是它自己。
|
||||
let (scheme, adjustments) = converge(card.scheme(flavor));
|
||||
let report = check(&scheme);
|
||||
outcomes[Level::Preset.index()] = if report.is_clean() {
|
||||
Outcome::Used
|
||||
} else {
|
||||
Outcome::Failed(Skip::Contrast {
|
||||
failures: report.failures.len(),
|
||||
})
|
||||
};
|
||||
(
|
||||
scheme,
|
||||
Provenance {
|
||||
level: Level::Preset,
|
||||
card: card_key(card),
|
||||
seed: None,
|
||||
flavor,
|
||||
adjustments,
|
||||
outcomes,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// 单级的处理结果。
|
||||
///
|
||||
/// `scheme` 装箱只是为了枚举大小(`Scheme` 是 49 个色值 ≈ 200 字节,逐级在栈上传会白白拷两次);
|
||||
/// 这条路径每次配色只走一次,堆分配的开销可忽略。
|
||||
enum Attempt {
|
||||
Absent,
|
||||
Failed(Skip),
|
||||
Used {
|
||||
scheme: Box<Scheme>,
|
||||
seed: Seed,
|
||||
adjustments: Vec<Adjustment>,
|
||||
},
|
||||
}
|
||||
|
||||
fn try_level(input: Option<ImageInput<'_>>, card: Card, flavor: SchemeFlavor) -> Attempt {
|
||||
let source = match input {
|
||||
None => return Attempt::Absent,
|
||||
Some(ImageInput::Undecodable) => return Attempt::Failed(Skip::Undecodable),
|
||||
Some(ImageInput::Pixels(source)) => source,
|
||||
};
|
||||
|
||||
let seed = match analyze_rgba(source.pixels, source.width, source.height) {
|
||||
Ok(seed) => seed,
|
||||
Err(failure) => return Attempt::Failed(Skip::from(failure)),
|
||||
};
|
||||
|
||||
// 取色方案的表面色温沿用当前色卡的中性彩度:换壁纸不该让窗口底色「冷热乱跳」。
|
||||
let (scheme, adjustments) = converge(seed.scheme(flavor, card.neutral_chroma));
|
||||
let report = check(&scheme);
|
||||
if !report.is_clean() {
|
||||
return Attempt::Failed(Skip::Contrast {
|
||||
failures: report.failures.len(),
|
||||
});
|
||||
}
|
||||
Attempt::Used {
|
||||
scheme: Box::new(scheme),
|
||||
seed,
|
||||
adjustments,
|
||||
}
|
||||
}
|
||||
|
||||
fn card_key(card: Card) -> CardKey {
|
||||
CardKey::from_key(card.key).unwrap_or(CardKey::Sky)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::color::Color;
|
||||
|
||||
fn solid(color: Color, width: u32, height: u32) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity((width * height * 4) as usize);
|
||||
for _ in 0..(width * height) {
|
||||
out.extend_from_slice(&[color.r, color.g, color.b, color.a]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_one_wins_when_background_is_good() {
|
||||
let pixels = solid(Color::rgb(0xC0, 0x39, 0x2B), 4, 4);
|
||||
let (scheme, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&pixels, 4, 4)),
|
||||
None,
|
||||
Some(CardKey::Forest),
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(provenance.level, Level::BackgroundImage);
|
||||
assert_eq!(provenance.background(), Outcome::Used);
|
||||
assert_eq!(provenance.preset(), Outcome::Absent);
|
||||
assert!(provenance.hint_key().is_none());
|
||||
assert!(check(&scheme).is_clean());
|
||||
assert!(provenance.seed.is_some());
|
||||
// 取色方案的表面色温来自当前色卡(森林绿),不是默认卡。
|
||||
assert_eq!(provenance.card, CardKey::Forest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gray_background_falls_back_to_wallpaper_with_reason() {
|
||||
// 反证 1:灰白背景图 → 不许凭空上色,必须带着 low-chroma 原因回落。
|
||||
let gray = solid(Color::rgb(0xF2, 0xF2, 0xF0), 8, 8);
|
||||
let teal = solid(Color::rgb(0x00, 0x79, 0x6B), 8, 8);
|
||||
let (_, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&gray, 8, 8)),
|
||||
Some(ImageInput::pixels(&teal, 8, 8)),
|
||||
Some(CardKey::Sky),
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(provenance.level, Level::Wallpaper);
|
||||
match provenance.background() {
|
||||
Outcome::Failed(Skip::LowChroma { chroma, .. }) => assert!(chroma < 8.0),
|
||||
other => panic!("背景图应当是低彩度失败,实际 {other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
provenance.hint_key(),
|
||||
Some("palette.hint.background.low-chroma")
|
||||
);
|
||||
assert!(provenance.did_fall_back());
|
||||
assert_eq!(
|
||||
provenance.reason_codes(),
|
||||
vec![("background-image", "low-chroma")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undecodable_and_tiny_inputs_are_distinguished_from_absent() {
|
||||
let tiny = solid(Color::rgb(0xFF, 0x00, 0x00), 1, 2);
|
||||
let (_, provenance) = resolve_source(
|
||||
Some(ImageInput::Undecodable),
|
||||
Some(ImageInput::pixels(&tiny, 1, 2)),
|
||||
None,
|
||||
SchemeFlavor::Dark,
|
||||
);
|
||||
assert_eq!(provenance.level, Level::Preset);
|
||||
assert_eq!(provenance.background(), Outcome::Failed(Skip::Undecodable));
|
||||
assert_eq!(
|
||||
provenance.wallpaper(),
|
||||
Outcome::Failed(Skip::TooSmall {
|
||||
width: 1,
|
||||
height: 2
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
provenance.hint_key(),
|
||||
Some("palette.hint.background.undecodable")
|
||||
);
|
||||
// 从没选过卡 → 出厂默认。
|
||||
assert_eq!(provenance.card, CardKey::Sky);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_provided_uses_default_card_without_hint() {
|
||||
let (scheme, provenance) = resolve_source(None, None, None, SchemeFlavor::Light);
|
||||
assert_eq!(provenance.level, Level::Preset);
|
||||
assert_eq!(provenance.preset(), Outcome::Used);
|
||||
assert!(!provenance.did_fall_back());
|
||||
assert!(provenance.adjustments.is_empty());
|
||||
assert!(provenance.hint_key().is_none());
|
||||
assert_eq!(scheme, DEFAULT_CARD.scheme(SchemeFlavor::Light));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_selection_decides_the_fallback_colour() {
|
||||
// 反证 2:回落落点是「用户选的那张卡」,不是默认卡。
|
||||
let gray = solid(Color::rgb(0x80, 0x80, 0x80), 4, 4);
|
||||
let (scheme, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&gray, 4, 4)),
|
||||
None,
|
||||
Some(CardKey::Rose),
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(provenance.level, Level::Preset);
|
||||
assert_eq!(provenance.card, CardKey::Rose);
|
||||
assert_eq!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_beats_wallpaper_when_both_work() {
|
||||
let bg = solid(Color::rgb(0xB2, 0x6A, 0x00), 8, 8);
|
||||
let wp = solid(Color::rgb(0x00, 0x79, 0x6B), 8, 8);
|
||||
let (_, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&bg, 8, 8)),
|
||||
Some(ImageInput::pixels(&wp, 8, 8)),
|
||||
Some(CardKey::Teal),
|
||||
SchemeFlavor::Dark,
|
||||
);
|
||||
assert_eq!(provenance.level, Level::BackgroundImage);
|
||||
assert_eq!(provenance.wallpaper(), Outcome::Absent, "② 根本没轮到");
|
||||
assert!(!provenance.did_fall_back());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent_background_reports_its_own_reason() {
|
||||
let transparent = solid(Color::rgba(0x3C, 0x8D, 0xBC, 0), 4, 4);
|
||||
let (_, provenance) = resolve_source(
|
||||
Some(ImageInput::pixels(&transparent, 4, 4)),
|
||||
None,
|
||||
None,
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(
|
||||
provenance.background(),
|
||||
Outcome::Failed(Skip::NoOpaquePixels)
|
||||
);
|
||||
assert_eq!(
|
||||
provenance.hint_key(),
|
||||
Some("palette.hint.background.no-opaque-pixels")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! 一次配色的「结果解释」:最终用了哪一级、前几级为什么没用、对比度被改了什么。
|
||||
//!
|
||||
//! 这些类型是**给 UI 看的契约**([`Provenance::hint_key`] 的键 → 文案的映射见
|
||||
//! `DevDocs/SubAgentsTalk/palette-source-contract.md`),所以刻意做得比内部需要更细:
|
||||
//! 每一种失败都带足以写出一句具体提示的数据(彩度是多少、图有多小、哪一级坏了)。
|
||||
//!
|
||||
//! 与 [`super`] 拆开只为单文件 ≤400 行;对外路径仍是 `palette::resolve::*`。
|
||||
|
||||
use super::super::card::CardKey;
|
||||
use super::super::seed::{Failure, Seed};
|
||||
use super::super::wcag::Adjustment;
|
||||
use crate::color::{Color, SchemeFlavor};
|
||||
|
||||
/// 方案的来源层级(D38 的三级)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Level {
|
||||
/// ① 启动器背景图取色。
|
||||
BackgroundImage,
|
||||
/// ② 桌面壁纸取色。
|
||||
Wallpaper,
|
||||
/// ③ 内置色卡。
|
||||
Preset,
|
||||
}
|
||||
|
||||
impl Level {
|
||||
/// 稳定码(日志/设置里用,别用 `Debug` 输出)。
|
||||
pub const fn code(self) -> &'static str {
|
||||
match self {
|
||||
Level::BackgroundImage => "background-image",
|
||||
Level::Wallpaper => "wallpaper",
|
||||
Level::Preset => "preset",
|
||||
}
|
||||
}
|
||||
|
||||
/// 在本模块内部当索引用(0 = ①)。
|
||||
///
|
||||
/// `pub(super)`:`resolve` 的编排函数要按下标填 `Provenance` 的三个格子,
|
||||
/// 但它不该出现在对外 API 里(外部只该问「哪一级」,不该拿索引当协议)。
|
||||
pub(super) const fn index(self) -> usize {
|
||||
match self {
|
||||
Level::BackgroundImage => 0,
|
||||
Level::Wallpaper => 1,
|
||||
Level::Preset => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 某一级的处置结果。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Outcome {
|
||||
/// 用了这一级。
|
||||
Used,
|
||||
/// 用户没提供这一级(没设背景图 / 没开莫奈取色)。
|
||||
Absent,
|
||||
/// 有输入但不合格,已跳过。
|
||||
Failed(Skip),
|
||||
}
|
||||
|
||||
/// 「有输入但不合格」的具体原因——每一条都要能变成一句人话。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Skip {
|
||||
/// 解码失败(原因在适配层,这里只知道失败)。
|
||||
Undecodable,
|
||||
/// 图太小(< 4 像素)。
|
||||
TooSmall {
|
||||
/// 宽。
|
||||
width: u32,
|
||||
/// 高。
|
||||
height: u32,
|
||||
},
|
||||
/// 像素缓冲长度与宽高不符(调用方给错了数据)。
|
||||
BadDimensions {
|
||||
/// 期望字节数。
|
||||
expected: usize,
|
||||
/// 实际字节数。
|
||||
actual: usize,
|
||||
},
|
||||
/// 全是透明像素。
|
||||
NoOpaquePixels,
|
||||
/// 彩度不足(D40:`< MIN_SEED_CHROMA`),硬取色会凭空上色。
|
||||
LowChroma {
|
||||
/// 实测彩度。
|
||||
chroma: f64,
|
||||
/// 最接近的候选色(提示文案里显示成色块)。
|
||||
color: Color,
|
||||
},
|
||||
/// 取色成功但对比度收敛后仍不达标。
|
||||
Contrast {
|
||||
/// 仍不达标的对数。
|
||||
failures: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Skip {
|
||||
/// 稳定原因码(`Provenance::hint_key` 的后半段)。
|
||||
pub const fn code(self) -> &'static str {
|
||||
match self {
|
||||
Skip::Undecodable => "undecodable",
|
||||
Skip::TooSmall { .. } => "too-small",
|
||||
Skip::BadDimensions { .. } => "bad-dimensions",
|
||||
Skip::NoOpaquePixels => "no-opaque-pixels",
|
||||
Skip::LowChroma { .. } => "low-chroma",
|
||||
Skip::Contrast { .. } => "contrast",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Failure> for Skip {
|
||||
fn from(failure: Failure) -> Self {
|
||||
match failure {
|
||||
Failure::BadDimensions {
|
||||
expected, actual, ..
|
||||
} => Skip::BadDimensions { expected, actual },
|
||||
Failure::TooSmall { width, height } => Skip::TooSmall { width, height },
|
||||
Failure::NoOpaquePixels => Skip::NoOpaquePixels,
|
||||
Failure::LowChroma { color, chroma } => Skip::LowChroma { color, chroma },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 这次配色「最后用了什么、前几级为什么没用」。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Provenance {
|
||||
/// 最终生效的层级。
|
||||
pub level: Level,
|
||||
/// 当前生效的色卡(取色成功时它仍然参与:中性彩度来自它)。
|
||||
pub card: CardKey,
|
||||
/// 取色成功时的种子(色卡档为 `None`)。
|
||||
pub seed: Option<Seed>,
|
||||
/// 深浅模式。
|
||||
pub flavor: SchemeFlavor,
|
||||
/// 对比度收敛改了哪些角色(空 = 没动)。
|
||||
pub adjustments: Vec<Adjustment>,
|
||||
/// 三级各自的处置结果(`pub(super)`:只能由 `resolve::resolve_source` 填,
|
||||
/// 外部一律通过 [`Provenance::outcome`] / [`Provenance::background`] 等访问器读)。
|
||||
pub(super) outcomes: [Outcome; 3],
|
||||
}
|
||||
|
||||
impl Provenance {
|
||||
/// 某一级发生了什么。
|
||||
pub fn outcome(&self, level: Level) -> Outcome {
|
||||
self.outcomes[level.index()]
|
||||
}
|
||||
|
||||
/// 背景图那一级。
|
||||
pub fn background(&self) -> Outcome {
|
||||
self.outcome(Level::BackgroundImage)
|
||||
}
|
||||
|
||||
/// 壁纸那一级。
|
||||
pub fn wallpaper(&self) -> Outcome {
|
||||
self.outcome(Level::Wallpaper)
|
||||
}
|
||||
|
||||
/// 色卡那一级。
|
||||
pub fn preset(&self) -> Outcome {
|
||||
self.outcome(Level::Preset)
|
||||
}
|
||||
|
||||
/// 这次配色是否发生过回落(用了 `level` 以下的层级)。
|
||||
pub fn did_fall_back(&self) -> bool {
|
||||
(0..self.level.index()).any(|i| !matches!(self.outcomes[i], Outcome::Absent))
|
||||
}
|
||||
|
||||
/// 第一个「有输入但不合格」的层级(提示文案优先说它)。
|
||||
pub fn first_failure(&self) -> Option<(Level, Skip)> {
|
||||
const LEVELS: [Level; 3] = [Level::BackgroundImage, Level::Wallpaper, Level::Preset];
|
||||
LEVELS
|
||||
.into_iter()
|
||||
.find_map(|level| match self.outcome(level) {
|
||||
Outcome::Failed(skip) => Some((level, skip)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// UI 提示键(`None` = 无需提示)。
|
||||
///
|
||||
/// 形如 `palette.hint.background.low-chroma`,文案映射表见
|
||||
/// `DevDocs/SubAgentsTalk/palette-source-contract.md`。`key` 稳定、可持久化进日志;
|
||||
/// 本 crate 不产出人话文案(文案属于 UI 层,D40 的分层纪律)。
|
||||
///
|
||||
/// 优先级:① 先解释「为什么回落」(第一个失败的层级)→ ② 再说「微调过对比度」。
|
||||
/// 前者对用户更重要:用户看到的是「我的壁纸怎么没生效」,而不是「颜色被调了 2%」。
|
||||
pub fn hint_key(&self) -> Option<&'static str> {
|
||||
if let Some((level, skip)) = self.first_failure() {
|
||||
return Some(match (level, skip) {
|
||||
(Level::BackgroundImage, Skip::Undecodable) => {
|
||||
"palette.hint.background.undecodable"
|
||||
}
|
||||
(Level::BackgroundImage, Skip::TooSmall { .. }) => {
|
||||
"palette.hint.background.too-small"
|
||||
}
|
||||
(Level::BackgroundImage, Skip::BadDimensions { .. }) => {
|
||||
"palette.hint.background.bad-dimensions"
|
||||
}
|
||||
(Level::BackgroundImage, Skip::NoOpaquePixels) => {
|
||||
"palette.hint.background.no-opaque-pixels"
|
||||
}
|
||||
(Level::BackgroundImage, Skip::LowChroma { .. }) => {
|
||||
"palette.hint.background.low-chroma"
|
||||
}
|
||||
(Level::BackgroundImage, Skip::Contrast { .. }) => {
|
||||
"palette.hint.background.contrast"
|
||||
}
|
||||
(Level::Wallpaper, Skip::Undecodable) => "palette.hint.wallpaper.undecodable",
|
||||
(Level::Wallpaper, Skip::TooSmall { .. }) => "palette.hint.wallpaper.too-small",
|
||||
(Level::Wallpaper, Skip::BadDimensions { .. }) => {
|
||||
"palette.hint.wallpaper.bad-dimensions"
|
||||
}
|
||||
(Level::Wallpaper, Skip::NoOpaquePixels) => {
|
||||
"palette.hint.wallpaper.no-opaque-pixels"
|
||||
}
|
||||
(Level::Wallpaper, Skip::LowChroma { .. }) => "palette.hint.wallpaper.low-chroma",
|
||||
(Level::Wallpaper, Skip::Contrast { .. }) => "palette.hint.wallpaper.contrast",
|
||||
(Level::Preset, Skip::Contrast { .. }) => "palette.hint.preset.contrast",
|
||||
// 色卡档不存在「解码/彩度」问题;落到这里说明是将来新增的原因,别乱编键。
|
||||
(Level::Preset, _) => "palette.hint.preset.unavailable",
|
||||
});
|
||||
}
|
||||
if !self.adjustments.is_empty() {
|
||||
return Some("palette.hint.adjusted");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 原因码列表(`[级别码, 原因码]`),写日志/上报用;给不了键的细节都在这里。
|
||||
pub fn reason_codes(&self) -> Vec<(&'static str, &'static str)> {
|
||||
const LEVELS: [Level; 3] = [Level::BackgroundImage, Level::Wallpaper, Level::Preset];
|
||||
LEVELS
|
||||
.into_iter()
|
||||
.filter_map(|level| match self.outcome(level) {
|
||||
Outcome::Failed(skip) => Some((level.code(), skip.code())),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user