Skip to main content

atmos/os_lib/layout/
pseudo_box.rs

1//! 位置指定された `::before`/`::after` の箱(純粋モジュール)。
2//!
3//! 【2026-10-03】AtmOS は疑似要素の**文字**(`content`)しか描いておらず、背景や位置を持つ**箱**を
4//! 作っていなかった。`content:''; position:absolute; inset:0; background:url(...)` のような装飾
5//! (オーバーレイ・年表の縦線・点)が丸ごと消えていた。sugi-lab.net/profile.htm のページ見出しで、
6//! 紫のグラデーションの下に透ける写真(`.page-hero::before`、`opacity:.2`)が出ていなかった。
7//!
8//! カスケードは疑似要素への宣言を要素自身の `x-before-<名前>` / `x-after-<名前>` として持つので、
9//! ここで接頭辞を外して疑似要素用の値の表にし、包含ブロック(要素のパディングボックス)から
10//! 矩形を決める。描画は通常の箱と同じ処理に任せる(`web_engine::layout`)。
11//! 対象は `position:absolute`/`fixed` の疑似要素だけ(流れの中に置く疑似要素の箱はレイアウトが要る)。
12
13extern crate alloc;
14use alloc::collections::BTreeMap;
15use alloc::string::String;
16
17/// 疑似要素へ引き継ぐ継承プロパティ(文字色・字体等。疑似要素の文字は別経路で描くが、
18/// `currentColor` 等の解決に要る)。
19const INHERITED: &[&str] = &["color", "font-family", "font-size", "font-weight", "font-style", "line-height", "visibility"];
20
21/// 要素の値の表から、位置指定された疑似要素 `which`("before"/"after")の値の表を作る。
22/// `content` が無い・`none`、`display:none`、位置指定が absolute/fixed 以外なら `None`。
23pub fn pseudo_values(parent: &BTreeMap<String, String>, which: &str) -> Option<BTreeMap<String, String>> {
24    let prefix = alloc::format!("x-{}-", which);
25    let content = parent.get(&alloc::format!("{}content", prefix))?;
26    if content.trim() == "none" || content.trim() == "normal" {
27        return None;
28    }
29    let mut out = BTreeMap::new();
30    for key in INHERITED {
31        if let Some(v) = parent.get(*key) {
32            out.insert(String::from(*key), v.clone());
33        }
34    }
35    for (k, v) in parent.range(prefix.clone()..) {
36        let Some(name) = k.strip_prefix(prefix.as_str()) else { break };
37        out.insert(String::from(name), v.clone());
38    }
39    if out.get("display").map(|d| d.trim()) == Some("none") {
40        return None;
41    }
42    match out.get("position").map(|p| p.trim()) {
43        Some("absolute") | Some("fixed") => {}
44        _ => return None,
45    }
46    // 疑似要素は文字を別経路で描くので、箱としては内容を持たない。
47    out.remove("content");
48    out.insert(String::from("display"), String::from("block"));
49    Some(out)
50}
51
52fn len(v: Option<&String>, base: i32) -> Option<i32> {
53    let v = v?.trim();
54    if v.is_empty() || v == "auto" {
55        return None;
56    }
57    super::values::resolve_len_or_pct(v, base)
58}
59
60/// 包含ブロック `cb`(x, y, 幅, 高さ)の中での疑似要素の矩形(x, y, 幅, 高さ)。
61/// 幅は `width`、無ければ `left`+`right` から、どちらも無ければ 0(`content:''` の箱は内容幅 0)。
62/// 高さも同様。`left` が無く `right` があれば右寄せ。
63pub fn abs_rect(cb: (i32, i32, i32, i32), v: &BTreeMap<String, String>) -> (i32, i32, i32, i32) {
64    let (cx, cy, cw, ch) = cb;
65    // `inset` の一括指定(1〜4 値)を個別値へ。
66    let mut sides: [Option<String>; 4] = [
67        v.get("top").cloned(),
68        v.get("right").cloned(),
69        v.get("bottom").cloned(),
70        v.get("left").cloned(),
71    ];
72    if let Some(inset) = v.get("inset") {
73        let p: alloc::vec::Vec<&str> = inset.split_whitespace().collect();
74        let pick = |i: usize| -> String {
75            String::from(match p.len() {
76                1 => p[0],
77                2 => p[i % 2],
78                3 => [p[0], p[1], p[2], p[1]][i],
79                _ => p.get(i).copied().unwrap_or("auto"),
80            })
81        };
82        if !p.is_empty() {
83            for (i, s) in sides.iter_mut().enumerate() {
84                if s.is_none() {
85                    *s = Some(pick(i));
86                }
87            }
88        }
89    }
90    let top = len(sides[0].as_ref(), ch);
91    let right = len(sides[1].as_ref(), cw);
92    let bottom = len(sides[2].as_ref(), ch);
93    let left = len(sides[3].as_ref(), cw);
94    let w = len(v.get("width"), cw)
95        .or_else(|| Some(cw - left? - right?))
96        .unwrap_or(0)
97        .max(0);
98    let h = len(v.get("height"), ch)
99        .or_else(|| Some(ch - top? - bottom?))
100        .unwrap_or(0)
101        .max(0);
102    let x = match (left, right) {
103        (Some(l), _) => cx + l,
104        (None, Some(r)) => cx + cw - r - w,
105        _ => cx,
106    };
107    let y = match (top, bottom) {
108        (Some(t), _) => cy + t,
109        (None, Some(b)) => cy + ch - b - h,
110        _ => cy,
111    };
112    (x, y, w, h)
113}
114
115/// 枠線の太さ(px、四辺同じとみなす)。`border-width` または `border` 一括指定の最初の長さ。
116/// `border:none`/指定なしは 0。年表の点(`border:3px solid #fff`)等の装飾用。
117pub fn border_px(v: &BTreeMap<String, String>) -> i32 {
118    let src = v.get("border-width").or_else(|| v.get("border"));
119    let Some(src) = src else { return 0 };
120    if src.split_whitespace().any(|t| t == "none" || t == "hidden") {
121        return 0;
122    }
123    src.split_whitespace()
124        .find_map(|t| match t {
125            "thin" => Some(1),
126            "medium" => Some(3),
127            "thick" => Some(5),
128            _ => super::values::resolve_len_or_pct(t, 0).filter(|_| !t.ends_with('%')),
129        })
130        .unwrap_or(0)
131        .max(0)
132}