Skip to main content

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}