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
@@ -41,7 +41,7 @@
|
|||||||
//! |---|---|---|
|
//! |---|---|---|
|
||||||
//! | 对比度三档(default/medium/high) | `ContrastCurve` + `contrastLevel` | **未实现**:只做 `contrastLevel = 0`。在 0 档下官方那层「tone 差对 + 对比度修正」机制的输入全是常数,等价于本模块的静态 tone 表(已由对齐测试反证) |
|
//! | 对比度三档(default/medium/high) | `ContrastCurve` + `contrastLevel` | **未实现**:只做 `contrastLevel = 0`。在 0 档下官方那层「tone 差对 + 对比度修正」机制的输入全是常数,等价于本模块的静态 tone 表(已由对齐测试反证) |
|
||||||
//! | 其他配色变体 | `Vibrant` / `Expressive` / `Fidelity` / `Content` / `Monochrome` / `Neutral` / `Rainbow` / `FruitSalad` / `CMF` | **未实现**:只有 `TonalSpot`(M3 默认、也是 Material Theme Builder 默认) |
|
//! | 其他配色变体 | `Vibrant` / `Expressive` / `Fidelity` / `Content` / `Monochrome` / `Neutral` / `Rainbow` / `FruitSalad` / `CMF` | **未实现**:只有 `TonalSpot`(M3 默认、也是 Material Theme Builder 默认) |
|
||||||
//! | 壁纸多色取色 | `QuantizerCelebi` / `Score` / 多 source 色 | **未实现**:桌面启动器用不到(安卓端特性),也避免引入图像依赖 |
|
//! | 壁纸多色取色 | `QuantizerCelebi` / `Score` / 多 source 色 | **单种子部分已实现**:量化(5 bit 直方图 + CIELAB k-means)与 `Score` 挑主色在 [`crate::palette`](M87)。**多 source 色**(一张图出 4 个种子、每个 `*_container` 换色相)仍未实现——产品口径是「一套主题一个强调色」(D38) |
|
||||||
//! | 换观察条件 | `Hct.inViewingConditions` | **未实现**:只有 sRGB 屏的默认条件(D65 / 200 lux / 背景 `L* 50`) |
|
//! | 换观察条件 | `Hct.inViewingConditions` | **未实现**:只有 sRGB 屏的默认条件(D65 / 200 lux / 背景 `L* 50`) |
|
||||||
//!
|
//!
|
||||||
//! ## 「自定义强调色」与官方 `from_seed` 的关系
|
//! ## 「自定义强调色」与官方 `from_seed` 的关系
|
||||||
@@ -65,8 +65,9 @@
|
|||||||
//!
|
//!
|
||||||
//! **定位(D38)**:这套默认只是「**莫奈关闭时的出厂预选色卡**」(首次启动、用户还没选过色卡时
|
//! **定位(D38)**:这套默认只是「**莫奈关闭时的出厂预选色卡**」(首次启动、用户还没选过色卡时
|
||||||
//! 用的那一套),**不是唯一的配色来源**。用户的配色来源有三级优先:① 启动器背景图取色(Monet)
|
//! 用的那一套),**不是唯一的配色来源**。用户的配色来源有三级优先:① 启动器背景图取色(Monet)
|
||||||
//! → ② 桌面壁纸取色 → ③ 莫奈关闭(默认)时从内置多套色卡里选。取色与「内置色卡库」都不在本 crate,
|
//! → ② 桌面壁纸取色 → ③ 莫奈关闭(默认)时从内置多套色卡里选。取色(量化 + 挑主色)、内置色卡库
|
||||||
//! 也还没实现;本模块只负责「给定一个种子 + 中性彩度,派生出 49 个角色」。
|
//! 与三级回落都在 [`crate::palette`](M87 落地,D40 分层);**本模块只负责**「给定种子 + 中性彩度,
|
||||||
|
//! 派生出 49 个角色」,不碰像素、不碰路径、不碰回落决策。
|
||||||
//!
|
//!
|
||||||
//! > ⚠️ **有意偏离登记(两处,同一处偏离的两半)**:
|
//! > ⚠️ **有意偏离登记(两处,同一处偏离的两半)**:
|
||||||
//! >
|
//! >
|
||||||
|
|||||||
@@ -40,8 +40,10 @@ pub const BASELINE_SEED: Color = Color::rgb(0x67, 0x50, 0xA4);
|
|||||||
///
|
///
|
||||||
/// 用户定了三级优先的配色来源:① 启动器背景图取色(Monet)→ ② 桌面壁纸取色 → ③ 莫奈关闭
|
/// 用户定了三级优先的配色来源:① 启动器背景图取色(Monet)→ ② 桌面壁纸取色 → ③ 莫奈关闭
|
||||||
/// (默认)时用户从内置多套色卡里选。本常量 + [`DEFAULT_NEUTRAL_CHROMA`] 只定义**第三级里
|
/// (默认)时用户从内置多套色卡里选。本常量 + [`DEFAULT_NEUTRAL_CHROMA`] 只定义**第三级里
|
||||||
/// 出厂预选的那一套**(首次启动、用户没选过色卡时的那一套)。取色(Monet / 壁纸 /
|
/// 出厂预选的那一套**(首次启动、用户没选过色卡时的那一套;`palette::DEFAULT_CARD` 的种子即本值)。
|
||||||
/// `QuantizerCelebi`)与「内置多色卡库」都不在本 crate,也还没实现。
|
///
|
||||||
|
/// 取色(量化 + 挑主色)、内置色卡库与三级回落都在 [`crate::palette`](M87,分层见 D40);
|
||||||
|
/// 本模块只负责「给定种子 + 中性彩度,派生出 49 个角色」。
|
||||||
pub const DEFAULT_SEED: Color = Color::rgb(0x3C, 0x8D, 0xBC);
|
pub const DEFAULT_SEED: Color = Color::rgb(0x3C, 0x8D, 0xBC);
|
||||||
|
|
||||||
/// 产品出厂默认的中性调色板彩度(官方 Tonal Spot 是 6,这里 12 = **有意偏离**)。
|
/// 产品出厂默认的中性调色板彩度(官方 Tonal Spot 是 6,这里 12 = **有意偏离**)。
|
||||||
@@ -54,8 +56,9 @@ pub const DEFAULT_SEED: Color = Color::rgb(0x3C, 0x8D, 0xBC);
|
|||||||
/// 13 时 nv = 15 仍低于 secondary 的 16;**14 才会撞上**(nv = 16 = secondary),届时要一并改
|
/// 13 时 nv = 15 仍低于 secondary 的 16;**14 才会撞上**(nv = 16 = secondary),届时要一并改
|
||||||
/// `NEUTRAL_VARIANT_OFFSET`(私有常量)的规则。nv 的那一半偏离见 `color` 模块文档的偏离登记。
|
/// `NEUTRAL_VARIANT_OFFSET`(私有常量)的规则。nv 的那一半偏离见 `color` 模块文档的偏离登记。
|
||||||
///
|
///
|
||||||
/// 同样只服务于「出厂预选色卡」(D38 第三级),不约束将来的取色结果。将来若做「内置多套色卡」,
|
/// 同样只服务于「出厂预选色卡」(D38 第三级),不约束将来的取色结果。
|
||||||
/// 每套色卡各带自己的中性彩度更合适——本常量到时候会退化成「默认那套的取值」。
|
/// 内置多套色卡落地时每张卡各带自己的中性彩度(`palette::Card::neutral_chroma`),八张卡
|
||||||
|
/// 当前都取本值——换卡只换色相,不让窗口底色的冷度跟着跳。
|
||||||
pub const DEFAULT_NEUTRAL_CHROMA: f64 = 12.0;
|
pub const DEFAULT_NEUTRAL_CHROMA: f64 = 12.0;
|
||||||
|
|
||||||
/// 调色板槽位(M3 的六条 tonal palette)。
|
/// 调色板槽位(M3 的六条 tonal palette)。
|
||||||
|
|||||||
@@ -32,11 +32,21 @@
|
|||||||
//! 2. **不许静默变黑白**。彩度不足的图**不**会被「压成灰方案」,而是直接回落到用户选的那张色卡。
|
//! 2. **不许静默变黑白**。彩度不足的图**不**会被「压成灰方案」,而是直接回落到用户选的那张色卡。
|
||||||
//! 3. **不许静默换成默认卡**。回落的落点是「用户当前选中的卡」(参数 `preset`),
|
//! 3. **不许静默换成默认卡**。回落的落点是「用户当前选中的卡」(参数 `preset`),
|
||||||
//! 只有用户从没选过卡(`None`)才用 [`super::DEFAULT_CARD`]。
|
//! 只有用户从没选过卡(`None`)才用 [`super::DEFAULT_CARD`]。
|
||||||
|
//!
|
||||||
|
//! ## 文件划分
|
||||||
|
//!
|
||||||
|
//! [`provenance`] 放「结果解释」的数据类型(`Level` / `Outcome` / `Skip` / `Provenance`),
|
||||||
|
//! 本文件放编排函数与测试。拆开只是为了单文件 ≤400 行的纪律,对外路径仍是
|
||||||
|
//! `palette::resolve::*`。
|
||||||
|
|
||||||
use super::card::{Card, CardKey, DEFAULT_CARD};
|
use super::card::{Card, CardKey, DEFAULT_CARD};
|
||||||
use super::seed::{analyze_rgba, Failure, Seed};
|
use super::seed::{analyze_rgba, Seed};
|
||||||
use super::wcag::{check, converge, Adjustment};
|
use super::wcag::{check, converge, Adjustment};
|
||||||
use crate::color::{Color, Scheme, SchemeFlavor};
|
use crate::color::{Scheme, SchemeFlavor};
|
||||||
|
|
||||||
|
mod provenance;
|
||||||
|
|
||||||
|
pub use provenance::{Level, Outcome, Provenance, Skip};
|
||||||
|
|
||||||
/// 已解码的图片:借用像素,不拷贝(解码在 `crates/palette-extract`)。
|
/// 已解码的图片:借用像素,不拷贝(解码在 `crates/palette-extract`)。
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -70,227 +80,6 @@ impl<'a> ImageInput<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 方案的来源层级(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 = ①)。
|
|
||||||
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>,
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 三级来源编排(纯函数)。
|
/// 三级来源编排(纯函数)。
|
||||||
///
|
///
|
||||||
/// 参数:
|
/// 参数:
|
||||||
@@ -409,6 +198,7 @@ fn card_key(card: Card) -> CardKey {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::color::Color;
|
||||||
|
|
||||||
fn solid(color: Color, width: u32, height: u32) -> Vec<u8> {
|
fn solid(color: Color, width: u32, height: u32) -> Vec<u8> {
|
||||||
let mut out = Vec::with_capacity((width * height * 4) as usize);
|
let mut out = Vec::with_capacity((width * height * 4) as usize);
|
||||||
@@ -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