atmos/os_lib/web_engine/repaint_scope.rs
1//! アニメーション/トランジションで変化したプロパティが、
2//! **再レイアウトを要するか / 再ペイントだけで足りるか**の判定。
3//!
4//! ## 背景
5//! CSS アニメーションは 1 フレームごとに `parse_and_layout`(HTML 再パース +
6//! 2403 ルール再カスケード + レイアウト)を丸ごと呼んでいた。実サイトでは
7//! これが 10 秒以上かかるため、描いたフレームが画面に出る前に次のレイアウトが
8//! 始まり、ページが永久に収束しなかった。
9//!
10//! しかし実際にアニメーションされるプロパティの大半(`opacity` / `color` /
11//! `background-color` / `transform` 等)は**ボックスの大きさも位置も変えない**。
12//! これらだけが変化したフレームは、算出値を差し替えて再ペイントするだけでよい。
13//!
14//! ## 安全側の既定
15//! 分類が不明なプロパティは**再レイアウト側**に倒す。再ペイントで済ませてよいものを
16//! 再レイアウトしても「遅いだけで絵は正しい」が、逆に再レイアウトが必要なものを
17//! 再ペイントで済ませると**レイアウトが崩れたまま画面に残る**ため。
18//!
19//! グローバル状態にもハードウェアにも依存しない純粋ロジックのみで構成する。
20
21extern crate alloc;
22
23use alloc::string::String;
24
25/// このフレームの変更を反映するのに必要な処理の範囲。
26#[derive(Debug, PartialEq, Eq, Clone, Copy)]
27pub enum RepaintScope {
28 /// 変化なし。何もしなくてよい。
29 Nothing,
30 /// 再ペイントのみ。カスケードもレイアウトも不要。
31 PaintOnly,
32 /// 再レイアウトが必要(ボックスの寸法・位置・フロー構造が変わり得る)。
33 Relayout,
34}
35
36/// 判定の引数が不正だった場合のエラー。
37#[derive(Debug, PartialEq, Eq, Clone)]
38pub enum ScopeError {
39 /// プロパティ名が空(または空白のみ)。
40 EmptyPropertyName(String),
41}
42
43/// ボックスの寸法・位置・フロー構造に影響せず、見た目だけを変えるプロパティ。
44/// ここに載っているものだけが再ペイント扱いになる。
45const PAINT_ONLY: &[&str] = &[
46 "color",
47 "background",
48 "background-color",
49 "background-image",
50 "background-position",
51 "background-repeat",
52 "background-size",
53 "opacity",
54 "visibility",
55 "border-color",
56 "border-top-color",
57 "border-right-color",
58 "border-bottom-color",
59 "border-left-color",
60 "outline-color",
61 "box-shadow",
62 "text-shadow",
63 "text-decoration-color",
64 "filter",
65 "backdrop-filter",
66 "transform",
67 "transform-origin",
68 "fill",
69 "stroke",
70 "caret-color",
71 "accent-color",
72];
73
74/// プロパティ 1 つが再ペイントだけで反映できるかを返す。
75///
76/// プロパティ名が空のときは黙って戻らず、エラーログを出して `Err` を返す。
77/// 呼び出し側は安全側(再レイアウト)へフォールバックすること。
78pub fn is_paint_only(prop: &str) -> Result<bool, ScopeError> {
79 let name = prop.trim();
80 if name.is_empty() {
81 crate::error!("[ANIM][SCOPE] プロパティ名が空です。再レイアウトへフォールバックします");
82 return Err(ScopeError::EmptyPropertyName(String::from(prop)));
83 }
84 // CSS プロパティ名は大文字小文字を区別しない。
85 // `no_std` でも動くよう、確保を避けて 1 文字ずつ比較する。
86 Ok(PAINT_ONLY.iter().any(|known| {
87 known.len() == name.len()
88 && known
89 .bytes()
90 .zip(name.bytes())
91 .all(|(a, b)| a == b.to_ascii_lowercase())
92 }))
93}
94
95/// このフレームで変化したプロパティ名の集合から、必要な処理範囲を決める。
96///
97/// - 空なら `Nothing`
98/// - 全て再ペイントで足りるなら `PaintOnly`
99/// - 1 つでもレイアウトに影響する/判定不能なものがあれば `Relayout`
100///
101/// 判定不能なプロパティ名(空文字)があった場合はエラーログを出したうえで
102/// `Relayout` を返す(安全側)。エラーを握りつぶさず、かつ描画は壊さない。
103pub fn scope_for<'a, I>(props: I) -> RepaintScope
104where
105 I: IntoIterator<Item = &'a str>,
106{
107 let mut seen = false;
108 for p in props {
109 seen = true;
110 match is_paint_only(p) {
111 Ok(true) => {}
112 // レイアウトに影響する、または判定不能 → 安全側へ倒す。
113 Ok(false) | Err(_) => return RepaintScope::Relayout,
114 }
115 }
116 if seen {
117 RepaintScope::PaintOnly
118 } else {
119 RepaintScope::Nothing
120 }
121}