Skip to main content

atmos/os_lib/web_engine/
box_shadow.rs

1//! `box-shadow` の解析と、ぼかしの濃度(純粋モジュール)。
2//!
3//! 【2026-09-26】www.sugi-lab.net のカードの影(`0 2px 10px rgba(0, 0, 0, 0.1)`)が、
4//! Chrome の淡くぼけた影ではなく**濃い灰色の塊**として描かれていた。原因は 3 つ:
5//!
6//! 1. 値を空白で単純に分割しており、`rgba(0, 0, 0, 0.1)` が `rgba(0,` `0,` … に割れて
7//!    色が読めず、既定の「50% の黒」になっていた。
8//! 2. 単位の無い `0` を長さとして読めず、横ずれ・縦ずれ・ぼかしが 1 つずつずれていた。
9//! 3. ぼかしを無視して、影の矩形をそのまま塗っていた。
10//!
11//! 描画(`kernel::draw`)はここで決めた値と濃度関数を使うだけにする。
12
13extern crate alloc;
14use alloc::vec::Vec;
15
16/// 色の指定が無いときの影の色(CSS では currentColor だが、影の色として一般的な
17/// 半透明の黒で代用する。従来の既定と同じ)。
18pub const DEFAULT_SHADOW_COLOR: u32 = 0x8000_0000;
19
20/// 影 1 個: (横ずれ, 縦ずれ, ぼかし半径, 色 ARGB, inset)。
21pub type Shadow = (i32, i32, i32, u32, bool);
22
23/// 括弧の外の空白で分割する(`rgba(0, 0, 0, 0.1)` を 1 トークンに保つ)。
24pub fn split_top_level_ws(s: &str) -> Vec<&str> {
25    let mut out = Vec::new();
26    let mut depth = 0i32;
27    let mut start: Option<usize> = None;
28    for (i, c) in s.char_indices() {
29        match c {
30            '(' => {
31                depth += 1;
32                start.get_or_insert(i);
33            }
34            ')' => {
35                depth -= 1;
36                start.get_or_insert(i);
37            }
38            c if c.is_whitespace() && depth <= 0 => {
39                if let Some(st) = start.take() {
40                    if let Some(t) = s.get(st..i) {
41                        out.push(t);
42                    }
43                }
44            }
45            _ => {
46                start.get_or_insert(i);
47            }
48        }
49    }
50    if let Some(st) = start {
51        if let Some(t) = s.get(st..) {
52            out.push(t);
53        }
54    }
55    out
56}
57
58/// 影の長さトークン(px / rem / em / 単位なしの 0)。1rem = 1em = 16px とする。
59pub fn parse_len(tok: &str) -> Option<i32> {
60    let t = tok.trim();
61    if let Some(n) = t.strip_suffix("px") {
62        return n.parse::<f32>().ok().filter(|v| v.is_finite()).map(|v| v as i32);
63    }
64    if let Some(n) = t.strip_suffix("rem").or_else(|| t.strip_suffix("em")) {
65        return n.parse::<f32>().ok().filter(|v| v.is_finite()).map(|v| (v * 16.0) as i32);
66    }
67    // 単位なしは 0 のときだけ長さとして認める(CSS の規則)。
68    match t.parse::<f32>() {
69        Ok(0.0) => Some(0),
70        _ => None,
71    }
72}
73
74/// 影 1 個を解析する(`inset`、長さ 2〜4 個、色は順不同)。長さが 2 個未満なら無効。
75/// 4 個目(spread)は読み飛ばす(未対応)。
76pub fn parse_one(seg: &str) -> Option<Shadow> {
77    let mut lens: Vec<i32> = Vec::new();
78    let mut color = DEFAULT_SHADOW_COLOR;
79    let mut inset = false;
80    for tok in split_top_level_ws(seg.trim()) {
81        if tok.eq_ignore_ascii_case("inset") {
82            inset = true;
83        } else if let Some(n) = parse_len(tok) {
84            lens.push(n);
85        } else if let Some(c) = crate::os_lib::css::parse_color(tok) {
86            color = c;
87        }
88    }
89    if lens.len() < 2 {
90        return None;
91    }
92    let blur = lens.get(2).copied().unwrap_or(0).max(0);
93    Some((lens[0], lens[1], blur, color, inset))
94}
95
96/// `box-shadow` の値全体(カンマ区切りの複数影)を解析する。`none` は空。
97pub fn parse_list(v: &str) -> Vec<Shadow> {
98    let v = v.trim();
99    if v.is_empty() || v.eq_ignore_ascii_case("none") {
100        return Vec::new();
101    }
102    crate::os_lib::css::split_top_level_commas(v)
103        .into_iter()
104        .filter_map(parse_one)
105        .collect()
106}
107
108/// 影の矩形の縁からの距離 `d`(外側が正、内側が負。px)での濃度 0.0〜1.0。
109///
110/// ぼかしはガウスぼかしの近似として、縁を中心に幅 `blur` の直線勾配にする
111/// (d = -blur/2 で 1、d = +blur/2 で 0)。ぼかし 0 なら内側 1・外側 0。
112pub fn coverage(d: f32, blur: i32) -> f32 {
113    if blur <= 0 {
114        return if d <= 0.0 { 1.0 } else { 0.0 };
115    }
116    (0.5 - d / blur as f32).clamp(0.0, 1.0)
117}