Skip to main content

atmos/os_lib/css/
pseudo_state.rs

1//! セレクタが動的疑似クラス(`:hover` / `:focus` / `:active`)を参照しているかの判定。
2//!
3//! 仕様は `spec/dynamic_pseudo_states.md`。
4//!
5//! ## なぜ必要か
6//! `cascade.rs` はノード 1 個につきカスケードを **4 回**走らせている
7//! (通常 / `:hover` / `:focus` / `:active` の事前計算)。
8//! 実測で `style_tree` のノード走査は 358 ノードで約 600 tick かかっており、
9//! セレクタ照合は索引で既に 2.0% まで落ちている(照合はボトルネックではない)。
10//! 残っているのはこの 4 倍の固定コスト。
11//!
12//! 候補ルールが該当の状態を一切参照していなければ、そのパスは走らせなくてよい。
13//!
14//! ## 方針(不変条件 D-1〜D-4)
15//! - **D-1**: 結果は従来と完全一致。差分が原理的に生じ得ない場合のみスキップ
16//! - **D-2**: 判定対象は候補ルール集合(全ルールへのフォールバック時も同じ)
17//! - **D-3**: セレクタの構文だけを見る。DOM 状態には依存しない
18//! - **D-4**: 迷ったら「参照している」とみなす(false negative を出さない)
19
20use super::types::{PseudoClass, Rule, Selector, SimpleSelector};
21
22/// `:focus` の判定に含めるべき別名。
23///
24/// `selector.rs` は `:focus-within` の判定に
25/// `active_pseudo_states.contains(&"focus")` を使っている。
26/// そのため `focus-within` / `focus-visible` を見つけたら
27/// `focus` を参照しているとみなす(D-4 に従い安全側へ倒す)。
28fn matches_state(name: &str, state: &str) -> bool {
29    if name == state {
30        return true;
31    }
32    state == "focus" && (name == "focus-within" || name == "focus-visible")
33}
34
35/// 単純セレクタ(コンパウンド 1 個)が `state` を参照しているか。
36///
37/// `:not()` / `:is()` / `:where()` / `:nth-child(... of S)` / `:has()` の
38/// 内側も再帰的に見る。
39/// 計算量: **O(P + 入れ子の総数)** — P は疑似クラス数。
40/// `:not()` / `:is()` / `:where()` / `:has()` の内側を再帰的に辿るため、
41/// 最悪はセレクタ内の単純セレクタ総数に比例する。
42pub fn simple_uses_state(s: &SimpleSelector, state: &str) -> bool {
43    for pc in &s.pseudo_classes {
44        let hit = match pc {
45            PseudoClass::Other(name) => matches_state(name.as_str(), state),
46            PseudoClass::Not(inner) => simple_uses_state(inner, state),
47            PseudoClass::Is(list) | PseudoClass::Where(list) => {
48                list.iter().any(|x| simple_uses_state(x, state))
49            }
50            PseudoClass::NthChild(_, _, Some(of)) | PseudoClass::NthLastChild(_, _, Some(of)) => {
51                simple_uses_state(of, state)
52            }
53            PseudoClass::Has(branches) => {
54                branches.iter().any(|(_, x)| simple_uses_state(x, state))
55            }
56            _ => false,
57        };
58        if hit {
59            return true;
60        }
61    }
62    false
63}
64
65/// セレクタ全体(チェーンなら**全ステップ**)が `state` を参照しているか。
66///
67/// `a:hover span` のように祖先側に付く場合があるため、末尾だけを見てはいけない。
68/// 計算量: **O(Steps × P)** — Steps はチェーンの段数。
69/// 末尾だけでなく**全段**を見る必要がある(`a:hover span`)。
70pub fn selector_uses_state(sel: &Selector, state: &str) -> bool {
71    match sel {
72        Selector::Simple(s) => simple_uses_state(s, state),
73        Selector::Chain(steps) => steps.iter().any(|st| simple_uses_state(&st.simple, state)),
74    }
75}
76
77/// ルールのいずれかのセレクタが `state` を参照しているか。
78/// 計算量: **O(Sel × Steps × P)** — Sel はルール内のセレクタ数。
79///
80/// 呼び出し側は候補ルール C 件に対してこれを回すので **O(C × …)**。
81/// それでもカスケード 1 回(`compute_styles_with_vars`)より桁違いに軽いため、
82/// 3 状態ぶんのカスケードを省ける効果が上回る。
83/// 実測でノード走査が 605 → 291 tick(`spec/dynamic_pseudo_states.md`)。
84pub fn rule_uses_state(rule: &Rule, state: &str) -> bool {
85    rule.selectors
86        .iter()
87        .any(|sel| selector_uses_state(sel, state))
88}