atmos/os_lib/web_engine/paint_order.rs
1//! 描画順(スタッキング順)の算出。
2//!
3//! ## 背景 — 実際に踏んだバグ
4//! 描画側は全要素を `z_index` でフラットにソートしていた。ところが実サイトの CSS には
5//! ```css
6//! section { position: relative; z-index: 5 }
7//! ```
8//! があり、`<section class="hero" id="home">` は z=5、その子孫
9//! (`.hero-overlay` / `.hero-content` / `.hero-title`)は `position: static` なので z=0 だった。
10//! フラットソートでは **セクションが自分の子孫より後に描かれ、自分の背景で
11//! 自分の中身を丸ごと消してしまう**。実測でも、ヒーローテキストは描画直後に
12//! 1655px 残っているのに `draw()` 終了時点で 0 になっていた。
13//!
14//! ## 仕様(CSS 2.1 Appendix E)
15//! スタッキングコンテキストを作る要素は、**自分の背景を先に**描き、子孫はその
16//! コンテキストの中で描かれる。**子孫が祖先の背景より前に描かれることは無い**。
17//!
18//! ## 実装方針
19//! 各要素の「実効 z」を `max(自分の z, 祖先の実効 z)` とする。
20//! 文書順では祖先が必ず子孫より先に現れるため、実効 z が同値なら安定ソートが
21//! 祖先→子孫の順を保つ。これで祖先の背景が子孫を塗り潰す事故は起きない。
22//! 子孫が自前でより大きい `z-index` を持つ場合はその値が勝ち、祖先より後=
23//! 手前に描かれる(これも仕様どおり)。
24//!
25//! グローバル状態にもハードウェアにも依存しない純粋ロジックのみで構成する。
26
27extern crate alloc;
28
29/// 実効 z の算出でおかしな入力を検出した場合のエラー。
30#[derive(Debug, PartialEq, Eq, Clone, Copy)]
31pub enum PaintOrderError {
32 /// 祖先チェーンの深さが上限を超えた(循環参照や壊れたツリーの疑い)。
33 DepthLimitExceeded(usize),
34}
35
36/// 祖先チェーンの深さ上限。これを超えるツリーは異常とみなす。
37pub const MAX_DEPTH: usize = 512;
38
39/// 要素の実効 z を求める。
40///
41/// - `own_z` … その要素自身の `z-index`(`position: static` なら 0)。
42/// - `ancestor_effective_z` … 親要素の**実効 z**(ルートなら `None`)。
43///
44/// 祖先より小さい z は祖先の値まで引き上げる。これにより
45/// 「子孫が祖先の背景より先に描かれる」ことが構造的に起こらなくなる。
46pub fn effective_z(own_z: i32, ancestor_effective_z: Option<i32>) -> i32 {
47 match ancestor_effective_z {
48 Some(a) if a > own_z => a,
49 _ => own_z,
50 }
51}
52
53/// 描画ソートキー。`(実効 z, 位置指定層, ツリー深さ)` の昇順で並べる。
54///
55/// **CSS 2.1 Appendix E によるスタッキング順序準拠**:
56/// - `effective_z != 0`: そのまま z-index の昇順(負 z は最奥、正 z は最前面)。
57/// - `effective_z == 0`:
58/// - `!is_positioned`: 通常フロー(ブロック、背景、インライン)→ 層 0
59/// - `is_positioned`: position: fixed/absolute/relative/sticky 要素とその子孫 → 層 1
60///
61/// これにより、先行する position:fixed 等のバナーやヘッダーが、後続の通常ブロックの
62/// 背景(z=0, !is_positioned)によって塗り潰される事故を防ぐ。
63/// 深さを第 3 キーにすることで、同一層内では浅い祖先が先に描画される(不変条件 I-1)。
64pub fn paint_sort_key(effective_z: i32, is_positioned: bool, depth: u32) -> (i32, u8, u32) {
65 let pos_layer = if effective_z == 0 && is_positioned { 1 } else { 0 };
66 (effective_z, pos_layer, depth)
67}
68
69/// 祖先チェーン(ルート→親の順)から実効 z を求める。
70///
71/// 深さが `MAX_DEPTH` を超えた場合は黙って打ち切らず、
72/// エラーログを出して `Err` を返す。呼び出し側は安全側
73/// (その要素の own_z をそのまま使う)へフォールバックすること。
74pub fn effective_z_from_chain(own_z: i32, ancestor_zs: &[i32]) -> Result<i32, PaintOrderError> {
75 if ancestor_zs.len() > MAX_DEPTH {
76 crate::error!(
77 "[PAINT][ORDER] 祖先チェーンが深すぎます: {} (上限 {})。own_z をそのまま使います",
78 ancestor_zs.len(),
79 MAX_DEPTH
80 );
81 return Err(PaintOrderError::DepthLimitExceeded(ancestor_zs.len()));
82 }
83 let mut acc: Option<i32> = None;
84 for &z in ancestor_zs {
85 acc = Some(effective_z(z, acc));
86 }
87 Ok(effective_z(own_z, acc))
88}
89
90/// 入れ子のスタッキングコンテキストの 1 段(`(z, 位置指定層, 文書順, 深さ)`)。
91/// 要素の描画キーは「祖先のコンテキストの段の列 + 自分の段」。
92pub type StackStep = (i32, u8, u32, u32);
93
94/// 要素自身の段。`seq` はその要素を生んだ箱の文書順(箱が要素列へ積み始めた位置)で、
95/// 祖先の箱の `seq` は子孫の箱の `seq` 以下になる。同値(祖先が子孫より後に積まれた場合)は
96/// 深さの浅い方が先(I-1)。深さを第 1 にしていた平坦版と違い、別の部分木どうしは文書順で並ぶ。
97pub fn stack_step(effective_z: i32, is_positioned: bool, depth: u32, seq: u32) -> StackStep {
98 let (z, layer, d) = paint_sort_key(effective_z, is_positioned, depth);
99 (z, layer, seq, d)
100}
101
102/// 2 要素の描画順の比較(`Less` が先=奥)。`path` は祖先のスタッキングコンテキストの段の列。
103///
104/// 【2026-10-18】CSS 2.1 Appendix E の入れ子。`z-index` はそれを含むスタッキングコンテキストの
105/// 中でだけ比べる。`#movie_player{position:relative;z-index:0}` の中の
106/// `.html5-video-container{z-index:10}` が、player の外の後続の操作部(z auto)より手前に
107/// 描かれていた(平坦な実効 z では 10 > 0)。段の列を辞書順で比べ、コンテキストを作る要素の
108/// 段は子孫の列の接頭辞になるので、その要素は自分の中身より必ず先(I-1)。
109pub fn paint_cmp(a_path: &[StackStep], a: StackStep, b_path: &[StackStep], b: StackStep) -> core::cmp::Ordering {
110 a_path
111 .iter()
112 .copied()
113 .chain(core::iter::once(a))
114 .cmp(b_path.iter().copied().chain(core::iter::once(b)))
115}